【OpenCV C++20 学习笔记】调节图片对比度和亮度(像素变换)

调节图片对比度和亮度(像素变换)

原理

关于OpenCV的配置和基础用法,请参阅本专栏的其他文章:垚武田的OpenCV合集

以下的原理来自Richard Szeliski的书《Computer Vision: Algorithms and Applications》(《计算机视觉:算法和应用》)。

像素变换

图片处理的操作基本上就是一个传入一张或多张图片,然后输出一张结果图片的方法。

对某个图片对象进行的操作可以分为以下两大类:

  • 点操作:像素变换
  • 域操作:涉及到相邻的像素

这章主要讨论像素变换。在像素变换中,每个像素的计算结果只与输入的像素和其他参数有关,不与图片中的其他像素相关。像素变换的应用包括图片亮度、对比度调整,以及颜色校正和颜色变换等。

亮度和对比度调整

在亮度和对比度的线性调整中,像素变换的算法非常简单,就是一个简单的线性变换:
g ( x ) = α f ( x ) + β g(x) = \alpha f(x) + \beta g(x)=αf(x)+β

  • α > 0 \alpha > 0 α>0,为增强参数; β \beta β为偏移参数
  • α \alpha α控制对比度; β \beta β用来控制亮度
  • f ( x ) f(x) f(x)为转换前的像素, g ( x ) g(x) g(x)为转换后的像素

也可以用行列坐标的形式来表示像素:
g ( i , j ) = α f ( i , j ) + β g(i, j) = \alpha f(i ,j) + \beta g(i,j)=αf(i,j)+β

  • i i i和 j j j分别代表行号和列号

代码实现

首先导入图片并储存到Mat对象中。

cpp 复制代码
//CommandLineParser对main函数输入的参数进行解析,最后的字符串代表以下意义:
//@input表示一个有顺序的参数,将其命名为input
//lena.jpg,代表input的默认值
//input image,是对input参数的解释,说明它是输入的图像
CommandLineParser parser(argc, argv, "{@input | lena.jpg | input image}");
Mat image{ imread(parser.get<String>("@input")) };	//获取参数解析中的input参数
if (image.empty()) {
	//如果打开失败,则输出错误信息,并退出程序
	cout << "无法打开图片!\n" << endl;
	cout << "输入图片:" << argv[0] << "<参数错误>" << endl;
	return -1;
}

接着,创建一个新的Mat对象来储存变换后的结果。这个新对象的所有值初始化为0,而且具有和原图像同样的大小和类型:

cpp 复制代码
Mat new_image{ Mat::zeros(image.size(), image.type()) };

Mat对象的创建方法可以参阅专栏中的《【OpenCV C++20 学习笔记】基本图像容器------Mat》

然后,声明 α \alpha α和 β \beta β这两个参数,并让用户能够通过控制台输入它们的值:

cpp 复制代码
double alpha{ 1.0 };	//对比度控制参数
int beta{ 0 };			//亮度控制参数

cout << "基础线性变换" << endl;
cout << "-----------" << endl;
cout << "* 输入alpha值 [1.0-3.0]:"; cin >> alpha;
cout << "* 输入beta值 [0-100]:"; cin >> beta;

现在,用一个嵌套的for循环语句,遍历原图片中的每一个像素,并对每一个像素都进行变换操作:

cpp 复制代码
for (int y{ 0 }; y < image.rows; y++) {	//遍历行
	for (int x{ 0 }; x < image.cols; x++) {	//遍历列
		for (int c{ 0 }; c < image.channels(); c++) {	//遍历颜色通道
			new_image.at<Vec3b>(y, x)[c] =
				saturate_cast<uchar>(alpha * image.at<Vec3b>(y, x)[c] + beta);
		}
	}
}
  • 因为前面读取图片的时候,我们使用的是默认的BGR3通道格式。所以对于矩阵中的每一个数据项,我们用Vec3b数据类型来接收,并用下标c对3个通道中的每个通道值进行访问,最终每个数值的访问都使用了y(行数)、x(列数)、c(通道数);
  • 因为线性变换的计算可能使得结果超出原有类型的值域,或者变成其他类型(比如,当alpha为浮点数时,计算结果就会自动转换成浮点数)。所以,必须使用saturate_cast对最终结果进行类型转换。
    最后,创建窗口分别展示原始图片和变换后的图片
