5.1 簡介
該部分的學習內容是對經典的閾值分割算法進行回顧,圖像閾值化分割是一種傳統的最常用的圖像分割方法,因其實現簡單、計算量小、性能較穩定而成爲圖像分割中最基本和應用最廣泛的分割技術。它特別適用於目標和背景佔據不同灰度級範圍的圖像。它不僅可以極大的壓縮數據量,而且也大大簡化了分析和處理步驟,因此在很多情況下,是進行圖像分析、特徵提取與模式識別之前的必要的圖像預處理過程。圖像閾值化的目的是要按照灰度級,對像素集合進行一個劃分,得到的每個子集形成一個與現實景物相對應的區域,各個區域內部具有一致的屬性,而相鄰區域不具有這種一致屬性。這樣的劃分可以通過從灰度級出發選取一個或多個閾值來實現。
5.2 學習目標
- 瞭解閾值分割基本概念
- 理解最大類間方差法(大津法)、自適應閾值分割的原理
- 掌握OpenCV框架下上述閾值分割算法API的使用
5.3 內容介紹
- 最大類間方差法、自適應閾值分割的原理
- OpenCV代碼實踐
5.4 算法理論介紹
5.4.1 最大類間方差法(大津閾值法)
大津法(OTSU)是一種確定圖像二值化分割閾值的算法,由日本學者大津於1979年提出。從大津法的原理上來講,該方法又稱作最大類間方差法,因爲按照大津法求得的閾值進行圖像二值化分割後,前景與背景圖像的類間方差最大。
它被認爲是圖像分割中閾值選取的最佳算法,計算簡單,不受圖像亮度和對比度的影響,因此在數字圖像處理上得到了廣泛的應用。它是按圖像的灰度特性,將圖像分成背景和前景兩部分。因方差是灰度分佈均勻性的一種度量,背景和前景之間的類間方差越大,說明構成圖像的兩部分的差別越大,當部分前景錯分爲背景或部分背景錯分爲前景都會導致兩部分差別變小。因此,使類間方差最大的分割意味着錯分概率最小。
- 應用: 是求圖像全局閾值的最佳方法,應用不言而喻,適用於大部分需要求圖像全局閾值的場合。
- 優點: 計算簡單快速,不受圖像亮度和對比度的影響。
- 缺點: 對圖像噪聲敏感;只能針對單一目標分割;當目標和背景大小比例懸殊、類間方差函數可能呈現雙峯或者多峯,這個時候效果不好。
原理非常簡單,涉及的知識點就是均值、方差等概念和一些公式推導。爲了便於理解,我們從目的入手,反推一下這著名的OTSU算法。
求類間方差:
OTSU算法的假設是存在閾值TH將圖像所有像素分爲兩類C1(小於TH)和C2(大於TH),則這兩類像素各自的均值就爲m1、m2,圖像全局均值爲mG。同時像素被分爲C1和C2類的概率分別爲p1、p2。因此就有:
根據原文,式(4)還可以進一步變形:
分割:
這個分割就是二值化,OpenCV給了以下幾種方式,很簡單,可以參考:
5.4.2 自適應閾值
前面介紹了OTSU算法,但這算法屬於全局閾值法,所以對於某些光照不均的圖像,這種全局閾值分割的方法會顯得蒼白無力,如下圖:
顯然,這樣的閾值處理結果不是我們想要的,那麼就需要一種方法來應對這樣的情況。
這種辦法就是自適應閾值法(adaptiveThreshold),它的思想不是計算全局圖像的閾值,而是根據圖像不同區域亮度分佈,計算其局部閾值,所以對於圖像不同區域,能夠自適應計算不同的閾值,因此被稱爲自適應閾值法。(其實就是局部閾值法)
如何確定局部閾值呢?
可以計算某個鄰域(局部)的均值、中值、高斯加權平均(高斯濾波)來確定閾值。
值得說明的是:如果用局部的均值作爲局部的閾值,就是常說的移動平均法。
5.5 基於OpenCV的實現
工具:OpenCV3.1.0+VS2013
平臺:WIN10
實現示例(python)
簡單閾值
在這裏,問題直截了當。對於每個像素,應用相同的閾值。如果像素值小於閾值,則將其設置爲0,否則將其設置爲最大值。函數cv.threshold用於應用閾值。第一個參數是源圖像,它應該是灰度圖像。第二個參數是閾值,用於對像素值進行分類。第三個參數是分配給超過閾值的像素值的最大值。OpenCV提供了不同類型的閾值,這由函數的第四個參數給出。通過使用cv.THRESH_BINARY類型。
Python-OpenCV中提供了閾值(threshold)函數:
cv2.threshold()
函數:第一個參數 src 指原圖像,原圖像應該是灰度圖。
第二個參數 x 指用來對像素值進行分類的閾值。
第三個參數 y 指當像素值高於(有時是小於)閾值時應該被賦予的新的像素值
第四個參數 Methods 指,不同的不同的閾值方法,這些方法包括:
cv.THRESH_BINARY
cv.THRESH_BINARY_INV
cv.THRESH_TRUNC
cv.THRESH_TOZERO
cv.THRESH_TOZERO_INV
通過類型的文檔來觀察區別。
該方法返回兩個輸出。第一個是使用的閾值,第二個輸出是閾值後的圖像。
此代碼比較了不同的簡單閾值類型:
import cv2 as cv
import numpy as np
from matplotlib import pyplot as plt
img = cv.imread('E:\python-project\hui.jpg',0)
ret,thresh1 = cv.threshold(img,127,255,cv.THRESH_BINARY)
ret,thresh2 = cv.threshold(img,127,255,cv.THRESH_BINARY_INV)
ret,thresh3 = cv.threshold(img,127,255,cv.THRESH_TRUNC)
ret,thresh4 = cv.threshold(img,127,255,cv.THRESH_TOZERO)
ret,thresh5 = cv.threshold(img,127,255,cv.THRESH_TOZERO_INV)
titles = ['Original Image','BINARY','BINARY_INV','TRUNC','TOZERO','TOZERO_INV']
images = [img, thresh1, thresh2, thresh3, thresh4, thresh5]
for i in range(6):
plt.subplot(2,3,i+1),plt.imshow(images[i],'gray')
plt.title(titles[i])
plt.xticks([]),plt.yticks([])
plt.show()
自適應閾值
當同一幅圖像上的不同部分的具有不同亮度時。這種情況下我們需要採用自適應閾值。此時的閾值是根據圖像上的每一個小區域計算與其對應的閾值。因此在同一幅圖像上的不同區域採用的是不同的閾值,從而使我們能在亮度不同的情況下得到更好的結果。
cv2.adaptiveThreshold()
函數:第一個參數 src 指原圖像,原圖像應該是灰度圖。
第二個參數 x 指當像素值高於(有時是小於)閾值時應該被賦予的新的像素值
第三個參數 adaptive_method 指: CV_ADAPTIVE_THRESH_MEAN_C 或 CV_ADAPTIVE_THRESH_GAUSSIAN_C
第四個參數 threshold_type 指取閾值類型:必須是下者之一 • CV_THRESH_BINARY,
• CV_THRESH_BINARY_INV
第五個參數 block_size 指用來計算閾值的象素鄰域大小: 3, 5, 7, ...
第六個參數 param1 指與方法有關的參數。對方法CV_ADAPTIVE_THRESH_MEAN_C 和 CV_ADAPTIVE_THRESH_GAUSSIAN_C, 它是一個從均值或加權均值提取的常數, 儘管它可以是負數。
自適應閾值:
-
對方法CV_ADAPTIVE_THRESH_MEAN_C,先求出塊中的均值,再減掉param1。
-
對方法 CV_ADAPTIVE_THRESH_GAUSSIAN_C ,先求出塊中的加權和(gaussian), 再減掉param1。
例如:
採用方法 CV_ADAPTIVE_THRESH_MEAN_C,閾值類型:CV_THRESH_BINARY, 閾值的象素鄰域大小 block_size 選取3,參數param1 取3和5時:
部分原圖像像素值 當參數param1爲5時
部分原圖像像素值 當參數param1爲7時
選取對應領域(3*3)求其均值,然後減去參數param1的值爲自適應閾值。測試時求得均值爲小數時,貌似進行四捨五入之後再減去參數param1。
import cv2
import numpy as np
from matplotlib import pyplot as plt
img = cv2.imread('E:\python-project\hui.jpg')
GrayImage=cv2.cvtColor(img,cv2.COLOR_BGR2GRAY)
# 中值濾波
GrayImage= cv2.medianBlur(GrayImage,5)
ret,th1 = cv2.threshold(GrayImage,127,255,cv2.THRESH_BINARY)
#3 爲Block size, 5爲param1值
th2 = cv2.adaptiveThreshold(GrayImage,255,cv2.ADAPTIVE_THRESH_MEAN_C,\
cv2.THRESH_BINARY,3,5)
th3 = cv2.adaptiveThreshold(GrayImage,255,cv2.ADAPTIVE_THRESH_GAUSSIAN_C,\
cv2.THRESH_BINARY,3,5)
titles = ['Gray Image', 'Global Thresholding (v = 127)',
'Adaptive Mean Thresholding', 'Adaptive Gaussian Thresholding']
images = [GrayImage, th1, th2, th3]
for i in range(4):
plt.subplot(2,2,i+1),plt.imshow(images[i],'gray')
plt.title(titles[i])
plt.xticks([]),plt.yticks([])
plt.show()
實現示例(c++)
函數原型(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.
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);
}