天天看點

9. 改變圖像的對比度和亮度(OpenCV 官方文檔翻譯)

官方文檔連結:https://docs.opencv.org/4.2.0/d3/dc1/tutorial_basic_linear_transform.html

目标 (Goal)

本教程學習:

  • 通路像素值
  • 用 0 初始化矩陣
  • cv::saturate_cast 的作用
  • 有關像素轉換的資訊
  • 提高圖像亮度的執行個體研究

理論 (Theory)

注意 (Note)

下面的理論解釋來自 Richard Szeliski 的 《計算機視覺:算法與應用》 一書。

圖像處理 (Image Processing)

  • 一個通用的圖像處理運算即是一個函數,它擷取一個或多個輸入圖像并生成輸出圖像。
  • 圖像變換可以看作:
    • 點算子(像素變換)
    • 領域(基于區域)算子

像素變換 (Pixel Transforms)

  • 在這種圖像處理變換中,每個輸出像素的值僅依賴于相應的輸入像素值(加上一些可能全局收集的資訊或參數)。
  • 此類運算符的執行個體包括亮度和對比度調整以及顔色校正和轉換。

亮度和對比度調整 (Brightness and contrast adjustments)

  • 兩個常用的點過程是帶常數的乘法和加法:

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

  • 參數 α>0 和 β 通常被稱為 增益 和 偏置參數;有時這些參數被稱為分别控制 對比度 和 亮度。
  • 可以将 f(x) 是為源圖像像素,将 g(x) 視為輸出圖像像素。然後,我們可以更友善地将表達式寫成:

g ( i , j ) = α ⋅ f ( i , j ) + β 其 中   i   和   j   表 示 像 素 位 于 第   i   行 和 第   j   列 中 。 g(i, j) = \alpha \cdot f(i, j) + \beta \\ 其中\,i\,和\,j\,表示像素位于第\,i\,行和第\,j\,列中。 g(i,j)=α⋅f(i,j)+β其中i和j表示像素位于第i行和第j列中。

代碼 (Code)

  • 下面的代碼執行的過程是

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

#include <iostream>

#include <opencv2/core/core.hpp>
#include <opencv2/imgproc/imgproc.hpp>
#include <opencv2/imgcodecs/imgcodecs.hpp>
#include <opencv2/highgui/highgui.hpp>

int main(int argc, char** argv)
{
	cv::Mat image = cv::imread(cv::samples::findFile("lena.jpg"));

	if (image.empty())
	{
		std::cout << "Could not open or find the image!\n" << std::endl;
		return -1;
	}

	cv::Mat new_image = cv::Mat::zeros(image.size(), image.type());

	double alpha = 1.0;		/*< Simple contrast control */
	int beta = 0;					/*< Simple brightness control */

	std::cout << " Basic Linear Transforms " << std::endl;
	std::cout << "----------------------------" << std::endl;
	std::cout << "* Enter the alpha value [1.0-3.0] : ";	std::cin >> alpha;
	std::cout << "* Enter the beta value [0-100] : ";		std::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<cv::Vec3b>(y, x)[c] =
					cv::saturate_cast<uchar>(alpha * image.at<cv::Vec3b>(y, x)[c] + beta);
			}

	cv::imshow("Original Image", image);
	cv::imshow("New Image", new_image);

	cv::waitKey(0);
	return 0;
}
           

解釋 (Explanation)

  • 我們使用 cv::imread 加載圖像并将其儲存在 Mat 對象中:
cv::Mat image = cv::imread(cv::samples::findFile("lena.jpg"));

	if (image.empty())
	{
		std::cout << "Could not open or find the image!\n" << std::endl;
		return -1;
	}
           

  • 現在,因為我們将對這個圖像進行一些轉換,我們需要一個新的 Mat 對象來存儲它。此外,我們也希望它具有以下功能:
    • 初始像素值等于 0
    • 與原始圖像大小和類型相同

可以看到 cv::Mat::zeros 會基于 image.size() 和 image.type() 也就是圖像的尺寸和類型傳回一個 Matlab 風格的零初始值設定項。

  • 然後要求使用者輸入 α 和 β 值:
double alpha = 1.0;		/*< Simple contrast control */
	int beta = 0;					/*< Simple brightness control */

	std::cout << " Basic Linear Transforms " << std::endl;
	std::cout << "----------------------------" << std::endl;
	std::cout << "* Enter the alpha value [1.0-3.0] : ";	std::cin >> alpha;
	std::cout << "* Enter the beta value [0-100] : ";		std::cin >> beta;
           

  • 現在,為了執行操作 g(i, j) = α · f(i, j) + β,我們将通路圖像中的每個像素。由于我們使用的是 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<cv::Vec3b>(y, x)[c] =
    				cv::saturate_cast<uchar>(alpha * image.at<cv::Vec3b>(y, x)[c] + beta);
    		}
           

使用 C++ 編寫代碼時注意以下幾點:

  • 要通路圖像中的每個像素,我們使用以下文法:image.atcv::Vec3b(y, x)[c],其中 y 是行,x 是列,c 是 B、G 或 R(0、1 或 2)。
  • 因為操作 α · p(i, j) + β 可以給出超出範圍的值或不是整數(如果 α 是浮點數的話),是以我們使用 cv::saturate_cast 來確定這些值是有效的。
  • 最後,我們建立視窗并以通常的方式顯示圖像。