cpp 复制代码
imshow("原始图片", image);
imshow("新图片", new_image);

waitKey(0);

更简便的方法

除了使用for循环对矩阵中的所有值进行遍历和转换之外,还可以使用更加便利的转换方法:

cpp 复制代码
image.convertTo(new_image, -1, alpha, beta);

正如我在《【OpenCV C++20 学习笔记】操作图片》一文中详细描述的那样,convertTo函数实际上就是在执行一个线性变化的操作。其函数原型如下:
void cv::Mat::convertTo(OutputArray m, int rtype, double alpha = 1, double beta = 0) const

其算法如下:
m ( x , y ) = s a t u r a t e _ c a s t < r T y p e > ( α ( ∗ t h i s ) ( x , y ) + β ) m(x,y) = saturate\_cast< rType>(\alpha(*this)(x, y)+\beta) m(x,y)=saturate_cast<rType>(α(∗this)(x,y)+β)

实质上就等于线性变化+类型转换的操作,即上一节代码中for循环体内的操作。所以上一节代码中的整个for循环,可以用convertTo函数代替。
上一节的代码只是为了展示像素变换的原理,在实际应用中还是建议使用convertTo()函数直接进行变换。

结果展示

使用2.2的 α \alpha α值和50的 β \beta β值

结果如下:

γ \gamma γ校正及其实操案例

在这个案例中将运用另外一种亮度调整方法------ γ \gamma γ校正,来修复一张低曝光的照片。

线性变换的缺点

在上述线性变换的例子中,亮度的调整是通过给每个像素值加上或减去一个常量,即偏移参数 β \beta β。如果调整后的结果超出了值域,则会用saturate_cast进行类型转换,使其仍然落在值域之中。

saturate_cast的具体原理,请参阅本专栏中的《【OpenCV C++20学习笔记】矩阵上的掩码(mask)操作》中的"类型转换"小节

下面的直方图展示了偏移参数为80时,像素分布的改变:

  • 灰色部分为图像的原始像素分布
  • 黑色部分为调整后的像素分布
  • 横坐标为每个颜色值
  • 纵坐标为每个颜色值对应的像素个数

可以看到颜色值整体往右偏移了,而且最大值和最小值上的像素个数显著增加,这是值域调整的结果。

另一方面,对比度的调整在上例中是通过改变 α \alpha α值实现的。 α \alpha α越大,对比度越高;反之,对比度越低。下面的直方图展示了,当 α \alpha α值小于1的时候,像素分布的改变如下:

  • 图例与上图相同

与上图对比,这里的黑色部分像被横向挤压了,颜色值的值域变窄了,像素分布也更加集中了。

通过这两张图我们也可以看到线性变换的一些缺点:

  • 由于saturate_cast的值域控制,会丢失一些图片的信息,即原始值域会被截断,导致变换后的颜色值值域变窄
  • 亮度的调整同时会影响图片的对比度,如第一张图中所示, β \beta β参数在偏移像素分布的同时,也使像素更加集中
  • 变换后颜色值最大值和最小值处的像素分布会激增,会导致图片过曝

γ \gamma γ校正

γ \gamma γ校正使用非线性变换来调整图片的亮度,其原理如下:
O = ( I 255 ) γ × 255 O= (\frac{I}{255})^\gamma \times 255 O=(255I)γ×255

  • I I I为像素的原值颜色值
  • O O O为像素变换后的颜色值
  • γ \gamma γ为变换系数

