摘 要:本文以笔者实现的数字图象处理程序为例,讲述了DIB的结构及操作、灰度直方图的绘制方法及模板卷积的实现方法。
关键字:数字图象处理 设备无关位图DIB 灰度直方图 模板卷积 Visual C++
人类对外部世界的感知大部分来自图象,然而许多图象不能直接使用或直接使用不能满足需求,因此我们必须对它进行一定的处理,以获取我们感兴趣的信息或形式。
随着计算机技术的发展,数字图象处理越来越体现出它的强大的生命力。从专业的遥感图象处理到大众化的Photoshop,无一不得益于数字图象处理技术。下面我们就以我们实现的数字图象处理程序中的部分功能模块对数字图象处理作一些初步的探讨。
一、设备无关位图DIB
图象在计算机中应以一定的形式来表示,在Windows平台中,我们使用设备无关位图(Device-Independent Bitmap)。
无论是在文件中还是在内存中,设备无关位图结构几乎相同。由一个文件头BITMAPFILEHEADER(只存在于文件中),一个位图信息块BITMAPINFO(包含位图信息BITMAPINFOHEADER和一个颜色表RGBQUAD数组)及一个位图数据块组成。
文件头结构的定义及含义如下:
typedef struct tagBITMAPFILEHEADER {
WORD bfType; // 位图文件标志,必须为BM(0x4D42)。
DWORD bfSize; // 标志文件长度,单位为字节。
WORD bfReserved1; // 保留,必须为0。
WORD bfReserved2; // 保留,必须为0。
DWORD bfOffBits;
// 标志从BITMAPFILEHEADER到位图数据的偏移量,单位为字节。
} BITMAPFILEHEADER;
紧跟文件头的是位图信息块BITMAPINFO,它由一个位图信息结构体BITMAPINFOHEADER和一个颜色表RGBQUAD数组组成,其定义及含义如下:
typedef struct tagBITMAPINFO {
BITMAPINFOHEADER bmiHeader; // 如下。
RGBQUAD bmiColors[1]; // 颜色表信息。
} BITMAPINFO;
typedef struct tagBITMAPINFOHEADER{
DWORD biSize;
// BITMAPINFOHEADER结构的长度,单位为字节。
LONG biWidth; // 位图宽度,单位为像素。
LONG biHeight; // 位图高度,单位为像素。
WORD biPlanes; // 位图位面数,必须为1。
WORD biBitCount; // 位图每个像素的位数。
DWORD biCompression; // 标志位图的压缩类型。
DWORD biSizeImage;
LONG biXPelsPerMeter;
LONG biYPelsPerMeter;
DWORD biClrUsed;
DWORD biClrImportant;
} BITMAPINFOHEADER;
typedef struct tagRGBQUAD {
BYTE rgbBlue; // 标志颜色中蓝色分量的亮度。
BYTE rgbGreen; // 标志颜色中绿色分量的亮度。
BYTE rgbRed; // 标志颜色中红色分量的亮度。
BYTE rgbReserved; // 保留,必须为0。
} RGBQUAD;
BITMAPINFO后就是图象数据了。图象的颜色数不同,每个像素所占用的字节数也不同。需要注意的是,出于对齐Cache边界的考虑,位图中每行的字节数必须是4的整数倍,不足的要用0补齐,所以位图每行所占用的字节数并不等于图象的宽度乘以每个像素所占用的字节数。
我们可以开发一个简单的类CDib来管理DIB,下面是这个类的头文件和实现文件:
// Dib.h: interface for the CDib class.
#if !defined(AFX_DIB_H__5C523E35_AE65_11D3_82A8_BFD134BF3343__INCLUDED_)
#define AFX_DIB_H__5C523E35_AE65_11D3_82A8_BFD134BF3343__INCLUDED_
#if _MSC_VER > 1000
#pragma once
#endif // _MSC_VER > 1000
class CDib : public CObject
{
public:
void Show( CDC* pDC );
CDib( LPCSTR lpcszFileName ); // 构造函数
virtual ~CDib(); // 析构函数
// 获得位图高度,单位为像素
LONG GetBmpHeight(){return m_lpBmpInfoHeader->biHeight;}
// 获得位图宽度,单位为像素
LONG GetBmpWidth(){return m_lpBmpInfoHeader->biWidth;}
// 获得位图信息块指针
LPBITMAPINFO GetBmpInfoPtr(){return m_lpBmpInfo;}
// 获得位图信息头指针
LPBITMAPINFOHEADER GetBmpInfoHeaderPtr(){return m_lpBmpInfoHeader;}
// 获得位图颜色表指针
LPRGBQUAD GetBmpColorTablePrt(){return m_lpColorTable;}
// 获得图象数据指针
LPBYTE GetImageDataPtr(){return m_lpImageData;}
// 获得每行的字节数
UINT GetBytesPerRow(){return m_nBytesPerRow;}
// 获得位图颜色数目
UINT GetBmpNumColors(){return m_numColors;}
private:
VOID LoadBitmapFile( LPCSTR lpcszFileName ); // 读取位图文件
LPBITMAPINFO m_lpBmpInfo; // 指向文件信息块的指针
LPBITMAPINFOHEADER m_lpBmpInfoHeader; // 指向文件信息头的指针
LPRGBQUAD m_lpColorTable; // 指向颜色表的指针
LPBYTE m_lpImageData; // 指向图象数据的指针
UINT m_numColors; // 图象的颜色数目
UINT m_nBytesPerRow; // 每行字节数
};
#endif // !defined(AFX_DIB_H__5C523E35_AE65_11D3_82A8_BFD134BF3343__INCLUDED_)
// Dib.cpp: implementation of the CDib class.
#include "stdafx.h"
#include "Test.h"
#include "Dib.h"
#include "HistogramDialog.h"
#include "ImageSegmentation.h"
#ifdef _DEBUG
#undef THIS_FILE
static char THIS_FILE[]=__FILE__;
#define new DEBUG_NEW
#endif
CDib::CDib(LPCSTR lpcszFileName)
{
LoadBitmapFile( lpcszFileName );
}
CDib::~CDib()
{
delete []m_lpBmpInfo;
}
VOID CDib::LoadBitmapFile(LPCSTR lpcszFileName)
{
BITMAPFILEHEADER bmpFileHeader;
CFile file( lpcszFileName, CFile::modeRead );
file.Read( (void*)&bmpFileHeader, sizeof( bmpFileHeader ) );
if( bmpFileHeader.bfType != 0x4d42 )
{ // 不是位图文件
AfxMessageBox( "Not a bitmap file." );
m_lpBmpInfo = NULL;
m_lpBmpInfoHeader = NULL;
m_lpColorTable = NULL;
m_lpImageData = NULL;
m_numColors = 0;
}
else
{
DWORD fileLength = file.GetLength();
DWORD dibSize = fileLength - sizeof( bmpFileHeader );
BYTE* pDib = new BYTE[ dibSize ];
file.Read( (void*)pDib, dibSize );
file.Close();
m_lpBmpInfo = (LPBITMAPINFO) pDib;
m_lpBmpInfoHeader = (LPBITMAPINFOHEADER) pDib;
m_lpColorTable = (RGBQUAD*)( pDib + m_lpBmpInfoHeader->biSize );
// 计算图象颜色数
if ( (m_lpBmpInfoHeader->biClrUsed == 0)
&& (m_lpBmpInfoHeader->biBitCount < 9) )
m_numColors = (1 << m_lpBmpInfoHeader->biBitCount);
else
m_numColors = (UINT) m_lpBmpInfoHeader->biClrUsed;
// 获得图象数据指针
m_lpImageData =(LPBYTE)m_lpColorTable+m_numColors* sizeof(RGBQUAD);
// 计算位图每行所占用的字节数
m_nBytesPerRow = ( GetBmpWidth()+3 )/4*4;
}
}
void CDib::Show(CDC *pDC)
{
if( !m_lpBmpInfo )
return;
HPALETTE hPal, hOldPal;
// 判断图象是否有调色板
if( m_lpBmpInfoHeader->biBitCount <= 8 )
{ // 有调色板则分配空间并填充
LPLOGPALETTE pLogPal = (LPLOGPALETTE) new char[2 * sizeof(WORD) + m_numColors * sizeof(PALETTEENTRY)];
pLogPal->palVersion = 0x300;
pLogPal->palNumEntries = m_numColors;
LPRGBQUAD pDibQuad = (LPRGBQUAD) m_lpColorTable;
for(UINT i = 0; i < m_numColors; i++)
{
pLogPal->palPalEntry[i].peRed = pDibQuad->rgbRed;
pLogPal->palPalEntry[i].peGreen = pDibQuad->rgbGreen;
pLogPal->palPalEntry[i].peBlue = pDibQuad->rgbBlue;
pLogPal->palPalEntry[i].peFlags = 0;
pDibQuad++;
}
// 创建并使用调色板
hPal = ::CreatePalette( (LPLOGPALETTE)pLogPal );
hOldPal = ::SelectPalette( pDC->m_hDC, hPal, FALSE );
::RealizePalette( pDC->m_hDC );
}
LONG bmpWidth = GetBmpWidth();
LONG bmpHeight = GetBmpHeight();
pDC->SetStretchBltMode(COLORONCOLOR);
StretchDIBits( pDC->m_hDC,
0, 0, bmpWidth, bmpHeight,
0, 0, bmpWidth, bmpHeight,
(void*)m_lpImageData, m_lpBmpInfo,
DIB_RGB_COLORS, SRCCOPY
);
if( m_lpBmpInfoHeader->biBitCount <= 8 )
{
::SelectPalette( pDC->m_hDC, hOldPal, FALSE );
::DeleteObject( hPal );
}
}
为简单起见,下面的内容都以灰度图象为例。
二、灰度直方图
灰度直方图是分析数字图象的强有力的工具。它以灰度值为横轴,以具有某一灰度值的像素在图象中出现的次数为纵轴。由此可见,灰度直方图反映了一幅图象的灰度分布情况。
我们在程序中实现了一个绘制灰度直方图的对话框。
创建一个如图一的对话框资源模板,
图一 直方图对话框资源模板
ID |
类型 |
属性 |
IDC_STATIC |
组框 |
General页的Caption为“图象灰度直方图”,其余缺省 |
IDC_HISTOGRAM |
静态文本框 |
General页的Caption为空,
Style页的Simple,其余缺省 |
DC_GRAYLEVEL |
静态文本框 |
同上 |
IDOK |
按键 |
缺省 |
其中IDC_HISTOGRAM用于绘制灰度直方图,创建时调整其宽高为172x75。IDC_GRAYLEVEL用于绘制直方图横轴灰度颜色,创建时调整其宽高为172x8。
运行ClassWizard为此对话框模板创建一个新类CHistogramDialog,向此类加入下列私有成员变量:
LONG m_naHistogram[256];
// m_naHistogram[i]用于存储灰度为i的像素在图象中出现的次数
LONG m_nMaxCount; // m_naHistogram[i]的最大值
将此类的构造函数原型改为
CHistogramDialog(CDib* pDib, CWnd* pParent = NULL);
并在构造函数中加入代码
CHistogramDialog::CHistogramDialog(CDib* pDib, CWnd* pParent /*=NULL*/)
: CDialog(CHistogramDialog::IDD, pParent)
{
LPBYTE pData; // 位图数据指针
UINT BytesPerRow; // 每行字节数
LONG nHeight, nWidth; // 位图高度
LONG w, h;
pData = pDib->GetImageDataPtr();
BytesPerRow = pDib->GetBytesPerRow();
nHeight = pDib->GetBmpHeight();
nWidth = pDib->GetBmpWidth();
memset( (void*)m_naHistogram, 0, 256*sizeof(LONG) );
for( h=0; h<nHeight; h++ )
for( w=0; w<nWidth; w++ )
m_naHistogram[ *(pData+h*BytesPerRow+w) ]++;
m_nMaxCount = 0;
for( i=0; i<256; i++ )
{
if( m_naHistogram[i]>m_nMaxCount )
m_nMaxCount = m_naHistogram[i];
}
}
void CHistogramDialog::OnPaint()
{
CPaintDC dc(this); // device context for painting
CRect rect;
CWnd* pWnd;
int i, j;
pWnd = GetDlgItem( IDC_HISTOGRAM );
pWnd->GetWindowRect( &rect );
ScreenToClient( &rect );
pWnd->Invalidate();
pWnd->UpdateWindow();
dc.Rectangle( &rect );
rect.DeflateRect( 1, 1 );
// 绘制直方图
for( i=0; i<256; i++ )
{
dc.MoveTo( rect.left + i, rect.bottom );
dc.LineTo( rect.left+i, rect.top * m_naHistogram[i]/m_nMaxCount );
}
pWnd = GetDlgItem( IDC_GRAYLEVEL );
pWnd->GetWindowRect( &rect );
ScreenToClient( &rect );
pWnd->Invalidate();
pWnd->UpdateWindow();
dc.Rectangle( &rect );
rect.DeflateRect( 1, 1 );
// 绘制横坐标
for( i=0; i<256; i++ )
for( j=rect.top; j<rect.bottom; j++ )
dc.SetPixel( rect.left+i, j, RGB(i,i,i) );
// Do not call CDialog::OnPaint() for painting messages
}
图二就是图三所示图象的灰度直方图。
图二 灰度直方图(源图象如图三)
三、模板卷积
数字图象处理中常用模板卷积来实现滤波器来对图象进行滤波处理。其步骤为:
1. 将一个矩形模板(实际上是一个二维矩阵)在图象中漫游,并将模板中心与图象中某个像素位置重合;
2. 将模板中的系数与图象中对应位置的像素相乘;
3. 将所有乘积相加;
4. 将和赋给图象中对应模板中心位置的像素。
需要注意的是,我们在计算机中表示的灰度图象数据通常都是正数,然而通过模板卷积运算后可能会出现负数,所以我们还要将图象数据恢复到以前的灰度空间。
下面是我们实现的模板卷积函数(为简单起见,我们没有考虑图象边缘像素):
/* 卷积模板大小可调节滤波器
参数:
mask 模板指针
wndWidth 模板的宽度
wndHeight 模板的高度
*/
void CImageSegmentation::AdjustableFilter(int *mask, int wndWidth, int wndHeight)
{
LONG w, h;
INT i, j;
INT halfHeight, halfWidth; // 滤波器窗口半高宽
LPBYTE pData, pDataTemp; // 位图数据指针
LPINT pBuffer, pBufferTemp; // 位图数据缓冲区指针
LPINT pMask;
// 确保滤波器窗口高宽均为奇数且大于一
ASSERT( wndHeight%2 && wndWidth%2 && wndHeight>1 && wndWidth>1 );
halfHeight = ( wndHeight-1 )/2;
halfWidth = ( wndWidth-1)/2;
pDataTemp = pData = m_lpImageData;
// 分配缓冲区
pBufferTemp = pBuffer = (LPINT)new INT[ m_nBytesPerRow * m_nHeight ];
memset( pBuffer, 0, m_nBytesPerRow*m_nHeight*sizeof(INT) );
// 模板卷积
for( h=1; h<m_nHeight-1; h++ )
{
for( w=1; w<m_nWidth-1; w++ )
{
pMask = mask;
for( i=-halfHeight; i<=halfHeight; i++ )
for( j=-halfWidth; j<=halfWidth; j++ )
*(pBufferTemp+w) += *( pData + (h+i)*m_nBytesPerRow + w+j ) * (*pMask++);
*(pBufferTemp+w) /= *pMask;
}
pBufferTemp += m_nBytesPerRow;
}
// 灰度范围变化
SpatialTransform( pBuffer );
// 将缓冲区数据拷贝到位图数据区中
pBufferTemp = pBuffer;
for( i=0; i<m_nHeight; i++ )
for( j=0; j<m_nBytesPerRow; j++ )
*pData++ = (BYTE)*pBufferTemp++;
delete []pBuffer;
}
// 灰度范围变换
void CImageSegmentation::SpatialTransform( INT* pBuffer )
{
LONG w, h;
INT maxGrayLevel, minGrayLevel;
// 统计图象的灰度区间
maxGrayLevel = minGrayLevel = 0;
for( h=0; h<m_nHeight; h++ )
for( w=0; w<m_nWidth; w++ )
{
if( maxGrayLevel < *(pBuffer+h*m_nBytesPerRow+w) )
maxGrayLevel = *(pBuffer+h*m_nBytesPerRow+w);
if( minGrayLevel > *(pBuffer+h*m_nBytesPerRow+w) )
minGrayLevel = *(pBuffer+h*m_nBytesPerRow+w); }
// 灰度空间变换
for( h=0; h<m_nHeight; h++ )
for( w=0; w<m_nWidth; w++ )
*(pBuffer+h*m_nBytesPerRow+w)= 255*( *(pBuffer+h*m_nBytesPerRow+w)-minGrayLevel ) /
(maxGrayLevel-minGrayLevel );
}
我们使用模板mask[10] = { -1,-1,-1,-1,-1,-1,-1,-1,8,1}可将图三变换为图四。
图象处理是一门复杂的学科,而作者水平有限,希望以上程序能给您一点启发,起到抛砖引玉的作用。
图三 灰度图象
图四 模板卷积后的图象
参考文献
1. 章毓晋. 1999. 图像处理和分析. 清华大学出版社
2. 孙家广,扬长贵. 1995. 计算机图形学(新版). 清华大学出版社
3. Clayton Walnum. 1995. Windows 95 Games SDK Strategy Guide. Que Corporation
4. Microsoft.1998. MSDN Library Visual Studio 6.0. Microsoft Corporation
|