cv::imshow("Original Image", image);
	cv::imshow("New Image", new_image);

	cv::waitKey(0);
           

注意 (Note)

我們不必使用 for 循環來通路每個像素,隻需使用以下指令:

其中 cv::Mat::convertTo 将有效地執行 *new_image = α*image + β*。但是,之前的代碼是想展示如何通路每個像素。無論如何,這兩種方法都給出了相同的結果,但 convertTo 更優化,工作速度更快。

結果 (Result)

  • 運作代碼并使用 以下三組值:
α β
1.5 30
2.5 30
2.5 50
  • 得到的結果如下:
9. 改變圖像的對比度和亮度(OpenCV 官方文檔翻譯)
9. 改變圖像的對比度和亮度(OpenCV 官方文檔翻譯)

執行個體 (Practical example)

在這個部分,我們将把我們所學的通過調整圖像的亮度和對比度來糾正曝光不足的圖像的方法付諸實踐。我們還将看到另一種校正圖像亮度的技術,稱為伽馬校正。

亮度和對比度調整 (Brightness and contrast adjustments)

增大(或減小) β 值将為每個像素加上(或減去)一個常量值。超出 [0, 255] 範圍的像素值将飽和(即高于(小于)255(0)的像素值)将被限制為 255(0)。

9. 改變圖像的對比度和亮度(OpenCV 官方文檔翻譯)
9. 改變圖像的對比度和亮度(OpenCV 官方文檔翻譯)

通過上述直方圖可以看到當 α = 1,β = 80 時的源圖像與轉換後的圖像以及各自的直方圖。

直方圖表示在圖像中每個灰階級的像素個數。一幅深色的圖像會有許多低顔色值的像素,是以直方圖會在其左側出現峰值。當添加一個恒定的偏移量時,即 β,直方圖右移,因為我們已經向所有像素添加了一個恒定的偏移量。

α 參數将修改灰階級的擴散方式。如果 α < 1,則灰階級将被壓縮,結果将是對比度較低的圖像。

9. 改變圖像的對比度和亮度(OpenCV 官方文檔翻譯)
9. 改變圖像的對比度和亮度(OpenCV 官方文檔翻譯)

上圖中,當 α = 0.5 時,變換之後的圖像的直方圖顯示,灰階級被壓縮了。

使用 β 偏置可以提高亮度,但同時随着對比度的降低,圖像會出現輕微的面紗 (veil 不知道應該如何翻譯,我了解的應該是圖像會像是蒙上了一層薄霧一樣的效果)。α 增益可以用來減小這種效應,但由于飽和,我們會丢失一些原始亮區的細節。

伽馬校正 (Gamma correction)

伽馬校正可用于校正圖像的亮度,方法是在輸入值和映射輸出值之間使用非線性變換:

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

由于這種關系是非線性的,是以對所有像素的效果不盡相同,這也取決于它們的原始值。

9. 改變圖像的對比度和亮度(OpenCV 官方文檔翻譯)

當 γ < 1 時,原始的暗區将變亮,直方圖右移;而 γ > 1 時則相反。

校正曝光不足的圖像 (Correct an underexposed image)

一下圖像已經校正為:α = 1.3 和 β = 40。結果如下:

9. 改變圖像的對比度和亮度(OpenCV 官方文檔翻譯)

整體亮度有所提高,但可以注意到,由于(攝影中的高光剪裁)數值飽和,雲現在已經大大飽和,直接導緻照片中的主體灰階級偏暗。

下面是用伽馬校正對照片進行校正,選擇 γ = 0.4。

9. 改變圖像的對比度和亮度(OpenCV 官方文檔翻譯)

由于映射是非線性的,并且不可能像以前的方法那樣存在數值飽和,是以伽馬校正隻會增加較少的飽和效應。

9. 改變圖像的對比度和亮度(OpenCV 官方文檔翻譯)

上圖比較了三幅圖像的直方圖。左圖是 α β 校正之後的直方圖,中間是原始圖像的直方圖,右側是伽馬校正後的直方圖。可以注意到,大多數像素值位于原始圖像直方圖的下部。經過 α β 校正後,由于飽和而右移,可以看到在灰階級為 255 是會有一個大的峰值。經過伽馬校正後,直方圖向右移動,但暗區域中的像素比亮區域中的像素移動的更多。

在本教程中,可以看到兩種簡單的方法來調整圖像的對比度和亮度。

代碼 (Code)

伽馬校正的代碼:

cv::Mat lookUpTable(1, 256, CV_8U);
    uchar* p = lookUpTable.ptr();
    for( int i = 0; i < 256; ++i)
        p[i] = cv::saturate_cast<uchar>(pow(i / 255.0, gamma_) * 255.0);
    cv::Mat res = img.clone();
    cv::LUT(img, lookUpTable, res);
           

查找表用于提高計算性能,隻需要計算一次 256 個值。