图像亮度、对比度和锐度是图像质量感知的重要参数,调节这些属性常用于图像增强、图像美化或图像分析的预处理阶段。本文将基于 OpenCV 实现这三项基础图像处理功能,并提供滑动条交互界面与直方图可视化分析,方便调试和理解效果。
亮度调整
图像亮度调整使得图像整体变亮或变暗,其数学表达式为:dst(x,y)=src(x,y) + beta。图像亮度调整本质上改变了图像像素灰度分布。如亮度加30使得其灰度分布向正方向平移30,反之则向负方向平移30。这里的灰度分布我们称之为图像直方图。我们使用眼底图像作为示例来展示效果:
左:眼底原图 右:图像亮度降低30
上:眼底原图直方图 下:亮度降低30后直方图
从直方图上我们可以清晰看出,亮度调整会导致直方图左右平移。我们需要注意的是,当亮度调整到饱和状态,如调整后亮度大于255或者小于0,可能会因为阶段而改变直方图形状。
如何使用代码实现图像亮度调整,直观代码如下:
cpp
for (int row = 0; row < src.rows; ++row)
{
uchar* data = src.ptr<uchar>(row);
for (int col = 0; col < src.cols * src.channels(); ++col)
{
// 遍历所有点,对每个点执行亮度调整adj_brightness操作
int val = data[col] + adj_brightness;
if (val < 0) val = 0;
if (val > 255) val = 255;
data[col] = val;
}
}
以上代码从原理上给出了一个实现逻辑,有利于我们理解具体实现。但该代码效率较低,主要原因包括:
1)内层for循环的条件语句可能打断流水线优化,流水线优化利用CPU的指令流水线结构实现多个指令并行处理。
2)可能没有有效利用SIMD指令,CPU通过SIMD指令可以实现多条数据同时处理的效果,也称作向量化。
由于OpenCV经过了大量的优化,通过调用OpenCV接口,在绝大多数情况下可以获得极佳的性能,因此我们会尽量使用OpenCV提供的接口来构造功能。
通过cv::Mat提供的接口convertTo可以实现图像亮度的调整,该函数的核心功能包括:
1)数据类型转换,如从CV_8U转换为CV_32F。
2)在转换过程中执行dst = src * alpha + beta操作,我们将alpha参数设置为1,beta参数设置为亮度调整参数即可实现亮度的调整。
为什么要调用convertTo实现亮度调整,而不是直接使用cv::add或者直接mat=mat+offset呢?
其实以上方法都可以实现亮度调整,这里使用convertTo的主要原因是该函数可以同步提高亮度调整与数据类型转换功能。通过一个函数实现两个功能,可以避免内存的多次拷贝,从而提升运行效率。
那么,为什么我们需要做数据类型转换呢?主要有两点理由:
1)我们一般将原始的8位无符号整数(CV_8U)图像转换为32位(CV_32F)浮点图像。在后续图像处理运算中,不可避免会产生浮点数值。如果仍旧使用整形表示则会产生阶段误差,从而降低调整后图像的信噪比。
2)在某些运算中可能产生负数,CV_8U类型无法有效表示负数,从而产生运算错误。
以下是亮度调整与类型转换的代码:
cpp
cv::Mat img_src_f;
img_src.convertTo(img_src_f, CV_32F, 1., adj_brightness);
对比度调整
对比度控制图像的明暗差异,通常用一个比例因子alpha控制像素的扩展或压缩,其数学表达式可写作:dst(x,y) = src(x,y) * alpha。当alpha>1时对比度增强,当alpha<1时对比度降低。
通过以上方法,我们会发现调整后图像的对比度会显著影响图像亮度。如增强对比度时也同样增强了图像亮度。如下图:
左:眼底原图 右:对比度增加1.5(亮度也同步增强)
上:原图直方图 下:对比度增强后直方图
从直方图上来看,图像平均亮度确实增加了50%,同时直方图的宽度也有所增加(即动态范围更大)。在现实应用中,我们在增加对比度时更加希望图像平均亮度基本保持不变,那么应该如何处理呢?
改进的对比度调整
为了保持图像亮度基本不变,我们可以引入一些对称性操作。在上一节内容中,图像对比度的扩张是以0点为原点进行缩放,即dst(x,y)=[src(x,y) - 0] * alpha。如果我们以图像均值为原点进行对比度缩放,则可以确保对比度缩放后的图像平均亮度基本保持不变,其表达式为:,这里的mean为图像的统计均值。
使用该改进后方案,我们的对比度调整有如下效果:
左:眼底原图 右:对比度增加1.5(保持亮度不变)
上:原图直方图 下:对比度增强直方图(亮度保持)
以上直方图表明,由于对比度增加使得直方图动态范围增加,但确保持了平均亮度。这正是我们在对比度调整时所期望的效果。
另外,对比度调整参数的取值范围理论上为。实际应用中,我们可能将其限制在
或者
。
锐度调整
我们之前讨论的图像亮度与图像对比度都是从全局角度来控制图像效果。因此,每一个有效的条件都会改变直方图的基本形态,如导致直方图平移或者缩放等。关于图像锐度的调整主要改变图像边缘清晰程度,这是一个邻域操作。也就是说,每一个像素变换后的值仅与其相邻的像素相关。
如果我们可以获取图像上每一个点的边缘强度信息,那么我们就可以通过对该位置的强度进行叠加,从而获得边缘更强的图像。因此,一个核心点就是如何获得每个点的边缘强度信息。
我们这里使用一个反锐化掩膜方式(Unsharp Masking)来获取每个点的边缘强度。公式如下:
,通过原图减去高斯模糊图像获取高频信息,作为每个点的边缘强度表征。
然后再在原图上叠加边缘强度信息,并通过参数控制叠加强度,如:
,
为增强系数,其范围为
。
左:眼底原图 右:锐度增强图像
上:原图直方图 下:锐度增强直方图
从以上直方图来看,锐度增强基本没有改变直方图的动态范围与均值,这也进一步验证图像锐化操作是一个邻域操作而非全局变换。同时,我们的锐化效果似乎也比较理想。然而仔细观察会发现:图像边缘虽然得到了有效增强,但图像噪声似乎也被增强了。我们需要进一步接近该问题。
改进的锐度调整
要想抑制锐化增强后的噪声,我们首先需要明白噪声的特点。噪声相对于信号来说比较弱,在一个完美信号上叠加随机噪声有如下效果:

