图像分割是指根据灰度、彩色、空间纹理、几何形状等特征把图像划分成若干个互不相交的区域,使得这些特征在同一区域内表现出一致性或相似性,而在不同区域间表现出明显的不同。简单的说就是在一副图像中,把目标从背景中分离出来。对于灰度图像来说,区域内部的像素一般具有灰度相似性,而在区域的边界上一般具有灰度不连续性。
一般来说,图像分割是进行图像分析、特征提取与模式识别之前的必要的图像预处理过程。到目前为止,图像分割可以分为两大方向:传统分割方法和深度学习的分割方法。
传统分割算法主要有4种,分为:
深度学习的分割方法主要有:
下面介绍传统数字图像分割方法中的基于阈值化的方法。
阈值化图像分割是通过设定不同的特征阈值(Feature Threshold),把图像象素点分为若干类,这些特征阈值通常来自原始图像的灰度或彩色特征。
图像阈值化的目的是要按照灰度级,对像素集合进行一个划分,得到的每个子集形成一个与现实景物相对应的区域,各个区域内部具有一致的属性,而相邻区域不具有这种一致属性。这样的划分可以通过从灰度级出发选取一个或多个阈值来实现。单个阈值时,对于原始图像 f ( x , y ) f(x,y) f(x,y),按照一定的准则找到特征值T,可将图像分割为黑(灰度值为0)白(灰度值为1)两个部分,即为我们通常所说的图像二值化,这样就可以单独处理我们感兴趣的黑色或白色区域了,公式可表示为:
可见,阈值分割算法的关键是按一定的准则确定阈值,如果能确定一个合适的阈值就可准确地将图像分割开来。阈值确定后,将阈值与像素点的灰度值逐个进行比较,从而得到阈值化图像。
阈值分割的优点是计算简单、运算效率较高、速度快,并且特别适用于目标和背景占据不同灰度级范围的图像。
根据《数字图像处理(第三版)》和网上相关博客参考,介绍一种常用的阈值化图像分割方法,包括全局阈值、自适应阈值、最佳阈值及改进方法等等。
一般选取阈值就是图像直方图的视觉检测。将区分度大的两个灰度级部分之间进行划分,取T为阈值来分开它们。在此基础上学习一种自动地选择阈值的算法,方法如下:
OTSU算法也称最大类间差法,有时也称之为大津算法,由大津于1979年提出。它是按图像的灰度特性,将图像分成背景和前景两部分。因方差是灰度分布均匀性的一种度量,背景和前景之间的类间方差越大,说明构成图像的两部分的差别越大,当部分前景错分为背景或部分背景错分为前景都会导致两部分差别变小。因此,使类间方差最大的分割意味着错分概率最小。
OSTU算法的特点:
原理:
假设一幅大小为 M × N M×N M×N像素的数字图像有 L ( 0 , 1 , 2... , L − 1 ) L({0,1,2...,L-1}) L(0,1,2...,L−1)个不同的灰度级, n i n_i ni表示灰度级为 i i i的像素数,则灰度级概率可表示为为: p i = n i / M N p_i=n_i/MN pi=ni/MN,且
∑ i = 1 L − 1 p i = 1 , p i ⩾ 0 \sum_{i=1}^{L-1}p_{i}=1,p_i\geqslant 0 i=1∑L−1pi=1,pi⩾0
现在,若可选择一个阈值 T ( k ) = k ( 0 < k < L − 1 ) T(k)=k(0
P 1 ( k ) = ∑ i = 0 k p i P_1(k)=\sum_{i=0}^{k}p_i P1(k)=i=0∑kpi P 2 ( k ) = ∑ i = k + 1 L − 1 p i = 1 − P 1 ( k ) P_2(k)=\sum_{i=k+1}^{L-1}p_i=1-P_1(k) P2(k)=i=k+1∑L−1pi=1−P1(k)
根据贝叶斯公式,可得分配到 C 1 、 C 2 C_1、C_2 C1、C2的像素的平均灰度值为:
m 1 ( k ) = ∑ i = 0 k i P ( i / C 1 ) = ∑ i = 0 k i P ( C 1 / i ) P ( i ) / P ( C 1 ) = 1 P 1 ( k ) ∑ i = 0 k i p i m_1(k)=\sum_{i=0}^{k}iP(i/C_1)=\sum_{i=0}^{k}iP(C_1/i)P(i)/P(C_1)=\frac{1}{P_1(k)}\sum_{i=0}^{k}ip_i m1(k)=i=0∑kiP(i/C1)=i=0∑kiP(C1/i)P(i)/P(C1)=P1(k)1i=0∑kipi
m 2 ( k ) = ∑ j = 0 k j P ( j / C 2 ) = ∑ j = k + 1 L − 1 j P ( C 2 / j ) P ( j ) / P ( C 2 ) = 1 P 2 ( k ) ∑ j = k + 1 L − 1 j p j m_2(k)=\sum_{j=0}^{k}jP(j/C_2)=\sum_{j=k+1}^{L-1}jP(C_2/j)P(j)/P(C_2)=\frac{1}{P_2(k)}\sum_{j=k+1}^{L-1}jp_j m2(k)=j=0∑kjP(j/C2)=j=k+1∑L−1jP(C2/j)P(j)/P(C2)=P2(k)1j=k+1∑L−1jpj
其中, P 1 ( k ) P_1(k) P1(k)、 P 2 ( k ) P_2(k) P2(k)由上式给出, P ( i / C 1 ) P(i/C_1) P(i/C1)项是灰度值 i i i得概率, i i i来自 C 1 C_1 C1类; P ( j / C 2 ) P(j/C_2) P(j/C2)项是灰度值 j j j得概率, j j j来自 C 2 C_2 C2类。
设灰度级 k k k的所有灰度级累加均值为 m m m,公式为:
m = ∑ i = 0 k i p i m=\sum_{i=0}^{k}ip_i m=i=0∑kipi
则整个图像的平均灰度(全局均值) 可由下式给出:
m G = ∑ i = 0 L − 1 i p i m_G=\sum_{i=0}^{L-1}ip_i mG=i=0∑L−1ipi
再根据全局均值和阈值分割后的两部分均值之间的关系,有:
{ P 1 m 1 + P 1 m 1 = m G P 1 + P 2 = 1 \left\{\begin{matrix} P_1m_1+P_1m_1=m_G\\ P_1+P_2=1 \end{matrix}\right. { P1m1+P1m1=mGP1+P2=1
为了清楚说明,暂时忽略了阈值 k k k,则可以根据方差的概念,得到类间方差表达式为:
σ 2 = P 1 ( m 1 − m G ) 2 + P 2 ( m 2 − m G ) 2 \sigma^2=P_1(m_1-m_G)^2+P_2(m_2-m_G)^2 σ2=P1(m1−mG)2+P2(m2−mG)2
化简并消去 m 2 m_2 m2和 P 2 P_2 P2项,可得:
σ 2 = ( m G P 1 − m ) 2 P 1 ( 1 − P 1 ) \sigma^2=\frac{(m_GP_1-m)^2}{P_1(1-P_1)} σ2=P1(1−P1)(mGP1−m)2
遍历图像灰度级0-255,可求出使得类间方差 σ 2 \sigma^2 σ2最大的阈值 k k k,以上就是通过Ostu方法求全局阈值的全部过程了。
在OpenCV中使用函数threshold() 来实现方框滤波操作,其函数模型如下:
double threshold(Mat InputArray src, //原始图像
Mat OutputArray dst, //目标图像
double thresh, //阈值
double maxval, //目标图像最大值
int type //阈值分割类型
)
这个type就代表不同的分割类型,OpenCV有以下几种方式可以参考:
Otsu算法的一些改进方法:
…
最大熵阈值分割法和OTSU算法类似,假设将图像分为背景和前景两个部分。熵代表信息量,图像信息量越大,熵就越大,最大熵算法就是找出一个最佳阈值使得背景与前景两个部分熵之和最大。
原理:
数字图像中给定一个估算的概率密度函数 p ( g ) p(g) p(g),数字图像中的熵定义为:
假设分割阈值为t(0<=t
C 0 : P 0 P n , P 1 P n , . . . , P t P n C 1 : P t + 1 1 − P n , P t + 2 1 − P n , . . . , P K − 1 1 − P n \begin{aligned} C_0&:\frac{P_0}{P_n},\frac{P_1}{P_n},...,\frac{P_t}{P_n}\\ C_1&:\frac{P_{t+1}}{1-P_n},\frac{P_{t+2}}{1-P_n},...,\frac{P_{K-1}}{1-P_n} \end{aligned} C0C1:PnP0,PnP1,...,PnPt:1−PnPt+1,1−PnPt+2,...,1−PnPK−1
其中, P n P_n Pn表示的是 t t t阈值分割的背景和前景像素的累计概率,且前后景所有像素的累计概率为1,公式表述为:
P n = ∑ i = 0 t P i ∑ i = 0 K − 1 P i = 1 , P i ≥ 0. \begin{aligned} &P_n=\sum_{i=0}^{t}P_i\\ &\sum_{i=0}^{K-1}P_i=1,P_i\geq 0. \end{aligned} Pn=i=0∑tPii=0∑K−1Pi=1,Pi≥0.
此时,可以根据熵的计算公式得到前景 C 0 C_0 C0和后景 C 1 C_1 C1的熵,公式如下:
H 0 ( t ) = − ∑ i = 0 t P i P n l n P i P n H 1 ( t ) = − ∑ i = t + 1 K − 1 P i 1 − P n l n P i 1 − P n \begin{aligned} H_0(t)&=-\sum_{i=0}^{t}\frac{P_i}{P_n}ln\frac{P_i}{P_n}\\ H_1(t)&=-\sum_{i=t+1}^{K-1}\frac{P_i}{1-P_n}ln\frac{P_i}{1-P_n} \end{aligned} H0(t)H1(t)=−i=0∑tPnPilnPnPi=−i=t+1∑K−11−PnPiln1−PnPi
则对于最佳阈值 t t t,应该使得图像的总熵 ϕ t \phi _t ϕt最大:
a r g m a x t ϕ t = a r g m a x t ( H 0 ( t ) + H 1 ( t ) ) \underset{t}{argmax}\phi _t=\underset{t}{argmax}(H_0(t)+H_1(t)) targmaxϕt=targmax(H0(t)+H1(t))
#include
#include
#include
#include
using namespace cv;
// 添加Gussia噪声
void addGaussianNoise(Mat &m, int mu, int sigma)
{
// 产生高斯分布随机数发生器
std::random_device rd;
std::mt19937 gen(rd());
std::normal_distribution<> d(mu, sigma); //高斯噪声
auto rows = m.rows; // 行数
auto cols = m.cols * m.channels(); // 列数
for (int i = 0; i < rows; i++){
auto p = m.ptr<uchar>(i); // 取得行首指针
for (int j = 0; j < cols; j++){
auto tmp = p[j] + d(gen);
tmp = tmp > 255 ? 255 : tmp;
tmp = tmp < 0 ? 0 : tmp;
p[j] = tmp;
}
}
}
int mean_pixel(Mat& src){
//计算图像平均灰度
int c=src.cols,r=src.rows;
int sum=0;
for(int i=0;i<r;i++){
for(int j=0;j<c;j++){
sum =sum+(int)src.ptr<uchar>(i)[j];
}
}
return (int)round(sum/(r*c));
}
int main()
{
Mat img=imread("C:/Users/Administrator/Desktop/beauty.jpg",0); //灰度图读入
Size dsize = Size(round(0.3 * img.cols), round(0.3 * img.rows));//Size型 改变尺寸
resize(img, img, dsize, 0, 0, INTER_LINEAR); //使用双线性插值缩放一下尺寸
//addGaussianNoise(img,0,0.2); //添加噪声
//基本全局阈值处理
int r=img.rows,c=img.cols;
Mat dst=img.clone();
int count = 0;
int g_max(0),g_min(0);
int max_count(0),min_count(0);
int T_next; //计算下一次平均灰度
int T=mean_pixel(img);
bool done = false;
while(!done){
count = count+1;
for(int i=0;i<r;i++){
for(int j=0;j<c;j++){
if(img.ptr<uchar>(i)[j] < T){
g_max += img.ptr<uchar>(i)[j];
max_count++;
}
else{
g_min += img.ptr<uchar>(i)[j];
min_count++;
}
}
}
T_next = 0.5*((int)g_max/max_count+(int)g_min/min_count);
done = abs(T-T_next)<0.5; //直到符合条件跳出循环
T =T_next; //否则继续
}
std::cout<<"count="<<count<<std::endl;
std::cout<<"T="<<T<<std::endl;
//二值化处理
for (int i = 0; i < r; ++i){
uchar* ptr = dst.ptr<uchar>(i);
for (int j = 0; j < c; ++j){
if (ptr[j]> T)
ptr[j] = 255;
else
ptr[j] = 0;
}
}
cv::imshow("srcImage",img);
cv::imshow("dstImage",dst);
cv::waitKey();
return 0;
}
实验结果如下:
count=6.T=160;也就是自动选择六次才达到预定的阈值分割目标,此时阈值为160。
#include
#include
#include
using namespace std;
using namespace cv;
int myOtsu(Mat & src)
{
int th;
const int GrayScale = 256; //单通道图像总灰度256级
int pixCount[GrayScale] = {
0};//每个灰度值所占像素个数
int pixSum = src.cols * src.rows;//图像总像素点
float pixPro[GrayScale] = {
0};//每个灰度值所占总像素比例
float p0, p1, p0tmp, p1tmp, m0, m1, deltaTmp, deltaMax = 0;
//以下给出了两种 图像像素访问的方法(1).at方法 (2).ptr指针访问(快一些)
for(int i = 0; i < src.cols; i++){
for(int j = 0; j < src.rows; j++){
//.at方法 只适合灰度值为8位的图像
pixCount[src.at<uchar>(j,i)]++;//统计每个灰度级中像素的个数
}
}
/*for (int i = 0; i < r; ++i){
const uchar* ptr = src.ptr(i);
for (int j = 0; j < c; ++j){ //统计每个灰度级中像素的个数
graynum[ptr[j]]++;
}
}*/
for(int i = 0; i < GrayScale; i++)
{
pixPro[i] = pixCount[i] * 1.0 / pixSum;//计算每个灰度级的像素数目占整幅图像的比例
}
for(int i = 0; i < GrayScale; i++)//遍历所有从0到255灰度级的阈值分割条件,测试哪一个的类间方差最大
{
p0 = p1 = p0tmp = p1tmp = m0 = m1 = deltaTmp = 0;
//w0(p0tmp)、w1(p1tmp)表示像素被分为C1、C2类中的概率(累计和)
//u0/u1表示像素被分为C1、C2类中的平均灰度
//deltaTmp和deltaMax维护一个最大类间方差
for(int j = 0; j < GrayScale; j++){
if(j <= i)//C1类
{
p0 += pixPro[j];
p0tmp += j * pixPro[j];
}
else//C2类
{
p1 += pixPro[j];
p1tmp += j * pixPro[j];
}
}
m0 = p0tmp / p0;
m1 = p1tmp / p1;
deltaTmp = (float)(p0 *p1* pow((m0 - m1), 2)); //类间方差公式 g = w1 * w2 * (u1 - u2) ^ 2
if(deltaTmp > deltaMax)
{
deltaMax = deltaTmp;
th = i;
}
}
return th;
}
int main()
{
Mat src = imread("C:/Users/Administrator/Desktop/beauty.jpg",0);//单通道读取图像
Size dsize = Size(round(0.2 * src.cols), round(0.2 * src.rows));//Size型 改变尺寸
resize(src, src, dsize, 0, 0, INTER_LINEAR); //使用双线性插值缩放一下尺寸
/*my_dst: 自己实现的大津法 得到的处理图像
otsu_dst:opencv自带的大津法 得到的处理图像
sub:两个处理图像相差图
*/
Mat my_dst, otsu_dst, sub;
/*my_th: 自己实现的大津法 得到的最大类件方差 即阈值
th:opencv自带的大津法 得到的最大类件方差 即阈值
*/
int my_th, th;
/*计算开销时间,对比两个算法效率*/
long my_start = clock(); //开始时间
{
my_th = myOtsu(src);
threshold(src,my_dst,my_th,255,CV_THRESH_BINARY);
}
long my_finish = clock(); //结束时间
long my_t = my_finish-my_start;
printf("The run time is:%9.3lf\n", my_t, "ms!\n"); //输出时间
cout << "myOtsu threshold >> " << my_th << endl;
long otsu_start = clock(); //开始时间
{
th = threshold(src,otsu_dst,0,255,CV_THRESH_OTSU);
}
long otsu_finish = clock(); //结束时间
long t = my_finish-my_start;
printf("The run time is:%9.3lf\n", (double) t / CLOCKS_PER_SEC, "ms!\n"); //输出时间
cout << "Otsu threshold >> " << th << endl;
subtract(otsu_dst,my_dst,sub);//两图像相减
imshow("src",src);
imshow("myOtsu",my_dst);
imshow("Otsu",otsu_dst);
imshow("Sub",sub);
waitKey(0);
return 0;
}
自己实现的Ostu和OpenCV自带的Ostu函数得到的阈值都是152,从色差图中也能看出来它们实现的效果一样。
#include
#include
#include
#include
int Max_Entropy(cv::Mat& src, cv::Mat& dst, int thresh, int p){
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){
if (ptr[j] == 0) //排除掉黑色的像素点
continue;
Graynum[ptr[j]]++;
}
}
float probability = 0.0; //概率
float max_Entropy = 0.0; //最大熵
int totalpix = r*c;
for (int i = 0; i < Grayscale; ++i){
float HO = 0.0; //前景熵
float HB = 0.0; //背景熵
//计算前景像素数
int frontpix = 0;
for (int j = 0; j < i; ++j){
frontpix += Graynum[j];
}
//计算前景熵
for (int j = 0; j < i; ++j){
if (Graynum[j] != 0){
probability = (float)Graynum[j] / frontpix;
HO = HO + probability*log(1/probability);
}
}
//计算背景熵
for (int k = i; k < Grayscale; ++k){
if (Graynum[k] != 0){
probability = (float)Graynum[k] / (totalpix - frontpix);
HB = HB + probability*log(1/probability);
}
}
//计算最大熵
if(HO + HB > max_Entropy){
max_Entropy = HO + HB;
thresh = i + p;
}
}
//阈值处理
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("C:/Users/Administrator/Desktop/beauty.jpg",0); //读入灰度图
if (src.empty()){
return -1;
}
cv::Size dsize = cv::Size(round(0.3 * src.cols), round(0.3 * src.rows));//Size型 改变尺寸
cv::resize(src, src, dsize, 0, 0, cv::INTER_LINEAR); //使用双线性插值缩放一下尺寸
cv::Mat dst, dst2;
int thresh = 0;
thresh = Max_Entropy(src, dst, thresh,30); //Max_Entropy
std::cout << "Mythresh=" << thresh << std::endl;
double Otsu = 0;
Otsu = cv::threshold(src, dst2, Otsu, 255, CV_THRESH_OTSU + CV_THRESH_BINARY);
std::cout << "Otsuthresh=" << Otsu << std::endl;
cv::imshow("srcImage", src);
cv::imshow("maxEntropy", dst);
cv::imshow("Otsuthresh", dst2);
cv::waitKey(0);}
实现效果:
通过调整阈值偏置,可以实现Ostu的全局阈值分割的效果,
参考博客:
【1】《数字图像处理(第三版)》_冈萨雷斯
【2】图像分割最全综述
【3】OTSU方法:https://blog.csdn.net/mary_0830/article/details/89597672
【4】最大熵阈值分割算法原理及实现