opencv 改变图像的对比度和亮度

429 阅读3分钟

1. 目标

在本文中,将学习如何:

  • 访问像素值
  • 用零初始化矩阵
  • 了解cv::saturate_cast的作用以及它为何有用
  • 获取有关像素转换的一些很酷的信息
  • 在实际示例中提高图像的亮度

2. 理论

2.1 图像处理

  • 通用图像处理算子是一种获取一个或多个输入图像并产生输出图像的函数。

  • 图像变换可以看作:

    • 点算子(像素变换)
    • 邻域(基于区域)的算子

2.2 像素变换

  • 在这种图像处理变换中,每个输出像素的值仅取决于相应的输入像素值(可能还有一些全局收集的信息或参数)。
  • 此类运算符的示例包括亮度和对比度调整以及颜色校正和转换。

2.3 亮度和对比度调整

  • 两个常用的点过程是带常数的乘法加法:

    g(x)=αf(x)+βg(x)=\alpha f(x)+\beta
  • 参数α > 0β\alpha > 0和 \beta 通常称为增益 gain偏置 bias参数;这两个参数分别控制对比度亮度

  • 可以f(x)作为源像素和g(x)作为输出像素。然后,可以更方便地把表达式写成:

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

i和j表示该像素位于第i行第j列。

3. 代码

  • 点击这里可下载代码
  • 以下代码执行操作g(i,j)=αf(i,j)+βg(i,j)=\alpha\cdot f(i,j)+\beta
#include "opencv2/imgcodecs.hpp"
#include "opencv2/highgui.hpp"
#include <iostream>
// we're NOT "using namespace std;" here, to avoid collisions between the beta variable and std::beta in c++17
using std::cin;
using std::cout;
using std::endl;
using namespace cv;
int main( int argc, char** argv )
{
    CommandLineParser parser( argc, argv, "{@input | lena.jpg | input image}" );
    Mat image = imread( samples::findFile( parser.get<String>( "@input" ) ) );
    if( image.empty() )
    {
      cout << "Could not open or find the image!\n" << endl;
      cout << "Usage: " << argv[0] << " <Input image>" << endl;
      return -1;
    }
    Mat new_image = Mat::zeros( image.size(), image.type() );
    double alpha = 1.0; /*< Simple contrast control */
    int beta = 0;       /*< Simple brightness control */
    cout << " Basic Linear Transforms " << endl;
    cout << "-------------------------" << endl;
    cout << "* Enter the alpha value [1.0-3.0]: "; cin >> alpha;
    cout << "* Enter the beta value [0-100]: ";    cin >> beta;
    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 );
            }
        }
    }
    imshow("Original Image", image);
    imshow("New Image", new_image);
    waitKey();
    return 0;
}

4. 代码解释

  • 使用cv::imread加载图像并将其保存在 Mat 对象中:
CommandLineParser parser( argc, argv, "{@input | lena.jpg | input image}" );

// 从parser 读取输入文件名称 然后加载文件
Mat image = imread( samples::findFile( parser.get<String>( "@input" ) ) );
if( image.empty() )
{
    cout << "Could not open or find the image!\n" << endl;
    cout << "Usage: " << argv[0] << " <Input image>" << endl;
    return -1;
}
  • 现在,由于对这个图像进行一些转换,这需要一个新的 Mat 对象来存储它。此外,它具有以下功能:

    • 初始像素值为零
    • 与原始图像相同的大小和类型
Mat new_image = Mat::zeros( image.size(), image.type() );

cv::Mat::zeros基于image.size()和image.type()返回一个初始化为零的Mat

  • 获取用户输入的 α 和β\alpha 和 \beta
double alpha = 1.0; /*< Simple contrast control */
int beta = 0; /*< Simple brightness control */
cout << " Basic Linear Transforms " << endl;
cout << "-------------------------" << endl;
cout << "* Enter the alpha value [1.0-3.0]: "; cin >> alpha;
cout << "* Enter the beta value [0-100]: "; cin >> beta;
  • 现在,对访问图像中的每个像素执行操作g(i,j)=αf(i,j)+βg(i,j)=\alpha\cdot f(i,j)+\beta。由于使用 BGR 图像进行操作,每个像素将具有三个值(B、G 和 R),因此必须分别访问它们。代码如下:

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 );
     }
  }
}