如上图所示,上半部分是一个理想的阶跃信息和叠加随机噪声的阶跃信号,下图是叠加随机噪声阶跃信号的梯度响应。我们在做图像增强时,期望增强的边缘显然是0坐标附近的阶跃边缘,而在非零附近区域的噪声响应是我们期望忽略掉的。 从梯度响应上来看(红色曲线),阶跃信号有一个很强的响应,噪声信号的响应较弱,这就带给给我们一个自适应边缘增强的启示。
如果我们将每个点的梯度响应强度作为其增强系数因素,则可以通过选择合适的策略在增强信号同时抑制噪声。具体如下:
1)将梯度响应归一化到(0,1)区间;
2)对现有锐度增强方案添加适当的控制因子,以实现增强信号同时抑制噪声,公式如下:。
以下为改进后锐化增强结果:
左:锐化增强 右:改进后的锐化增强(降低了噪声)
很明显,通过引入梯度响应权值,我们能够获得更加理想的锐化增强图像。
基于OpenCV的图像调整实现
下面给出一个完整的基于OpenCV图像调整实现,通过引入Trackbar,我们可以做一个简单的实时调节交互。具体代码如下:
cpp
#include "opencv2/highgui/highgui_c.h"
#include "opencv2/highgui/highgui.hpp"
#include "opencv2/imgproc/imgproc.hpp"
// 设置滑动条绑定参数变量,该变量必须为整数类型
// 同时分别设置滑块的取值范围,如下:
// 亮度[0,512], 对比度[0,500], 锐度[0,1000]
int bar_brightness = 256;
int bar_contrast = 0;
int bar_sharpness = 0;
cv::Mat img_src, img_dst;
void applyAdjustment(int, void*)
{
// 根据对应滑块的取值范围换算出不同类型图像调整的参数值
// 换算后的取值范围为:亮度[-256,256], 对比度[1.,6.],锐度[0.,10.]
float adj_brightness = bar_brightness - 256;
float adj_contrast = bar_contrast / 100. + 1.;
float adj_sharpness = bar_sharpness / 100.;
// 通过convertTo函数同步实现了浮点类型转换和亮度调整,使得计算效率更高
cv::Mat img_src_f;
img_src.convertTo(img_src_f, CV_32F, 1., adj_brightness);
// 对比度增强
// 注意:以下矩阵img_src_f, img_diff, img_con均公用数据区(浅拷贝)
cv::Scalar m = cv::mean(img_src_f);
cv::Mat img_diff = img_src_f - m;
cv::Mat img_con = img_diff * adj_contrast + m;
// 计算自适应增强系数
cv::Mat img_green;
cv::extractChannel(img_con, img_green, 1);
cv::GaussianBlur(img_green, img_green, cv::Size(3, 3), 0);
cv::Mat grad_dx, grad_dy, grad_mag, grad_magc3;
cv::Sobel(img_green, grad_dx, CV_32F, 1, 0, 3);
cv::Sobel(img_green, grad_dy, CV_32F, 0, 1, 3);
cv::magnitude(grad_dx, grad_dx, grad_mag);
cv::normalize(grad_mag, grad_mag, 0.0, 1.0, cv::NORM_MINMAX);
cv::cvtColor(grad_mag, grad_magc3, CV_GRAY2BGR);
// 锐化增强
cv::Mat img_blur;
cv::GaussianBlur(img_con, img_blur, cv::Size(15, 15), 0);
cv::Mat img_diff2 = cv::Mat::zeros(img_con.size(), img_con.type());
img_diff2 = img_con - img_blur;
cv::Mat img_con2= cv::Mat::zeros(img_con.size(), img_con.type());
img_con2 = img_con + img_diff2.mul(grad_magc3) * adj_sharpness;
// 转换为8位深度图像,用于显示
img_con2.convertTo(img_dst, CV_8U);
cv::imshow("调整图像", img_dst);
}
void briConSharpAdjust()
{
img_src = cv::imread("fundus3.png", cv::IMREAD_COLOR);
if (img_src.empty()) return; // 检查是否成功读取图像
// 打开图像调整窗口
cv::namedWindow("调整图像", cv::WINDOW_NORMAL);
// 创建Trackbar
cv::createTrackbar("亮度", "调整图像", &bar_brightness, 512, applyAdjustment);
cv::createTrackbar("对比度", "调整图像", &bar_contrast, 500, applyAdjustment);
cv::createTrackbar("锐度", "调整图像", &bar_sharpness, 1000, applyAdjustment);
applyAdjustment(0, 0);
cv::waitKey(0);
}
int main()
{
briConSharpAdjust();
return 0;
}
结语
本文深入浅出的讲解了图像调整的基本算法,通过启发式的方式引入一些深入问题研究。同时给出了一个完整的Demo,该Demo基于OpenCV进行UI交互,可以实现基本的图像调整演示。通过对Demo改进可以快速落地一些相关应用。
另外,我们将所有的图像处理均放在一个函数中,包括图像亮度对比度,图像锐度处理。对于小图像来说,这样做当然没有太多问题。当我们在项目中要求更高的实时性,或者所处理图像的尺寸特别大(如4000*3000),那么我们可以拆分每项处理,从而达到更好的实时性。主要策略包括:
1)拆分对比度与锐度处理,每次调整仅做一项调整,这也是用户交互的一个自然逻辑;
2)保存中间图像,如第一次实现锐度增强后,第二次进行对比度调整时基于锐度增强图像即可;
3)所保存的中间图像一定为浮点类型,尽可能规避整形图像的截断误差累积;
4)在用户拉动滑块时(如锐度调整),一些公用数据仅需要计算一次即可,如自适应增强系数,差分图等。