视频1 视频21 视频41 视频61 视频文章1 视频文章21 视频文章41 视频文章61 推荐1 推荐3 推荐5 推荐7 推荐9 推荐11 推荐13 推荐15 推荐17 推荐19 推荐21 推荐23 推荐25 推荐27 推荐29 推荐31 推荐33 推荐35 推荐37 推荐39 推荐41 推荐43 推荐45 推荐47 推荐49 关键词1 关键词101 关键词201 关键词301 关键词401 关键词501 关键词601 关键词701 关键词801 关键词901 关键词1001 关键词1101 关键词1201 关键词1301 关键词1401 关键词1501 关键词1601 关键词1701 关键词1801 关键词1901 视频扩展1 视频扩展6 视频扩展11 视频扩展16 文章1 文章201 文章401 文章601 文章801 文章1001 资讯1 资讯501 资讯1001 资讯1501 标签1 标签501 标签1001 关键词1 关键词501 关键词1001 关键词1501 专题2001
数字图像处理源程序及详解之灰度直方图
2025-09-25 21:36:32 责编:小OO
文档
图像的点运算 

  

图像的点运算是图像处理中相对简单的技术,它主要用于改变一幅图像的灰度分布范围。点运算通过一个变换函数将图 像的像素一一转换,最终构成一幅新的图像。由于操作对象是图像的一个个像素,故得名为“点运算”。点运算的最大特点是输出像素值只与当前输入像素值有关。 点运算的图像处理过程可以用以下公式表示:

g(x, y)=T[f(x, y)]

其中f(x, y)表示输入图像,g(x, y)表示处理后的图像。函数T是对f的一种变换操作,在这里它表示灰度变换公式。可以看到,对于点运算而言,最重要的是确定灰度变换公式。变换公式一旦确 定,点运算对于图像的处理效果就确定了。

本章研究的主要内容包括灰度直方图、线性变换、非线性变换、阈值变换、灰度拉伸及灰度均衡等。若无特别说明,本 章的点运算函数所针对的待处理对象即为8位灰度图。

■ 本章学习地图

◆ 学会利用灰度直方图查看图像信息

◆ 了解各种灰度变换公式

◆ 掌握各种灰度变换的实现方法

◆ 进一步了解各种点运算对于图像效果的影响

9.1  灰度直方图

本节介绍灰度直方图的相关概念和实现原理,它是提取图像信息的重要工具之一。

9.1.1  灰度直方图

任何一幅图像都包含着丰富的图像信息,对于图像处理而言,如何提取这些信息并找出其中的特征就显得十分关键。 灰度直方图直观地显示了图像灰度分布的情况,这些信息在图像灰度变换等处理过程中显得十分重要。在本章随后的内容中,也会经常通过直方图来分析变换后的图 像效果。

图9-1显示一幅灰度图及它所对应的灰度直方图。 可以看到,灰度直方图是一个二维图。从数学上来说,它描绘了图像各灰度值的统计特性,显示了各个灰度级出现的次数或概率。从图形上来说,其横坐标表示图像 的灰度值,取值范围是0到255。其纵坐标则通过高度来表示出现次数的多少或者概率的高低。在本章中,纵坐标表示像素出现的次数,最大值为该图像在0至 255阶灰度上分布像素出现次数的最大值。

图9-1  利用灰度直方图显示图像灰度分布

需要说明的是,如果没有特别指出,在本章后续内容 中的所有变换都是基于图9-1中左侧的图像进行的。

接下来分析直方图的作用。图9-2所示的4幅图像 都是根据同一幅图像进行基本灰度特征处理后获得的,分别是高亮度、低亮度、高对比度、低对比度4种类型的图像。每幅图像右侧都显示了对应的直方图。仔细观 察可以发现如下特征。

图9-2  4种基本图像及对应的直方图

