图像的放大缩小其实是一回事,都是先创建一张空白目标图像(放大缩小后的图像),其大小就是想要放大缩小后所得到的图像大小。创建图像后我们并不知道这张图像里面的各个像素点RGB(或灰度)值是多少,这个时候就需要经过一个算法去算目标图像的像素点RGB(或灰度)值。基本上所有相关的算法都是通过算出目标图像的像素跟原图像的像素的映射关系来实现的,但是不同的算法由于这个映射关系的求取不同,处理效率和处理效果会有所差异。本文介绍的是比较常用到的双线性插值算法,并通过C/C++ BMP(24位真彩色)图像处理这一系列所用到的基本框架来实现。
关于双线性插值算法的原理,由下图解释:
如上面所言,我们需要算出目标图像像素跟原图像像素的映射关系,我们从遍历目标图像像素点开始,对每一个像素点进行比例计算,算出其如果投影到原图像应该是处于原图像的哪个位置,通常所得到的结果正如上图的所示,求得的点P处于原图像的四个真实像素点中。我们假定原图像像素点间的RGB(或灰度)值是呈线性变化的,则我们可以通过A、B两点RGB(或灰度)值算出AB线段上面的E点的RGB(或灰度)值,同理亦可算出F点的RGB(或灰度)值。得到了E、F两点的RGB(或灰度)值后可经由相同的方法得到P点的RGB(或灰度)值。到此,我们就知道该如何通过映射关系去求得目标图像的RGB(或灰度)值了。
我们把点A、B、C、D、E、F、P的RGB(或灰度)值分别记为FA、FB、FC、FD、FE、FF、FP(注意由于RGB是三个值,这个记法其实不严谨,可以理解为是FA等是RGB的其中一个值,然后另外两个也可同理得到),则有FE=(1-0.7)*FA+(1-0.3)*FB,FF=(1-0.7)*FC+(1-0.3)*FD,可得FP=(1-0.7)*FE+(1-0.3)*FF=0.3*0.3*FA+0.3*0.7*FB+0.7*0.3*FC+0.7*0.7*FD。
到此计算公式推导完毕,可以着手写代码了,由于我们分析的时候是用一般的情况(点P落在原图像四个像素点中),如果点P刚好落在原图像的最右端或最下端时,需要做特殊的处理。主要实现代码如下:
#define MYDRAW_HEIGHT 600 //目标图像高度
#define MYDRAW_WIDTH 800 //目标图像
/*******************图像处理部分******************/
/*******************双线性插值******************/
for (int hnum = 0; hnum < MYDRAW_HEIGHT; hnum++)
for (int wnum = 0; wnum < MYDRAW_WIDTH; wnum++)
{
double d_original_img_hnum = hnum*height / (double)MYDRAW_HEIGHT;
double d_original_img_wnum = wnum*width / (double)MYDRAW_WIDTH;
int i_original_img_hnum = d_original_img_hnum;
int i_original_img_wnum = d_original_img_wnum;
double distance_to_a_x = d_original_img_wnum - i_original_img_wnum;//在原图像中与a点的水平距离
double distance_to_a_y = d_original_img_hnum - i_original_img_hnum;//在原图像中与a点的垂直距离
int original_point_a = i_original_img_hnum*l_width + i_original_img_wnum * 3;//数组位置偏移量,对应于图像的各像素点RGB的起点,相当于点A
int original_point_b = i_original_img_hnum*l_width + (i_original_img_wnum + 1) * 3;//数组位置偏移量,对应于图像的各像素点RGB的起点,相当于点B
int original_point_c = (i_original_img_hnum + 1)*l_width + i_original_img_wnum * 3;//数组位置偏移量,对应于图像的各像素点RGB的起点,相当于点C
int original_point_d = (i_original_img_hnum + 1)*l_width + (i_original_img_wnum + 1) * 3;//数组位置偏移量,对应于图像的各像素点RGB的起点,相当于点D
if (i_original_img_hnum + 1== MYDRAW_HEIGHT - 1)
{
original_point_c = original_point_a;
original_point_d = original_point_b;
}
if (i_original_img_wnum + 1== MYDRAW_WIDTH - 1)
{
original_point_b = original_point_a;
original_point_d = original_point_c;
}
int pixel_point = hnum*write_width + wnum * 3;//映射尺度变换图像数组位置偏移量
pColorDataMid[pixel_point] =
pColorData[original_point_a] * (1 - distance_to_a_x)*(1 - distance_to_a_y) +
pColorData[original_point_b] * distance_to_a_x*(1 - distance_to_a_y) +
pColorData[original_point_c] * distance_to_a_y*(1 - distance_to_a_x) +
pColorData[original_point_c] * distance_to_a_y*distance_to_a_x;
pColorDataMid[pixel_point + 1] =
pColorData[original_point_a + 1] * (1 - distance_to_a_x)*(1 - distance_to_a_y) +
pColorData[original_point_b + 1] * distance_to_a_x*(1 - distance_to_a_y) +
pColorData[original_point_c + 1] * distance_to_a_y*(1 - distance_to_a_x) +
pColorData[original_point_c + 1] * distance_to_a_y*distance_to_a_x;
pColorDataMid[pixel_point + 2] =
pColorData[original_point_a + 2] * (1 - distance_to_a_x)*(1 - distance_to_a_y) +
pColorData[original_point_b + 2] * distance_to_a_x*(1 - distance_to_a_y) +
pColorData[original_point_c + 2] * distance_to_a_y*(1 - distance_to_a_x) +
pColorData[original_point_c + 2] * distance_to_a_y*distance_to_a_x;
}
/*******************双线性插值******************/
/*******************图像处理部分******************/
处理结果如下:
原图像
缩小后图像
放大后图像
整个工程代码如下:
#include
#include
#include
#include
#include
#include//时间相关头文件,可用其中函数计算图像处理速度
#define WIDTHBYTES(bits) (((bits)+31)/32*4)//用于使图像宽度所占字节数为4byte的倍数
#define MYDRAW_HEIGHT 1080 //目标图像高度
#define MYDRAW_WIDTH 1920 //目标图像宽度
typedef unsigned char BYTE;
typedef unsigned short WORD;
typedef unsigned long DWORD;
typedef long LONG;
//位图文件头信息结构定义
//其中不包含文件类型信息(由于结构体的内存结构决定,要是加了的话将不能正确读取文件信息)
typedef struct tagBITMAPFILEHEADER {
DWORD bfSize; //文件大小
WORD bfReserved1; //保留字,不考虑
WORD bfReserved2; //保留字,同上
DWORD bfOffBits; //实际位图数据的偏移字节数,即前三个部分长度之和
} BITMAPFILEHEADER;
//信息头BITMAPINFOHEADER,也是一个结构,其定义如下:
typedef struct tagBITMAPINFOHEADER {
//public:
DWORD biSize; //指定此结构体的长度,为40
LONG biWidth; //位图宽
LONG biHeight; //位图高
WORD biPlanes; //平面数,为1
WORD biBitCount; //采用颜色位数,可以是1,2,4,8,16,24,新的可以是32
DWORD biCompression; //压缩方式,可以是0,1,2,其中0表示不压缩
DWORD biSizeImage; //实际位图数据占用的字节数
LONG biXPelsPerMeter; //X方向分辨率
LONG biYPelsPerMeter; //Y方向分辨率
DWORD biClrUsed; //使用的颜色数,如果为0,则表示默认值(2^颜色位数)
DWORD biClrImportant; //重要颜色数,如果为0,则表示所有颜色都是重要的
} BITMAPINFOHEADER;
void main()
{
long now = 0;
now = clock();//存储图像处理开始时间
BITMAPFILEHEADER bitHead, writebitHead;
BITMAPINFOHEADER bitInfoHead, writebitInfoHead;
FILE* pfile;//输入文件
FILE* wfile;//输出文件
char strFile[50] = "demo.bmp";//打开图像路径,BMP图像必须为24位真彩色格式
char strFilesave[50] = "16.bmp";//处理后图像存储路径
fopen_s(&pfile, strFile, "rb");//文件打开图像
fopen_s(&wfile, strFilesave, "wb");//打开文件为存储修改后图像做准备
//读取位图文件头信息
WORD fileType;
fread(&fileType, 1, sizeof(WORD), pfile);
fwrite(&fileType, 1, sizeof(WORD), wfile);
if (fileType != 0x4d42)
{
printf("file is not .bmp file!");
return;
}
//读取位图文件头信息
fread(&bitHead, 1, sizeof(tagBITMAPFILEHEADER), pfile);
writebitHead = bitHead;//由于截取图像头和源文件头相似,所以先将源文件头数据赋予截取文件头
//读取位图信息头信息
fread(&bitInfoHead, 1, sizeof(BITMAPINFOHEADER), pfile);
writebitInfoHead = bitInfoHead;//同位图文件头相似
writebitInfoHead.biHeight = MYDRAW_HEIGHT;//为截取文件重写位图高度
writebitInfoHead.biWidth = MYDRAW_WIDTH;//为截取文件重写位图宽度
int mywritewidth = WIDTHBYTES(writebitInfoHead.biWidth*writebitInfoHead.biBitCount);//BMP图像实际位图数据区的宽度为4byte的倍数,在此计算实际数据区宽度
writebitInfoHead.biSizeImage = mywritewidth*writebitInfoHead.biHeight;//计算位图实际数据区大小
writebitHead.bfSize = 54 + writebitInfoHead.biSizeImage;//位图文件头大小为位图数据区大小加上54byte
fwrite(&writebitHead, 1, sizeof(tagBITMAPFILEHEADER), wfile);//写回位图文件头信息到输出文件
fwrite(&writebitInfoHead, 1, sizeof(BITMAPINFOHEADER), wfile);//写回位图信息头信息到输出文件
int width = bitInfoHead.biWidth;
int height = bitInfoHead.biHeight;
//分配内存空间把源图存入内存
int l_width = WIDTHBYTES(width*bitInfoHead.biBitCount);//计算位图的实际宽度并确保它为4byte的倍数
int write_width = WIDTHBYTES(writebitInfoHead.biWidth*writebitInfoHead.biBitCount);//计算写位图的实际宽度并确保它为4byte的倍数
BYTE *pColorData = (BYTE *)malloc(height*l_width);//开辟内存空间存储图像数据
memset(pColorData, 0, height*l_width);
BYTE *pColorDataMid = (BYTE *)malloc(mywritewidth*MYDRAW_HEIGHT);//开辟内存空间存储图像处理之后数据
memset(pColorDataMid, 0, mywritewidth*MYDRAW_HEIGHT);
long nData = height*l_width;
long write_nData = mywritewidth*MYDRAW_HEIGHT;//截取的位图数据区长度定义
//把位图数据信息读到数组里
fread(pColorData, 1, nData, pfile);//图像处理可通过操作这部分数据加以实现
/*******************图像处理部分******************/
/*******************双线性插值******************/
for (int hnum = 0; hnum < MYDRAW_HEIGHT; hnum++)
for (int wnum = 0; wnum < MYDRAW_WIDTH; wnum++)
{
double d_original_img_hnum = hnum*height / (double)MYDRAW_HEIGHT;
double d_original_img_wnum = wnum*width / (double)MYDRAW_WIDTH;
int i_original_img_hnum = d_original_img_hnum;
int i_original_img_wnum = d_original_img_wnum;
double distance_to_a_x = d_original_img_wnum - i_original_img_wnum;//在原图像中与a点的水平距离
double distance_to_a_y = d_original_img_hnum - i_original_img_hnum;//在原图像中与a点的垂直距离
int original_point_a = i_original_img_hnum*l_width + i_original_img_wnum * 3;//数组位置偏移量,对应于图像的各像素点RGB的起点,相当于点A
int original_point_b = i_original_img_hnum*l_width + (i_original_img_wnum + 1) * 3;//数组位置偏移量,对应于图像的各像素点RGB的起点,相当于点B
int original_point_c = (i_original_img_hnum + 1)*l_width + i_original_img_wnum * 3;//数组位置偏移量,对应于图像的各像素点RGB的起点,相当于点C
int original_point_d = (i_original_img_hnum + 1)*l_width + (i_original_img_wnum + 1) * 3;//数组位置偏移量,对应于图像的各像素点RGB的起点,相当于点D
if (i_original_img_hnum +1== MYDRAW_HEIGHT - 1)
{
original_point_c = original_point_a;
original_point_d = original_point_b;
}
if (i_original_img_wnum +1== MYDRAW_WIDTH - 1)
{
original_point_b = original_point_a;
original_point_d = original_point_c;
}
int pixel_point = hnum*write_width + wnum * 3;//映射尺度变换图像数组位置偏移量
pColorDataMid[pixel_point] =
pColorData[original_point_a] * (1 - distance_to_a_x)*(1 - distance_to_a_y) +
pColorData[original_point_b] * distance_to_a_x*(1 - distance_to_a_y) +
pColorData[original_point_c] * distance_to_a_y*(1 - distance_to_a_x) +
pColorData[original_point_c] * distance_to_a_y*distance_to_a_x;
pColorDataMid[pixel_point + 1] =
pColorData[original_point_a + 1] * (1 - distance_to_a_x)*(1 - distance_to_a_y) +
pColorData[original_point_b + 1] * distance_to_a_x*(1 - distance_to_a_y) +
pColorData[original_point_c + 1] * distance_to_a_y*(1 - distance_to_a_x) +
pColorData[original_point_c + 1] * distance_to_a_y*distance_to_a_x;
pColorDataMid[pixel_point + 2] =
pColorData[original_point_a + 2] * (1 - distance_to_a_x)*(1 - distance_to_a_y) +
pColorData[original_point_b + 2] * distance_to_a_x*(1 - distance_to_a_y) +
pColorData[original_point_c + 2] * distance_to_a_y*(1 - distance_to_a_x) +
pColorData[original_point_c + 2] * distance_to_a_y*distance_to_a_x;
}
/*******************双线性插值******************/
/*******************图像处理部分******************/
fwrite(pColorDataMid, 1, write_nData, wfile); //将处理完图像数据区写回文件
fclose(pfile);
fclose(wfile);
printf("图像处理完成\n");
printf("运行时间为:%dms\n", int(((double)(clock() - now)) / CLOCKS_PER_SEC * 1000));//输出图像处理花费时间信息
}
另外需要注意到,双线性插值算法在图像缩小超过两倍之后,效果会比较差,特别是缩小了五六倍之后,里面的很多细节会丢失,这个是因为关键点丢失造成的。可以通过逐级缩小的方法来解决这个问题。