请注意以下内容(仅限 C++ 代码):

  • 要访问图像中的每个像素,使用以下语法:image.at(y,x)[c] 其中y是行,x是列,c是 B、G 或 R (0, 1 or 2)。
  • 因为αf(i,j)+β\alpha\cdot f(i,j)+\beta计算结果可能超出范围或不是整数(如果一个是浮点数),使用cv::saturate_cast来确保这些值是有效的。
  • 最后,以通常的方式创建窗口并显示图像。
 imshow("Original Image", image);
 imshow("New Image", new_image);
 waitKey();
  • 笔记

    可以简单地使用以下命令,而不是使用for循环: 来访问每个像素

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

其中cv::Mat::convertTo将有效地执行 newimage=aimage+betanew_image = a*image + beta。但是,这里只是为了展示如何访问每个像素。在这两种情况下,这两种方法都给出相同的结果,但 convertTo 更加优化并且运行速度更快。

5. 结果

  • 运行我们的代码并使用α = 2.2和β\beta=50

    $ ./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

6. 实际例子

在本段中,将通过调整图像的亮度和对比度来实践修正曝光不足的图像。我们还将看到另一种校正图像亮度的技术,称为伽马校正。

6.1 亮度和对比度调整

增加(或减少)betabeta值 将为每个像素增加(或减去)一个常量值。[0;255]范围之外的像素值将会饱和(即高于255的像素值将被设置为 255,低于0的像素值被设置为0)。

Basic_Linear_Transform_Tutorial_hist_beta.png

原始图像的直方图浅灰色,在 Gimp 中亮度 = 80 时为深灰色

对于每个亮度级别,直方图表示具有该亮度级别的像素数。深色图像将具有许多低亮度的像素,因此直方图左侧出现峰值。当添加一个恒定偏差bias时,直方图向右移动,因为为所有像素添加了一个恒定偏差bias。

α参数将修改 亮度级别的分布范围。如果α < 1,亮度级别将被压缩,结果将是对比度较低的图像。

Basic_Linear_Transform_Tutorial_hist_alpha.png

原始图像的直方图浅灰色,在 Gimp 中 对比度<0 时为深灰色

注意,这些直方图是使用 Gimp 软件中的亮度对比度工具获得的。亮度工具应与β\beta bias参数,但对比工具似乎与一个输出范围似乎以 Gimp 为中心的α 增益(正如在前面的直方图中所注意到的那样)。

β\beta bias会提高亮度,但同时图像会出现轻微的朦胧,因为对比度会降低。α 增益可以用来减弱这种影响,但是由于饱和,会丢失原始图像明亮区域的一些细节。

6.2 伽玛校正

Gamma 校正用于使用非线性变换映射输入值为输出值来校正图像的亮度:

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

由于这种关系是非线性的,因此所有像素的效果都不相同,并且取决于它们的原始值。

Basic_Linear_Transform_Tutorial_gamma.png

绘制不同的伽马值

γ<1\gamma<1 ,原来的暗区会更亮,直方图会向右移动,而γ>1\gamma>1 时 相反 .

6.3 校正曝光不足的图像

下图已修正为:α = 1.3和b=40.

Basic_Linear_Transform_Tutorial_linear_transform_correction.jpg

由 Visem(自己的作品)[CCBY-SA 3.0],通过 Wikimedia Commons

整体亮度得到了改善,但可以注意到,由于使用的实现的数值饱和度(摄影中的高光剪切) ,所以云现在非常饱和。

下图已修正为:γ=0.4\gamma=0.4 .

Basic_Linear_Transform_Tutorial_gamma_correction.jpg

By Visem (Own work) [CC BY-SA 3.0], via Wikimedia Commons

伽马校正应该倾向于增加较少的饱和度效果,因为映射是非线性的,并且没有像以前的方法那样的数值饱和度。

Basic_Linear_Transform_Tutorial_histogram_compare.png

左:alpha、beta 校正后的直方图;中心:原始图像的直方图;右:伽马校正后的直方图

上图比较了三个图像的直方图(三个直方图之间的 y 范围不一样)。注意到,大多数像素值位于原始图像直方图的下部。在α,β\alpha,\beta 校正后,可以在 255 处观察到一个大峰值,这是由于饱和值向右移动。伽马校正后,直方图向右移动,但暗区的像素比亮区的像素偏移更多(参见伽马曲线图)。

在本教程中,已经看到了两种简单的方法来调整图像的对比度和亮度。它们是基本技术,不能用作光栅图形编辑器的替代品!

6.4 代码

本教程的代码在这里

伽玛校正代码:

    
   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 = img.clone();
   LUT(img, lookUpTable, res);

查找表用于提高计算性能,因为一次只需计算 256 个值。

6.5 其他资源

原文地址