C++ 中的图像处理应用程序






4.98/5 (35投票s)
代码描述了一个利用 C++ 中 CImage 类的多文档界面 (MDI) 图像处理应用程序
引言
一些基本的图像处理功能涉及使用滤镜或直方图函数来处理像素,这些函数会修改像素的分布。其中一些可以以各种方式增强图像显示或去除噪声。本文将介绍为MFC多文档界面(MDI)图像处理应用程序开发的C++代码。
背景
该应用程序名为Imagr
(出于历史原因,省略了“e”),构建为MFC多文档界面,并利用Microsoft ATL的CImage
类,因为它已经内置了打开和保存最流行格式(bmp、jpg、tif、gif和png)图像的功能。Imagr
还能够打开pcx类型的图像(感谢Roger Evans提供的原始代码)、某些ASCII文本图像和某些“原始”二进制类型图像。CImage
的成员函数CImage::Load
将图像文件读取到自下而上(原点在图像左下角)的设备无关位图(DIB)中。但是,这在访问像素时可能不方便,因此图像被重新格式化为自顶向下(原点在图像左上角)的DIB和32位(因为现在大多数显示适配器都是真彩)。请参见Convert32Bit()
代码。
一旦进入自顶向下模式,像素操作就变得简单多了。可以使用CImage::GetBits()
返回指向第一个像素(左上角像素)的指针,然后通过在单个for
循环中简单地递增指针来访问后续像素。图像被分为三种像素类型:灰度、彩色和整数(有时称为原始,它们是从特殊数据采集过程派生的)。灰度图像的处理速度比具有三个颜色通道的彩色图像快。原始整数图像可以具有更高的位深度进行处理,但必须将其降低到8位(值0…255)才能显示。
需要强调的一点是,Microsoft的Cimage
类存储颜色位的顺序与常规位图不同。它不是RGB,而是BGR(蓝、绿、红)。因此,如果您使用典型的GetRValue()
函数来获取红色位,它将返回蓝色位。如果您使用RGB(red, green, blue)宏存储红色、绿色和蓝色值,红色和蓝色将显示错误。绿色位是相同的,只是蓝色和红色被翻转了。因此,在代码中(请参见ImagrDoc.h),使用了以下定义来帮助正确处理颜色。
#define RED(rgb) (LOBYTE((rgb) >> 16))
#define GRN(rgb) (LOBYTE(((WORD)(rgb)) >> 8))
#define BLU(rgb) (LOBYTE(rgb))
#define BGR(b,g,r) RGB(b,g,r)
因此,例如,RED(p)
将返回从传入的像素p
中预期的红色位,而BGR(b, g, r)
将红色、绿色和蓝色字节存储到32位像素中以正确显示。
MDI的优点
将Imagr
设计为MDI应用程序具有能够比较图像或执行双图像操作(如下文所述)的优点。此外,能够制作图像的副本也很重要,例如,您可以保存滤镜操作的状态,然后再尝试其他滤镜,或者使用图像的副本或处理过的图像副本进行双图像操作。MDI还会执行一些重要的后台任务。通过调用SetModifiedFlag()
,可以维护图像的更改状态,因此如果图像被关闭,MDI将自动提示用户先保存。MDI还允许将文件拖放到应用程序中,并通过鼠标单击来协调哪个图像是活动的。
Using the Code
该应用程序的代码包含为Microsoft Visual Studio 2008或2010设计的完整项目。
直方图函数
在当前应用程序的上下文中,直方图是图像中像素值分布的图形表示。以一个256维数组(或彩色图像为256 x 3)维护每个像素值的计数,并绘制到对话框窗口。(注意:在Imagr
代码中,您会看到像素<0和>255也显示在直方图中,以处理原始类型的完整整数图像)。下图显示了一个灰度图像及其相关的直方图。从直方图中可以看出,该图像中的大多数像素位于32…112的区域。
下面彩色图像的直方图示例显示了红色、绿色和蓝色通道的分布。
通过一个归一化函数(也称为对比度拉伸),可以根据需要拉伸或压缩直方图曲线。通常,这是为了将像素范围扩展到完整的亮度范围(0到255),从而获得更均匀对比度的图像。下图显示了一个低对比度图像在归一化之前和之后的示例,以及相应的直方图。归一化有效地重新分布了直方图,而不会明显改变直方图曲线。
应用于每个像素的归一化算法是
pixel = (pixel - min)*(nmax - nmin) / (max - min) + nmin
其中max
和min
是图像中初始的最大值和最小值像素值,而nmax
和nmin
是选择用于归一化的新最大值和新最小值像素值。
下面显示了处理三种图像像素类型(灰度、彩色和“原始”整数)的归一化代码。在灰度代码中调用了RED()
宏来隔离整数像素的低字节,实际上在这个例子中它不是红色(为清楚起见,我可能应该创建一个GREY()
宏来完成同样的事情)。此函数从一个双滑块类调用(感谢CodeProject文章“A Slider with Two Buttons”,2006年8月9日,作者includeh10)。归一化函数与双滑块绑定,因此您可以在调整滑块时近乎实时地对图像进行归一化(取决于图像大小和系统速度)。在每次滑块调整之前,图像会被复制到“撤销”缓冲区,以便函数每次更改滑块时都在相同的起始图像上操作。滑块控制传递给函数的nmin
和nmax
变量。
/*----------------------------------------------------------------------
This function normalizes the bitmap to the passed range.
(For calling interactively from the slider dialog.)
----------------------------------------------------------------------*/
void CImagrDoc::Nrmlz(int nmin, int nmax)
{
int d;
float factor;
byte r = 0, g = 0, b = 0;
OnDo(); // Save prev. image for Undo
int *min = &(m_image.minmax.min);
int *max = &(m_image.minmax.max);
if (*max - *min == 0)
factor = 32767.; // Avoid div. by 0
else
factor = (float)((float)(nmax - nmin) / (*max - *min));
int *p = (int *) m_image.GetBits(); // Ptr to bitmap
unsigned long n = GetImageSize();
switch (m_image.ptype) {
case GREY: // Grey scale pixels
for ( ; n > 0; n--, p++) {
r = RED(*p);
d = (int)((float)(r - *min) * factor + nmin + 0.5);
r = (byte)THRESH(d);
*p = BGR(r, r, r);
}
break;
case cRGB: // RGB color pixels
for ( ; n > 0; n--, p++) {
r = RED(*p);
d = (int)((float)(r - *min) * factor + nmin + 0.5);
r = (byte)THRESH(d);
g = GRN(*p);
d = (int)((float)(g - *min) * factor + nmin + 0.5);
g = (byte)THRESH(d);
b = BLU(*p);
d = (int)((float)(b - *min) * factor + nmin + 0.5);
b = (byte)THRESH(d);
*p = BGR(b, g, r);
}
break;
default: // INTG
for ( ; n > 0; n--, p++) {
r = (int)((float)(*p - *min) * factor + nmin + 0.5);
*p = BGR(r, r, r);
}
m_image.ptype = GREY; // Changed type
break;
}
*min = nmin;
*max = nmax;
UpdateAllViews(NULL, ID_SBR_IMAGEMINMAX);
}
请注意,此归一化仅限于在最小到最大像素范围上进行操作。有时,人们可能希望扩展或收缩直方图的狭窄范围。Imagr
在NrmlzRange()
函数中包含了此功能,算法如下:
pixel = (pixel - rmin)*255 / (rmax - rmin)
其中rmax
到rmin
是选择的像素值范围。还使用双滑块来调整rmin
和rmax
变量。这允许选择一个直方图范围并将其归一化到0到255的范围。此过程可能会导致像素值小于0或大于255,这超出了可显示范围。值超出0到255范围的像素将被阈值化到该范围,以便它们可以显示。任何pixels < 0
都被设置为等于0
,任何pixels > 255
都被设置为等于255
。下面的图像和直方图演示了这一点。图像在其整个范围内具有更高的对比度,但代价是在直方图的末端进行了阈值处理。
另一个流行的直方图函数“均衡化”或展平像素分布,从而在整个像素范围内获得更均匀的对比度,如下所示(感谢Frank Hoogterp和Steven Caito提供的原始Fortran代码)。可以看出,均衡化可能会使图像“斑驳”,并有效丢失分辨率,但可能对某些图像有用。均衡化使用直方图数组来重新分配像素。(有关详细信息,请参见Eqliz()
代码)。
Imagr
还有一个阈值菜单功能,该功能与双滑块绑定,因此可以用来限制直方图中像素的范围并近乎实时地观察结果。这对于选择性地削减图像中的指定亮度非常有用。
图像处理滤镜
可以应用许多滤镜来实现不同的图像处理功能。convolve
函数(Convl.cpp)用于将3x3内核(矩阵)滤镜应用于图像。Imagr
当前包含许多滤镜的菜单选项,包括:低通、高通、Sobel、Prewitt、Frei-Chen、各种边缘增强和拉普拉斯滤镜、浮雕滤镜,以及一个内核输入对话框,允许用户尝试自己的3x3内核。这些滤镜的具体功能可以在互联网上找到,此处不再赘述。其中一些滤镜已应用于图1的图像,如下所示。
convolve
方程如下:
P5 =∑i=1…9 (Ki * Pi) / ∑i=1…9 (Ki)
其中P5
= 3x3像素区域的中心,Pi
= 九个像素中的每一个,而Ki
= 九个内核值中的每一个。中心像素将被其周围3x3像素(包括自身)与其相应内核值乘积的总和,除以内核值的总和所替换。此操作应用于图像中的每个像素。
高通内核如下:
-1.0, -1.0, -1.0
-1.0, 9.0, -1.0
-1.0, -1.0, -1.0
这基本上是将周围像素的倒数加上中心像素(加权为* 9)的总和。请参阅ImagrDoc.h以了解Imagr
中使用的其他内核。
下面显示了卷积滤镜代码的一部分,为了简洁起见,仅包含灰度部分(请参阅Convl.cpp获取完整代码)。3x3内核被传递给函数。
/*----------------------------------------------------------------------
This function performs a 3 x 3 convolution on the active image. The
kernel array is passed externally. Edges are added (doubly weighted)
for the computation.
----------------------------------------------------------------------*/
void CImagrDoc::Convl(float k1, float k2, float k3,
float k4, float k5, float k6,
float k7, float k8, float k9)
{
int *p; /* Image ptr */
unsigned long i, j, nx, ny;
int *m1, *m2, *m3; // Pointers to buffers to free()
int *old_r1, *r1, *r2, *r3; /* Cycling pointers to rows */
float s, fsum;
int t;
byte r, g, b;
nx = m_image.GetWidth();
ny = m_image.GetHeight();
p = (int *) m_image.GetBits(); // Ptr to bitmap
/* Allocate row buffers */
if (!(m1 = (int *) malloc((nx+2) * sizeof(*m1)))) {
fMessageBox("Error - " __FUNCTION__, MB_ICONERROR, "malloc() error m1");
return;
}
if (!(m2 = (int *) malloc((nx+2) * sizeof(*m2)))) {
fMessageBox("Error - " __FUNCTION__, MB_ICONERROR, "malloc() error m2");
free(m1);
return;
}
if (!(m3 = (int *) malloc((nx+2) * sizeof(*m3)))) {
fMessageBox("Error - " __FUNCTION__, MB_ICONERROR, "malloc() error m3");
free(m1);
free(m2);
return;
}
r1 = m1;
r2 = m2;
r3 = m3;
// Initialize rows
memcpy_s(&r1[1], nx * sizeof(int), p, nx * sizeof(int));
r1[0] = r1[1]; /* Doubly weight edges */
r1[nx+1] = r1[nx];
/* Start r2 = r1 (doubly weight 1st row) */
memcpy_s(r2, (nx+2) * sizeof(int), r1, (nx+2) * sizeof(int));
// Calc. sum of kernel
fsum = k1 + k2 + k3 + k4 + k5 + k6 + k7 + k8 + k9;
if (fsum == 0)
fsum = 1; // Avoid div. by 0
else
fsum = 1/fsum; // Invert so can mult.
OnDo(); // Save image for Undo
BeginWaitCursor();
switch (m_image.ptype) {
case GREY:
for (j = 1; j <= ny; j++, p += nx) {
if (j == ny) { /* Last row */
r3 = r2; /* Last row doubly weighted */
}
else { /* Read next row (into the 3rd row) */
memcpy_s(&r3[1], nx * sizeof(int), p + nx, nx * sizeof(int));
r3[0] = r3[1]; /* Doubly weight edges */
r3[nx+1] = r3[nx];
}
for (i = 0; i < nx; i++) {
s = k1 * (float)RED(r1[i])
+ k2 * (float)RED(r1[i+1])
+ k3 * (float)RED(r1[i+2])
+ k4 * (float)RED(r2[i])
+ k5 * (float)RED(r2[i+1])
+ k6 * (float)RED(r2[i+2])
+ k7 * (float)RED(r3[i])
+ k8 * (float)RED(r3[i+1])
+ k9 * (float)RED(r3[i+2]);
t = NINT(s * fsum);
r = (byte)THRESH(t);
p[i] = RGB(r, r, r);
}
/* Cycle row pointers */
old_r1 = r1; // To save addr. for r3
r1 = r2;
r2 = r3;
r3 = old_r1;
}
break;
}
EndWaitCursor();
free(m1);
free(m2);
free(m3);
ChkData(); // Re-check range
SetModifiedFlag(true); // Set flag
UpdateAllViews(NULL); // Still needed even though called by ChkData()
}
卷积函数代码(再次感谢Frank Hoogterp和Steven Caito提供的原始Fortran代码)通过将图像的三行存储在三个数组中来一次访问它们。行数组的指针被维护,以便在从图像加载新行时方便地向上移动行。例如,在处理新行时,指向行数组二和三(r2
和r3
)的指针分别设置为指向r1
和r2
。因此,只有第三行需要用图像中的像素更新,而这成为新的r3
(之前指向r1
的指针)。
边缘行和列通过双重加权来处理。例如,在处理第一行中的像素时,第一行被复制到第二行数组中,以便仍然有三行(第三行数组包含第二行)用于处理。这就像第一行被复制到实际第一行的上方一样。垂直边缘(列)通过在行的开头和结尾复制一个额外的像素来类似地处理。
双图像处理
双图像函数接收两个图像作为输入并生成第三个图像。一些操作包括:加法、减法、乘法、除法、平均、最小值、最大值以及逻辑按位函数OR、AND和XOR。例如,add
函数从图像A的(x, y)位置获取像素,并将其添加到图像B的相应像素(x, y),然后将此和存储在图像C的相应像素(x, y)中。此操作对图像中的每个像素进行。Imagr
的当前版本仅允许相同大小的图像进行这些操作。减法操作产生一个由两个输入图像之间的差异组成的图像。减法有时在边缘增强后进行,以显示叠加在原始图像上的边缘效果。下面的同一对图像显示了加法和减法的效果。
撤销栈
如上文简要提到的,Imagr
还具有撤销功能。这对于图像处理应用程序非常重要,因为许多函数可能会对图像执行不期望的操作,因此能够轻松返回到之前的状态并尝试其他操作非常重要。OnDo
和UnDo
函数(在文件Undo.cpp中)分别从内存栈中“压入”和“弹出”图像,该内存栈由链表实现。当要对图像进行更改时,首先调用OnDo
来保存其当前状态。
Undo
数据结构如下:
struct Undo_type { // Linked list of image buffers for undo
int *p; // Ptr to pixel buffer
BOOL mod; // Doc. modified status
int ptype; // Pixel type needed in case changed
char hint[80]; // OnDo() caller's hint for OnUndo()
Undo_type *next; // Ptr to next node
};
变量“mod
”维护图像的保存状态,因此在从栈中弹出图像时,调用SetModifiedFlag()
将恢复“已保存”状态。“hint
”string
的目的是跟踪某些操作,这些操作可以在不要求完全内存保存图像的情况下进行撤销。例如,如果图像旋转90度,UnDo
函数可以使用提示来知道只需执行-90度的旋转来恢复图像。因此,不需要将所有图像像素保存在内存中。尽管此撤销功能目前在Imagr
中尚未实现,但它可以提供更快的运行速度并节省内存资源。
如上所述,一些使用滑块对话框的函数会利用撤销功能,在操纵滑块时快速对图像进行OnDo
(压入)和UnDo
(弹出)。这提供了交互式功能,可以近乎实时地看到滑块操作对图像的影响。
结论
总而言之,讨论了用于直方图、卷积滤镜和双图像操作的一些基本图像处理代码。尽管存在许多图像处理应用程序,但开发自己的应用程序以获得自定义功能也非常有价值。Imagr
包含其他未在此处讨论的图像处理功能。请参阅我之前在CodeProject上发表的文章“Drawing an Image as a 3-D Surface”,2011年7月,其中讨论了Imagr
的3D图形功能。