Datawhale 計算機視覺基礎-圖像處理(上)-Task05 圖像分割/二值化
5.1 簡介
該部分的學習內容是對經典的閾值分割算法進行回顧,圖像閾值化分割是一種傳統的最常用的圖像分割方法,因其實現簡單、計算量小、性能較穩定而成爲圖像分割中最基本和應用最廣泛的分割技術。它特別適用於目標和背景佔據不同灰度級範圍的圖像。它不僅可以極大的壓縮數據量,而且也大大簡化了分析和處理步驟,因此在很多情況下,是進行圖像分析、特徵提取與模式識別之前的必要的圖像預處理過程。圖像閾值化的目的是要按照灰度級,對像素集合進行一個劃分,得到的每個子集形成一個與現實景物相對應的區域,各個區域內部具有一致的屬性,而相鄰區域不具有這種一致屬性。這樣的劃分可以通過從灰度級出發選取一個或多個閾值來實現。
5.2 學習目標
-
瞭解閾值分割基本概念
-
理解最大類間方差法(大津法)、自適應閾值分割的原理
-
掌握OpenCV框架下上述閾值分割算法API的使用
5.3 內容介紹
1、最大類間方差法、自適應閾值分割的原理
2、OpenCV代碼實踐
3、動手實踐並打卡(讀者完成)
5.4 算法理論介紹
5.4.1 最大類間方差法(大津閾值法)
大津法(OTSU)是一種確定圖像二值化分割閾值的算法,由日本學者大津於1979年提出。從大津法的原理上來講,該方法又稱作最大類間方差法,因爲按照大津法求得的閾值進行圖像二值化分割後,前景與背景圖像的類間方差最大。
它被認爲是圖像分割中閾值選取的最佳算法,計算簡單,不受圖像亮度和對比度的影響,因此在數字圖像處理上得到了廣泛的應用。它是按圖像的灰度特性,將圖像分成背景和前景兩部分。因方差是灰度分佈均勻性的一種度量,背景和前景之間的類間方差越大,說明構成圖像的兩部分的差別越大,當部分前景錯分爲背景或部分背景錯分爲前景都會導致兩部分差別變小。因此,使類間方差最大的分割意味着錯分概率最小。
其實可以這麼理解,比如一張圖的像素直方圖如下:
OTSU就是找到一個閾值k,使左右兩個分佈的類間方差最小
應用: 是求圖像全局閾值的最佳方法,應用不言而喻,適用於大部分需要求圖像全局閾值的場合。
優點: 計算簡單快速,不受圖像亮度和對比度的影響。
缺點: 對圖像噪聲敏感;只能針對單一目標分割;當目標和背景大小比例懸殊、類間方差函數可能呈現雙峯或者多峯,這個時候效果不好。
原理非常簡單,涉及的知識點就是均值、方差等概念和一些公式推導。爲了便於理解,我們從目的入手,反推一下這著名的OTSU算法。
求類間方差:
OTSU算法的假設是存在閾值TH將圖像所有像素分爲兩類C1(小於TH)和C2(大於TH),則這兩類像素各自的均值就爲m1、m2,圖像全局均值爲mG。同時像素被分爲C1和C2類的概率分別爲p1、p2,pi爲i這個像素值的個數除以總像素值的個數。因此就有:
根據原文,式(4)還可以進一步變形:
分割:
這個分割就是二值化,OpenCV給了以下幾種方式,很簡單,可以參考:
!
5.4.2 三角法
三角法求閾值最早見於Zack的論文《Automatic measurement of sister chromatid exchange frequency》主要是用於染色體的研究,該方法是使用直方圖數據,基於純幾何方法來尋找最佳閾值,它的成立條件是假設直方圖最大波峯在靠近最亮的一側,然後通過三角形求得最大直線距離,根據最大直線距離對應的直方圖灰度等級即爲分割閾值,圖示如下:
對上圖的詳細解釋:
在直方圖上從最高峯處bmx到最暗對應直方圖bmin(p=0)%構造一條直線,從bmin處開始計算每個對應的直方圖b到直線的垂直距離,知道bmax爲止,其中最大距離對應的直方圖位置即爲圖像二值化對應的閾值T。
擴展情況:
有時候最大波峯對應位置不在直方圖最亮一側,而在暗的一側,這樣就需要翻轉直方圖,翻轉之後求得值,用255減去即得到爲閾值T。擴展情況的直方圖表示如下:
三角閾值化處理細胞之類的圖片會有比較好的效果
5.4.3 自適應閾值
前面介紹了OTSU算法,但這算法屬於全局閾值法,所以對於某些光照不均的圖像,這種全局閾值分割的方法會顯得蒼白無力,如下圖:
顯然,這樣的閾值處理結果不是我們想要的,那麼就需要一種方法來應對這樣的情況。
這種辦法就是自適應閾值法(adaptiveThreshold),它的思想不是計算全局圖像的閾值,而是根據圖像不同區域亮度分佈,計算其局部閾值,所以對於圖像不同區域,能夠自適應計算不同的閾值,因此被稱爲自適應閾值法。(其實就是局部閾值法)
如何確定局部閾值呢?可以計算某個鄰域(局部)的均值、中值、高斯加權平均(高斯濾波)來確定閾值。值得說明的是:如果用局部的均值作爲局部的閾值,就是常說的移動平均法。
例如採用方法 CV_ADAPTIVE_THRESH_MEAN_C,閾值類型:CV_THRESH_BINARY, 閾值的象素鄰域大小 block_size 選取3,參數param1 取3時,opencv輸出結果:
下面我們來手算一下:
因爲193大於190,所以取0
因爲175小於180,所以取255
高斯的處理方法也和均值相同,只不過核換成高斯核
5.5 基於OpenCV的實現
- 工具:OpenCV3.1.0+VS2013
- 平臺:WIN10
函數原型(c++)
1.最大類間方差法
double cv::threshold ( InputArray src,
OutputArray dst,
double thresh,
double maxval,
int type
)
參數:
- src — input array (single-channel, 8-bit or 32-bit floating point).
- dst — output array of the same size and type as src.
- thresh — threshold value.
- maxval — maximum value to use with the THRESH_BINARY and THRESH_BINARY_INV thresholding types.
- type — thresholding type 參考:thresholdType
1.自適應閾值
void adaptiveThreshold(InputArray src, OutputArray dst,
double maxValue,
int adaptiveMethod,
int thresholdType,
int blockSize, double C)
參數:
Parameters
- src — Source 8-bit single-channel image.
- dst — Destination image of the same size and the same type as src.
- maxValue — Non-zero value assigned to the pixels for which the condition is satisfied
- adaptiveMethod — Adaptive thresholding algorithm to use,參考:cv::AdaptiveThresholdTypes
- thresholdType — Thresholding type that must be either THRESH_BINARY or THRESH_BINARY_INV, 可參考:thresholdType
blockSize Size of a pixel neighborhood that is used to calculate a threshold value for the pixel: 3, 5, 7, and so on. - C — Constant subtracted from the mean or weighted mean (see the details below). Normally, it is positive but may be zero or negative as well.
實現示例(c++)
- 1、大津閾值
#include <iostream>
#include <opencv2/opencv.hpp>
using namespace std;
using namespace cv;
int main(int argc, char* argv[])
{
Mat img = imread(argv[1], -1);
if (img.empty())
{
cout <<"Error: Could not load image" <<endl;
return 0;
}
Mat gray;
cvtColor(img, gray, CV_BGR2GRAY);
Mat dst;
threshold(gray, dst, 0, 255, CV_THRESH_OTSU);
imshow("src", img);
imshow("gray", gray);
imshow("dst", dst);
waitKey(0);
return 0;
}
- 2、自適應閾值
#include <iostream>
#include <opencv2/opencv.hpp>
using namespace std;
using namespace cv;
int main(int argc, char* argv[])
{
Mat img = imread(argv[1], -1);
if (img.empty())
{
cout <<"Error: Could not load image" <<endl;
return 0;
}
Mat gray;
cvtColor(img, gray, CV_BGR2GRAY);
Mat dst;
cv::adaptiveThreshold(gray,, dst, 255, cv::ADAPTIVE_THRESH_MEAN_C, cv::THRESH_BINARY, 21, 10);;
imshow("src", img);
imshow("gray", gray);
imshow("dst", dst);
waitKey(0);
return 0;
}
進階實現(根據原理自己實現)
實現示例(c++)
- 1、大津閾值
#include <iostream>
#include <opencv2/core.hpp>
#include <opencv2/highgui.hpp>
#include <opencv2/imgproc.hpp>
int Otsu(cv::Mat& src, cv::Mat& dst, int thresh){
const int Grayscale = 256;
int graynum[Grayscale] = { 0 };
int r = src.rows;
int c = src.cols;
for (int i = 0; i < r; ++i){
const uchar* ptr = src.ptr<uchar>(i);
for (int j = 0; j < c; ++j){ //直方圖統計
graynum[ptr[j]]++;
}
}
double P[Grayscale] = { 0 };
double PK[Grayscale] = { 0 };
double MK[Grayscale] = { 0 };
double srcpixnum = r*c, sumtmpPK = 0, sumtmpMK = 0;
for (int i = 0; i < Grayscale; ++i){
P[i] = graynum[i] / srcpixnum; //每個灰度級出現的概率
PK[i] = sumtmpPK + P[i]; //概率累計和
sumtmpPK = PK[i];
MK[i] = sumtmpMK + i*P[i]; //灰度級的累加均值
sumtmpMK = MK[i];
}
//計算類間方差
double Var=0;
for (int k = 0; k < Grayscale; ++k){
if ((MK[Grayscale-1] * PK[k] - MK[k])*(MK[Grayscale-1] * PK[k] - MK[k]) / (PK[k] * (1 - PK[k])) > Var){
Var = (MK[Grayscale-1] * PK[k] - MK[k])*(MK[Grayscale-1] * PK[k] - MK[k]) / (PK[k] * (1 - PK[k]));
thresh = k;
}
}
//閾值處理
src.copyTo(dst);
for (int i = 0; i < r; ++i){
uchar* ptr = dst.ptr<uchar>(i);
for (int j = 0; j < c; ++j){
if (ptr[j]> thresh)
ptr[j] = 255;
else
ptr[j] = 0;
}
}
return thresh;
}
int main(){
cv::Mat src = cv::imread("I:\\Learning-and-Practice\\2019Change\\Image process algorithm\\Img\\Fig1039(a)(polymersomes).tif");
if (src.empty()){
return -1;
}
if (src.channels() > 1)
cv::cvtColor(src, src, CV_RGB2GRAY);
cv::Mat dst,dst2;
int thresh=0;
double t2 = (double)cv::getTickCount();
thresh=Otsu(src , dst, thresh); //Otsu
std::cout << "Mythresh=" << thresh << std::endl;
t2 = (double)cv::getTickCount() - t2;
double time2 = (t2 *1000.) / ((double)cv::getTickFrequency());
std::cout << "my_process=" << time2 << " ms. " << std::endl << std::endl;
double Otsu = 0;
Otsu=cv::threshold(src, dst2, Otsu, 255, CV_THRESH_OTSU + CV_THRESH_BINARY);
std::cout << "OpenCVthresh=" << Otsu << std::endl;
cv::namedWindow("src", CV_WINDOW_NORMAL);
cv::imshow("src", src);
cv::namedWindow("dst", CV_WINDOW_NORMAL);
cv::imshow("dst", dst);
cv::namedWindow("dst2", CV_WINDOW_NORMAL);
cv::imshow("dst2", dst2);
//cv::imwrite("I:\\Learning-and-Practice\\2019Change\\Image process algorithm\\Image Filtering\\MeanFilter\\TXT.jpg",dst);
cv::waitKey(0);
}
- 2、自適應閾值
#include <iostream>
#include <opencv2/core.hpp>
#include <opencv2/highgui.hpp>
#include <opencv2/imgproc.hpp>
enum adaptiveMethod{meanFilter,gaaussianFilter,medianFilter};
void AdaptiveThreshold(cv::Mat& src, cv::Mat& dst, double Maxval, int Subsize, double c, adaptiveMethod method = meanFilter){
if (src.channels() > 1)
cv::cvtColor(src, src, CV_RGB2GRAY);
cv::Mat smooth;
switch (method)
{
case meanFilter:
cv::blur(src, smooth, cv::Size(Subsize, Subsize)); //均值濾波
break;
case gaaussianFilter:
cv::GaussianBlur(src, smooth, cv::Size(Subsize, Subsize),0,0); //高斯濾波
break;
case medianFilter:
cv::medianBlur(src, smooth, Subsize); //中值濾波
break;
default:
break;
}
smooth = smooth - c;
//閾值處理
src.copyTo(dst);
for (int r = 0; r < src.rows;++r){
const uchar* srcptr = src.ptr<uchar>(r);
const uchar* smoothptr = smooth.ptr<uchar>(r);
uchar* dstptr = dst.ptr<uchar>(r);
for (int c = 0; c < src.cols; ++c){
if (srcptr[c]>smoothptr[c]){
dstptr[c] = Maxval;
}
else
dstptr[c] = 0;
}
}
}
int main(){
cv::Mat src = cv::imread("I:\\Learning-and-Practice\\2019Change\\Image process algorithm\\Img\\Fig1049(a)(spot_shaded_text_image).tif");
if (src.empty()){
return -1;
}
if (src.channels() > 1)
cv::cvtColor(src, src, CV_RGB2GRAY);
cv::Mat dst, dst2;
double t2 = (double)cv::getTickCount();
AdaptiveThreshold(src, dst, 255, 21, 10, meanFilter); //
t2 = (double)cv::getTickCount() - t2;
double time2 = (t2 *1000.) / ((double)cv::getTickFrequency());
std::cout << "my_process=" << time2 << " ms. " << std::endl << std::endl;
cv::adaptiveThreshold(src, dst2, 255, cv::ADAPTIVE_THRESH_MEAN_C, cv::THRESH_BINARY, 21, 10);
cv::namedWindow("src", CV_WINDOW_NORMAL);
cv::imshow("src", src);
cv::namedWindow("dst", CV_WINDOW_NORMAL);
cv::imshow("dst", dst);
cv::namedWindow("dst2", CV_WINDOW_NORMAL);
cv::imshow("dst2", dst2);
//cv::imwrite("I:\\Learning-and-Practice\\2019Change\\Image process algorithm\\Image Filtering\\MeanFilter\\TXT.jpg",dst);
cv::waitKey(0);
}
效果
- 1、大津閾值
- 2、自適應閾值
相關技術文檔、博客、教材、項目推薦
opencv文檔: https://docs.opencv.org/3.1.0/d7/d1b/group__imgproc__misc.html#gae8a4a146d1ca78c626a53577199e9c57
博客:https://blog.csdn.net/weixin_40647819/article/details/90179953
https://blog.csdn.net/weixin_40647819/article/details/90213858
python版本:https://www.kancloud.cn/aollo/aolloopencv/267591 http://www.woshicver.com/FifthSection/4_3_%E5%9B%BE%E5%83%8F%E9%98%88%E5%80%BC/
5.6 總結
該部分對兩種經典閾值分割方法進行了介紹,讀者可根據提供的資料進行學習,然後參考示例代碼自行實現。Otsu的二值化有一些優化方法,讀者可以嘗試學習並實現。
Task05 閾值分割/二值化END.
— By: 小武
關於Datawhale:
Datawhale是一個專注於數據科學與AI領域的開源組織,彙集了衆多領域院校和知名企業的優秀學習者,聚合了一羣有開源精神和探索精神的團隊成員。Datawhale以“for the learner,和學習者一起成長”爲願景,鼓勵真實地展現自我、開放包容、互信互助、敢於試錯和勇於擔當。同時Datawhale 用開源的理念去探索開源內容、開源學習和開源方案,賦能人才培養,助力人才成長,建立起人與人,人與知識,人與企業和人與未來的聯結。