- Visual C++数字图像处理技术详解
- 刘海波 沈晶 郭耸等编著
- 4870字
- 2024-12-20 23:09:55
第二篇 数字图像处理核心技术
本篇介绍数字图像处理的核心技术,包括图像几何变换、正交变换、图像增强、复原、重建、分割、匹配和形态学处理等。针对每一项处理技术,本篇介绍若干常用算法,对每个算法给出了基本原理、算法描述、完整的程序代码和演示效果。每章都配有综合实例,详细讲解应用开发的具体步骤。本篇内容可以将数字图像处理的门外汉培养成精通数字图像处理理论和算法编程的行家。
第2章 图像几何变换
数字图像处理的方法主要有两类:空域法和频域法。图像变换是数字图像处理的基本技术,包括图像几何变换和图像正交变换两种重要方式。图像的几何变换也称为空间变换,是指原始图像按照需要进行大小、形状和位置的变化,属于空域法数字图像处理方式。本章将以实例的方式系统介绍几种常用的图像几何变换,并以VC代码实现。
2.1 图像位置变换
图像的位置变换包括图像平移、图像旋转、图像镜像及图像转置等几个方面的常用技术,是图像几何变换中最基本的操作。
实现二维(2D)图像几何变换的基本变换通常是利用齐次坐标及改成3×3阶形式的变换矩阵,其一般过程为:将2×n阶的二维点集矩阵表示成齐次坐标的形式,然后乘以相应的变换矩阵,即变换后的点集矩阵=变换矩阵T×变换前的点集矩阵。其中,二维点集矩阵是指离散化的二维图像中,各像素在坐标空间中的位置。变换前和交换后的点集矩阵均用规范化齐次坐标表示。
齐次坐标指用n+1维向量表示n维向量的方法。二维图像中的点坐标(x,y)可以表示成齐次坐标(Hx,Hy, H),其中H表示非零的任意实数;当H=1时,则(x, y, 1)称为点(x, y)的规范化齐次坐标。
变换矩阵T为
变换矩阵T可分为4个子矩阵。子矩阵可使图像实现恒等、比例、反射(或镜像)、错切和旋转变换。矩阵[lm]可以使图像实现平移变换。矩阵[p q]T可以使图像实现透视变换,但当p=0,q=0时它无透视作用。矩阵[s]可以使图像实现全比例变换。
由此,上述图像几何变换的基本过程可表示为
变换后的点集矩阵可表示为
本节将采取这种矩阵线性变换的形式,对图像位置变换进行讲解,并以VC编码实现。
数字图像变换的本质是建立输入图像与输出图像中所有各点之间映射关系的函数。
2.1.1 图像平移
图像平移是指将图像中所有像素点按照指定的平移量水平或垂直移动到期望的位置。图像的平移只是改变图像在屏幕上的位置,图像本身并不发生变化,其实质是一种坐标的变换,是图像几何变换中最简单的一种方式。
1. 基本原理
设源图像中某像素点原始坐标(x0,y0),向x方向和y方向分别平移lXOffset和lYOffset的距离后,坐标变为(x1,y1),则该点移动前后坐标的关系为
利用齐次坐标,这种关系可以用矩阵变换的方式表示为
由此,可以计算出平移后每个像素点的新位置,实现图像平移。
对变换矩阵求逆,得到式(2-1)的逆变换为
即
由此,可以根据平移后图中的每一像素点的坐标计算出其对应的源图中像素点的坐标,判断此坐标是否在源图的范围内。如果超出源图的范围,则将该点的像素值统一设置为255(白色)。也就是说,对于平移后不在源图区域的点都处理为不显示。
2. 算法描述
实现数字图像在屏幕上的平移,可以分为以下4个步骤:
[1] 将源图像保存到缓冲区,并记录下缓冲区的地址。
[2] 采用某种交互方式(如对话框),分别设定在水平和垂直方向上的平移量。
[3] 重新分配一个与源图像一样大小的临时缓冲区。
[4] 根据步骤[2]中设定的平移量及源图中每个像素点的坐标值,获得平移后各像素点的新坐标值,实现图像的平移,并且不显示已经移出源图区域的图像。
3. 编程实现
下面通过编写VC的函数Translation( )来实现数字图像的平移,其具体代码如下:
/************************************************************************* * 函数名称:Translation(LPSTR lpSrcStartBits, long lWidth, long lHeight, long lXOffset, long lYOffset,long lLineBytes,long lDstLineBytes) * 函数参数: * LPSTR lpSrcStartBits 指向源DIB起始像素的指针 * long lWidth DIB图像的宽度 * long lHeight DIB图像的高度 * long lXOffset x方向偏移量 * long lYOffset y方向偏移量 * long lLineBytes DIB图像的行字节数,为4的倍数 * long lDstLineBytes 临时DIB图像的行字节数,为4的倍数 * 函数功能:该函数用来平移DIB图像 ************************************************************************/ BOOL Translation(LPSTR lpSrcStartBits, long lWidth, long lHeight, long lXOffset, long lYOffset,long lLineBytes,long lDstLineBytes) { long i; // 行循环变量 long j; // 列循环变量 LPSTR lpSrcDIBBits; // 指向源像素的指针 LPSTR lpDstDIBBits; // 指向临时图像对应像素的指针 LPSTR lpDstStartBits; // 指向临时图像对应像素的指针 HLOCAL hDstDIBBits; // 临时图像句柄 hDstDIBBits= LocalAlloc(LHND, lWidth * lDstLineBytes); // 分配临时内存 lpDstStartBits= (char * )LocalLock(hDstDIBBits); // 锁定内存 if (hDstDIBBits== NULL) // 判断是否内存分配 return FALSE; // 分配内存失败 for(i = 0; i < lHeight; i++) // 行 { for(j = 0; j < lWidth; j++) // 列 { // 指向新DIB第i行、第j个像素的指针 lpDstDIBBits=(char*)lpDstStartBits+lLineBytes*(lHeight-1-i) +j; // 判断是否在源图范围内 if( (j-lYOffset>= 0) && (j-lYOffset< lWidth) && (i-lXOffset>= 0) && (i-lXOffset < lHeight)) // 像素在源DIB中的坐标 j-lXOffset { // 指向源DIB第i0行、第j0个像素的指针 lpSrcDIBBits=(char *)lpSrcStartBits+lLineBytes*(lHeight-1- (i-lXOffset))+(j-lYOffset); *lpDstDIBBits= *lpSrcDIBBits; // 复制像素 } else { * ((unsigned char*)lpDstDIBBits) = 255; // 源图中没有的像素,赋值为255 } } } memcpy(lpSrcStartBits, lpDstStartBits, lLineBytes * lHeight); // 复制图像 LocalUnlock(hDstDIBBits); // 释放内存 LocalFree(hDstDIBBits); return TRUE; }
可以通过在数字图像处理程序的框架窗口上增加菜单【几何变换】|【图像平移】项,如图2-1所示。对应的处理函数是CDImageProcessView视图类中的CDImage Process View::OnTranslation( )。在进行函数的调用时,可以采用对话框的形式对平移量进行设定,如图2-2所示。具体代码如下:
图2-1 菜单栏
图2-2 【平移参数】对话框
void CDImageProcessView::OnTranslation() { CDImageProcessDoc* pDoc = GetDocument(); long lSrcLineBytes; // 图像每行的字节数 long lSrcWidth; // 图像的宽度 long lSrcHeight; // 图像的高度 LPSTR lpSrcDib; // 指向源图像的指针 LPSTR lpSrcStartBits; // 指向源像素的指针 long lDstLineBytes; // 新图像每行的字节数 lpSrcDib= (LPSTR) ::GlobalLock((HGLOBAL) pDoc->GetHObject()); // 锁定DIB if (pDoc->m_dib.GetColorNum(lpSrcDib) != 256) // 判断是否是8-bpp位图, // 不是则返回 { AfxMessageBox(_T ("对不起,不是256色位图!")); // 警告 ::GlobalUnlock((HGLOBAL) pDoc->GetHObject()); // 解除锁定 return; // 返回 } lpSrcStartBits=pDoc->m_dib.GetBits(lpSrcDib); // 找到DIB图像像素起始位置 lSrcWidth= pDoc->m_dib.GetWidth(lpSrcDib); // 获取图像的宽度 lSrcHeight= pDoc->m_dib.GetHeight(lpSrcDib); // 获取图像的高度 lSrcLineBytes=pDoc->m_dib.GetReqByteWidth(lSrcWidth * 8); //计算图像每行的字节数 lDstLineBytes=pDoc->m_dib.GetReqByteWidth(lSrcHeight * 8); //计算新图像每行的字节数 CDlgTran TranPara; // 创建对话框 if (TranPara.DoModal() != IDOK) // 显示对话框,提示用户设定量 return; int temver=TranPara.m_verOff; int temhor=TranPara.m_horOff; if (Translation(lpSrcStartBits, lSrcWidth,lSrcHeight, // 调用Translation( )函数平移DIB temver,temhor,lSrcLineBytes,lDstLineBytes)) { pDoc->SetModifiedFlag(TRUE); // 设置标记 pDoc->UpdateAllViews(NULL); // 更新视图 ::GlobalUnlock((HGLOBAL) pDoc->GetHObject()); // 解除锁定 } else { AfxMessageBox(_T("分配内存失败!")); // 警告 } }
本程序中用到了CDib类的成员及函数,关于CDib类的定义、实现及有关对话框的编程将在2.3节的综合实例中加以讲解。
4. 效果演示
源图像如图2-3a所示,对其进行平移后,结果如图2-3b所示,其中,参数设定见图2-2。
图2-3 图像平移效果演示
2.1.2 图像旋转
图像旋转是数字图像处理中的一种常用技术,是一种比较复杂的图像几何变换。其本质是以图像的中心为原点,将图像上的所有像素都旋转一个相同的角度。与图像平移一样,图像旋转也是图像的位置变换,对于旋转后超出源图像范围的区域要处理为不显示。
1. 基本原理
下面具体分析源图像与旋转后图像的对应像素之间的关系,如图2-4所示。
图2-4 坐标系对照示意图
在笛卡儿坐标系O2中原始坐标(x0, y0),旋转α角后,坐标变为(x1, y1),旋转前的极坐标表示为
旋转后的极坐标表示为
写成矩阵的形式为
其逆变换为
在屏幕坐标系O1中,把笛卡儿坐标系中的坐标转换到屏幕坐标系,计算原始坐标(x0, y0)绕坐标点(a0, b0)旋转α角后的新坐标(x1, y1),可先将笛卡儿坐标系原点(0, 0)平移到坐标点(a0, b0),再根据式(2-2)、式(2-3)进行旋转,最后平移回新的坐标原点。
设旋转后新图像的左上角为原点,旋转前的中心坐标为(a0, b0),旋转后的中心坐标为(a1, b1),则旋转α角后的新坐标(x1, y1),可由如下矩阵计算。
其逆变换为
即
逆变换为
由此,可以根据旋转后图中的每一像素点的坐标计算出其对应的源图中像素点的坐标,判断此坐标是否在新计算出的图像范围内,如果超出范围,则将该点的像素值处理为不显示,即统一设置为255(白色)。
2. 算法描述
实现数字图像在屏幕上的旋转,可以分为以下5个步骤:
[1] 将源图像保存到缓冲区,并记录下缓冲区的地址。
[2] 获取源图像的高度和宽度,计算出源图像的中心点坐标(a0, b0)。以(a0, b0)为坐标原点,计算出源图像4个顶点的坐标。
[3] 采用某种交互方式(如对话框),获取图像的旋转角度。
[4] 分配内存,以保存旋转后的图像。
[5] 根据步骤[3]中设定的旋转角度及源图中每个像素点的坐标值,获得旋转后各像素点的新坐标值,实现图像的旋转。根据源图像4个顶点的坐标旋转后的坐标值,确定新图像中心及高度和宽度,不显示已经超出范围的图像。
3. 编程实现
下面通过编写VC的函数Rotate( )来实现数字图像的旋转,其具体代码如下:
/************************************************************************* * 函数名称:Rotate(LPSTRlt@span b=1> lpSrcDib,lt@span b=1> LPSTRlt@span b=1> lpSrcStartBits,longlt@span b=1> lWidth,lt@span b=1> longlt@span b=1> lHeight, long lLineBytes,WORD palSize, long lDstWidth, long lDstHeight,long lDstLineBytes,float fSina, float fCosa) * 函数参数: * LPSTR lpSrcDib 指向源DIB的指针 * LPSTR lpSrcStartBits 指向源DIB的起始像素的指针 * long lWidth 源DIB图像宽度 * long lHeight 源DIB图像高度 *lt@span b=1> lt@span b=1> lt@span b=1> lt@span b=1> longlt@span b=1> lLineByteslt@span b=1> lt@span b=1> lt@span b=1> lt@span b=1> lt@span b=1> lt@span b=1> lt@span b=1> lt@span b=1> lt@span b=1> lt@span b=1> lt@span b=1> lt@span b=1> lt@span b=1> lt@span b=1> lt@span b=1> lt@span b=1> lt@span b=1> lt@span b=1> lt@span b=1> lt@span b=1> lt@span b=1> lt@span b=1> lt@span b=1> lt@span b=1> lt@span b=1> lt@span b=1> lt@span b=1> lt@span b=1> lt@span b=1> lt@span b=1> lt@span b=1> lt@span b=1> lt@span b=1> 源DIB图像字节宽度(4的倍数) * WORD palSize 源DIB图像调色板大小 * long lDstWidth 目标图像宽度 * long lDstHeight 目标DIB图像高度 * long lDstLineBytes 目标DIB图像行字节数(4的倍数) * float fSina 旋转角的余弦,为了避免两次求取正余弦,这里作为两个函数参数来用 * float fCosa 旋转角的正弦 * 函数功能:用来旋转DIB图像 ************************************************************************/ HGLOBAL Rotate(LPSTR lpSrcDib, LPSTR lpSrcStartBits,long lWidth, long lHeight, long lLineBytes, WORD palSize, long lDstWidth, long lDstHeight,long lDstLineBytes,float fSina, float fCosa) { float varFloat1; // 浮点参数变量1 float varFloat2; // 浮点参数变量2 LPSTR lpDstDib; // 指向临时图像的指针 long i; // 行循环变量 long j; // 列循环变量 long i1; // 行循环变量 long j1; // 列循环变量 LPSTR lpSrcDIBBits; // 指向源像素的指针 LPSTR lpDstDIBBits; // 指向临时图像对应像素的指针 LPSTR lpDstStartBits; // 指向临时图像对应像素的指针 LPNITMAPINEFOHEADER lpbmi; // 指向BITMAPINFOHEADER结构的指针 // 将经常用到的两个常数事先求出,以便作为常数使用 varFloat1= (float) (-0.5 * (lDstWidth -1) * fCosa -0.5 * (lDstHeight -1) * fSina + 0.5 * (lDstWidth -1)); varFloat2= (float) ( 0.5 * (lDstWidth -1) * fSina -0.5 * (lDstHeight -1) * fCosa + 0.5 * (lDstHeight -1)); HGLOBAL hDIB = (HGLOBAL) ::GlobalAlloc(GHND, lDstLineBytes * lDstHeight + * (LPDWORD)lpSrcDib +palSize); // 分配内存,以保存新DIB if (hDIB == NULL) // 判断是否是有效的DIB对象 return FALSE; // 不是,则返回 lpDstDib= (char * )::GlobalLock((HGLOBAL) hDIB); // 锁定内存 memcpy(lpDstDib,lpSrcDib, *(LPDWORD)lpSrcDib +palSize); // 复制DIB信息头和调色板 lpbmi = (LPBITMAPINFOHEADER)lpDstDib; // 获取指针 lpbmi->biHeight=lDstHeight; // 更新DIB中图像的高度和宽度 lpbmi->biWidth =lDstWidth; // 求像素起始位置,作用如同::FindDIBBits(gCo.lpSrcDib),这里尝试使用了这种方法,以 // 避免对全局函数的调用 lpDstStartBits=lpDstDib+ *(LPDWORD)lpDstDib +palSize; for(i = 0; i < lDstHeight; i++) // 行操作 { for(j = 0; j < lDstWidth; j++) // 列操作 { // 指向新DIB第i行、第j个像素的指针 lpDstDIBBits= (char *)lpDstStartBits+ lDstLineBytes * (lDstHeight -1- i) + j; // 计算该像素在源DIB中的坐标 i1= (long) (-((float) j) * fSina + ((float) i) * fCosa + varFloat2 + 0.5); j1= (long) ( ((float) j) * fCosa + ((float) i) * fSina + varFloat1 + 0.5); if( (j1>= 0) && (j1< lWidth) && (i1>= 0) && (i1< lHeight)) // 判断是否在源图内 { // 指向源DIB第i1行、第j1个像素的指针 lpSrcDIBBits= (char *)lpSrcStartBits+ lLineBytes * (lHeight -1-i1) + j1; *lpDstDIBBits= *lpSrcDIBBits; // 复制像素 } else { * ((unsigned char*)lpDstDIBBits) = 255; // 源图中不存在的像素,赋值为255 } } } return hDIB; }
可以通过在数字图像处理程序的框架窗口上增加菜单【几何变换】|【图像旋转】项。对应的处理函数是视图类中的CDImageProcessView:: OnRotation ( )。在进行函数的调用时,可以采用对话框的形式对旋转角度进行设定,如图2-5所示。
图2-5 【旋转参数】对话框
具体代码如下:
void CDImageProcessView::OnRotation() { CDImageProcessDoc* pDoc = GetDocument(); longlt@span b=1> lt@span b=1> lt@span b=1> lt@span b=1> lt@span b=1> lt@span b=1> lSrcLineBytes;lt@span b=1> lt@span b=1> lt@span b=1> lt@span b=1> lt@span b=1> lt@span b=1> lt@span b=1> lt@span b=1> lt@span b=1> lt@span b=1> lt@span b=1> lt@span b=1> // 图像每行的字节数 long lSrcWidth; // 图像的宽度 long lSrcHeight; // 图像的高度 LPSTR lpSrcDib; // 指向源图像的指针 LPSTR lpSrcStartBits; // 指向源像素的指针 long lDstWidth; // 临时图像的宽度和高度 long lDstHeight; lpSrcDib= (LPSTR) ::GlobalLock((HGLOBAL) pDoc->GetHObject()); // 锁定DIB if (pDoc->m_dib.GetColorNum(lpSrcDib) != 256)//判断是否是8-bpp位图,不是则返回 { AfxMessageBox (_T("对不起,不是色位图!")); // 警告 ::GlobalUnlock((HGLOBAL) pDoc->GetHObject()); // 解除锁定 return; // 返回 } lpSrcStartBits=pDoc->m_dib.GetBits(lpSrcDib); // 找到DIB图像像素起始位置 lSrcWidth= pDoc->m_dib.GetWidth(lpSrcDib); // 获取图像的宽度 lSrcHeight= pDoc->m_dib.GetHeight(lpSrcDib); // 获取图像的高度 lSrcLineBytes=pDoc->m_dib.GetReqByteWidth(lSrcWidth * 8); // 计算图像每行的字节数 long lDstLineBytes; CDlgRot RotPara; // 创建对话框 if(RotPara.DoModal() != IDOK) // 显示对话框,设定旋转角度 return; DWORD palSize=pDoc->m_dib.GetPalSize(lpSrcDib); // 将旋转角度从度转换到弧度 float fRotateAngle = (float) AngleToRadian(RotPara.m_rotAngle); float fSina = (float) sin((double)fRotateAngle); // 计算旋转角度的正余弦 float fCosa = (float) cos((double)fRotateAngle); // 旋转前4个角的坐标(以图像中心为坐标系原点) float fSrcX1,fSrcY1,fSrcX2,fSrcY2,fSrcX3,fSrcY3,fSrcX4,fSrcY4; // 旋转后4个角的坐标(以图像中心为坐标系原点) float fDstX1,fDstY1,fDstX2,fDstY2,fDstX3,fDstY3,fDstX4,fDstY4; fSrcX1 = (float) (- (lSrcWidth -1) / 2); // 计算源图的4个角的坐标 fSrcY1 = (float) ( (lSrcHeight -1) / 2); fSrcX2 = (float) ( (lSrcWidth -1) / 2); fSrcY2 = (float) ( (lSrcHeight -1) / 2); fSrcX3 = (float) (- (lSrcWidth -1) / 2); fSrcY3 = (float) (- (lSrcHeight -1) / 2); fSrcX4 = (float) ( (lSrcWidth -1) / 2); fSrcY4 = (float) (- (lSrcHeight -1) / 2); fDstX1 = fCosa * fSrcX1 + fSina * fSrcY1; // 计算新图4个角的坐标 fDstY1 = -fSina * fSrcX1 + fCosa * fSrcY1; fDstX2 = fCosa * fSrcX2 + fSina * fSrcY2; fDstY2 = -fSina * fSrcX2 + fCosa * fSrcY2; fDstX3 = fCosa * fSrcX3 + fSina * fSrcY3; fDstY3 = -fSina * fSrcX3 + fCosa * fSrcY3; fDstX4 = fCosa * fSrcX4 + fSina * fSrcY4; fDstY4 = -fSina * fSrcX4 + fCosa * fSrcY4; // 计算旋转后的图像实际宽度 lDstWidth= (long) ( max( fabs(fDstX4- fDstX1), fabs(fDstX3- fDstX2) ) + 0.5); // 计算新图像每行的字节数 lDstLineBytes=pDoc->m_dib.GetReqByteWidth(lDstWidth * 8); // 计算旋转后的图像高度 lDstHeight= (long) ( max( fabs(fDstY4- fDstY1), fabs(fDstY3- fDstY2) ) + 0.5); HGLOBAL hDstDIB = NULL; // 创建新DIB // 调用Rotate()函数旋转DIB hDstDIB = (HGLOBAL) Rotate(lpSrcDib,lpSrcStartBits,lSrcWidth, lSrcHeight,lSrcLineBytes, palSize,lDstWidth,lDstHeight,lDstLineBytes,fSina,fCosa); if(hDstDIB != NULL) // 判断旋转是否成功 { pDoc->UpdateObject(hDstDIB); // 替换DIB,同时释放旧DIB对象 pDoc->SetDib(); // 更新DIB大小和调色板 pDoc->SetModifiedFlag(TRUE); // 设置修改标记 pDoc->UpdateAllViews(NULL); // 更新视图 ::GlobalUnlock((HGLOBAL) pDoc->GetHObject());// 解除锁定 } else { AfxMessageBox(_T("分配内存失败!")); // 警告 } }
4. 效果演示
源图像如图2-6a所示,对其进行旋转后,结果如图2-6b所示。其中,参数设定见图2-5。
图2-6 图像旋转效果演示
2.1.3 图像镜像
镜像是两个物体关于中轴线对称的一种状态。图像的镜像分为两种:一种是水平镜像,另外一种是垂直镜像。图像的水平镜像操作是将图像左半部分和右半部分以图像垂直中轴线为中心进行镜像对换;图像的垂直镜像操作是将图像上半部分和下半部分以图像水平中轴线为中心进行镜像对换。
1. 基本原理
图像镜像的原理比较简单,若源图像中某一像素点的坐标为(x0, y0),容易得出,该点关于y轴的水平镜像的点的坐标为(-x0, y0),该点关于x轴的垂直镜像的点的坐标为(x0,-y0)。因此,在屏幕坐标系中,设源图像宽度为lWidth,高度为lHeight,源图像中的点(x0, y0)经过水平镜像后坐标为(lWidth-(x0, y0)),即
其矩阵表达式为
同理,对于垂直镜像
其矩阵表达式为
2. 算法描述
实现数字图像在屏幕上的镜像,可以分为以下4个步骤:
[1] 将源图像保存到缓冲区,并记录下缓冲区的地址。
[2] 分配内存,以保存镜像后的图像。
[3] 确定图像的镜像方式,是水平镜像还是垂直镜像。
[4] 根据设定的镜像方式及源图中每个像素点的坐标值,计算出镜像后各像素点的新坐标值,实现图像的镜像。
3. 编程实现
下面通过编写VC的函数Mirror ( )来实现数字图像的水平镜像,其具体代码如下:
/************************************************************************* * 函数名称:Mirror(LPSTR lpSrcStartBits, long lWidth, long lHeight,long lLineBytes) * 函数参数: LPSTR lpSrcStartBits 指向DIB起始像素的指针 long lWidth DIB图像的宽度 long lHeight DIB图像的高度 long lLineBytes 图像的行字节数,为4的倍数 * 函数功能:该函数用来水平镜像DIB图像 ************************************************************************/ BOOL Mirror(LPSTR lpSrcStartBits, long lWidth, long lHeight,long lLineBytes) { long i; // 行循环变量 long j; // 列循环变量 LPSTR lpSrcDIBBits; // 指向源像素的指针 LPSTR lpDstDIBBits; // 指向临时图像对应像素的指针 HLOCAL hDstDIBBits; // 临时图像句柄 LPSTR lpBits; // 指向中间像素的指针,当复制图像时,提供临时的像素内存空间 hDstDIBBits= LocalAlloc(LHND, lLineBytes); // 分配临时内存保存行图像 if (hDstDIBBits == NULL) return FALSE; // 分配内存失败 lpDstDIBBits= (char * )LocalLock(hDstDIBBits); // 锁定 for(i = 0; i < lHeight; i++) // 水平镜像,针对图像每行进行操作 { for(j = 0; j < lWidth / 2; j++) // 针对每行图像左半部分进行操作 { // 指向倒数第i行、第j个像素的指针 lpSrcDIBBits= (char *)lpSrcStartBits + lLineBytes * i + j; // 指向倒数第i+1行、倒数第j个像素的指针 lpBits= (char *)lpSrcStartBits + lLineBytes * (i + 1) - j; *lpDstDIBBits=*lpBits; // 保存中间像素 // 将倒数第i行、第j个像素复制到倒数第i行、倒数第j个像素 *lpBits = *lpSrcDIBBits; // 将倒数第i行、倒数第j个像素复制到倒数第i行、第j个像素 *lpSrcDIBBits=*lpDstDIBBits; } } LocalUnlock(hDstDIBBits);lt@span b=1> lt@span b=1> lt@span b=1> lt@span b=1> lt@span b=1> lt@span b=1> lt@span b=1> lt@span b=1> lt@span b=1> lt@span b=1> lt@span b=1> lt@span b=1> lt@span b=1> lt@span b=1> lt@span b=1> lt@span b=1> lt@span b=1> lt@span b=1> // 释放内存 LocalFree(hDstDIBBits); return TRUE; }
通过编写VC的函数Mirror2 ( )来实现数字图像的垂直镜像,其具体代码如下:
/************************************************************************* * 函数名称:Mirror2(LPSTR lpSrcStartBits, long lWidth, long lHeight,long lLineBytes) * 函数参数: LPSTR lpSrcStartBits 指向DIB起始像素的指针 long lWidth DIB图像的宽度 long lHeight DIB图像的高度 long lLineBytes DIB图像的行字节数,为4的倍数 * 函数功能:该函数用来垂直镜像DIB图像 ************************************************************************/ BOOL Mirror2(LPSTR lpSrcStartBits, long lWidth, long lHeight,long lLineBytes) { long i; // 行循环变量 LPSTR lpSrcDIBBits; // 指向源像素的指针 LPSTR lpDstDIBBits; // 指向临时图像对应像素的指针 HLOCAL hDstDIBBits; // 临时图像句柄 LPSTR lpBits; // 指向中间像素的指针,当复制图像时,提供临时的像素内存空间 hDstDIBBits= LocalAlloc(LHND, lLineBytes); // 分配临时内存保存行图像 if (hDstDIBBits == NULL) return FALSE; // 分配内存失败 lpDstDIBBits= (char * )LocalLock(hDstDIBBits); // 锁定 for(i = 0; i < lHeight / 2; i++) // 垂直镜像,针对图像每行进行操作 { // 指向倒数第i行的指针 lpSrcDIBBits= (char *)lpSrcStartBits + lLineBytes * i ; // 指向倒数第i+1行的指针 lpBits= (char *)lpSrcStartBits + lLineBytes * (lHeight - i + 1); memcpy(lpDstDIBBits, lpBits, lLineBytes); memcpy(lpBits, lpSrcDIBBits, lLineBytes); memcpy(lpSrcDIBBits, lpDstDIBBits, lLineBytes); } LocalUnlock(hDstDIBBits); // 释放内存 LocalFree(hDstDIBBits); return TRUE; }
可以通过在数字图像处理程序的框架窗口上增加菜单【几何变换】|【图像镜像】|【水平】和【几何变换】|【图像镜像】|【垂直】项,如图2-7所示。分别对视图类中的CDImage Process View:: OnMirror ( )和CDImageProcessView:: OnMirror2 ( )进行函数的调用。
图2-7 图像镜像的设定
图像的水平镜像的代码如下:
void CDImageProcessView::OnMirror() { CDImageProcessDoc* pDoc = GetDocument(); // 获取文档 long lSrcLineBytes; // 图像每行的字节数 long lSrcWidth; // 图像的宽度 long lSrcHeight; // 图像的高度 LPSTR lpSrcDib; // 指向源图像的指针 LPSTR lpSrcStartBits; // 指向源像素的指针 lpSrcDib= (LPSTR) ::GlobalLock((HGLOBAL) pDoc->GetHObject()); // 锁定DIB if (pDoc->m_dib.GetColorNum(lpSrcDib) != 256)// 判断是否是8-bpp位图 { AfxMessageBox(_T ("对不起,不是256色位图!")); // 警告 ::GlobalUnlock((HGLOBAL) pDoc->GetHObject()); // 解除锁定 return; // 返回 } // 判断是否是8-bpp位图,不是则返回 lpSrcStartBits=pDoc->m_dib.GetBits(lpSrcDib);// 找到DIB图像像素起始位置 lSrcWidth= pDoc->m_dib.GetWidth(lpSrcDib); // 获取图像的宽度 lSrcHeight= pDoc->m_dib.GetHeight(lpSrcDib); // 获取图像的高度 lSrcLineBytes=pDoc->m_dib.GetReqByteWidth(lSrcWidth * 8); // 计算图像每行的字节数 // 调用Mirror()函数水平镜像DIB if (Mirror(lpSrcStartBits,lSrcWidth, lSrcHeight,lSrcLineBytes)) { pDoc->SetModifiedFlag(TRUE); // 设置修改标记 pDoc->UpdateAllViews(NULL); // 更新视图 ::GlobalUnlock((HGLOBAL) pDoc->GetHObject()); // 解除锁定 } else { AfxMessageBox(_T("分配内存失败!")); // 警告 } }
图像的垂直镜像的代码如下:
void CDImageProcessView::OnMirror2() { CDImageProcessDoc* pDoc = GetDocument(); // 获取文档 long lSrcLineBytes; // 图像每行的字节数 long lSrcWidth; // 图像的宽度 long lSrcHeight; // 图像的高度 LPSTR lpSrcDib; // 指向源图像的指针 LPSTR lpSrcStartBits; // 指向源像素的指针 lpSrcDib= (LPSTR) ::GlobalLock((HGLOBAL) pDoc->GetHObject()); // 锁定DIB if (pDoc->m_dib.GetColorNum(lpSrcDib) != 256) { AfxMessageBox(_T ("对不起,不是256色位图!")); ::GlobalUnlock((HGLOBAL) pDoc->GetHObject()); return; } lpSrcStartBits=pDoc->m_dib.GetBits(lpSrcDib);// 找到DIB图像像素起始位置 lSrcWidth= pDoc->m_dib.GetWidth(lpSrcDib); // 获取图像的宽度 lSrcHeight= pDoc->m_dib.GetHeight(lpSrcDib); // 获取图像的高度 lSrcLineBytes=pDoc->m_dib.GetReqByteWidth(lSrcWidth * 8); // 计算图像每行的字节数 // 调用Mirror2()函数垂直镜像DIB if (Mirror2(lpSrcStartBits,lSrcWidth, lSrcHeight,lSrcLineBytes)) { pDoc->SetModifiedFlag(TRUE); pDoc->UpdateAllViews(NULL); ::GlobalUnlock((HGLOBAL) pDoc->GetHObject()); } else { AfxMessageBox(_T("分配内存失败!")); } }
4. 效果演示
源图像如图2-8a所示,对其进行水平镜像,结果如图2-8b所示。
图2-8 水平镜像效果演示
源图像如图2-9a所示,对其进行垂直镜像,结果如图2-9b所示。
图2-9 垂直镜像效果演示
2.1.4 图像转置
图像的转置是将图像像素的x坐标和y坐标互换。该操作将改变图像的高度和宽度,转置后图像的高度和宽度将互换。
1. 基本原理
图像转置是一种较简单的几何变换,设源图像中的某个像素点的坐标为(x0, y0),其转置后对应新图上的像素点坐标为(x1, y1),则二者的关系如下:
2. 算法描述
实现数字图像在屏幕上的转置,可以分为以下3个步骤:
[1] 将源图像保存到缓冲区,并记录下缓冲区的地址。
[2] 分配内存,以保存转置后的图像。
[3] 根据源图中每个像素点的坐标值,计算出转置后各像素点的新坐标值,实现图像的转置。
3. 编程实现
下面通过编写VC的函数Transpose ( )来实现数字图像的转置,其具体代码如下:
/************************************************************************* * 函数名称:Transpose(LPSTR lpSrcDib,LPSTR lpDibBits,long lWidth,long lHeight, long lLineBytes,long lDstLineBytes) * 函数参数: * LPSTR lpSrcDib 指向源DIB的指针 LPSTR lpSrcStartBits 指向DIB起始像素的指针 long lWidth DIB图像的宽度 long lHeight DIB图像的高度 long lLineBytes DIB图像的行字节数,为4的倍数 long lDstLineBytes 临时DIB图像的行字节数,为4的倍数 * 函数功能:该函数用来转置DIB图像 ************************************************************************/ BOOL Transpose(LPSTR lpSrcDib,LPSTR lpSrcStartBits,long lWidth,long lHeight, long lLineBytes,long lDstLineBytes) { long i; // 行循环变量 long j; // 列循环变量 LPSTR lpSrcDIBBits; // 指向源像素的指针 LPSTR lpDstDIBBits; // 指向临时图像对应像素的指针 LPSTR lpDstStartBits; // 指向临时图像对应像素的指针 HLOCAL hDstDIBBits; // 临时图像句柄 LPBITMAPINFOHEADER lpbmi; // 指向BITMAPINFOHEADER结构的指针 lpbmi = (LPBITMAPINFOHEADER)lpSrcDib; hDstDIBBits= LocalAlloc(LHND, lWidth * lDstLineBytes); // 分配临时内存 if (hDstDIBBits== NULL) // 判断是否内存分配 return FALSE; // 分配内存失败 lpDstStartBits= (char * )LocalLock(hDstDIBBits); // 锁定内存 for(i = 0; i < lHeight; i++) // 针对图像每行进行操作 { for(j = 0; j < lWidth; j++) // 针对每行图像每列进行操作 { // 指向源DIB第i行、第j个像素的指针 lpSrcDIBBits= (char *)lpSrcStartBits + lLineBytes * (lHeight -1- i) + j; // 指向转置DIB第j行、第i个像素的指针 lpDstDIBBits= (char *)lpDstStartBits + lDstLineBytes * (lWidth -1- j) + i; *(lpDstDIBBits)= *(lpSrcDIBBits); // 复制像素 } } memcpy(lpSrcStartBits, lpDstStartBits, lWidth * lDstLineBytes); // 复制转置后的图像 lpbmi->biWidth = lHeight; lpbmi->biHeight = lWidth; LocalUnlock(hDstDIBBits); // 释放内存 LocalFree(hDstDIBBits); return TRUE; }
可以通过在数字图像处理程序的框架窗口上增加菜单【几何变换】|【图像转置】项。用对应视图类中的CDImageProcessView:: OnTranspose ( )进行函数的调用。
void CDImageProcessView::OnTranspose() { CDImageProcessDoc* pDoc = GetDocument(); long lSrcLineBytes; // 图像每行的字节数 long lSrcWidth; // 图像的宽度 long lSrcHeight; // 图像的高度 LPSTR lpSrcDib; // 指向源图像的指针 LPSTR lpSrcStartBits; // 指向源像素的指针 long lDstLineBytes; // 新图像每行的字节数 lpSrcDib= (LPSTR) ::GlobalLock((HGLOBAL) pDoc->GetHObject()); // 锁定DIB if (pDoc->m_dib.GetColorNum(lpSrcDib) != 256) { AfxMessageBox(_T ("对不起,不是256色位图!")); ::GlobalUnlock((HGLOBAL) pDoc->GetHObject()); return; } lpSrcStartBits=pDoc->m_dib.GetBits(lpSrcDib); // 找到DIB图像像素起始位置 lSrcWidth= pDoc->m_dib.GetWidth(lpSrcDib); // 获取图像的宽度 lSrcHeight= pDoc->m_dib.GetHeight(lpSrcDib); // 获取图像的高度 lSrcLineBytes=pDoc->m_dib.GetReqByteWidth(lSrcWidth * 8); //计算图像每行的字节数 lDstLineBytes=pDoc->m_dib.GetReqByteWidth(lSrcHeight * 8); //计算新图像每行的字节数 if (Transpose(lpSrcDib,lpSrcStartBits,lSrcWidth, lSrcHeight,lSrcLineBytes,lDstLineBytes)) // 调用Transpose()函数转置DIB { pDoc->SetDib(); // 更新DIB大小和调色板 pDoc->SetModifiedFlag(TRUE); // 设置修改标记 pDoc->UpdateAllViews(NULL); // 更新视图 ::GlobalUnlock((HGLOBAL) pDoc->GetHObject()); // 解除锁定 } else { AfxMessageBox(_T("分配内存失败!")); // 警告 } }
4. 效果演示
源图像如图2-10a所示,对其进行水平转置,结果如图2-10b所示。
图2-10 图像水平转置效果演示
2.2 图像尺度变换
图像的尺度变换包括图像缩放及插值算法等技术,是数字图像几何变换中比较复杂的操作,不同的算法对最终得到的图像质量影响较大。
2.2.1 图像缩放
图像缩放是根据需要改变图像的大小尺寸,使图像按照一定的比例缩小或放大。图像缩放是一种常用的、相对复杂的图像几何变换技术,因为缩放后产生的新图像的像素,很有可能在源图像中找不到与之相对应的像素点,只能用插值的方法近似进行处理。通常采用两种插值的方法:一种是最近邻插值法,另一种是线性插值法。关于插值的方法,将在2.2.2节中讲解。
1. 基本原理
设源图像在水平方向和垂直方向的缩放比率分别为ZoomX、ZoomY,源图像中某个像素点的坐标为(x0, y0),经缩放后的坐标为(x1, y1),则有
即
其逆变换为
所以有
当ZoomX、ZoomY均大于1时,图像被放大。新图像中的某些像素,对应源图像中的像素可能是实际不存在的。例如,在ZoomX、ZoomY均为2时,新图像中的像素(0,1)对应源图像中的像素(0,0.5)是不存在的。此时,只能采取插值法,从源图像中近似找到或者计算某个像素值赋给新图像中的对应像素。
当ZoomX、ZoomY均小于1时,图像被缩小,源图像中的某些像素可能被舍弃。例如,在ZoomX、ZoomY均为0.5时,新图像中的像素点对应源图像中像素只能每行中相邻两个像素取一个,每隔一行取一行。
2. 算法描述
实现数字图像在屏幕上的缩放,可以分为以下5个步骤:
[1] 将源图像保存到缓冲区,并记录下缓冲区的地址。
[2] 采用某种交互方式(如对话框),分别设定图像水平方向和垂直方向的缩放比率。
[3] 获取源图像的高度和宽度,计算出缩放后新图像的高度和宽度。
[4] 分配内存,以保存缩放后的图像。
[5] 根据步骤[2]中设定的缩放比率及源图中每个像素点的坐标值,结合图像放大和缩小的像素取值原则,获得缩放后各像素点的新坐标值,实现图像的缩放,根据新图像高度和宽度,不显示已经超出范围的图像。
3. 编程实现
下面通过编写VC的函数Zoom ( )来实现数字图像的缩放,其具体代码如下:
/************************************************************************* * 函数名称:Zoom(LPSTR lpSrcDib, LPSTR lpSrcStartBits,long lWidth, long lHeight, long lLineBytes,WORD palSize, long lDstWidth, long lDstHeight, long lDstLineBytes,float fXZoomRatio, float fYZoomRatio) * 函数参数: * LPSTR lpSrcDib 指向源DIB的指针 * LPSTR lpSrcStartBits 指向源DIB的起始像素的指针 * long lWidth 源DIB图像宽度 * long lHeight 源DIB图像高度 * long lLineBytes 源DIB图像字节宽度(4的倍数) * WORD palSize 源DIB图像调色板大小 * long lDstWidth 目标图像宽度 * long lDstHeight 目标DIB图像高度 * long lDstLineBytes 目标DIB图像行字节数(4的倍数) * float fhorRatio 水平缩放比率 * float fverRatio 垂直缩放比率 * 函数功能:用来缩放DIB图像 ************************************************************************/ HGLOBAL Zoom(LPSTR lpSrcDib, LPSTR lpSrcStartBits,long lWidth, long lHeight, long lLineBytes,WORD palSize,long lDstWidth,long lDstLineBytes,long lDstHeight, float fhorRatio,float fverRatio) { LPSTR lpDstDib; // 指向临时图像的指针 long i; // 行循环变量 long j; // 列循环变量 long i1; // 行循环变量 long j1; // 列循环变量 LPSTR lpSrcDIBBits; // 指向源像素的指针 LPSTR lpDstDIBBits; // 指向临时图像对应像素的指针 LPSTR lpDstStartBits; // 指向临时图像对应像素的指针 LPBITMAPINFOHEADER lpbmi; // 指向BITMAPINFO结构的指针 HGLOBAL hDIB = (HGLOBAL) ::GlobalAlloc(GHND, lDstLineBytes* lDstHeight + *(LPDWORD)lpSrcDib +palSize); // 分配内存,以保存缩放后的DIB if (hDIB == NULL) // 判断是否是有效的DIB对象 return FALSE; // 不是,则返回 lpDstDib= (char * )::GlobalLock((HGLOBAL) hDIB); // 锁定内存 memcpy(lpDstDib, lpSrcDib, *(LPDWORD)lpSrcDib +palSize); //复制DIB信息头和调色板 // 找到新DIB像素起始位置,求像素起始位置,作用如同::FindDIBBits(lpSrcDib),这里尝 // 试使用了这种方法,以避免对全局函数的调用 lpDstStartBits=lpDstDib+ *(LPDWORD)lpDstDib +palSize; lpbmi = (LPBITMAPINFOHEADER)lpDstDib; // 获取指针 lpbmi->biWidth = lDstWidth; // 更新DIB中图像的高度和宽度 lpbmi->biHeight =lDstHeight; for(i = 0; i < lDstHeight; i++) // 行操作 { for(j = 0; j < lDstWidth; j++) // 列操作 { // 指向新DIB第i行、第j个像素的指针 lpDstDIBBits= (char *)lpDstStartBits + lDstLineBytes * (lDstHeight-1-i)+j; i1= (long) (i / fverRatio + 0.5); // 计算该像素在源DIB中的坐标 j1= (long) (j / fhorRatio + 0.5); if( (j1>= 0) && (j1< lWidth) && (i1>= 0) && (i1< lHeight)) // 判断是否在源图内 { // 指向源DIB第i1行、第j1个像素的指针 lpSrcDIBBits= (char *)lpSrcStartBits+ lLineBytes * (lHeight -1-i1) + j1; *lpDstDIBBits= *lpSrcDIBBits; // 复制像素 } else { * ((unsigned char*)lpDstDIBBits) = 255; // 源图中不存在的像素,赋值为255 } } } return hDIB; }
可以通过在数字图像处理程序的框架窗口上增加菜单【几何变换】|【图像缩放】项。对应的处理函数是视图类中的CDImageProcessView:: OnZoom ( )。在进行函数的调用时,可以采用对话框的形式对缩放比率进行设定。具体代码如下:
void CDImageProcessView::OnZoom() { CDImageProcessDoc* pDoc = GetDocument(); long lSrcLineBytes; // 图像每行的字节数 long lSrcWidth; // 图像的宽度 long lSrcHeight; // 图像的高度 LPSTR lpSrcDib; // 指向源图像的指针 LPSTR lpSrcStartBits; // 指向源像素的指针 long lDstWidth; // 临时图像的宽度和高度 long lDstHeight; long lDstLineBytes; lpSrcDib= (LPSTR) ::GlobalLock((HGLOBAL) pDoc->GetHObject()); // 锁定DIB if (pDoc->m_dib.GetColorNum(lpSrcDib) != 256) { AfxMessageBox(_T ("对不起,不是256色位图!")); ::GlobalUnlock((HGLOBAL) pDoc->GetHObject()); return; } lpSrcStartBits=pDoc->m_dib.GetBits(lpSrcDib);// 找到DIB图像像素起始位置 lSrcWidth= pDoc->m_dib.GetWidth(lpSrcDib); // 获取图像的宽度 lSrcHeight= pDoc->m_dib.GetHeight(lpSrcDib); // 获取图像的高度 lSrcLineBytes=pDoc->m_dib.GetReqByteWidth(lSrcWidth * 8); //计算图像每行的字节数 DWORD palSize=pDoc->m_dib.GetPalSize(lpSrcDib); CDlgZoom ZoomPara; // 创建对话框,设定缩放比率 if (ZoomPara.DoModal() != IDOK) return; float fX = ZoomPara.m_horZoom; // 获取设定的缩放比率 float fY = ZoomPara.m_verZoom; // 计算缩放后的图像实际宽度,加0.5是由于强制类型转换时不四舍五入,而是直接截 // 去小数部分 lDstWidth= (long) (lSrcWidth*fX + 0.5); //转换后图像应有的行字节数,为4的倍数 lDstLineBytes=pDoc->m_dib.GetReqByteWidth(lDstWidth * 8); lDstHeight= (long) (lSrcHeight * fY + 0.5); // 计算缩放后的图像高度 HGLOBAL hDstDIB = NULL; // 创建新DIB // 调用Zoom( )函数 hDstDIB = (HGLOBAL) Zoom(lpSrcDib,lpSrcStartBits,lSrcWidth,lSrcHeight, lSrcLineBytes,palSize,lDstWidth,lDstLineBytes,lDstHeight,fX, fY); if(hDstDIB != NULL) // 判断缩放是否成功 { pDoc->UpdateObject(hDstDIB); // 替换DIB,同时释放旧DIB对象 pDoc->SetDib(); // 更新DIB大小和调色板 pDoc->SetModifiedFlag(TRUE); // 设置修改标记 pDoc->UpdateAllViews(NULL); // 更新视图 ::GlobalUnlock((HGLOBAL) pDoc->GetHObject()); // 解除锁定 } else { AfxMessageBox(_T("分配内存失败!"));// 警告 } }
4. 效果演示
源图像如图2-11a所示,缩放的效果如图2-11b所示。其中,设定水平方向的缩放比率为0.7,垂直方向的缩放比率为1.2。
图2-11 图像缩放效果演示
2.2.2 插值算法
数字图像的几何变换,尤其是在进行图像的缩放、旋转变换时,整个变换过程由两部分组成。首先,需要一种算法来完成几何变换本身,用它描述源(输入)图像每个像素如何从其初始位置变换到新(输出)图像对应的位置;同时,还需要一个用于灰度级插值的算法,因为在一般情况下,输入图像的位置坐标(x, y)为整数,而变换后输出图像的位置坐标为非整数,灰度值为空,反过来也是如此。因此,在进行图像的几何变换时,除了要进行其本身的几何变换外,还要进行灰度级插值处理,即需要这两个独立的算法来完成。
1. 基本原理
灰度级插值处理可采用如下两种方法。第一,把几何变换想象成将输入图像的灰度级像素逐个地转移到输出图像中。如果一个输入像素被映射到4个输出像素之间的位置,则其灰度值就按插值算法在4个输出像素之间进行分配。这种灰度级插值处理称为像素移交或向前映射法。
另一种更有效的灰度级插值处理方法是像素填充或向后映射算法。在这种算法中,输出像素逐个地映射回原始(输入)图像中,以确定其灰度级。如果一个输出像素被映射到4个输入像素之间,则其灰度值由灰度级插值决定。
下面介绍几种常用的插值算法。
(1)最近邻插值
最近邻插值是对于通过反向变换得到的一个浮点坐标,对其进行简单取整,得到一个整数型坐标,这个整数型坐标对应的像素值就是目标像素的像素值。简言之,取浮点坐标最邻近的左上角点(对于DIB是右上角,因为它的扫描行是逆序存储的)对应的像素值。最近邻插值法是一种最简单的插值方法,它思想直观,但得到的图像质量不高。
(2)双线性插值
双线性插值就是根据输出图像宽度和高度,将输入图像的宽度和高度均分,来确定输出图像的灰度值的方法。如图2-12所示,假设输出图像的宽度为lDstWidth,高度为lDstHeight,输入图像的宽度为lWidth,高度为lHeight,要将输入图像的尺度拉伸或压缩变换至输出图像的宽度方向分为lDstWidth等份,高度方向分为lDstHeight等份,那么输出图像中任意一点(x, y)的灰度值就应该由输入图像中4点(a, b)、(a+1, b)、(a, b+1)和(a+1, b+1)的灰度值来确定。
图2-12 双线性插值示意图
a和b的值分别为
则像素点(x, y)的灰度值f (x, y)应为
f (x, y) = (b + 1-y) f (x, b) + (y-b) f(x, b + 1)
其中
f (x, b + 1) =(x-a) f (a + 1, b + 1) + (a + 1-x) f(a, b + 1)
f (x, b) = (x-a) f (a + 1, b) + (a + 1-x) f(a, b )
以上是双线性内插值法。值得注意的是:这种方法缩放后图像质量高,不会出现像素值不连续的情况,但是计算量很大,而且由于双线性插值具有低通滤波器的性质,使高频分量受损,所以可能会使图像轮廓在一定程度上变得模糊。
(3)双三次插值法
双三次插值法又叫三次卷积法,它能够克服以上两种算法的不足,考虑一个浮点坐标(i + u, j+ v)周围16个邻点,计算精度高,计算量大,目标点的像素值f (i + u, j + v)可由以下插值公式得
f (i + u, j + v) = [A] [B] [C]
其中
这里
上式是对的逼近。)
最邻近插值法、双线性插值法及双三次插值法对于旋转变换、错切变换、一般线性变换和非线性变换都适用。
2. 算法描述
以图像几何变换中的图像旋转为例,如果输出图像中的某些像素在输入图像中找不到相对应的像素值,那么使用双线性插值的方法,计算出输出图像的像素值,实现数字图像在屏幕上的旋转。该算法可以分为以下6个步骤:
[1] 将源图像保存到缓冲区,并记录下缓冲区的地址。
[2] 获取源图像的高度和宽度,计算出源图像的中心点坐标(a0, b0)。以(a0, b0)为坐标原点,计算出源图像4个顶点的坐标。
[3] 采用某种交互方式(如对话框),获取图像的旋转角度。
[4] 分配内存,以保存旋转后的图像。
[5] 根据步骤[3]中设定的旋转角度及源图中每个像素点的坐标值,获得旋转后各像素点的新坐标值,实现图像的旋转。
[6] 对于输出图像中的某些像素在输入图像中找不到相对应的像素值,先计算出其4个最近邻点的像素值,再由双线性插值的方法,计算输出图像的像素值。
3. 编程实现
下面通过编写VC的函数RotateDIB2( )来完成使用双线性插值算法实现图像的旋转,其具体代码如下:
/************************************************************************* * 函数名称: RotateDIB2 * 参数: * LPSTR lpDIB 指向源DIB的指针 * int iRotateAngle 旋转的角度(0˚~360˚) * 返回值:HGLOBAL 旋转成功返回新DIB句柄,否则返回NULL * 说明: * 该函数用来以图像中心为中心旋转DIB图像,返回新生成DIB的句柄。 * 调用该函数会自动扩大图像以显示所有的像素。函数中采用双线性插值算法进行插值。 ************************************************************************/ HGLOBAL RotateDIB2(LPSTR lpSrcDib, float fRotateAngle,LPSTR lpSrcStartBits, long lWidth, long lHeight,WORD palSize) { LONG lNewWidth; // 旋转后图像的宽度 LONG lNewHeight; // 旋转后图像的高度 LONG lNewLineBytes; // 旋转后图像的宽度(lNewWidth必须是4的倍数) LPSTR lpDIBBits; // 指向源图像的指针 HGLOBAL hDIB; // 旋转后新DIB句柄 LPSTR lpDst; // 指向旋转图像对应像素的指针 LPSTR lpNewDIB; // 指向旋转图像的指针 LPSTR lpNewDIBBits; LPBITMAPINFOHEADER lpbmi; // 指向BITMAPINFO结构的指针 LPBITMAPCOREHEADER lpbmc; // 指向BITMAPCOREINFO结构的指针 LONG i; // 循环变量(像素在新DIB中的坐标) LONG j; FLOAT i0; // 像素在源DIB中的坐标 FLOAT j0; float fSina, fCosa; // 旋转角度的正弦和余弦 // 源图4个角的坐标(以图像中心为坐标系原点) float fSrcX1,fSrcY1,fSrcX2,fSrcY2,fSrcX3,fSrcY3,fSrcX4,fSrcY4; // 旋转后4个角的坐标(以图像中心为坐标系原点) float fDstX1,fDstY1,fDstX2,fDstY2,fDstX3,fDstY3,fDstX4,fDstY4; float f1,f2; lpDIBBits = lpSrcStartBits; // 找到源DIB图像像素起始位置 fSinalt@span b=1> =lt@span b=1> (float)lt@span b=1> sin((double)fRotateAngle);lt@span b=1> lt@span b=1> lt@span b=1> lt@span b=1> lt@span b=1> lt@span b=1> lt@span b=1> lt@span b=1> lt@span b=1> // 计算旋转角度的正弦 fCosa = (float) cos((double)fRotateAngle); // 计算旋转角度的余弦 // 计算原图的4个角的坐标(以图像中心为坐标系原点) fSrcX1 = (float) (- (lWidth -1) / 2); fSrcY1 = (float) ( (lHeight -1) / 2); fSrcX2 = (float) ( (lWidth -1) / 2); fSrcY2 = (float) ( (lHeight -1) / 2); fSrcX3 = (float) (- (lWidth -1) / 2); fSrcY3 = (float) (- (lHeight -1) / 2); fSrcX4 = (float) ( (lWidth -1) / 2); fSrcY4 = (float) (- (lHeight -1) / 2); // 计算新图4个角的坐标(以图像中心为坐标系原点) fDstX1 = fCosa * fSrcX1 + fSina * fSrcY1; fDstY1 = -fSina * fSrcX1 + fCosa * fSrcY1; fDstX2 = fCosa * fSrcX2 + fSina * fSrcY2; fDstY2 = -fSina * fSrcX2 + fCosa * fSrcY2; fDstX3 = fCosa * fSrcX3 + fSina * fSrcY3; fDstY3 = -fSina * fSrcX3 + fCosa * fSrcY3; fDstX4 = fCosa * fSrcX4 + fSina * fSrcY4; fDstY4 = -fSina * fSrcX4 + fCosa * fSrcY4; // 计算旋转后的图像实际宽度 lNewWidth = (LONG)(max(fabs(fDstX4- fDstX1), fabs(fDstX3- fDstX2)) + 0.5); lNewLineBytes = WIDTHBYTES(lNewWidth * 8); // 计算旋转后的图像实际高度 lNewHeight = (LONG)(max(fabs(fDstY4- fDstY1), fabs(fDstY3- fDstY2)) + 0.5); f1 = (float) (-0.5 * (lNewWidth -1) * fCosa -0.5 * (lNewHeight -1) * fSina + 0.5 * (lWidth -1)); f2 = (float) ( 0.5 * (lNewWidth -1) * fSina -0.5 * (lNewHeight -1) * fCosa + 0.5 * (lHeight -1)); // 分配内存,以保存新DIB hDIB = (HGLOBAL) ::GlobalAlloc(GHND, lNewLineBytes * lNewHeight + *(LPDWORD)lpSrcDib + palSize); if (hDIB == NULL) // 判断内存分配是否成功 return NULL; lpNewDIB = (char * )::GlobalLock((HGLOBAL) hDIB); // 复制DIB信息头和调色板 memcpy(lpNewDIB, lpSrcDib, *(LPDWORD)lpSrcDib + palSize); // 找到新DIB像素起始位置 lpNewDIBBits = lpNewDIB+ *(LPDWORD)lpNewDIB +palSize;;//FindDIBBits(lpNewDIB); lpbmi = (LPBITMAPINFOHEADER)lpNewDIB; lpbmc = (LPBITMAPCOREHEADER)lpNewDIB; lpbmi->biWidth = lNewWidth; lpbmi->biHeight = lNewHeight; for(i = 0; i < lNewHeight; i++) // 针对图像每行进行操作 { for(j = 0; j < lNewWidth; j++) // 针对图像每列进行操作 { // 指向新DIB第i行、第j个像素的指针,注意此处宽度和高度是新DIB的宽度和高度 lpDst = (char *)lpNewDIBBits + lNewLineBytes * (lNewHeight -1- i) + j; // 计算该像素在源DIB中的坐标 i0 = -((float) j) * fSina + ((float) i) * fCosa + f2; j0 = ((float) j) * fCosa + ((float) i) * fSina + f1; // 利用双线性插值算法来估算像素值 *lpDst = Interpolation (lpDIBBits, lWidth, lHeight, j0, i0); } } return hDIB; }
使用Interpolation ( )函数利用双线性插值算法估算像素值,其具体代码如下:
/************************************************************************* * 函数名称:Interpolation * 参数: * LPSTR lpDIBBits 指向源DIB图像指针 * LONG lWidth 源图像宽度(像素数) * LONG lHeight 源图像高度(像素数) * FLOAT x 插值元素的x坐标 * FLOAT y 插值元素的y坐标 *lt@span b=1> lt@span b=1> 返回值: * unsigned char 返回插值计算结果 * 说明: * 该函数利用双线性插值算法来估算像素值。对于超出图像范围的像素,直接返回255。 ************************************************************************/ unsigned char Interpolation (LPSTR lpDIBBits, LONG lWidth, LONG lHeight, FLOAT x, FLOAT y) { // 4个最临近像素的坐标(i1, j1), (i2, j1), (i1, j2), (i2, j2) LONG i1, i2; LONG j1, j2; unsigned char f1, f2, f3, f4; // 4个最临近像素值 unsigned char f12, f34; // 2个插值中间值 FLOAT EXP; // 定义一个值,当像素坐标相差小于该值时认为坐标相同 LONG lLineBytes; // 图像每行的字节数 lLineBytes = WIDTHBYTES(lWidth * 8); EXP = (FLOAT) 0.0001; // 计算4个最临近像素的坐标 i1 = (LONG) x; i2 = i1 + 1; j1 = (LONG) y; j2 = j1 + 1; // 根据不同情况分别处理 if( (x < 0)||(x>lWidth -1)||(y < 0)||(y>lHeight -1)) { return 255; // 要计算的点不在源图范围内,直接返回255。 } else { if (fabs(x - lWidth + 1) <= EXP) { if (fabs(y - lHeight + 1) <= EXP) // 要计算的点在图像右边缘上 { // 要计算的点正好是图像最右下角的那个像素,直接返回该点像素值 f1 = *((unsigned char *)lpDIBBits + lLineBytes * (lHeight -1- j1) + i1); return f1; } else { // 在图像右边缘上且不是最后一点,直接一次插值即可 f1 = *((unsigned char *)lpDIBBits + lLineBytes * (lHeight -1-j1)+ i1); f3 = *((unsigned char *)lpDIBBits + lLineBytes * (lHeight -1-j1)+i2); return ((unsigned char) (f1 + (y -j1) * (f3- f1))); // 返回插值结果 } } else if (fabs(y - lHeight + 1) <= EXP) { // 要计算的点在图像下边缘上且不是最后一点,直接一次插值即可 f1 = *((unsigned char*)lpDIBBits + lLineBytes * (lHeight-1- j1)+i1); f2 = *((unsigned char*)lpDIBBits + lLineBytes * (lHeight-1- j2)+i1); return ((unsigned char)(f1+(x-i1) *(f2-f1))); // 返回插值结果 } else { // 计算4个最临近像素值 f1 = *((unsigned char*)lpDIBBits + lLineBytes * (lHeight-1- j1)+i1); f2 = *((unsigned char*)lpDIBBits + lLineBytes * (lHeight-1- j2)+i1); f3 = *((unsigned char*)lpDIBBits + lLineBytes * (lHeight-1- j1)+i2); f4 = *((unsigned char*)lpDIBBits + lLineBytes * (lHeight-1- j2)+i2); f12 = (unsigned char) (f1 + (x - i1) * (f2-f1)); // 插值1 f34 = (unsigned char) (f3 + (x - i1) * (f4-f3)); // 插值2 return ((unsigned char) (f12 + (y -j1) * (f34-f12))); // 插值3 } } }
通过在数字图像处理程序的框架窗口上增加菜单【几何变换】|【插值算法】项。对应的处理函数是视图类中的CDImageProcessView:: OnGeomRota ( )。在进行函数的调用时,可以采用对话框的形式对旋转参数进行设定。具体代码如下:
void CDImageProcessView::OnGeomRota() { CDImageProcessDoc* pDoc = GetDocument(); long lSrcLineBytes; // 图像每行的字节数 long lSrcWidth; // 图像的宽度 long lSrcHeight; // 图像的高度 LPSTR lpSrcDib; // 指向源图像的指针 LPSTR lpSrcStartBits; // 指向源像素的指针 lpSrcDib = (LPSTR) ::GlobalLock((HGLOBAL) pDoc->GetHObject()); // 判断是否是8-bpp位图(这里为了方便,只处理8-bpp位图的旋转,其他的可以类推) if (pDoc->m_dib.GetColorNum(lpSrcDib) != 256) { AfxMessageBox (_T("对不起,不是色位图!")); // 警告 ::GlobalUnlock((HGLOBAL) pDoc->GetHObject()); // 解除锁定 return; // 返回 } lpSrcStartBits=pDoc->m_dib.GetBits(lpSrcDib);// 找到DIB图像像素起始位置 lSrcWidth= pDoc->m_dib.GetWidth(lpSrcDib); // 获取图像的宽度 lSrcHeight= pDoc->m_dib.GetHeight(lpSrcDib); // 获取图像的高度 lSrcLineBytes=pDoc->m_dib.GetReqByteWidth(lSrcWidth * 8); // 计算图像每行的字节数 CDlggeo RotPara;//CGeoRotaDlg dlgPara; // 显示对话框,提示用户设定旋转角度 if (RotPara.DoModal() != IDOK) return; float fRotateAngle = (float) AngleToRadian(RotPara.m_rotAngle); delete RotPara; DWORD palSize=pDoc->m_dib.GetPalSize(lpSrcDib); HGLOBAL hNewDIB = NULL; BeginWaitCursor(); hNewDIB=(HGLOBAL)RotateDIB2(lpSrcDib, otateAngle,lpSrcStartBits,lSrcWidth,lSrcHeight,palSize); if (hNewDIB != NULL) { pDoc->UpdateObject(hNewDIB); // 替换DIB,同时释放旧DIB对象 pDoc->SetDib(); // 更新DIB大小和调色板 pDoc->SetModifiedFlag(TRUE);// 设置修改标记 pDoc->UpdateAllViews(NULL); // 更新视图 } else { AfxMessageBox(_T("分配内存失败!")); } ::GlobalUnlock((HGLOBAL) pDoc->GetHObject()); // 解除锁定 EndWaitCursor(); }
4. 效果演示
源图像如图2-13a所示,对其进行旋转后,结果如图2-13b所示。其中,参数设定见图2-5。
图2-13 使用双线性插值法实现图像旋转效果演示
为了便于与2.1.2节中图像旋转效果进行对比,依然设定旋转角度为30˚。
2.3 综合实例—魔镜
本节将通过一个VC工程实例具体介绍图像几何变换的具体实现和应用过程,这个实例就像一个“魔镜”一样,能把原始图像“照”出各种变换效果。这是一个基于MFC的VC++多文档Win32应用程序。
【例2-1】 图像几何变换综合实例—魔镜
[1] 打开Visual Studio 2005,创建一个基于MFC的多文档应用程序,项目名称为“DImageProcess”,创建过程中取消勾选【使用Unicode库(N)】复选框,最后单击【完成】按钮,结束应用程序的创建。
[2] 将Dib.h文件和Dib.cpp文件复制到“DImageProcess”工程所在的目录下,并分别添加到工程中。其中Dib.h文件具体代码如下:
// Dib.h: interface for the CDib class. #if !defined(AFX_DIB_H__AC952C3A_9B6B_4319_8D6E_E7F509348A88__INCLUDED_) #define AFX_DIB_H__AC952C3A_9B6B_4319_8D6E_E7F509348A88__INCLUDED_ #if _MSC_VER > 1000 #pragma once #endif // _MSC_VER > 1000 #define PalVersion 0x300 // 调色板版本 class CDib : public CObject { public: CDib(); virtual ~CDib(); //operations public: // 用于操作DIB的函数声明 BOOL DrawDib(HDC, LPRECT,HGLOBAL, LPRECT,CPalette*); // 显示位图 BOOL ConstructPalette(HGLOBAL,CPalette* ); // 构造逻辑调色板 LPSTR GetBits(LPSTR); // 取得位图数据的入口地址 DWORD GetWidth(LPSTR); // 取得位图的宽度 DWORD GetHeight(LPSTR); // 取得位图的高度 WORD GetPalSize(LPSTR); // 取得调色板的大小 WORD GetColorNum(LPSTR); // 取得位图包含的颜色数目 WORD GetBitCount(LPSTR); // 取得位图的颜色深度 HGLOBAL CopyObject(HGLOBAL); // 用于复制位图对象 BOOL SaveFile(HGLOBAL , CFile&); // 存储位图为文件 HGLOBAL LoadFile(CFile&); // 从文件中加载位图 // 在对图像进行处理时,针对位图的字节宽度必须是4的倍数的这一要求, // 设计了函数GetRequireWidth来处理这种比较特殊的情况。 int GetReqByteWidth(int ); // 转换后的字节数 GetRequireByteWidth long GetRectWidth(LPCRECT ); // 取得区域的宽度 long GetRectHeight(LPCRECT); // 取得区域的高度 public: void ClearMemory(); void InitMembers(); public: LPBITMAPINFO lpbminfo; // 指向BITMAPINFO结构的指针 LPBITMAPINFOHEADER lpbmihrd; // 指向BITMAPINFOHEADER结构的指针 BITMAPFILEHEADER bmfHeader; // BITMAPFILEHEADER结构 LPSTR lpdib; // 指向DIB的指针 LPSTR lpDIBBits; // DIB像素指针 DWORD dwDIBSize; // DIB大小 HGLOBAL m_hDib; // DIB对象的句柄 RGBQUAD* lpRgbQuag; // 指向颜色表的指针 }; #endif
Dib.cpp文件的具体代码如下:
//Dib.cpp #include "stdafx.h" #include "Dib.h" #include <math.h> #ifdef _DEBUG #undef THIS_FILE static char THIS_FILE[]=__FILE__; #define new DEBUG_NEW #endif /* * Dib文件头标志(字符串"BM")*/ #define DIB_MARKER ((WORD) ('M' << 8) | 'B') // 用于判断位图的标志宏 // Construction/Destruction CDib::CDib() { InitMembers(); } CDib::~CDib() { ClearMemory(); } /************************************************************************* * 函数名称:DrawDib * 参数说明: * HDC hDC 输出设备DC * LPRECT lpDCRect 绘制矩形区域 * HGLOBAL hDIB DIB对象的句柄 * LPRECT lpDIBRect DIB的输出区域 * CPalette *pPal 调色板的指针 * 函数功能:该函数主要用来绘制DIB对象 ************************************************************************/ BOOL CDib::DrawDib(HDC hDC, LPRECT lpDCRect, HGLOBAL hDIB, LPRECT lpDIBRect,CPalette* pPal) { BOOL bSuccess=FALSE; // 重画成功标志 HPALETTE hOldPal=NULL; // 以前的调色板 if (hDIB == NULL) // 判断是否是有效的DIB对象 return FALSE; // 不是,则返回 lpdib = (LPSTR) ::GlobalLock((HGLOBAL) hDIB); // 锁定DIB lpDIBBits = GetBits(lpdib); // 找到DIB图像像素起始位置 if (pPal != NULL) // 获取DIB调色板,并选取到设备环境中 { HPALETTE hPal = (HPALETTE) pPal->m_hObject; hOldPal = ::SelectPalette(hDC, hPal, TRUE); } ::SetStretchBltMode(hDC, COLORONCOLOR); // 设置显示模式 bSuccess = ::StretchDIBits(hDC, // 设备环境句柄 lpDCRect->left, // 目标x坐标 lpDCRect->top, // 目标y坐标 GetRectWidth(lpDCRect), // 目标宽度 GetRectHeight(lpDCRect), // 目标高度 lpDIBRect->left, // 源x坐标 lpDIBRect->top, // 源y坐标 GetRectWidth(lpDIBRect), // 源宽度 GetRectHeight(lpDIBRect), // 源高度 lpDIBBits, // 指向dib像素的指针 (LPBITMAPINFO)lpdib, // 指向位图信息结构的指针 DIB_RGB_COLORS, // 使用的颜色数目 SRCCOPY); // 光栅操作类型 ::GlobalUnlock(hDIB); // 解除锁定 if (hOldPal != NULL) // 恢复系统调色板 { ::SelectPalette(hDC, hOldPal, TRUE); } return bSuccess; } /************************************************************************* * 函数名称:ConstructPalette(HGLOBAL hDIB, CPalette* pPal) * 函数参数: * HGLOBAL hDIB DIB对象的句柄 * CPalette *pPal 调色板的指针 * 函数说明:该函数按照DIB创建一个逻辑调色板 ************************************************************************/ BOOL CDib::ConstructPalette(HGLOBAL hDIB, CPalette* pPal) { HANDLE hLogPal; // 逻辑调色板的句柄 int iLoop; // 循环变量 BOOL bSuccess = FALSE; // 创建结果 if (hDIB == NULL) // 判断是否是有效的DIB对象 { return FALSE; // 返回FALSE } lpdib = (LPSTR) ::GlobalLock((HGLOBAL) hDIB);// 锁定DIB lpbminfo= (LPBITMAPINFO)lpdib; long wNumColors =GetColorNum(lpdib); // 获取DIB中颜色表中的颜色数目 if (wNumColors != 0) { // 分配为逻辑调色板内存 hLogPal = ::GlobalAlloc(GHND, sizeof(LOGPALETTE) + sizeof(PALETTEENTRY) * wNumColors); if (hLogPal == 0) // 如果失败则退出 { ::GlobalUnlock((HGLOBAL) hDIB); // 解除锁定 return FALSE; } LPLOGPALETTE lpPal = (LPLOGPALETTE) ::GlobalLock((HGLOBAL) hLogPal); lpPal->palVersion = PalVersion; // 设置调色板版本号 lpPal->palNumEntries = (WORD)wNumColors; // 设置颜色数目 for (iLoop=0; iLoop<(int)wNumColors;iLoop++) // 读取调色板 { // 读取三原色分量 lpPal->palPalEntry[iLoop].peRed =lpbminfo->bmiColors[iLoop].rgbRed; lpPal->palPalEntry[iLoop].peGreen =lpbminfo->bmiColors[iLoop].rgbGreen; lpPal->palPalEntry[iLoop].peBlue =lpbminfo->bmiColors[iLoop].rgbBlue; lpPal->palPalEntry[iLoop].peFlags =0; // 保留位 } bSuccess=pPal->CreatePalette(lpPal); // 按照逻辑调色板创建调色板,并返回指针 ::GlobalUnlock((HGLOBAL) hLogPal); // 解除锁定 ::GlobalFree((HGLOBAL) hLogPal); // 释放逻辑调色板 } ::GlobalUnlock((HGLOBAL) hDIB); // 解除锁定 return bSuccess; // 返回结果 } /************************************************************************* * 函数名称:GetBits(LPSTRlt@span b=1> lpdib) * 函数参数:LPSTR lpdib 指向DIB对象的指针 * 函数功能:计算DIB像素的起始位置,并返回指向它的指针 ************************************************************************/ LPSTR CDib::GetBits(LPSTR lpdib) { return (lpdib + ((LPBITMAPINFOHEADER)lpdib)->biSize+GetPalSize(lpdib)); } /************************************************************************* * 函数名称:GetWidth(LPSTR lpdib) * 函数参数:LPSTR lpdib 指向DIB对象的指针 * 函数功能:该函数返回DIB中图像的宽度 ************************************************************************/ DWORD CDib::GetWidth(LPSTR lpdib) { return ((LPBITMAPINFOHEADER)lpdib)->biWidth; //返回DIB宽度 } /************************************************************************* * 函数名称:GetHeight(LPSTR lpdib) * 函数参数:LPSTR lpdib 指向DIB对象的指针 * 函数功能:该函数返回DIB中图像的高度 ************************************************************************/ DWORD CDib::GetHeight(LPSTR lpdib) { return ((LPBITMAPINFOHEADER)lpdib)->biHeight; // 返回DIB高度 } /************************************************************************* * 函数名称:GetPalSize(LPSTR lpdib) * 函数参数:LPSTR lpdib 指向DIB对象的指针 * 函数功能:该函数返回DIB中调色板的大小 ************************************************************************/ WORD CDib::GetPalSize(LPSTR lpdib) { return (WORD)(GetColorNum(lpdib) * sizeof(RGBQUAD)); // 计算DIB中调色板的大小 } /************************************************************************* * 函数名称:GetColorNum(LPSTR lpdib) * 函数参数:LPSTR lpdib 指向DIB对象的指针 * 函数功能:该函数返回DIB中调色板的颜色的种数 ************************************************************************/ WORD CDib::GetColorNum(LPSTR lpdib) { long dwClrUsed = ((LPBITMAPINFOHEADER)lpdib)->biClrUsed; // 读取dwClrUsed值 if (dwClrUsed != 0) { return (WORD)dwClrUsed; // 如果dwClrUsed不为0,直接返回该值 } WORD wBitCount = ((LPBITMAPINFOHEADER)lpdib)->biBitCount; //读取biBitCount值 switch (wBitCount) // 按照像素的位数计算颜色数目 { case 1: return 2; case 4: return 16; case 8: return 256; default: return 0; } } /************************************************************************* * 函数名称:GetBitCount(LPSTR lpdib) * 函数参数:LPSTR lpdib 指向DIB对象的指针 * 函数功能:该函数返回DIBBitCount ************************************************************************/ WORD CDib::GetBitCount(LPSTR lpdib) { return ((LPBITMAPINFOHEADER)lpdib)->biBitCount; // 返回位宽 } /************************************************************************* * 函数名称:CopyObject (HGLOBAL hGlob) * 函数参数:HGLOBAL hGlob 要复制的内存区域 * 函数功能:该函数复制指定的内存区域 ************************************************************************/ HGLOBAL CDib::CopyObject (HGLOBAL hGlob) { if (hGlob== NULL) return NULL; DWORD dwLen = ::GlobalSize((HGLOBAL)hGlob); // 获取指定内存区域大小 HGLOBAL hTemp = ::GlobalAlloc(GHND, dwLen); // 分配新内存空间 if (hTemp!= NULL) // 判断分配是否成功 { void* lpTemp = ::GlobalLock((HGLOBAL)hTemp); // 锁定 void* lp = ::GlobalLock((HGLOBAL) hGlob); memcpy(lpTemp, lp, dwLen); // 复制 ::GlobalUnlock(hTemp); // 解除锁定 ::GlobalUnlock(hGlob); } return hTemp; } /************************************************************************* * 函数名称:SaveFile(HGLOBAL hDib, CFile& file) * 函数参数: * HGLOBAL hDib 要保存的DIB * CFile &file 保存文件CFile * 函数功能:将指定的DIB对象保存到指定的CFile中 ***********************************************************************/ BOOL CDib::SaveFile(HGLOBAL hDib, CFile& file) { if (hDib == NULL) return FALSE; // 如果DIB为空,返回FALSE // 读取BITMAPINFO结构,并锁定 lpbmihrd = (LPBITMAPINFOHEADER) ::GlobalLock((HGLOBAL) hDib); if (lpbmihrd == NULL) return FALSE; // 为空,返回FALSE bmfHeader.bfType = DIB_MARKER; // 填充文件头 // 文件头大小+颜色表大小 dwDIBSize = *(LPDWORD)lpbmihrd + GetPalSize((LPSTR)lpbmihrd); DWORD dwBmBitsSize; // 像素的大小 dwBmBitsSize =GetReqByteWidth((lpbmihrd->biWidth)*((DWORD)lpbmihrd->biBitCount)) * lpbmihrd->biHeight; // 大小为Width×Height dwDIBSize += dwBmBitsSize; // 计算后DIB每行字节数为4的倍数时的大小 lpbmihrd->biSizeImage = dwBmBitsSize; // 更新biSizeImage bmfHeader.bfSize = dwDIBSize + sizeof(BITMAPFILEHEADER); // 文件大小 bmfHeader.bfReserved1 = 0; // 两个保留字 bmfHeader.bfReserved2 = 0; bmfHeader.bfOffBits = (DWORD)sizeof(BITMAPFILEHEADER) + lpbmihrd->biSize + GetPalSize((LPSTR)lpbmihrd); // 计算偏移量bfOffBits TRY { file.Write(&bmfHeader, sizeof(BITMAPFILEHEADER)); // 写文件头 file.Write(lpbmihrd, dwDIBSize); // 写DIB头和像素 } CATCH (CFileException, e) { ::GlobalUnlock((HGLOBAL) hDib); // 解除锁定 THROW_LAST(); // 抛出异常 } END_CATCH ::GlobalUnlock((HGLOBAL) hDib); // 解除锁定 return TRUE; // 返回TRUE } /************************************************************************* * 函数名称:LoadFile(CFile& file) * 函数参数:CFile&file 要读取的文件CFile * 函数功能:将指定的文件中的DIB对象读到指定的内存区域中 ***********************************************************************/ HGLOBAL CDib::LoadFile(CFile& file) { DWORD dwFileSize; dwFileSize= file.GetLength(); // 获取文件大小 // 读取DIB文件头 if (file.Read((LPSTR)&bmfHeader, sizeof(bmfHeader)) != sizeof(bmfHeader)) return NULL; // 大小不一致,返回NULL if (bmfHeader.bfType != DIB_MARKER) // 判断是否是DIB对象 return NULL; // 如果不是则返回NULL m_hDib= (HGLOBAL) ::GlobalAlloc(GMEM_MOVEABLE | GMEM_ZEROINIT, dwFileSize-sizeof(BITMAPFILEHEADER)); // 分配DIB内存 if (m_hDib== 0) return NULL; // 分配失败,返回NULL //给CDib类的成员变量赋值 lpdib = (LPSTR) ::GlobalLock((HGLOBAL) m_hDib); // 锁定 lpbminfo=(BITMAPINFO*)lpdib; lpbmihrd=(BITMAPINFOHEADER*)lpdib; lpRgbQuag=(RGBQUAD*)(lpdib+lpbmihrd->biSize); int m_numberOfColors =GetColorNum((LPSTR)lpbmihrd); if (lpbmihrd->biClrUsed == 0) lpbmihrd->biClrUsed =m_numberOfColors; DWORD colorTableSize = m_numberOfColors *sizeof(RGBQUAD); lpDIBBits=lpdib+lpbmihrd->biSize+colorTableSize; if (file.Read(lpdib, dwFileSize - sizeof(BITMAPFILEHEADER)) != // 读像素 dwFileSize - sizeof(BITMAPFILEHEADER) ) // 大小不一致 { ::GlobalUnlock((HGLOBAL) m_hDib); // 解除锁定 ::GlobalFree((HGLOBAL) m_hDib); // 释放内存 return NULL; } ::GlobalUnlock((HGLOBAL) m_hDib); // 解除锁定 return m_hDib; // 返回DIB句柄 } /************************************************************************* * 函数名称:GetReqByteWidth(int bits) * 函数参数:int bits 位数 * 函数功能:获取需要的行字节数,应为4的倍数 ***********************************************************************/ int CDib::GetReqByteWidth(int bits) { int getBytes=(bits + 31) / 32 * 4; return getBytes; } /************************************************************************* * 函数名称:GetRectWidth(LPCRECT lpRect) * 函数参数:LPCRECT lpRect 指向矩形区域的指针 * 函数功能:获取矩形区域的宽度 ************************************************************************/ long CDib::GetRectWidth(LPCRECT lpRect) { long nWidth=lpRect->right - lpRect->left; return nWidth; } /************************************************************************ * 函数名称:GetRectHeight(LPCRECT lpRect) * 函数参数:LPCRECT lpRect 指向矩形区域的指针 * 函数功能:获取矩形区域的高度 ***********************************************************************/ long CDib::GetRectHeight(LPCRECT lpRect) { long nHeight=lpRect->bottom - lpRect->top; return nHeight; } /*********************************************************************** * 函数名称:InitMembers() * 函数功能:初始化类的成员变量 ***********************************************************************/ void CDib::InitMembers() { m_hDib=NULL; lpbmihrd=NULL; // 指向BITMAPINFO结构的指针 lpdib=NULL; lpDIBBits=NULL; dwDIBSize=0; lpRgbQuag=NULL; } /*********************************************************************** * 函数名称:ClearMemory() * 函数功能:复位类的成员变量 ***********************************************************************/ void CDib::ClearMemory() { if(m_hDib!=NULL) ::GlobalFree(m_hDib); lpbmihrd=NULL; // 指向BITMAPINFO结构的指针 lpdib=NULL; dwDIBSize=0; lpRgbQuag=NULL; dwDIBSize=0; }
[3] 为程序添加图像变换的【几何变换】,在【几何变换】菜单下又分别设置了【图像镜像】、【图像转置】、【图像平移】、【图像旋转】、【图像缩放】和【插值算法】6个子菜单,在【图像镜像】下设置了【水平】和【垂直】两个三级菜单,如图2-14所示。
图2-14 【几何变换】菜单
[4] 更改菜单ID号。新增菜单对应的ID号设定如表2-1所示。
表2-1 新增菜单对应的ID号
[5] 设计对话框。为了实现对平移距离、旋转角度和缩放比率等具体数据进行控制,使用对话框的形式对相应的参数进行设定。为此设计【平均参数】、【旋转参数】、【缩放参数】和【使用双线性插值法完成图像旋转】4个对话框,分别如图2-15、图2-16、图2-17和图2-18所示。
图2-15 【平移参数】对话框
图2-16 【旋转参数】对话框
图2-17 【缩放参数】对话框
图2-18 【使用双线性插值法完成图像旋转】对话框
[6] 设定4个对话框的类名以及对话框上主要控件所对应ID号,如表2-2所示。
表2-2 各对话框名称及其主要控件对应的ID号
[7] 为各个对话框添加变量及编程。
为CDlgTran类添加int型变量m_horOff和m_verOff,并且添加如下代码初始化变量:
m_horOff = 50; m_verOff = 50;
为CDlgTran类添加OnInitDialog函数,并且添加如下代码:
CSpinButtonCtrl* pSpinHor=(CSpinButtonCtrl*)GetDlgItem(IDC_SPIN_hor); pSpinHor->SetRange(-100,100); CSpinButtonCtrl* pSpinVer=(CSpinButtonCtrl*)GetDlgItem(IDC_SPIN_ver); pSpinVer->SetRange(-100,100);
为CDlgTran类添加DoDataExchange函数,并且添加如下代码:
DDX_Text(pDX, IDC_horOffSet, m_horOff); DDX_Text(pDX, IDC_verOffSet, m_verOff);
为CDlgRot类添加int型变量m_rotAngle,并且添加如下代码初始化变量:
m_rotAngle = 90;
为CDlgRot类添加OnInitDialog函数,并且添加如下代码:
CSpinButtonCtrl* pSpinAng=(CSpinButtonCtrl*)GetDlgItem(IDC_SPIN_ang); pSpinAng->SetRange(-360,360);
为CDlgRot类添加DoDataExchange函数,并且添加如下代码:
DDX_Text(pDX, IDC_rot_angle, m_rotAngle);
为CDlgZoom类添加float型变量m_horZoom和m_verZoom,并添加代码初始化变量:
m_horZoom = 0.5f; m_verZoom = 0.5f;
为CDlgZoom类添加DoDataExchange函数,并且添加如下代码:
DDX_Text(pDX, IDC_EDIT_XZoom, m_horZoom); DDX_Text(pDX, IDC_EDIT_YZoom, m_verZoom);
为CDlggeo类添加int型变量m_rotAngle,并且添加如下代码初始化变量:
m_rotAngle = 90;
为CDlggeo类添加OnInitDialog函数,并且添加如下代码:
CSpinButtonCtrl* pSpinAng=(CSpinButtonCtrl*)GetDlgItem(IDC_SPIN_geo); pSpinAng->SetRange(-360,360);
为CDlggeo类添加DoDataExchange函数,并且添加如下代码:
DDX_Text(pDX, IDC_rot_geo, m_rotAngle);
有关生成菜单及对话框的方法参见第1章。
[8] 创建function.h头文件。
为了更好地进行函数调用控制,将所有关于图像的几何变换的函数统一放在function.h头文件中,这个头文件还包含了图像变换各个函数需要调用的一些常用计算函数,其具体代码如下:
#pragma once #endif #include "Dib.h" #include <math.h> #define pi 3.1415926535// 常数π #define WIDTHBYTES(bits) (((bits) + 31) / 32 * 4) #include <direct.h> #include <complex> using namespace std; #define PI 3.14159265358979323846 typedef struct{ int Value; int Dist; int AngleNumber; } MaxValue; struct CplexNum { double re; double im; }; // 用于复数运算 CplexNum Add(CplexNum c1,CplexNum c2) { CplexNum c; c.re=c1.re+c2.re; c.im=c1.im+c2.im; return c; } CplexNum Sub(CplexNum c1,CplexNum c2) { CplexNum c; c.re=c1.re-c2.re; c.im=c1.im-c2.im; return c; } CplexNum Mul(CplexNum c1,CplexNum c2) { CplexNum c; c.re=c1.re*c2.re-c1.im*c2.im; c.im=c1.re*c2.im+c2.re*c1.im; return c; } double AngleToRadian(int angle) // 角度到弧度的转换 { return ((angle)*pi/180.0); } BYTE FloatToByte(double f) // 该函数将输入的双精度变量转换为BYTE型变量 { if (f<=0) return (BYTE)0; else if (f>=255) return (BYTE)255; else return (BYTE)(f+0.5); } char FloatToChar(double f) // 该函数将输入的双精度变量转换为char型变量 { if (f>=0) if (f>=127.0) return (char)127; else return (char)(f+0.5); else if (f<=-128) return (char)-128; else return -(char)(-f+0.5); } BOOL Transpose(LPSTR lpSrcDib,LPSTR lpSrcStartBits,long lWidth,long lHeight, long lLineBytes,long lDstLineBytes) { ⋯⋯ } BOOL Mirror(LPSTR lpSrcStartBits, long lWidth, long lHeight,long lLineBytes) { ⋯⋯ } BOOL Mirror2(LPSTR lpSrcStartBits, long lWidth, long lHeight,long lLineBytes) { ⋯⋯ } ⋯⋯ HGLOBAL Rotate(LPSTR lpSrcDib, LPSTR lpSrcStartBits,long lWidth, long lHeight, long lLineBytes,WORD palSize, long lDstWidth, long lDstHeight,long lDstLineBytes,float fSina, float fCosa) { ⋯⋯ } HGLOBAL Zoom(LPSTR lpSrcDib, LPSTR lpSrcStartBits,long lWidth, long lHeight, long lLineBytes, WORD palSize,long lDstWidth,long lDstLineBytes, long lDstHeight, float fhorRatio,float fverRatio) { ⋯⋯ } HGLOBAL RotateDIB2(LPSTR lpSrcDib, float fRotateAngle,LPSTR lpSrcStartBits,long lWidth, long lHeight,WORD palSize) { ⋯⋯ } #endif
有关图像几何变换的实现函数已在前面各节中有详细叙述,这里不再重复。
[9] 完善视类文件CDImageProcessView的代码。
在视类实现文件DImageProcessView.cpp文件中加入对function.h和各个对话框类头文件的引用,其具体代码如下:
#include "DlgTran.h" #include "DlgRot.h" #include "DlgZoom.h" #include "Dlggeo.h" #include "function.h"
对CDImageProcessView的构造函数编写代码如下:
CDImageProcessView::CDImageProcessView() { m_pDbImage=NULL; m_nDWTCurDepth=0; }
为CDImageProcessView分别添加PreCreateWindow、AssertValid、Dump和OnDraw函数。并且为OnDraw函数编写代码如下:
// CDImageProcessView drawing void CDImageProcessView::OnDraw(CDC* pDC) { CDImageProcessDoc* pDoc = GetDocument(); // 获取文档 ASSERT_VALID(pDoc); HGLOBAL hDIB = pDoc->GetHObject(); if (hDIB != NULL) // 判断DIB是否为空 { LPSTR lpDibSection = (LPSTR) ::GlobalLock((HGLOBAL) hDIB); int cxDIB = (int) pDoc->m_dib.GetWidth(lpDibSection); // 获取DIB宽度 int cyDIB = (int) pDoc->m_dib.GetHeight(lpDibSection);// 获取DIB高度 ::GlobalUnlock((HGLOBAL) hDIB); CRect rcDIB; rcDIB.top = rcDIB.left = 0; rcDIB.right = cxDIB; rcDIB.bottom = cyDIB; CRect rcDest= rcDIB; pDoc->m_dib.DrawDib(pDC->m_hDC, &rcDest, pDoc->GetHObject(), &rcDIB, pDoc->GetDocPal()); // 输出DIB } }
[10] 为增加的菜单添加消息处理函数,将这些函数添加到CDImageProcessView.cpp视图类中。具体形式如下:
void CDImageProcessView::OnTranspose() { ⋯⋯ } void CDImageProcessView::OnMirror() { ⋯⋯ } void CDImageProcessView::OnTranslation() { ⋯⋯ } void CDImageProcessView::OnRotation() { ⋯⋯ } void CDImageProcessView::OnZoom() { ⋯⋯ } void CDImageProcessView::OnMirror2() { ⋯⋯ } void CDImageProcessView::OnGeomRota () { ⋯⋯ }
根据VC编程习惯,“函数处理程序名称”是程序为此菜单默认给出的一个处理函数的名称,一般不必更改。消息处理函数的具体内容已在图像几何变换的相关节中给出。
[11] 按F5键或工具栏中的运行按钮运行程序。
程序运行界面如图2-19所示。以图像旋转变换为例,按下【文件】【打开】菜单,从【打开】对话框中输入一幅图片,此时程序界面上出现该图片,并且在程序菜单栏上出现【几何变换】菜单,如图2-20所示。在【几何变换】菜单下选择对该图片进行的变换,例如【图像旋转】,在弹出的【旋转参数】对话框中输入适当的数据,如图2-21所示。图片按照设定的角度进行旋转,如图2-22所示。
图2-19 程序运行窗口
图2-20 在窗口中打开图像
图2-21 【旋转参数】对话框
图2-22 旋转图像
本程序所要求输入的图像都是256色的位图,读者可以根据前面所讲的知识自行扩展处理真彩色图像。
2.4 实践拓展
程序设计是一个不断尝试、探索的过程。在编写一个完整的VC++程序时,往往会遇到许多意想不到的问题。尤其是在编写有关数字图像处理方面的程序时,需要开发者有足够的耐心和毅力来面对这些问题,不断地总结经验,更好地提高自己。关于本章在编程实践中需要注意的问题以及一些经验和技巧总结如下。
1. 怎么样才能使对话框上的微调控件控制对应文本编辑框中的数字
在编辑对话框程序的过程中发现,要使对话框上的微调控件控制对应文本编辑框中的数字,除了加入相应的程序代码外,还要将“微调控件”的“Auto Buddy ”和“Set Buddy Integer ”属性改为“true”。在VC 2005中微调控件的这两个属性的默认值是“False”,如果不修改过来,程序将无法实现微调控件对其对应的文本编辑框的数字控制。
2. 为什么选择256色位图进行图像变换
BMP(bitmap的缩写)文件格式是Windows本身的位图文件格式,所谓“Windows本身”是指Windows操作系统内部存储位图就采用这种格式。一个.BMP格式的文件可用每像素1、4、8、16或24位来编码颜色信息,这个位数称作图像的颜色深度,它决定了图像所含的最大颜色数。所以8位(8-bpp)位图所能表示的最大图像颜色数是256。在本章所给出的图像变换程序中,总是要先判断源图像是否是8位BMP位图,其类似代码如下:
if (pDoc->m_dib.GetColorNum(lpSrcDib) != 256) // 判断是否是8-bpp位图 { AfxMessageBox(_T ("对不起,不是256色位图!")); // 警告 ::GlobalUnlock((HGLOBAL) pDoc->GetHObject()); // 解除锁定 return; // 返回 } // 不是则返回
这是因为针对256色位图的图像变换编程及演示有助于程序的简化,便于将重点放在算法本身。256色位图每个像素位数正好是8位,即1个字节,可以不用考虑编程过程中的拼凑字节问题。另外,在进行数字图像处理时,一般采用灰度图像,因为进行灰度图像处理时不必考虑调色板的问题。但是BMP格式的文件中并没有灰度图这个概念,我们可以使用BMP文件来表示灰度图,方法就是用256色的调色板。使用256色调色板来表示灰度图时,每一项的R、G、B值都是相同的(R=G=B),即R、G、B值从(0,0,0),⋯,(1,1,1)一直到(255,255,255)。(0,0, 0)表示全黑色,(255, 255, 255)表示全白色,中间是灰色。对于其他类型的图像,要根据其不同的性质设计不同的程序。