图像的点运算是图像处理中相对简单的技术,它主要用于改变一幅图像的灰度分布范围。点运算通过一个变换函数将图 像的像素一一转换,最终构成一幅新的图像。由于操作对象是图像的一个个像素,故得名为“点运算”。点运算的最大特点是输出像素值只与当前输入像素值有关。 点运算的图像处理过程可以用以下公式表示:
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_HISTOGRAM | CStatic | m_stiHistogram |
2 | 文本框 | IDC_LIMIT_LOWER | int | m_nLimitLow |
3 | 文本框 | IDC_LIMIT_UP | int | m_nLimitUp |
4 | 静态文本 | IDC_STATIC_GRAY | int | M_nGray |
5 | 静态文本 | IDC_STATIC_PER | float | M_dPer |
下面结合灰度直方图的基本原理一步一步剖析对话框 的实现过程。
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();
}