Skip to content

改变图像的对比度与亮度

在本教程中,你将学到如何:

  • 访问像素值;
  • 用零初始化一个矩阵;
  • 了解 cv::saturate_cast 的作用以及它为何有用;
  • 获取一些关于像素变换的实用知识;
  • 通过一个实际示例改善图像的亮度。

注: 以下解释摘自 Richard Szeliski 所著 Computer Vision: Algorithms and Applications 一书。

  • 一个通用的图像处理运算符是一个函数,它接收一幅或多幅输入图像并产生一幅输出图像。
  • 图像变换可以划分为:
    • 点运算符(像素变换);
    • 邻域(基于区域)运算符。
  • 在这类图像处理变换中,每个输出像素的值仅取决于对应的输入像素值(可能再加上某些全局收集的信息或参数)。
  • 此类运算符的例子包括亮度和对比度调整,以及颜色校正与变换。
  • 两种常用的点处理是乘法和加法一个常数:

    g(x)=αf(x)+βg(x) = \alpha f(x) + \beta

  • 参数 α>0\alpha > 0 和 β\beta 通常被称为增益(gain)和偏置(bias)参数;有时也说这两个参数分别控制对比度和亮度。

  • 你可以把 f(x)f(x) 看作源图像像素,把 g(x)g(x) 看作输出图像像素。于是,更方便地我们可以将表达式写成:

    g(i,j)=α⋅f(i,j)+βg(i,j) = \alpha \cdot f(i,j) + \beta

    其中 ii 和 jj 表示该像素位于第 i 行、第 j 列。

C++

  • 可下载代码:点击此处。

  • 以下代码执行操作 g(i,j)=α⋅f(i,j)+βg(i,j) = \alpha \cdot f(i,j) + \beta:

(完整代码见源代码。)

Java

  • 可下载代码:点击此处。

  • 以下代码执行操作 g(i,j)=α⋅f(i,j)+βg(i,j) = \alpha \cdot f(i,j) + \beta:

(完整代码见源代码。)

Python

  • 可下载代码:点击此处。

  • 以下代码执行操作 g(i,j)=α⋅f(i,j)+βg(i,j) = \alpha \cdot f(i,j) + \beta:

(完整代码见源代码。)

  • 我们使用 cv::imread 加载一幅图像并存入 Mat 对象:

(加载图像的代码实现见源代码。)

  • 现在,由于我们要对这幅图像做一些变换,我们需要一个新的 Mat 对象来存储结果。同时,我们希望它具有以下特征:

    • 初始像素值全为零;
    • 与原图像具有相同的尺寸和类型。

(创建输出矩阵的代码实现见源代码。)

可以看到,cv::Mat::zeros 返回的是一个基于 image.size() 和 image.type() 的、Matlab 风格的零初始化器。

  • 接下来我们要求用户输入 α\alpha 和 β\beta 的值:

(读取参数的代码实现见源代码。)

  • 现在,为执行操作 g(i,j)=α⋅f(i,j)+βg(i,j) = \alpha \cdot f(i,j) + \beta,我们要访问图像中的每个像素。由于我们处理的是 BGR 图像,每个像素有三个值(B、G、R),因此我们也要分别访问它们。代码片段如下:

(执行变换的代码实现见源代码。)

请注意以下要点(仅限 C++ 代码):

  • 我们使用这样的语法来访问图像中的每个像素:image.at<Vec3b>(y, x)[c],其中 y 是行,x 是列,c 是 B、G 或 R(0、1 或 2)。

  • 由于运算 α⋅p(i,j)+β\alpha \cdot p(i,j) + \beta 可能产生超出范围的值或非整数(当 α\alpha 为浮点数时),我们使用 cv::saturate_cast 来确保值是有效的。

  • 最后,我们创建窗口并显示图像,这也是常规做法。

(显示图像的代码实现见源代码。)

注: 除了使用 for 循环来访问每个像素,我们本可以简单地使用这条命令:

C++

image.convertTo(new_image, -1, alpha, beta);

Java

image.convertTo(newImage, -1, alpha, beta);

Python

new_image = cv.convertScaleAbs(image, alpha=alpha, beta=beta)

