摘要:本文介绍了用VC编程实现BMP图像裁切的两种方法:第一种方法是将图像数据全部读入内存,然后将感兴趣区域裁切下来;第二种方法是只读取感兴趣区域的图像数据,然后保存读取的数据实现图像的裁切。
关键词:图像裁切,VC,BMP
随着计算电子技术和计算机技术的发展,数字图像处理进入高速发展时期,许多成熟的图像处理软件如雨后春笋般层出不穷。在大多数图像处理软件中都有图像裁切功能,用它我们能够快速提取感兴趣区域,去掉多余的图像内容。那么怎样编程实现图像裁切呢,下面以BMP图像为例介绍一下如何用VC实现图像裁切。
先介绍第一种方法,将图像数据全部读入内存,然后将感兴趣区域裁切下来。在许多数字图像处理的书中都有关于BMP图像存储结构的章节,这里就不再详细介绍了。BMP文件一般分为四个部分:位图头文件、位图信息头、调色板和图像数据。图像裁切要用到位图信息头中的几个参数值:biWidth(图像宽度)、biHeight(图像高度)、biBitCount(每个像素的位数)、biSizeImage(图像长度)。图像裁切首先要确定裁切区域内每个像素在整幅图像中的位置,我们以裁切区域中心点像素位置起算,要注意的是图像数据的存储是从最下面一行的左边开始的。如图(1)所示,Height是图像高,Width是图像宽,ctPoint是裁切区域中心点坐标,dwX和dwY分别是裁切区域的宽和高。以256色图像为例(每个像素占一个字节),裁切区域左下角像素(也就是裁切后图像的第一个像素)位置为(Height-ctPoint.y-dwY/2-1)×Width+ctPoint.x-dwX/2,左下角像素位置确定了,裁切区域内的其它像素位置就很容易确定。确定了裁切区域内每个像素的位置后,就可以把这些像素的值赋给裁切后图像的相应像素。裁切后图像的位图信息头和调色板只要从原图像数据中拷贝就可以了,修改信息头中图像宽、高和长度值为裁切后的值。
图1 感兴趣区域位置
按照上面的思路笔者用VC++ 6.0编写了一个图像裁切函数ClipDIB(),该函数首先计算裁切区域图像数据的大小,为裁切后的图像分配内存,然后将原图像的信息头、调色板拷贝给裁切后的图像,最后将原图像中裁切区域内的像素值赋给裁切后影像。
HDIB CBMP_ViewView::ClipDIB(HDIB hDib,CPoint ctpoint,DWORD dwX,DWORD dwY)
{
// 假如图像为空则返回
if (hDib == NULL)
{
return NULL ;
}
// 获得图像指针
LPSTR lpDIB = (LPSTR) ::GlobalLock((HGLOBAL) hDib);
// 图像信息头指针
LPBITMAPINFOHEADER lpBMIH = (LPBITMAPINFOHEADER) lpDIB ;
// 如果裁切中心点超出图像范围
if (ctpoint.x > (int)DIBWidth(lpDIB)||ctpoint.y > (int)DIBHeight(lpDIB))
{
AfxMessageBox("中心点不在图像范围内") ;
return NULL ;
}
DWORD dwxSave ; // 实际影像保存的宽度
DWORD dwBitsSize ; // 裁切后图像区域大小
if ((int(ctpoint.x - dwX/2)<0)&&(int(ctpoint.y - dwY/2)<0)
&&(ctpoint.x + dwX/2)>DIBWidth(lpDIB)&&(ctpoint.y + dwY/2)>DIBHeight(lpDIB))
{
AfxMessageBox("裁切区域超出图像范围") ;
return NULL ;
}
// 实际影像保存的宽度为4字节的整数倍
dwxSave = (dwX * lpBMIH->biBitCount + 31)/32 * 4 ;
// 实际影像块的大小
dwBitsSize = dwxSave * dwY ;
// 裁切后的DIB图像大小(不包含文件头)
DWORD dwClipedSize ;
dwClipedSize = sizeof(BITMAPINFOHEADER) + PaletteSize(lpDIB) + dwBitsSize ;
// 为裁切后的DIB分配内存
HDIB hClipedDIB ;
hClipedDIB = (HDIB)::GlobalAlloc(GMEM_MOVEABLE | GMEM_ZEROINIT,
dwClipedSize) ;
// 获得图像指针
LPSTR lpClipedDIB = (LPSTR) ::GlobalLock((HGLOBAL) hClipedDIB) ;
// 拷贝信息头和调色板
memcpy(lpClipedDIB,lpDIB,sizeof(BITMAPINFOHEADER) + PaletteSize(lpDIB)) ;
// 修改信息头中影像长、宽、长度
LPBITMAPINFOHEADER pClipedDibIFH = (LPBITMAPINFOHEADER) lpClipedDIB ;
pClipedDibIFH->biWidth = dwX ;
pClipedDibIFH->biHeight = dwY ;
pClipedDibIFH->biSizeImage = dwX * dwY ;
LPSTR pBits = FindDIBBits(lpDIB) ; // 图像数据指针
LPSTR pClipedBits = FindDIBBits(lpClipedDIB) ; // 裁切后的图像数据指针
// 每个像素占的字节数
UCHAR BytesPerPixel = lpBMIH->biBitCount / 8 ;
// 将原图像中裁切区域的像素值赋给裁切后影像
for(UINT i = 0;i < dwY;i++)
{
memcpy(pClipedBits + dwxSave * i ,
pBits + ((DIBHeight(lpDIB) - ctpoint.y - dwY/2 - 1 + i) * DIBWidth(lpDIB)
+ ctpoint.x - dwX/2 ) * BytesPerPixel,
dwxSave) ;
}
::GlobalUnlock((HGLOBAL) hDib) ;
::GlobalUnlock((HGLOBAL) hClipedDIB) ;
return hClipedDIB ;
}
第二种方法是只读取感兴趣区域的图像数据,然后保存读取的数据实现图像的裁切。在图像比较大的情况下,该方法可以省去将图像数据读入内存的漫长时间,直接实现对图像数据的裁切。该方法的实质就是读取裁切区域内的图像数据,关键是把图像文件的指针定位到裁切区域,基本思路也是按照图1得出的。为实现这一功能,在文档类中添加ReadSubDIBFile()函数,该函数的代码如下所示:
void CBMP_ViewDoc::ReadSubDIBFile(CFile& file,CPoint ctpoint,DWORD dwX ,DWORD dwY)
{
BITMAPFILEHEADER bmfHeader ;
LPBITMAPINFOHEADER pBmIfo ;
HDIB hSubDIB ;
try
{
// 读文件头
file.Read((LPSTR)&bmfHeader, sizeof(bmfHeader)) ;
// 判断是否是DIB对象,检查头两个字节是否是"BM"
if (bmfHeader.bfType != DIB_HEADER_MARKER)
{
throw new CException ;
}
// 计算信息头加上调色板的大小并分内存
int nSize = bmfHeader.bfOffBits - sizeof(BITMAPFILEHEADER) ;
pBmIfo = (LPBITMAPINFOHEADER) new BYTE[nSize] ;
// 读取信息头和调色板
file.Read(pBmIfo, nSize);
if ((int(ctpoint.x - dwX/2)<0)||(int(pBmIfo->biWidth - ctpoint.x - dwX/2)<0)
||(int(ctpoint.y - dwY/2)<0)||(int(pBmIfo->biHeight - ctpoint.y - dwY/2)<0))
{
AfxMessageBox("裁切图像超出原图像范围") ;
return ;
}
DWORD dwxSave ; // 实际影像保存的宽度
DWORD dwBitsSize ; // 子图像区域大小
// 实际影像保存的宽度为4字节的整数倍
dwxSave = (dwX * pBmIfo->biBitCount + 31)/32 * 4 ;
// 裁切后影像块的大小
dwBitsSize = dwxSave * dwY ;
// 裁切后图像大小(不包含文件头)
DWORD dwSubImageSize ;
dwSubImageSize = sizeof(BITMAPINFOHEADER) + PaletteSize((LPSTR)pBmIfo)
+ dwBitsSize ;
// 为裁切后图像分配内存
hSubDIB=(HDIB)::GlobalAlloc(GMEM_MOVEABLE | GMEM_ZEROINIT,
dwSubImageSize) ;
// 获得裁切后图像指针
LPSTR lpSubDIB = (LPSTR) ::GlobalLock((HGLOBAL) hSubDIB) ;
// 拷贝信息头和调色板
memcpy(lpSubDIB,(LPSTR)pBmIfo,sizeof(BITMAPINFOHEADER)
+ PaletteSize((LPSTR) pBmIfo)) ;
// 每个像素占的字节数
UCHAR BytesPerPixel = pBmIfo->biBitCount / 8 ;
// 读取裁切后图像每个像素
for(UINT i = 0;i < dwY;i++)
{
if (i == 0)
{
file.Seek(((pBmIfo->biHeight - ctpoint.y - dwY/2 - 1 + i) * pBmIfo->biWidth
+ ctpoint.x - dwX/2 + 1) * BytesPerPixel,CFile::current) ;
}
else
{
file.Seek(pBmIfo->biWidth * BytesPerPixel - dwxSave,CFile::current) ;
}
file.Read(FindDIBBits(lpSubDIB) + dwxSave * i , dwxSave) ;
}
// 更改信息头中影像的长、宽、大小
LPBITMAPINFOHEADER pSubDIBIfo = (LPBITMAPINFOHEADER) lpSubDIB ;
pSubDIBIfo->biWidth = dwX ;
pSubDIBIfo->biHeight = dwY ;
pSubDIBIfo->biSizeImage = dwX * dwY ;
}
catch (CException * pe)
{
AfxMessageBox("Read Subimage Error") ;
pe->Delete() ;
return ;
}
::GlobalUnlock((HGLOBAL) hSubDIB) ;
m_hDIB = hSubDIB ;
}