iT邦幫忙

2023 iThome 鐵人賽

DAY 12
1
Software Development

圖解C++影像處理與OpenCV應用:從基礎到高階,深入學習超硬核技術!系列 第 12

【Day12】OpenCV 自適應二值化(Adaptive Thresholding):降低亮度干擾

  • 分享至 

  • xImage
  •  

一、 介紹

當我們處理光度不均勻的影像並嘗試進行二值化處理時,可能會遇到光線變化引起的閾值不適當的情況。特別是在影像中存在大量光線變化的情況下,使用單一閾值可能無法適應整個影像的光度變化。這可能導致一些區域的像素被誤認為背景或前景,最終導致生成不正確的二值化結果。

為了解決這個問題,自適應二值化應運而生。它通過計算不同區域的閾值,以適應影像中的光線變化,從而提高了二值化的準確性。

https://ithelp.ithome.com.tw/upload/images/20230920/20161732esU8lKzAdS.jpg

圖片源自:OpenCV https://docs.opencv.org/4.x/d7/d4d/tutorial_py_thresholding.html

二、 原理

1. 積分圖(Integral Image)

積分圖是一種用於加速影像處理和計算的技術,可以應用於計算影像的各種區域的總和、平均值等。自適應二值化因為會需要計算出區域內的總合,為了加速運算,我們會使用積分圖來求出區域總合。

複習上一章的主題,我們只要將積分圖的值帶入下列的公式,就可以求出下圖面積E內元素的加總。
https://ithelp.ithome.com.tw/upload/images/20230920/20161732ny6hItcRD6.png
如果你對積分圖感到陌生,可以參考【Day11】OpenCV 積分圖:影像處理的加速神器

https://ithelp.ithome.com.tw/upload/images/20230920/20161732SukUszHbM8.png

2. 自適應二值化(Adaptive Thresholding)

自適應二值化與單一值二值化不同,自適應二值化考慮了影像中不同區域的亮度差異,從而能夠處理光線變化和背景不均勻的情況。

自適應二值化的主要原理以及流程如下:

  1. 求出灰階影像f(x,y)積分圖
  2. 求出核函數w(x,y)內元素的總和,並除以核函數的像素數量3x3=9,得到平均值1.1
  3. 計算出閾值Theshold,其中C是自訂的常數。假設C為0,Theshold等於1.1。
  4. f(x,y)的錨(中心點),依照上一步求出的閾值進行二值化。如果f(3,4)值超出1.1,輸出為255,反之輸出為0。

https://ithelp.ithome.com.tw/upload/images/20230920/20161732u787Hur94M.png

https://ithelp.ithome.com.tw/upload/images/20230920/201617323v0QmIH3Kz.png

三、 程式碼

1. 逐行解釋

這個程式碼提供了兩種運算方式,一個是使用OpenCV內建的函式,另一個是使用蜂巢迴圈實現的演算法,兩者效果一樣。你可以透過設定USE_OPENCV10來決定你要使用前者還是後者的實現方式。

#define USE_OPENCV 1

1) 調整視窗大小

建立一個OpenCV視窗用於顯示二值化結果,並調整視窗大小以符合影像的寬高比例。

cv::namedWindow("Binary Image", cv::WindowFlags::WINDOW_NORMAL);
cv::resizeWindow("Binary Image", 512.0f* ((float)grayImage.cols / grayImage.rows),512);

2) 核函數

這個回調函式 trackbar_callback用於調整核函數的大小和自適應二值化的常數C,從滑動條中讀取 SizeOffset 的值。 然而,因為核函數需要有錨(Anchor)中心點,所以size 必須是是奇數,如果不是,size加1,此外size必須不為1。

if (size % 2 == 0) {
    // 確保"Size"是奇數,因為自適應二值化需要奇數的核函數大小
    size++; 
}
if (size == 1)
    return;

3) 使用OpenCV進行自適應二值化

使用 cv::adaptiveThreshold 函式進行自適應二值化,並使用所調整的參數進行運算。

cv::Mat binaryImage;
cv::adaptiveThreshold(grayImage, binaryImage, 255, cv::ADAPTIVE_THRESH_MEAN_C, cv::THRESH_BINARY,size,offset);
  • grayImage: 輸入的灰階影像,即待處理的影像。
  • binaryImage: 輸出的二值影像。
  • 255: 最大的輸出像素值,為是255,代表白色像素。
  • cv::ADAPTIVE_THRESH_MEAN_C: 參數指定了使用的自適應二值化方法。也可以使用高斯核函數ADAPTIVE_THRESH_GAUSSIAN_C的方式進行加總。
  • cv::THRESH_BINARY: 指定二值化的模式。cv::THRESH_BINARY 表示當像素值大於閾值時,將其設置為最大輸出像素值(255),否則設置為0。
  • size: 核函數的尺寸,決定了用於計算每個像素的二值化閾值的像素區域的大小。
  • offset: 這是二值化閾值的偏移量,它用於調整計算的二值化閾值。根據這個偏移量,閾值會相對於區域的平均灰階值進行調整。

4) 使用演算法實踐自適應二值化

使用 cv::integral 函式計算灰階影像的積分圖,積分出來後的integralImage型別是32位元整數或浮點型 (CV_32SC1CV_32FC1)。

cv::Mat integralImage;
cv::integral(grayImage, integralImage);

size 是選定的核函數大小。

  1. 計算核函數的起始 x 座標,確保核函數不會超出影像的左邊界。
  2. 計算核函數的起始 y 座標,確保核函數不會超出影像的上邊界。
  3. 計算核函數的結束 x 座標,確保核函數不會超出影像的右邊界。
  4. 計算核函數的結束 y 座標,確保核函數不會超出影像的下邊界。