高亮度图像的直方图组成集中在灰度高的一侧。8位 灰度图能表示256种灰度,也就是灰度取值范围为0至255。其中0表示黑色,255表示白色。对于高亮度图像,整个画面的颜色偏亮,故灰度直方图偏向灰 度高的一侧。相反,低亮度图像的直方图则偏向灰度较低的一侧。

在高对比度的图像中,直方图的覆盖范围很广。图像 在任意一段灰度范围中都有一定的像素数量。同时,高对比图像的灰度分布相比其他图像而言较为均匀,整个直方图显得比较平滑。而低对比度图像的灰度则主要分 布在中间狭窄的区域中,图像就像被冲淡了一样。

9.1.2  基本原理

灰度直方图的基本思想是统计。对于拥有256种灰度的图像,灰度值为k的像素个数由一个离散函 数确定:

其中nk表 示当前图像灰度值为k的像素个数,则对应的出现概率可以使用如下公式表示:

,显然有成 立。

其中n表示图像像素个数的总和,可 以用图像宽度与高度的乘积来表示。

本章灰度直方图的坐标系为。 横坐标表示输入灰度值k,而纵坐标表示对应灰度值的统计个数nk。可见,绘制直方图最重要的是确定灰 度值为k的像素个数。直方图在绘制时采用相对高度,即纵坐标的最大值为ymax=max(n0,n1,n2……,n254,n255), 如果ymax的绘制高度为1,灰度值k的绘制高度为。

9.1.3  编程实现

1.效果预览

本节通过对话框实现灰度直方图的显示,其效果见图 9-3所示。

图9-3  灰度直方图对话框

灰度直方图对话框实现了如下功 能。

l          灰度直方图的显示。

l          允许用户修改显示灰度的上限和下限。对于一些图像而言,可能某个像素值的计数远远大于其他像素的个数,这样往往看不清其他灰度的分布。例如白色背景的图 片,拥有灰度值255的像素个数相对较多。该对话框提供了一个灰度区间显示的功能,如图9-4所示。

图9-4  灰度直方图的区间显示

可以看到,改变显示灰度的范围后,用户能够更加清 楚地了解某一区域的灰度分布特征。

l          允许用户通过鼠标操作来改变显示灰度的上限和下限。方法很简单,只需要拖动对话框上两侧的蓝色虚线即可。

l          显示当前鼠标所在位置的灰度值及出现的几率。

2.构建MagicHouse

MagicHouse是贯穿本书第3篇的重点实 例。它是一款以图像处理为主,综合图像浏览等多种功能的实用软件。MagicHouse支持多种常见图片格式,并实现了以下功能。

l          图像的浏览

l          图像的特效显示

l          图像的点运算

l          图像的几何变换

l          图像的增强

l          图像的滤镜效果

本章将完成MagicHouse框架的搭建工作并 在其基础上添加点运算的功能,随后的3章内容都是在已有的MagicHouse框架上进行功能的完善和补充的。

MagicHouse框架是在第8章实例 GraphShower的基础上改进的,它继承了GraphShower的框架结构和图像浏览、特效显示等功能,并新增了如下功能。

1)运行模式

添加了“运行模式”菜单,提供两种运行模式:图片 浏览模式,该模式功能与GraphShower完全相同,提供图像的浏览功能;编辑模式,这个模式允许对图像进行各种处理,本章及后面3章的功能都是在该 模式下实现的。编辑模式下的图像会被锁定,不能进行图像的浏览。

2)JPEG解码器

MagicHouse使用了第4章编写的JPEG 解码器完成JPEG文件的解码工作。用户只需要将第4章的对应文件添加到MagicHouse的工程下即可。

3)Dib封装类

MagicHouse使用第5章介绍的MyDib 类获取Bmp文件的像素信息,MagicHouse支持的图像为8位灰度图与24位、32位彩色图像。

4)预览对话框

预览对话框用于显示图像处理后的效果图。它与第5 章中介绍的CPreViewDlg类很类似,不同的是这里采用GDI+显示图像。

