二值图像的腐蚀和膨胀图像数字处理中应用相当广泛,代码处理也很简单,只不过一些资料在介绍腐蚀和膨胀原理时,用一些形态学、集合上的概念和术语,搞得也有些”高深莫测“了。
从图像处理角度看,二值图像的腐蚀和膨胀就是将一个小型二值图(结构元素,一般为3*3大小)在一个大的二值图上逐点移动并进行比较,根据比较的结果作出相应处理而已。以二值图的骨架为黑色点为例:
作图像腐蚀处理时,如果结构元素中的所有黑色点与它对应的大图像素点完全相同,该点为黑色,否则为白色。
作图像膨胀处理时,如果结构元素中只要有一个及以上黑色点与它对应的大图像素点相同,该点为黑色,否则为白色。也就是说,如果结构元素中的所有黑色点与它对应的大图像素点没有一个相同,该点为白色,否则为黑色。结构元素中的所有黑色点与它对应的大图像素点没有一个相同,说明大图的这些像素点都是白色的,假如二值图的骨架为白色点,这个对黑色骨架二值图的膨胀处理恰好是对白色骨架二值图的腐蚀处理。同理,对黑色骨架二值图的腐蚀处理也就是对白色骨架的膨胀处理。
根据这个道理,我们完全可以把对黑色骨架和白色骨架分别所作的腐蚀和膨胀处理代码统一起来,使得原来所需要的四段处理代码变成二段甚至一段处理代码。
下面是一个对32位像素格式二值图像数据的腐蚀和膨胀处理的全部代码:
// 定义ARGB像素结构
typedef union
{
ARGB Color;
struct
{
BYTE Blue;
BYTE Green;
BYTE Red;
BYTE Alpha;
};
}ARGBQuad, *PARGBQuad;
// ---------------------------------------------------------------------------
// 获取二值图像data的字节图数据map,骨架像素是否为黑色
VOID GetDataMap(CONST BitmapData *data, BitmapData *map, BOOL blackPixel)
{
// 字节图边缘扩展1字节,便于处理data的边缘像素
map->Width = data->Width + 2;
map->Height = data->Height + 2;
map->Stride = map->Width;
map->Scan0 = ( void*) new char[map->Stride * map->Height + 1]; // +1防最末字节越界
BYTE *ps = (BYTE*)data->Scan0;
BYTE *pd0 = (BYTE*)map->Scan0;
BYTE *pd = pd0 + map->Stride;
BYTE *pt = pd;
INT srcOffset = data->Stride - data->Width * sizeof(ARGBQuad);
UINT x, y;
// 如果骨架像素为黑色,获取异或字节图
if (blackPixel)
{
for (y = 0; y < data->Height; y ++, ps += srcOffset)
{
*pd ++ = *ps ^ 255;
for (x = 0; x < data->Width; x ++, ps += sizeof(ARGBQuad))
*pd ++ = *ps ^ 255;
*pd ++ = *(ps - sizeof(ARGBQuad)) ^ 255;
}
}
// 否则,获取正常字节图
else
{
for (y = 0; y < data->Height; y ++, *pd ++ = *(ps - sizeof(ARGBQuad)), ps += srcOffset)
{
for (x = 0, *pd ++ = *ps; x < data->Width; x ++, *pd ++ = *ps, ps += sizeof(ARGBQuad));
}
}
ps = pd - map->Stride;
for (x = 0; x < map->Width; x ++, *pd0 ++ = *pt ++, *pd ++ = *ps ++);
}
// ---------------------------------------------------------------------------
// 按结构元素模板templet制作字节掩码数组masks
// templet低3字节的低3位对应结构元素,如下面的结构元素:
// 水平 垂直 十字 方形 其它
// ○ ○ ○ ○ ● ○ ○ ● ○ ● ● ● ○ ● ○
// ● ● ● ○ ● ○ ● ● ● ● ● ● ● ● ●
// ○ ○ ○ ○ ● ○ ○ ● ○ ● ● ● ○ ● ●
// 用templet分别表示为:0x000700, 0x020202, 0x020702, 0x070707, 0x020703
VOID GetTempletMasks(DWORD templet, DWORD masks[])
{
for (INT i = 2; i >= 0; i --, templet >>= 8)
{
masks[i] = 0;
for (UINT j = 4; j; j >>= 1)
{
masks[i] <<= 8;
if (templet & j) masks[i] |= 1;
}
}
}
// ---------------------------------------------------------------------------
VOID Erosion_Dilation(BitmapData *data, DWORD templet, BOOL blackPixel)
{
BitmapData map;
GetDataMap(data, &map, blackPixel);
PARGBQuad pd = (PARGBQuad)data->Scan0;
BYTE *ps = (BYTE*)map.Scan0 + map.Stride;
INT width = (INT)data->Width;
INT height = (INT)data->Height;
INT dstOffset = data->Stride - width * sizeof(ARGBQuad);
INT value = blackPixel? 0 : 255;
INT x, y;
if (templet == 0x0700) // 水平结构元素单独处理,可提高处理速度
{
for (y = 0; y < height; y ++, (BYTE*)pd += dstOffset, ps += 2)
{
for (x = 0; x < width; x ++, pd ++, ps ++)
{
if (*(DWORD*)ps & 0x010101)
pd->Blue = pd->Green = pd->Red = value;
}
}
}
else
{
DWORD masks[ 3];
GetTempletMasks(templet, masks);
for (y = 0; y < height; y ++, (BYTE*)pd += dstOffset, ps += 2)
{
for (x = 0; x < width; x ++, pd ++, ps ++)
{
if (*(DWORD*)(ps - map.Stride) & masks[ 0] ||
*(DWORD*)ps & masks[ 1] ||
*(DWORD*)(ps + map.Stride) & masks[ 2])
pd->Blue = pd->Green = pd->Red = value;
}
}
}
delete map.Scan0;
}
// ---------------------------------------------------------------------------
// 二值图膨胀。参数:二值图数据,结构元素模板,是否黑色像素骨架
FORCEINLINE
VOID Dilation(BitmapData *data, DWORD templet, BOOL blackPixel = TRUE)
{
Erosion_Dilation(data, templet, blackPixel);
}
// ---------------------------------------------------------------------------
// 二值图腐蚀。参数:二值图数据,结构元素模板,是否黑色像素骨架
FORCEINLINE
VOID Erosion(BitmapData *data, DWORD templet, BOOL blackPixel = TRUE)
{
Erosion_Dilation(data, templet, !blackPixel);
}
// ---------------------------------------------------------------------------
本文的二值图像的腐蚀和膨胀处理代码有以下特点:
1、可使用任意的3*3结构元素进行处理。
2、可对黑色或者白色骨架二值图进行处理。
3、在复制字节图时对边界像素作了扩展,以便对二值图的边界处理。
4、没有采用结构元素逐点比较的作法,而是使用结构元素掩码,每次对三个像素进行逻辑运算,特别是对水平结构元素的单独处理,大大提高了处理效率。
5、上面的代码虽然针对的是32位像素格式的二值图,但稍作修改即可适应24位或者8位像素格式二值图像数据。其实,对于32位像素格式的二值图,改用下面的处理代码可提高图像处理速度(因其使用位运算一次性对像素的R、G、B分量进行了赋值):
VOID _Dilation(BitmapData *data, BitmapData *map, DWORD templet)
{
PARGBQuad pd = (PARGBQuad)data->Scan0;
BYTE *ps = (BYTE*)map->Scan0 + map->Stride;
INT width = (INT)data->Width;
INT height = (INT)data->Height;
INT dstOffset = data->Stride - width * sizeof(ARGBQuad);
INT x, y;
if (templet == 0x0700) // 水平结构元素单独处理,可提高处理速度
{
for (y = 0; y < height; y ++, (BYTE*)pd += dstOffset, ps += 2)
{
for (x = 0; x < width; x ++, pd ++, ps ++)
if (*(DWORD*)ps & 0x010101)
pd->Color &= 0xff000000;
}
}
else
{
DWORD masks[ 3];
GetTempletMasks(templet, masks);
for (y = 0; y < height; y ++, (BYTE*)pd += dstOffset, ps += 2)
{
for (x = 0; x < width; x ++, pd ++, ps ++)
if (*(DWORD*)(ps - map->Stride) & masks[ 0] ||
*(DWORD*)ps & masks[ 1] ||
*(DWORD*)(ps + map->Stride) & masks[ 2])
pd->Color &= 0xff000000;
}
}
}
// ---------------------------------------------------------------------------
VOID _Erosion(BitmapData *data, BitmapData *map, DWORD templet)
{
PARGBQuad pd = (PARGBQuad)data->Scan0;
BYTE *ps = (BYTE*)map->Scan0 + map->Stride;
INT width = (INT)data->Width;
INT height = (INT)data->Height;
INT dstOffset = data->Stride - width * sizeof(ARGBQuad);
INT x, y;
if (templet == 0x0700) // 水平结构元素单独处理,可提高处理速度
{
for (y = 0; y < height; y ++, (BYTE*)pd += dstOffset, ps += 2)
{
for (x = 0; x < width; x ++, pd ++, ps ++)
if (*(DWORD*)ps & 0x010101)
pd->Color |= 0x00ffffff;
}
}
else
{
DWORD masks[ 3];
GetTempletMasks(templet, masks);
for (y = 0; y < height; y ++, (BYTE*)pd += dstOffset, ps += 2)
{
for (x = 0; x < width; x ++, pd ++, ps ++)
if (*(DWORD*)(ps - map->Stride) & masks[ 0] ||
*(DWORD*)ps & masks[ 1] ||
*(DWORD*)(ps + map->Stride) & masks[ 2])
pd->Color |= 0x00ffffff;
}
}
}
// ---------------------------------------------------------------------------
// 二值图膨胀。参数:二值图数据,结构元素模板,是否黑色像素骨架
VOID Dilation(BitmapData *data, DWORD templet, BOOL blackPixel = TRUE)
{
BitmapData map;
GetDataMap(data, &map, blackPixel);
if (blackPixel)
_Dilation(data, &map, templet);
else
_Erosion(data, &map, templet);
delete map.Scan0;
}
// ---------------------------------------------------------------------------
// 二值图腐蚀。参数:二值图数据,结构元素模板,是否黑色像素骨架
VOID Erosion(BitmapData *data, DWORD templet, BOOL blackPixel = TRUE)
{
Dilation(data, templet, !blackPixel);
}
// ---------------------------------------------------------------------------
下面是使用BCB2007和GDI+位图对黑色骨架二值图进行的开运算处理,也可看作是对白色骨架二值图的闭运算处理(先腐蚀后膨胀为开运算;先膨胀后腐蚀为闭运算),其中的GrayAnd2Values函数是对图像作灰度和二值化处理,代码见《图像的灰度与二值化》一文:
// 锁定GDI+位位图扫描线到data
FORCEINLINE
VOID LockBitmap(Gdiplus::Bitmap *bmp, BitmapData *data)
{
Gdiplus::Rect r( 0, 0, bmp->GetWidth(), bmp->GetHeight());
bmp->LockBits(&r, ImageLockModeRead | ImageLockModeWrite,
PixelFormat32bppARGB, data);
}
// ---------------------------------------------------------------------------
// GDI+位图扫描线解锁
FORCEINLINE
VOID UnlockBitmap(Gdiplus::Bitmap *bmp, BitmapData *data)
{
bmp->UnlockBits(data);
}
// ---------------------------------------------------------------------------
void __fastcall TForm1::Button1Click(TObject *Sender)
{
Gdiplus::Bitmap *bmp = new Gdiplus::Bitmap(L " d:\\source1.jpg ");
Gdiplus::Graphics *g = new Gdiplus::Graphics(Canvas->Handle);
BitmapData data;
LockBitmap(bmp, &data);
GrayAnd2Values(&data, 128);
Erosion(&data, 0x020702);
Dilation(&data, 0x020702);
UnlockBitmap(bmp, &data);
g->DrawImage(bmp, 0, 0);
delete g;
delete bmp;
}
// ---------------------------------------------------------------------------
下面是使用本文代码所作的各种处理效果图,结构元素为“十”字形:
左上原图,右上二值图,左中腐蚀图,右中膨胀图,左下开运算图,右下闭运算图。这是对黑色骨架二值图而言,如果是白色骨架二值图,中间和下边的左右处理效果就是相反的了。
如有错误或者建议,请来信指导:maozefa@hotmail.com