Datawhale 計算機視覺基礎-圖像處理(上)-Task05 圖像分割/二值化
5.1 簡介
該部分的學習內容是對經典的閾值分割算法進行回顧,圖像閾值化分割是一種傳統的最常用的圖像分割方法,因其實現簡單、計算量小、性能較穩定而成爲圖像分割中最基本和應用最廣泛的分割技術。它特別適用於目標和背景佔據不同灰度級範圍的圖像。它不僅可以極大的壓縮數據量,而且也大大簡化了分析和處理步驟,因此在很多情況下,是進行圖像分析、特徵提取與模式識別之前的必要的圖像預處理過程。圖像閾值化的目的是要按照灰度級,對像素集合進行一個劃分,得到的每個子集形成一個與現實景物相對應的區域,各個區域內部具有一致的屬性,而相鄰區域不具有這種一致屬性。這樣的劃分可以通過從灰度級出發選取一個或多個閾值來實現。
5.2 學習目標
- 瞭解閾值分割基本概念
- 理解最大類間方差法(大津法)、自適應閾值分割的原理
- 掌握OpenCV框架下上述閾值分割算法API的使用
5.3 內容介紹
1、最大類間方差法、自適應閾值分割的原理
2、OpenCV代碼實踐
3、動手實踐並打卡(讀者完成)
5.4 算法理論介紹
5.4.1 最大類間方差法(大津閾值法)
大津法(OTSU)是一種確定圖像二值化分割閾值的算法,由日本學者大津於1979年提出。從大津法的原理上來講,該方法又稱作最大類間方差法,因爲按照大津法求得的閾值進行圖像二值化分割後,前景與背景圖像的類間方差最大。
它被認爲是圖像分割中閾值選取的最佳算法,計算簡單,不受圖像亮度和對比度的影響,因此在數字圖像處理上得到了廣泛的應用。它是按圖像的灰度特性,將圖像分成背景和前景兩部分。因方差是灰度分佈均勻性的一種度量,背景和前景之間的類間方差越大,說明構成圖像的兩部分的差別越大,當部分前景錯分爲背景或部分背景錯分爲前景都會導致兩部分差別變小。因此,使類間方差最大的分割意味着錯分概率最小。
應用: 是求圖像全局閾值的最佳方法,應用不言而喻,適用於大部分需要求圖像全局閾值的場合。
優點: 計算簡單快速,不受圖像亮度和對比度的影響。
缺點: 對圖像噪聲敏感;只能針對單一目標分割;當目標和背景大小比例懸殊、類間方差函數可能呈現雙峯或者多峯,這個時候效果不好。
原理非常簡單,涉及的知識點就是均值、方差等概念和一些公式推導。爲了便於理解,我們從目的入手,反推一下這著名的OTSU算法。
求類間方差:
OTSU算法的假設是存在閾值TH將圖像所有像素分爲兩類C1(小於TH)和C2(大於TH),則這兩類像素各自的均值就爲m1、m2,圖像全局均值爲mG。同時像素被分爲C1和C2類的概率分別爲p1、p2。因此就有:
關於m1計算的補充
根據原文,式(4)還可以進一步變形:
分割:
這個分割就是二值化,OpenCV給了以下幾種方式,很簡單,可以參考:
!
5.4.2 自適應閾值
前面介紹了OTSU算法,但這算法屬於全局閾值法,所以對於某些光照不均的圖像,這種全局閾值分割的方法會顯得蒼白無力,如下圖:
顯然,這樣的閾值處理結果不是我們想要的,那麼就需要一種方法來應對這樣的情況。
這種辦法就是自適應閾值法(adaptiveThreshold),它的思想不是計算全局圖像的閾值,而是根據圖像不同區域亮度分佈,計算其局部閾值,所以對於圖像不同區域,能夠自適應計算不同的閾值,因此被稱爲自適應閾值法。(其實就是局部閾值法)
如何確定局部閾值呢?可以計算某個鄰域(局部)的均值、中值、高斯加權平均(高斯濾波)來確定閾值。值得說明的是:如果用局部的均值作爲局部的閾值,就是常說的移動平均法。
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.
實現示例(Python)
import cv2 as cv
"""大津閾值"""
# img = cv.imread('1.jpg')
# gray = cv.cvtColor(img,cv.COLOR_BGR2GRAY)
# dst = cv.threshold(gray,0,255,cv.THRESH_OTSU)
# print(dst[0])
# cv.imshow("src", img)
# cv.imshow("gray", gray)
#
# cv.imshow("dst", dst[1])
#
#
# cv.waitKey(0)
"""自適應閾值"""
img = cv.imread('1.jpg')
gray = cv.cvtColor(img,cv.COLOR_BGR2GRAY)
dst = cv.adaptiveThreshold(gray, 255, cv.ADAPTIVE_THRESH_MEAN_C, cv.THRESH_BINARY, 21, 10)
cv.imshow("src", img)
cv.imshow("gray", gray)
cv.imshow("dst", dst)
cv.waitKey(0)
大津閾值:
實現示例(Python)
- 1、大津閾值
def Otsu(src):
Grayscale = 256
graynum = [0] * 256
r,c= src.shape
"""統計直方圖數據作爲概率"""
for i in range(r):
for j in range(c):
graynum[src[i,j]] += 1
P = [0] * 256
PK = [0] * 256 # 概率累計和
MK = [0] * 256
srcpixnum = r * c
sumtmpPK = 0
sumtmpMK = 0
for i in range(Grayscale):
P[i] = graynum[i] / srcpixnum # 每個灰度級出現的概率
PK[i] = sumtmpPK + P[i]
sumtmpPK = PK[i]
MK[i] = sumtmpMK + i * P[i] #灰度級的累加均值
sumtmpMK = MK[i]
var = 0
thresh = 0
for k in range(0,Grayscale):
if (PK[k] == 0):
continue
if ((MK[Grayscale-1] * PK[k] - MK[k])**2 / (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
dst = src.copy()
for i in range(r):
for j in range(c):
if dst[i,j] >thresh:
dst[i, j]= 255
else:
dst[i, j]= 0
return thresh
img = cv.imread('1.jpg')
gray = cv.cvtColor(img,cv.COLOR_BGR2GRAY)
print(Otsu(gray))
- 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的二值化有一些優化方法,讀者可以嘗試學習並實現。