5)统一的图像处理接口

MagicHouse的核心是 CMagicHouseView类,它不但提供了图像的显示和处理功能,还提供了图像的处理接口。下面是CMagicHouseView类中关于图像处理 十分重要的成员变量。

public:

BYTE*   m_pImageBuffer;             // 编辑图像原始像素数组

BYTE*   m_pImageTempBuffer;         // 处理后的像素数组

UINT        m_nPicWidth;                    // 当前编辑图像宽度

UINT        m_nPicHeight;               // 当前编辑图像高度

UINT        m_nTempWidth;               // 处理后图像的宽度

UINT        m_nTempHeight;              // 处理后图像的高度

在编辑模式下,所有图像都会保存在一个线性数组 中,且图像的每一个像素都是以32位的形式保存的。图像处理函数可以通过m_pImageBuffer指针获取原始图像信息,然后将处理后的图像保存在另 一个线性数组中。处理后的图像信息可以通过m_pImageTempBuffer指针访问。最后通过预览对话框显示处理后的图像效果。后续的图像处理函数 会经常使用这6个变量。

MagicHouse拥有良好的可扩展性,它对底 层的解析过程进行了良好的封装,并采用统一的接口对图像进行处理,它不仅是一款数字图像处理工具,更是一款理想的数字图像算法试验平台,读者可以在其基础 上扩展更多的图像处理功能。由于篇幅有限,这里不再给出MagicHouse框架的代码,读者可到指定网站下载。

3.概要设计

下面开始完成灰度直方图对话框的设计。

启动MagicHouse项目文件,打开“资源视 图”,添加ID为IDD_HISTOGRAM的对话框资源并按照图9-5完成设计。然后为其创建对话框类CHistogramDlg并按照表9-1关联对 应变量。

图9-5  灰度直方图对话框的设计

表9-1  灰度直方图对话框资源与变量的关系

编    号资 源 类 型资源ID关联变量类型关联变量名称
1图片控件IDC_HISTOGRAMCStaticm_stiHistogram
2文本框IDC_LIMIT_LOWERintm_nLimitLow
3文本框IDC_LIMIT_UPintm_nLimitUp
4静态文本IDC_STATIC_GRAYintM_nGray
5静态文本IDC_STATIC_PERfloatM_dPer
4.代码分析

下面结合灰度直方图的基本原理一步一步剖析对话框 的实现过程。

1)各级灰度数量的统计

统计数据是绘制灰度直方图的依据。该对话框在初始 化时,也就是在OnInitDialog函数中完成统计工作。正如前面介绍的一样,MagicHouse将图像保存在一个线性数组中,图像的每个像素都统 一采用32位形式存储,故8位灰度图的一个像素也会占用32位。为方便统计,对话框默认当前处理图像为灰度图,即R=G=B,故每次只需要统计其中一种颜 色的数量。

2)灰度直方图重绘过程

绘制工作主要由Refresh函数完成,它完成了 以下功能。

l          双缓存的创建

以双缓存模式绘制直方图,防止闪烁。

l          绘制直方图的坐标系和刻度

l          查找所有灰度中最多的出现次数

该次数会显示在Y轴的旁边,拥有该 次数灰度值的直方图最高。

l          以相对高度绘制直方图

其余灰度值的高度由其出现次数与最大出现次数的比 值决定。

l          利用鼠标操作改变显示灰度的上下限

灰度上下限在对话框中以两条蓝色的虚线表示,用户 可以通过鼠标操作改变两条虚线的位置来改变显示灰度的上下限。

用户在单击鼠标左键时程序会根据当前鼠标的位置, 判断鼠标是否在上下限虚线的范围内。如果在上限虚线的范围中,则将m_nIsDraging变量赋值为DT_UP,如果在下限虚线的范围中,则赋值为 DT_LOW,否则赋值为DT_NULL。