使用積分圖計算核函數內像素值的總和,通過計算四個角落的積分值差,可以得到核函數內像素值的總和。

透過其求出平均值,最後計算核函數的閥值。

int startX = std::max(x - size / 2, 0);
int startY = std::max(y - size / 2, 0);
int endX = std::min(x + size / 2, grayImage.cols - 1);
int endY = std::min(y + size / 2, grayImage.rows - 1);

//求出Kernal內的加總
int blockSum = integralImage.at<int>(endY + 1, endX + 1) - integralImage.at<int>(endY + 1, startX) - integralImage.at<int>(startY, endX + 1) + integralImage.at<int>(startY, startX);
//求出Kernal內的像素總數
int blockSizePixels = (endY - startY + 1) * (endX - startX + 1);
//求出Kernal內的平均值
double mean = static_cast<double>(blockSum) / blockSizePixels;
double threshold = mean - offset;

將閥值與原始灰階影像進行比較,如果像素值大於閥值,則將二值影像對應位置的像素設為255(白色),否則設為0(黑色)。最後顯示二值化結果。

if (grayImage.at<uchar>(y, x) > threshold) {
    binaryImage.at<uchar>(y, x) = 255;
}

2. 完整程式碼

#include <iostream>
#include "opencv2/opencv.hpp"
#include "opencv2/core/utils/logger.hpp"

#define USE_OPENCV 1

using namespace std;

void trackbar_callback(int position, void*);

// 存儲灰階影像的變數
cv::Mat grayImage; 

int main()
{
	// 設定OpenCV日誌級別為SILENT,禁止輸出日誌
	cv::utils::logging::setLogLevel(cv::utils::logging::LOG_LEVEL_SILENT); 
	grayImage = cv::imread("C:\\Users\\vince\\Downloads\\adaptive_thresholding.jpg", cv::IMREAD_GRAYSCALE);

    cv::namedWindow("Binary Image", cv::WindowFlags::WINDOW_NORMAL); 
    cv::resizeWindow("Binary Image", 512.0f * ((float)grayImage.cols / grayImage.rows), 512);

	// 創建名為"Offset"的滑動條,範圍為0到255
    cv::createTrackbar("Offset", "Binary Image", NULL, 255, trackbar_callback); 
	// 創建名為"Size"的滑動條,範圍為0到1023
    cv::createTrackbar("Size", "Binary Image", NULL, 1023, trackbar_callback);
	// 初始化"Size"滑動條的值為3
	cv::setTrackbarPos("Size", "Binary Image", 3); 

	// 等待使用者操作,直到按下任意按鍵結束程式
	cv::waitKey(0); 
	return 0;
}

void trackbar_callback(int position, void*) {
	// 讀取"Offset"滑動條的當前值
	int offset = cv::getTrackbarPos("Offset", "Binary Image"); 
	// 讀取"Size"滑動條的當前值
	int size = cv::getTrackbarPos("Size", "Binary Image");
	if (size % 2 == 0) {
		// 確保"Size"是奇數,因為自適應二值化需要奇數的核函數大小
		size++; 
	}
	if (size == 1)
		return;

	// 存儲自適應二值化的結果影像
	cv::Mat binaryImage; 

#if USE_OPENCV
	// 使用OpenCV的自適應二值化函數
	cv::adaptiveThreshold(grayImage, binaryImage, 255, cv::ADAPTIVE_THRESH_MEAN_C, cv::THRESH_BINARY, size, offset); 
#else
	// 創建一個初始值為0的二值影像
	binaryImage = cv::Mat::zeros(grayImage.rows, grayImage.cols, CV_8UC1);
	cv::Mat integralImage;
	// 計算灰階影像的積分圖
	cv::integral(grayImage, integralImage); 
	for (int y = 0; y < binaryImage.rows; y++) {
		for (int x = 0; x < binaryImage.cols; x++) {
			int startX = std::max(x - size / 2, 0);
			int startY = std::max(y - size / 2, 0);
			int endX = std::min(x + size / 2, grayImage.cols - 1);
			int endY = std::min(y + size / 2, grayImage.rows - 1);

			//求出Kernal內的加總
			int blockSum = integralImage.at<int>(endY + 1, endX + 1) - integralImage.at<int>(endY + 1, startX) - integralImage.at<int>(startY, endX + 1) + integralImage.at<int>(startY, startX);
			//求出Kernal內的像素總數
			int blockSizePixels = (endY - startY + 1) * (endX - startX + 1);
			//求出Kernal內的平均值
			double mean = static_cast<double>(blockSum) / blockSizePixels;

			//求出二值化的閥值
			double threshold = mean - offset;

			if (grayImage.at<uchar>(y, x) > threshold) {
				// 設置二值影像的像素值
				binaryImage.at<uchar>(y, x) = 255; 
			}
		}
	}
#endif

	cv::imshow("Binary Image", binaryImage); // 顯示自適應二值化的結果影像
}

3. 測試圖

這是一張含有不均勻光線的方格紙照片,把它下載下來測試看看吧。沒意外的話,使用單一值二值化效果會很差。

https://ithelp.ithome.com.tw/upload/images/20230920/20161732tmqVqEyvMb.jpg

4. 測試結果

可以看到結果,使用自適應二值化的方式可以減少不均勻光線分布帶來的誤判。

https://ithelp.ithome.com.tw/upload/images/20230920/20161732ZUnQ5XqdbT.jpg


上一篇
【Day11】OpenCV 積分圖:影像處理的加速神器
下一篇
【Day13】使用OpenCV實現OTSU大津演算法
系列文
圖解C++影像處理與OpenCV應用:從基礎到高階,深入學習超硬核技術!30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言