其中 cv::Mat::convertTo 实际上会执行 new_image = a*image + beta。不过,我们想向你展示如何访问每个像素。无论如何,两种方法会给出相同的结果,但 convertTo 经过更充分的优化,运行速度快得多。

  • 运行我们的代码,并使用 α=2.2\alpha = 2.2 和 β=50\beta = 50:
Terminal window
$ ./BasicLinearTransforms lena.jpg
Basic Linear Transforms
-------------------------
* Enter the alpha value [1.0-3.0]: 2.2
* Enter the beta value [0-100]: 50

Basic_Linear_Transform_Tutorial_Result_big.jpg Basic_Linear_Transform_Tutorial_hist_beta.png Basic_Linear_Transform_Tutorial_hist_alpha.png Basic_Linear_Transform_Tutorial_gamma.png Basic_Linear_Transform_Tutorial_histogram_compare.png

  • 我们得到:

在本节中,我们将把所学付诸实践,通过调整图像的亮度和对比度来校正一幅曝光不足的图像。我们还将了解另一种校正图像亮度的技术——伽马校正(gamma correction)。

增大(/减小)β\beta 的值会给每个像素加上(/减去)一个常数值。超出 [0; 255] 范围的像素值会被饱和(即大于 255(/小于 0)的像素值会被钳制到 255(/0))。

直方图表示每种颜色等级下具有该颜色等级的像素数量。一幅偏暗的图像会有许多低颜色值的像素,因此直方图会在其左半部分呈现一个峰值。当加上一个常数偏置时,由于我们给所有像素都加上了常数偏置,直方图会向右移动。

α\alpha 参数会改变各级别的分布方式。如果 α<1\alpha < 1,颜色级别会被压缩,结果将是一幅对比度较低的图像。

请注意,这些直方图是使用 Gimp 软件中的”亮度-对比度”工具得到的。亮度工具应该与 β\beta 偏置参数相同,但对比度工具似乎与 α\alpha 增益有所不同——在 Gimp 中输出范围似乎是以中心为基准的(正如你在上面直方图中所注意到的那样)。

可能会出现这样的情况:调整 β\beta 偏置改善了亮度,但与此同时图像会显得蒙上一层淡淡的薄雾,因为对比度降低了。α\alpha 增益可用来减轻这种效果,但由于饱和的存在,我们会在原始的明亮区域丢失一些细节。

Basic_Linear_Transform_Tutorial_linear_transform_correction.jpg Basic_Linear_Transform_Tutorial_gamma_correction.jpg

伽马校正(gamma correction)可通过在输入值与映射输出值之间使用一个非线性变换来校正图像的亮度:

O=(I255)γ×255O = \left( \frac{I}{255} \right)^{\gamma} \times 255

由于这一关系是非线性的,其效果对所有像素并不相同,而是取决于它们的原始值。

当 γ<1\gamma < 1 时,原始的暗区域会变亮,直方图会向右移动;而当 γ>1\gamma > 1 时则相反。

下图使用以下参数进行了校正:α=1.3\alpha = 1.3 且 β=40\beta = 40。

整体亮度得到了改善,但你可以注意到,由于所用实现中的数值饱和,云层现在严重过曝(摄影中的高光溢出(highlight clipping))。

下图使用以下参数进行了校正:γ=0.4\gamma = 0.4。

伽马校正应当会产生更少的饱和效应,因为映射是非线性的,不会像前一种方法那样出现数值饱和。

上图比较了三幅图像的直方图(三幅直方图的 y 轴范围并不相同)。你可以注意到,对于原图像,大部分像素值集中在直方图的下半部分。经过 α\alpha、β\beta 校正后,我们可以观察到由于饱和在 255 处出现了一个大峰值,同时整体向右移动。经过伽马校正后,直方图向右移动,但暗区域的像素移动得更多(参见伽马曲线(参见伽马曲线)),而亮区域的像素移动得较少。

在本教程中,你了解了两种调整图像对比度和亮度的简单方法。它们是基础技术,并不能替代光栅图形编辑器!

C++

本教程的代码在此处。

Java

本教程的代码在此处。

Python

本教程的代码在此处。

伽马校正的代码:

(伽马校正的代码实现见源代码。)

其中使用了查找表(look-up table)来提升计算性能,因为只需要一次性计算 256 个值。