用户在移动鼠标时如果m_nIsDraging不 为DT_NULL,则表示用户此时正在拖动上限或者下限的虚线,此时就需要更改上下限的值,并将其限定在合法的取值范围中。

5.实例代码清单

下面列出HistogramDlg.h的代码清 单:

#pragma once

#include "afxwin.h"

// CHistogramDlg 对话框

class CHistogramDlg : public CDialog

{

    DECLARE_MESSAGE_MAP()

    DECLARE_DYNAMIC(CHistogramDlg)

public:

    CStatic     m_stiHistogram;             // 直方图显示区域

    int     m_nLimitLow;                    // 显示灰度的下限

    int     m_nLimitUp;                 // 显示灰度的上限

    long        m_lCounts[256];             // 各级灰度出现的个数

    long        m_nPixelCount;              // 图像像素总数

    CPoint  m_psMove;                   // 记录拖动时的鼠标位置

    int     m_nIsDraging;               // 鼠标是否正在拖动

    int     m_nGray;                        // 当前鼠标位置的灰度级数

    float   m_dPer;                     // 出现概率

    CHistogramDlg(CWnd* pParent = NULL);        // 标准构造函数

    virtual ~CHistogramDlg();

    afx_msg void OnEnChangeLimitLower();        // 灰度下限改变的响应函数

    afx_msg void OnEnChangeLimitUp();       // 灰度上限改变的响应函数

    afx_msg void OnPaint();                 // 绘制对话框

    virtual BOOL OnInitDialog();                // 对话框初始化时计算各级灰度数量

    afx_msg void OnMouseMove(UINT nFlags, CPoint point);

                                            // 鼠标移动时响应拖动动作

    afx_msg BOOL OnSetCursor(CWnd* pWnd, UINT nHitTest, UINT message);

                                            // 拖动时改变鼠标光标

    afx_msg void OnLButtonDown(UINT nFlags, CPoint point);

                                        // 鼠标按下时判断是否在灰度上下限直线范围中

    afx_msg void OnLButtonUp(UINT nFlags, CPoint point);

                                            // 释放鼠标的响应函数

    // 对话框数据

    enum { IDD = IDD_HISTOGRAM };

protected:

    virtual void DoDataExchange(CDataExchange* pDX);    // DDX/DDV 支持

private:

    void Refresh(void);                 // 刷新直方图

    // 拖动枚举

    enum DragingType

    {

        DT_NULL,                            // 无拖动

        DT_LOW,                         // 拖动下限

        DT_UP                           // 拖动上限

    };

};

下面列出HistogramDlg.h的核心代码清单:

// HistogramDlg.cpp : 实现文件

#include "stdafx.h"

#include "MagicHouse.h"

#include "HistogramDlg.h"

#include "Mainfrm.h"

#include "MagicHouseDoc.h"

#include "MagicHouseView.h"

// CHistogramDlg 消息处理程序

/***************************************************************************

*   作用: 对话框初始化时计算各级灰度数量

***************************************************************************/

BOOL CHistogramDlg::OnInitDialog()

{

    CDialog::OnInitDialog();

    CMainFrame* pMain = (CMainFrame*)AfxGetMainWnd();

    CMagicHouseView* pView = (CMagicHouseView*)pMain->GetActiveView();

    for (UINT i = 0; i < pView->m_nPicWidth * pView->m_nPicHeight; i++)

    {

        int value = pView->m_pImageTempBuffer[i * 4];

        m_lCounts[value]++;

    }

    // 计算像素总个数

    m_nPixelCount = pView->m_nPicWidth * pView->m_nPicHeight;

    return TRUE;  // return TRUE unless you set the focus to a control

    // 异常:OCX 属性页应返回 FALSE

}

/***************************************************************************

*   作用: 刷新直方图

*   备注: 双缓存绘制方法

***************************************************************************/

