这次做的图像处理主要还是处理的8位的BMP图像,二值化和素描功能模块。
BMP文件总体上由4部分组成,分别是位图文件头、位图信息头、调色板和图像数据
1.位图文件头位图文件头包含了图像类型、图像大小、图像数据存放地址和两个保留未使用的字段。
typedef struct tagBITMAPFILEHEADER {
WORD bfType; //位图类别,根据不同的操作系统而不同,在Windows中,此字段的值总为‘BM’
DWORD bfSize; //BMP图像文件的大小
WORD bfReserved1;
WORD bfReserved2;
DWORD bfOffBits; //BMP图像数据的地址,也即离文件头的偏移量
} BITMAPFILEHEADER
2.位图信息头
位图信息头包含了位图信息头的大小、图像的宽高、图像的色深、压缩说明图像数据的大小和其他一些参数。
typedef struct tagBITMAPINFOHEADER{
DWORD biSize; //本结构的大小,根据不同的操作系统而不同,在Windows中,此字段的值总为28h字节=40字节
LONG biWidth; //BMP图像的宽度,单位像素
LONG biHeight;
WORD biPlanes;
WORD biBitCount; //BMP图像的色深,即一个像素用多少位表示,常见有1、4、8、16、24和32,分别对应单色、16色、256色、16位 高彩色、24位真彩色和32位增强型真彩色
DWORD biCompression; //压缩方式,0表示不压缩,1表示RLE8压缩,2表示RLE4压缩,3表示每个像素值由指定的掩码决定
DWORD biSizeImage; //BMP图像数据大小,必须是4的倍数,图像数据大小不是4的倍数时用0填充补足
LONG biXPelsPerMeter; //水平分辨率,单位像素/m
LONG biYPelsPerMeter; //垂直分辨率,单位像素/m
DWORD biClrUsed; //BMP图像使用的颜色,0表示使用全部颜色,对于256色位图来说,此值为100h=256
DWORD biClrImportant; //重要的颜色数,此值为0时所有颜色都重要,对于使用调色板的BMP图像来说,当显卡不能够显示所有颜色时,此值将辅助驱动程序显示颜色
} BITMAPINFOHEADER,
3.彩色表/调色板
彩色表/调色板是单色、16色和256色图像文件所特有的,相对应的调色板大小是2、16和256,调色板以4字节为单位,每4个字节存放一个颜色值,图像 的数据是指向调色板的索引。
typedef struct tagRGBQUAD {
BYTE rgbBlue; //蓝色值
BYTE rgbGreen; //绿色值
BYTE rgbRed; //红色值
BYTE rgbReserved; //保留,总为0
} RGBQUAD;
4.位图数据
如果图像是单色、16色和256色,则紧跟着调色板的是位图数据,位图数据是指向调色板的索引序号。
如果位图是16位、24位和32位色,则图像文件中不保留调色板,即不存在调色板,图像的颜色直接在位图数据中给出。
16位图像使用2字节保存颜色值,常见有两种格式:5位红5位绿5位蓝和5位红6位绿5位蓝,即555格式和565格式。555格式只使用了15 位,最后一位保留,设为0。
24位图像使用3字节保存颜色值,每一个字节代表一种颜色,按红、绿、蓝排列。
32位图像使用4字节保存颜色值,每一个字节代表一种颜色,除了原来的红、绿、蓝,还有Alpha通道,即透明色。
同时,还有一种结构,由位图信息头和颜色表组成位图信息结构BITMAPINFO
typedef struct tagBITMAPINFO{
BITMAPINFOHEADER bmiHeader; //位图信息头
RGBQUAD bmiColors[1]; //颜色表
}BITMAPINFO;
在我的图像处理当中,为方便处理图像数据,于是定义了一个CMyImage类,用于存放相关数据
class CMyImage
{
public:
CMyImage(void);
~CMyImage(void);
HANDLE hdib; //用于存放需要操作的图像内存地址
BITMAPINFO *lpbi; //指向一个BITMAPINFO结构的一个指针
LPSTR lpbits; //指向实际图像数据
DWORD wid; //图像的宽度
DWORD hei; //图像的高度
WORD bitcount; //每像素位数
WORD bpl; //每行字节数
WORD bpp; //每像素字节数
};
使用图像时候那么就需要在内存中申请空间来存储空间进而进行操作
image->hdib=GlobalAlloc(GPTR, size); // 申请内存空间
image->lpbi=(BITMAPINFO *) GlobalLock(image->hdib); // 得位图信息指针
image->lpbits=(LPSTR) image->lpbi + offset; // 得DIB中像素数据起始位置
在申请内存空间前MyImage的成员数据也就需要赋值了
image->bpp=(WORD) ((bitcount+7)/8); // 计算每像素字节数
image->bpl=(WORD) ((wid*bitcount+31)/32*4); // 计算每行字节数
size=40+palette+image->bpl*hei+64; // 计算DIB尺寸
既然每使用一次新的图像就需要创建一个MyImage,那么内存回收的问题也就显得更为重要,一不小心造成内存泄露,那么想找出根源也就很苦恼了。
所以,每当要创建一个新的图像时,那么需要检查一下看是否需要先释放内存
if(image->hdib != NULL)
{
GlobalUnlock(image->hdib); //解除图像在内存中的锁定
GlobalFree(image->hdib); // 释放内存空间
image->hdib=NULL; // 指针清零
}
开始进行操作,图像二值化,基本上就是把图像先进行灰度处理,转化成灰度图像,然后选举合适的阈值再进行处理。 灰度大于或等于阀值的像素被判定为属于特定物体,其灰度值为255表示,否则这些像素点被排除在物体区域以外,灰度值为0。( 灰度值255即白色,灰度值0即黑色)
那么二值化处理的主要不同就是在选举合适的阈值上。常见有大津法,迭代法,双峰法。在取阈值前,通常要获取到图像的直方图,统计图像灰度值从0-255每个灰度值中的个数
大津法取阈值
对图像Image,记t为前景与背景的分割阈值,前景点数占图像比例为w0,平均灰度为u0;背景点数占图像比例为w1,平均灰度为u1。图像的总平均灰度为:u=w0*u0+w1*u1。从最小灰度值到最大灰度值遍历t,当t使得值g=w0*(u0-u)2+w1*(u1-u)2 最大时t即为分割的最佳阈值。对大津法可作如下理解:该式实际上就是类间方差值,阈值t分割出的前景和背景两部分构成了整幅图像,而前景取值u0,概率为 w0,背景取值u1,概率为w1,总均值为u,根据方差的定义即得该式。
代码实现:
int CThreShold::Otsu(long *pg) // 大津法取阈值 pg[256] 存放索引对应灰度值数量
{
int i,j,p;
double A,B,An,Bn,u,v,qqq[256],max,min;
An=Bn=0;
for (i=0;i<256;i++)
{
An+=pg[i]; Bn+=pg[i]*(i+1);
}
for (j=0;j<256;j++)
{
A=B=0;
for (i=0;i<=j;i++)
{
A+=pg[i]; B+=pg[i]*(i+1);
}
if (A) u=B/A;
else u=0;
if (An-A) v=(Bn-B)/(An-A);
else v=0;
qqq[j]=A*(An-A)*(u-v)*(u-v);
}
max=min=qqq[0]; p=0;
for (i=1;i<256;i++)
{
if (qqq[i]>max)
{
max=qqq[i];
p=i;
}
else if (qqq[i]<min) min=qqq[i];
}
return(p);
}
迭代法取阈值
迭代法是基于逼近的思想,其步骤如下:
1. 求出图象的最大灰度值和最小灰度值,分别记为ZMAX和ZMIN,令初始阈值T0=(ZMAX+ZMIN)/2;
2. 根据阈值TK将图象分割为前景和背景,分别求出两者的平均灰度值ZO和ZB;
3. 求出新阈值TK+1=(ZO+ZB)/2;
4. 若TK=TK+1,则所得即为阈值;否则转2,迭代计算
代码实现:
int CThreShold::Iterative(long *pg)
{
int i;
long nZmax,nZmin,nTFirst,temp,nT;
long Tsum = 0,Tnum =0,averHead,averTail;
for(i =0;pg[i] == 0;i++)
break;
nZmin = static_cast<long>(i);
for(i = 255;pg[i] ==0;i--)
break;
nZmax = static_cast<long>(i);
nTFirst = (nZmin+nZmax)/2;
nT = nTFirst;
temp = nT;
while(nT != temp)
{
temp = nT;
for(i=0;i<nT;i++)
{
Tsum+=pg[i]*i;
Tnum+=pg[i];
}
averHead = Tsum/Tnum;
Tsum = 0;
Tnum = 0;
for(i=nT;i<256;i++)
{
Tsum+=pg[i]*i;
Tnum+=pg[i];
}
averTail = Tsum/Tnum;
Tsum = 0;
Tnum = 0;
nT = (averHead+averTail)/2;
}
return nT;
}
双峰法取阈值
双峰法的原理:它认为图像由前景和背景组成,在灰度直方图上,前后二景都形成高峰,在双峰之间的最低谷处就是图像的阈值所在。从分割的效果来看,当前后景的对比较为强烈时,分割效果较好;否则基本无效。
代码实现:
int CThreShold::DoublePeak(long *pg,int width)
{
long nPeakS = 0, nPeakE = 0,nValley = 0;
int nSID = 0,nEID =0,nVID =0,i = 0;
for(i =0;i<255;i++)
{
if(nPeakS<pg[i])
{
nPeakS = pg[i];
nSID = i;
}
}
if( nSID-width>0)
{
for(i =0;i<nSID-width;i++)
{
if(nPeakE<pg[i])
{
nPeakE = pg[i];
nEID = i;
}
}
}
if( nSID- width<255)
{
for(i =nSID+width;i<256;i++)
{
if(nPeakE<pg[i])
{
nPeakE = pg[i];
nEID = i;
}
}
}
nValley = pg[nSID];
int t1 = nSID,t2 = nEID;
if(nSID>nEID)
{
t1 = nEID;
t2 = nSID;
}
for(i = t1;i<t2;i++)
{
if(pg[i]<nValley)
{
nValley = pg[i];
nVID = i;
}
}
return nVID;
}
素描功能,主要采用的Laplacian算子的素描功能,使用的8邻点计算。
g(i,j) = 8 f(i,j) -f(i-1,j) -f(i+1,j)-f(i,j-1) -f(i,j+1)- f(i-1,j-1)-f(i-1,j+1)-f(i+1,j-1)-f(i+1,j+1)+offset
其中offset=255。当g(x)>255时 g(x) =255,当g(x)<0时,g(x) = 0;
这样处理后有素描效果。
代码实现:
void CLaplacianSketch::Template(BYTE **list1,BYTE **list0,int Dx,int Dy)
{
int i,j,g;
for (i=1;i<Dy-1;i++)
{
for (j=1;j<Dx-1;j++)
{
g = (Mask[0][0]*list1[i-1][j-1]+Mask[0][1]*list1[i-1][j]
+Mask[0][2]*list1[i-1][j+1]+Mask[1][0]*list1[i][j-1]
+Mask[1][1]*list1[i][j]+Mask[1][2]*list1[i][j+1]
+Mask[2][0]*list1[i+1][j-1]+Mask[2][1]*list1[i+1][j]
+Mask[2][2]*list1[i+1][j+1])/Scale+Offset;
if (g>0xff) g=0xff;
else if (g<0) g=0;
list0[i][j] = (BYTE) g;
}
}
list1是实际图像数据的缓冲区,list存放处理后的图像数据,Dx为图像宽度,Dy为图像高度。
以上即为基本的实现过程,在编码过程中,出现了一些问题,
1.关于指针做函数形参时, 函数的参数传递,本质上都是值传递,指针做形参时,形参的值实际为实参地址的拷贝,那么当需要修改参数地址时,那么可以用二维指针,即传入指针的指针来修改指针的地址。
2.关于封装成DLL的要点。
在.cpp中
#define DLL_API_SKY extern "C" _declspec(dllexport)
函数定义时,
void _stdcall ValueOfTwo(BITMAPINFO* lpbiIn,LPSTR lpbitsIn,BITMAPINFO** lpbiOut,LPSTR &lpbitsOut)
{
}
在.h中
#ifdef DLL_API_SKY
#else
#define DLL_API_SKY extern "C" _declspec(dllimport)
#endif
函数声明则是
DLL_API_SKY void _stdcall ValueOfTwo(BITMAPINFO* lpbiIn,LPSTR lpbitsIn,BITMAPINFO** lpbiOut,LPSTR &lpbitsOut);
在.def中
LIBRARY
EXPORTS
ValueOfTwo
这样导出的函数名就不会发生改变了。
3.在用SDI编写测试程序时,要在DOC中包含VIEW的头文件,那么就需要将VIEW.cpp里包含DOC的语句转移到VIEW.h里,
做完了8位BMP图像的一些处理,下一步再去做下24位的。