65.9K
CodeProject 正在变化。 阅读更多。
Home

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

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.98/5 (35投票s)

2014年6月24日

CPOL

11分钟阅读

viewsIcon

127033

downloadIcon

8082

代码描述了一个利用 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的区域。

图 1 - 带有直方图的灰度图像

下面彩色图像的直方图示例显示了红色、绿色和蓝色通道的分布。

图 2 - 全彩色图像直方图

通过一个归一化函数(也称为对比度拉伸),可以根据需要拉伸或压缩直方图曲线。通常,这是为了将像素范围扩展到完整的亮度范围(0到255),从而获得更均匀对比度的图像。下图显示了一个低对比度图像在归一化之前和之后的示例,以及相应的直方图。归一化有效地重新分布了直方图,而不会明显改变直方图曲线。

图 3 - 归一化之前(上)和之后(下)带有直方图的图像

应用于每个像素的归一化算法是

pixel = (pixel - min)*(nmax - nmin) / (max - min) + nmin

其中maxmin是图像中初始的最大值和最小值像素值,而nmaxnmin是选择用于归一化的新最大值和新最小值像素值。

下面显示了处理三种图像像素类型(灰度、彩色和“原始”整数)的归一化代码。在灰度代码中调用了RED()宏来隔离整数像素的低字节,实际上在这个例子中它不是红色(为清楚起见,我可能应该创建一个GREY()宏来完成同样的事情)。此函数从一个双滑块类调用(感谢CodeProject文章“A Slider with Two Buttons”,2006年8月9日,作者includeh10)。归一化函数与双滑块绑定,因此您可以在调整滑块时近乎实时地对图像进行归一化(取决于图像大小和系统速度)。在每次滑块调整之前,图像会被复制到“撤销”缓冲区,以便函数每次更改滑块时都在相同的起始图像上操作。滑块控制传递给函数的nminnmax变量。

/*----------------------------------------------------------------------
  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);    
}

请注意,此归一化仅限于在最小到最大像素范围上进行操作。有时,人们可能希望扩展或收缩直方图的狭窄范围。ImagrNrmlzRange()函数中包含了此功能,算法如下:

pixel = (pixel - rmin)*255 / (rmax - rmin)

其中rmaxrmin是选择的像素值范围。还使用双滑块来调整rminrmax变量。这允许选择一个直方图范围并将其归一化到0到255的范围。此过程可能会导致像素值小于0或大于255,这超出了可显示范围。值超出0到255范围的像素将被阈值化到该范围,以便它们可以显示。任何pixels < 0都被设置为等于0,任何pixels > 255都被设置为等于255。下面的图像和直方图演示了这一点。图像在其整个范围内具有更高的对比度,但代价是在直方图的末端进行了阈值处理。

图 4 - 范围归一化

另一个流行的直方图函数“均衡化”或展平像素分布,从而在整个像素范围内获得更均匀的对比度,如下所示(感谢Frank Hoogterp和Steven Caito提供的原始Fortran代码)。可以看出,均衡化可能会使图像“斑驳”,并有效丢失分辨率,但可能对某些图像有用。均衡化使用直方图数组来重新分配像素。(有关详细信息,请参见Eqliz()代码)。

图 5 - 均衡化

Imagr还有一个阈值菜单功能,该功能与双滑块绑定,因此可以用来限制直方图中像素的范围并近乎实时地观察结果。这对于选择性地削减图像中的指定亮度非常有用。

图像处理滤镜

可以应用许多滤镜来实现不同的图像处理功能。convolve函数(Convl.cpp)用于将3x3内核(矩阵)滤镜应用于图像。Imagr当前包含许多滤镜的菜单选项,包括:低通、高通、Sobel、Prewitt、Frei-Chen、各种边缘增强和拉普拉斯滤镜、浮雕滤镜,以及一个内核输入对话框,允许用户尝试自己的3x3内核。这些滤镜的具体功能可以在互联网上找到,此处不再赘述。其中一些滤镜已应用于图1的图像,如下所示。

   

    图 6 - 高通                       图 7 - Sobel                    图 8 - 边缘增强

 

        图 9 - 浮雕                图 10 - 拉普拉斯锐化

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代码)通过将图像的三行存储在三个数组中来一次访问它们。行数组的指针被维护,以便在从图像加载新行时方便地向上移动行。例如,在处理新行时,指向行数组二和三(r2r3)的指针分别设置为指向r1r2。因此,只有第三行需要用图像中的像素更新,而这成为新的r3(之前指向r1的指针)。

边缘行和列通过双重加权来处理。例如,在处理第一行中的像素时,第一行被复制到第二行数组中,以便仍然有三行(第三行数组包含第二行)用于处理。这就像第一行被复制到实际第一行的上方一样。垂直边缘(列)通过在行的开头和结尾复制一个额外的像素来类似地处理。

双图像处理

双图像函数接收两个图像作为输入并生成第三个图像。一些操作包括:加法、减法、乘法、除法、平均、最小值、最大值以及逻辑按位函数OR、AND和XOR。例如,add函数从图像A的(x, y)位置获取像素,并将其添加到图像B的相应像素(x, y),然后将此和存储在图像C的相应像素(x, y)中。此操作对图像中的每个像素进行。Imagr的当前版本仅允许相同大小的图像进行这些操作。减法操作产生一个由两个输入图像之间的差异组成的图像。减法有时在边缘增强后进行,以显示叠加在原始图像上的边缘效果。下面的同一对图像显示了加法和减法的效果。

图 11 - 图像加法

图 12 - 图像减法

撤销栈

如上文简要提到的,Imagr还具有撤销功能。这对于图像处理应用程序非常重要,因为许多函数可能会对图像执行不期望的操作,因此能够轻松返回到之前的状态并尝试其他操作非常重要。OnDoUnDo函数(在文件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()将恢复“已保存”状态。“hintstring的目的是跟踪某些操作,这些操作可以在不要求完全内存保存图像的情况下进行撤销。例如,如果图像旋转90度,UnDo函数可以使用提示来知道只需执行-90度的旋转来恢复图像。因此,不需要将所有图像像素保存在内存中。尽管此撤销功能目前在Imagr中尚未实现,但它可以提供更快的运行速度并节省内存资源。

如上所述,一些使用滑块对话框的函数会利用撤销功能,在操纵滑块时快速对图像进行OnDo(压入)和UnDo(弹出)。这提供了交互式功能,可以近乎实时地看到滑块操作对图像的影响。

结论

总而言之,讨论了用于直方图、卷积滤镜和双图像操作的一些基本图像处理代码。尽管存在许多图像处理应用程序,但开发自己的应用程序以获得自定义功能也非常有价值。Imagr包含其他未在此处讨论的图像处理功能。请参阅我之前在CodeProject上发表的文章“Drawing an Image as a 3-D Surface”,2011年7月,其中讨论了Imagr的3D图形功能。

© . All rights reserved.