void CHistogramDlg::Refresh()

{

    CDC* pDC = m_stiHistogram.GetDC();

    CRect rect;

    CDC memDC;

    CBitmap MemBitmap;

    // 获取绘图区域

    m_stiHistogram.GetClientRect(rect);

    // 设备描述表初始化

    memDC.CreateCompatibleDC(NULL);

    // 建立与屏幕显示兼容的内存显示设备

    MemBitmap.CreateCompatibleBitmap(pDC, rect.Width(), rect.Height());

    // 选取空白位图

    memDC.SelectObject(MemBitmap);

    memDC.FillSolidRect(0, 0, rect.Width(), rect.Height(), RGB(255,255,255));

    Graphics graph(memDC.GetSafeHdc());

    // 使用白色背景

    graph.FillRectangles(&SolidBrush(Color::White), 

&Rect(0, 0, rect.Width(), rect.Height()), 1);

    // 绘制y轴

    graph.DrawLine(&Pen(Color::Black), 10, 10, 10, 280);

    graph.DrawLine(&Pen(Color::Black), 10, 10, 5, 15);

    graph.DrawLine(&Pen(Color::Black), 10, 10, 15, 15);

    // 绘制x轴

    graph.DrawLine(&Pen(Color::Black), 10, 280, 290, 280);

    graph.DrawLine(&Pen(Color::Black), 290, 280, 285, 285);

    graph.DrawLine(&Pen(Color::Black), 290, 280, 285, 275);

    // 绘制坐标原点

    CString strNum;

    Font font(L"宋体", 10);

    strNum = L"0";

    graph.DrawString(strNum, -1, &font, 

        PointF(8, 290), &SolidBrush(Color::Black));

    for (int i = 0; i < 256; i += 5)

    {

        if (i % 50 == 0)

            graph.DrawLine(&Pen(Color::Black), 10 + i, 280, 10 + i, 286);

        else if (i % 10 == 0)

            graph.DrawLine(&Pen(Color::Black), 10 + i, 280, 10 + i, 283);

    }

    // 绘制x轴刻度

    strNum = L"50";

    graph.DrawString(strNum, -1, &font, PointF(53, 290),

&SolidBrush(Color::Black));

    strNum = L"100";

    graph.DrawString(strNum, -1, &font, PointF(100, 290),

&SolidBrush(Color::Black));

    strNum = L"150";

    graph.DrawString(strNum, -1, &font, PointF(150, 290),

&SolidBrush(Color::Black));

    strNum = L"200";

    graph.DrawString(strNum, -1, &font, 

                     PointF(200, 290), &SolidBrush(Color::Black));

    strNum = L"255";

    graph.DrawString(strNum, -1, &font, PointF(255, 290),

&SolidBrush(Color::Black));

    // 绘制当前灰度区域

    Pen pen(Color::Blue);

    pen.SetDashStyle(DashStyleDash);

    graph.DrawLine(&pen, 10 + m_nLimitLow, 280, 10 + m_nLimitLow, 10);

    graph.DrawLine(&pen, 10 + m_nLimitUp, 280, 10 + m_nLimitUp, 10);

    long lMax = 0;

    REAL dHeight = 0.0;

    // 查找最大值

    for (int i = m_nLimitLow; i <= m_nLimitUp; i++)

        lMax = max(lMax, m_lCounts[i]);

    // y轴刻度

    strNum.Format(L"%d", lMax);

    graph.DrawString(strNum, -1, &font, 

                   PointF(10, 25), &SolidBrush(Color::Black));

    // 绘制柱状图

    for (int i = m_nLimitLow; i <= m_nLimitUp; i++)

    {

        dHeight = (REAL)(m_lCounts[i]) / lMax * 250;

        graph.DrawLine(&Pen(Color::Gray), i + 10.0f, 280.0f, i + 10.0f,

280 - dHeight);

    }

    // 复制内存画布内容

    pDC->BitBlt(0, 0, rect.Width(), rect.Height(), &memDC, 0, 0, SRCCOPY);

    m_stiHistogram.ReleaseDC(pDC);

}

