大家好,又见面了,我是你们的朋友全栈君。
写在前面
大津法(OTSU)是一种确定图像二值化分割阈值的算法,由日本学者大津于1979年提出。从大津法的原理上来讲,该方法又称作最大类间方差法,因为按照大津法求得的阈值进行图像二值化分割后,前景与背景图像的类间方差最大。
它被认为是图像分割中阈值选取的最佳算法,计算简单,不受图像亮度和对比度的影响,因此在数字图像处理上得到了广泛的应用。它是按图像的灰度特性,将图像分成背景和前景两部分。因方差是灰度分布均匀性的一种度量,背景和前景之间的类间方差越大,说明构成图像的两部分的差别越大,当部分前景错分为背景或部分背景错分为前景都会导致两部分差别变小。因此,使类间方差最大的分割意味着错分概率最小。
应用:是求图像全局阈值的最佳方法,应用不言而喻,适用于大部分需要求图像全局阈值的场合。
优点:计算简单快速,不受图像亮度和对比度的影响。
缺点:对图像噪声敏感;只能针对单一目标分割;当目标和背景大小比例悬殊、类间方差函数可能呈现双峰或者多峰,这个时候效果不好。
Opencv 接口:
double threshold(InputArray src, OutputArray dst, double thresh, double maxval, int type)
Github: https://github.com/2209520576/Image-Processing-Algorithm 。欢迎Star、Fork。
原理
原理非常简单,涉及的知识点就是均值、方差等概念和一些公式推导。为了便于理解,我们从目的入手,反推一下这著名的OTSU算法。
求类间方差:
OTSU算法的假设是存在阈值TH将图像所有像素分为两类C1(小于TH)和C2(大于TH),则这两类像素各自的均值就为m1、m2,图像全局均值为mG。同时像素被分为C1和C2类的概率分别为p1、p2。因此就有:
p1*m1+p2*m2=mG (1)
p1+p2=1 (2)
根据方差的概念,类间方差表达式为:
(3)
我们把上式化简,将式(1)代入式(3),可得:
(4)
其实求能使得上式最大化的灰度级 k 就是OTSU阈值了,很多博客也是这样做的。
其中:
(5)
(6)
(7)
照着公式,遍历0~255个灰度级,求出使式(4)最大的 k 就ok了。
————————————————————————-分割线————————————————————————————-
但是根据原文(为了尊重原文),式(4)还可以进一步变形。
首先灰度级K的累加均值m和图像全局均值mG分别为:
(8)
(9)
再瞅瞅式(6),m1、m2就可变为:
(10)
(11)
式(10)、(11)代入式(4),我们可得原文最终的类间方差公式:
(12)
根据公式(5)、(8)、(9)求能使得上式(12)最大化的灰度级 k 就是OTSU阈值。
分割:
这个分割就是二值化,OpenCV给了以下几种方式,很简单,可以参考:
基于OpenCV实现
#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);
}
效果
先看看分割效果(和OpenCV自带构造函数对比):效果一样
本文实现 原图 opencv自带构造函数
再看看阈值求取的准确性和效率吧(和OpenCV自带构造函数对比):图像分辨率为702 * 648
求出来的阈值和opencv一样,时间为1ms左右,速度还行。
参考:
https://www.cnblogs.com/ranjiewen/p/6385564.html
http://www.dididongdong.com/archives/4614
发布者:全栈程序员-用户IM,转载请注明出处:https://javaforall.cn/125492.html原文链接:https://javaforall.cn
【正版授权,激活自己账号】: Jetbrains全家桶Ide使用,1年售后保障,每天仅需1毛
【官方授权 正版激活】: 官方授权 正版激活 支持Jetbrains家族下所有IDE 使用个人JB账号...