变换结果 O O O和原始值 I I I之间由于是非线性的关系,所以并不是每个像素的变换效果都是一样的。下图显示在不同的 γ \gamma γ值下, O O O 和 I I I之间的关系:

  • 横坐标为原始值I
  • 纵坐标为变换值O

可以看到,当 γ < 1 \gamma<1 γ<1的时候,原始的最小值(即I=0)的增加更多;反之,当 γ > 1 \gamma>1 γ>1时,原始的最小值增加更少。

低曝光图片矫正案例

下面两张图,左边是原图,右边是用线性变换矫正后的图片( α = 1.3 \alpha=1.3 α=1.3, β = 40 \beta=40 β=40):

图片的整体亮度被调高了,但是很明显,天空的细节也丢失了,显得有点过曝。这就是上面所说的saturate_cast值域控制的结果。


下面是 γ \gamma γ校正( γ = 0.4 \gamma=0.4 γ=0.4)的结果:

效果高下立判!

原图、线性变换和 γ \gamma γ校正的像素分布直方图如下:

  • 左图:线性变换后
  • 中图:原始图片
  • 右图: γ \gamma γ校正后
  • 3幅图的y轴并不一致

可以看到,在原图中,左边的像素偏多,也就是颜色值低(暗部)的像素偏多。在线性矫正之后,即左图中,可以看到最右边有个到顶的颜色值,这就是值域控制后的最大颜色值的像素分布(saturate_cast将所有超出最大值的变换结果都变成了最大值)。但是在 γ \gamma γ校正之后,即右图中,可以看到相对于原图往右偏移了,同时,暗部和亮部也发生了分布的改变。但是显然,暗部的变化更多(数量减少,且更分散),亮部的变化偏少。这就防止了图片的过曝。下图标注了对比的结果:

所以可以得出以下结论:
相对于线性变换, γ \gamma γ校正在调整图片亮度上效果更好,也更能保留原始图片的细节

代码实现

在OpenCV中可以用LUT函数实现 γ \gamma γ校正。

其逻辑就是:用非线性算法计算出所有颜色值变换后的值,储存到一个查询表中;然后,用查询表的值一一替换原始图片中对应的颜色值。

cpp 复制代码
double gamma_{ 0.4 };	//确定gamma值
Mat lookUpTable(1, 256, CV_8U);	//新建查询表
uchar* p = lookUpTable.ptr();	//获取查询表的指针,方便后面填充值
for (int i{ 0 }; i < 256; ++i)	//填充查询表
	p[i] = saturate_cast<uchar>(pow(i / 255.0, gamma_) * 255.0);	//非线性转换算法

Mat res = image.clone();	//复制原始图片对象,作为储存变换结果的对象
LUT(image, lookUpTable, res);	//按查询表中的值,替换原始图片中的值

使用查询表能够提高替换原图中所有颜色值的速度。

查询表原理及LUT函数的用法,可以参阅本专栏中的【OpenCV C++20 学习笔记】扫描图片数据一文。

相关推荐
IVEN_4 天前
Python OpenCV: RGB三色识别的最佳工程实践
python·opencv
西岸行者10 天前
学习笔记:SKILLS 能帮助更好的vibe coding
笔记·学习
悠哉悠哉愿意10 天前
【单片机学习笔记】串口、超声波、NE555的同时使用
笔记·单片机·学习
别催小唐敲代码10 天前
嵌入式学习路线
学习
毛小茛10 天前
计算机系统概论——校验码
学习
babe小鑫10 天前
大专经济信息管理专业学习数据分析的必要性
学习·数据挖掘·数据分析
winfreedoms10 天前
ROS2知识大白话
笔记·学习·ros2
在这habit之下10 天前
Linux Virtual Server(LVS)学习总结
linux·学习·lvs
我想我不够好。10 天前
2026.2.25监控学习
学习
im_AMBER10 天前
Leetcode 127 删除有序数组中的重复项 | 删除有序数组中的重复项 II
数据结构·学习·算法·leetcode