/***************************************************************************

*   作用: 鼠标移动时响应拖动动作

***************************************************************************/

void CHistogramDlg::OnMouseMove(UINT nFlags, CPoint point)

{

    CRect rect;

    m_stiHistogram.GetWindowRect(rect);

    if ( (nFlags & MK_LBUTTON) && m_nIsDraging )

    {

        // 拖动偏移量

        int offset = point.x - m_psMove.x;

        // 如果拖动的是上限

        if (m_nIsDraging == DT_UP)

        {

            // 如果没有超界

            if ( (offset + m_nLimitUp) <= 255 )

            {

                if ( (offset + m_nLimitUp) >= m_nLimitLow )

                    m_nLimitUp += offset;

                else

                    m_nLimitUp = m_nLimitLow;

            }

            else

                m_nLimitUp = 255;

        }

        else

        {

            // 如果没有超界

            if ( (offset + m_nLimitLow) >= 0 )

            {

                if ( (offset + m_nLimitLow) <= m_nLimitUp )

                    m_nLimitLow += offset;

                else

                    m_nLimitLow = m_nLimitUp;

            }

            else

                m_nLimitLow = 0;

        }

        UpdateData(FALSE);

        Refresh();

        m_psMove = point;

    }

    else

        m_nIsDraging = DT_NULL;

    ClientToScreen(&point);

    // 鼠标当前所在灰度位置,如果不在0~255之间则表示鼠标不在指定区域内

    int x = point.x - rect.left - 10;

    if (abs(x - m_nLimitUp) > 3 && abs(x - m_nLimitLow) > 3)

        m_nIsDraging = DT_NULL;

    // 如果鼠标在直方图区域中

    if (rect.PtInRect(point))

    {

        if (x >= m_nLimitLow && x <= m_nLimitUp)

        {

            m_nGray = x;

            m_dPer = float(m_lCounts[x]) / m_nPixelCount * 100;

        }

        UpdateData(FALSE);

    }

}

/***************************************************************************

*   作用: 鼠标按下时判断是否在灰度上下限直线范围中

***************************************************************************/

void CHistogramDlg::OnLButtonDown(UINT nFlags, CPoint point)

{

    CRect rect;

    CPoint oldPoint = point;

    m_stiHistogram.GetWindowRect(rect);

    ClientToScreen(&point);

    int x = point.x - rect.left - 10;

    if (abs(x - m_nLimitUp) <= 3)

    {

        m_psMove = oldPoint;

        m_nIsDraging = DT_UP;

    }

    else if (abs(x - m_nLimitLow) <= 3)

    {

        m_psMove = oldPoint;

        m_nIsDraging = DT_LOW;

    }

}

/***************************************************************************

*   作用: 释放鼠标的响应函数

***************************************************************************/

void CHistogramDlg::OnLButtonUp(UINT nFlags, CPoint point)

{

    m_nIsDraging = DT_NULL;

}

6.使用该对话框

打开“资源视图”中的菜单资源 IDR_MAINFRAME,添加一个新菜单栏“点运算”,并在其下拉菜单中添加一个新栏,如图9-6所示。然后将其Caption属性设为“灰度直方 图”,其ID为ID_POINT_HISTOGRAM。

图9-6  在菜单资源中添加新项

为其添加命令响应函数,具体内容如下:

void CMagicHouseView::OnPointHistogram()

{

    if (!m_bIsEditMode || m_nPos == -1)

    {

        MessageBox(L"请先打开图像文件,然后选择编辑模式!");

        return;

    }

    CHistogramDlg dlg;

    ResetImage();   // 该函数用于清空m_pImageTempBuffer数组

    dlg.DoModal();

}下载本文

显示全文
专题