當我們處理光度不均勻的影像並嘗試進行二值化處理時,可能會遇到光線變化引起的閾值不適當的情況。特別是在影像中存在大量光線變化的情況下,使用單一閾值可能無法適應整個影像的光度變化。這可能導致一些區域的像素被誤認為背景或前景,最終導致生成不正確的二值化結果。
為了解決這個問題,自適應二值化應運而生。它通過計算不同區域的閾值,以適應影像中的光線變化,從而提高了二值化的準確性。
圖片源自:OpenCV https://docs.opencv.org/4.x/d7/d4d/tutorial_py_thresholding.html
積分圖是一種用於加速影像處理和計算的技術,可以應用於計算影像的各種區域的總和、平均值等。自適應二值化因為會需要計算出區域內的總合,為了加速運算,我們會使用積分圖來求出區域總合。
複習上一章的主題,我們只要將積分圖的值帶入下列的公式,就可以求出下圖面積E內元素的加總。
如果你對積分圖感到陌生,可以參考【Day11】OpenCV 積分圖:影像處理的加速神器。
自適應二值化與單一值二值化不同,自適應二值化考慮了影像中不同區域的亮度差異,從而能夠處理光線變化和背景不均勻的情況。
自適應二值化的主要原理以及流程如下:
這個程式碼提供了兩種運算方式,一個是使用OpenCV內建的函式,另一個是使用蜂巢迴圈實現的演算法,兩者效果一樣。你可以透過設定USE_OPENCV
成1
或0
來決定你要使用前者還是後者的實現方式。
#define USE_OPENCV 1
建立一個OpenCV視窗用於顯示二值化結果,並調整視窗大小以符合影像的寬高比例。
cv::namedWindow("Binary Image", cv::WindowFlags::WINDOW_NORMAL);
cv::resizeWindow("Binary Image", 512.0f* ((float)grayImage.cols / grayImage.rows),512);
這個回調函式 trackbar_callback
用於調整核函數的大小和自適應二值化的常數C,從滑動條中讀取 Size
、Offset
的值。 然而,因為核函數需要有錨(Anchor)中心點,所以size
必須是是奇數,如果不是,size
加1,此外size
必須不為1。
if (size % 2 == 0) {
// 確保"Size"是奇數,因為自適應二值化需要奇數的核函數大小
size++;
}
if (size == 1)
return;
使用 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
: 這是二值化閾值的偏移量,它用於調整計算的二值化閾值。根據這個偏移量,閾值會相對於區域的平均灰階值進行調整。使用 cv::integral
函式計算灰階影像的積分圖,積分出來後的integralImage
型別是32位元整數或浮點型 (CV_32SC1
或 CV_32FC1
)。
cv::Mat integralImage;
cv::integral(grayImage, integralImage);
size
是選定的核函數大小。
使用積分圖計算核函數內像素值的總和,通過計算四個角落的積分值差,可以得到核函數內像素值的總和。
透過其求出平均值,最後計算核函數的閥值。
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;
}
#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); // 顯示自適應二值化的結果影像
}
這是一張含有不均勻光線的方格紙照片,把它下載下來測試看看吧。沒意外的話,使用單一值二值化效果會很差。
可以看到結果,使用自適應二值化的方式可以減少不均勻光線分布帶來的誤判。