根据模糊阈值分割的原理,本文在“模糊阈值分割代码(1)"的基础上进行了拓展,主要是利用图像直方图,搜索最佳波谷阈值,实现多阈值分割。
其过程主要如下:
1. 获取图像直方图
2. 进行直方图平滑,以去毛刺
3. 进行波峰搜索。这部分比较复杂,需要考虑的问题也多。包括波峰的判定准则、伪峰值的去除、半峰值的判断等等。
一般来说,一个峰值包括起始位置(上坡段)、终止位置(下坡段)和峰值位置。本文上、下坡段判断是通过连续增长(下降)的数量来判断的,比如连续超过10个增长的就认为是上坡段,而连续10个下降的就是下坡段。当然了,有些峰值非常小,这个就需要缩小判定阈值。峰值的位置是在上下坡段的区间内取重心的方式得到的(见函数GetPeaks)。
4. 在波峰之间采用S函数的模糊化(MemshipFunc),获得模糊曲线。我们知道,模糊化的过程窗口的宽度很重要,一般窗宽取峰值距离的0.3~0.8。由于上一步把峰值取到了,所以这一步就可以根据峰值间的距离设定窗宽,达到自适应的目的。
5. 获得模糊率曲线后,搜索不同峰值间的最小值就可以获得分割阈值。
struct Peack
{
int nStat;
int nEnd;
int nValue;
Peack():nStat(0),nEnd(0),nValue(0)
{}
};
/*
名称:BlurCure
参数:lpBmp - 图像数据指针
lSrcWidth - 图像宽度
lSrcHeight - 图像高度
dLineBites - 图像单行数据量
说明:获取模糊阈值分割的模糊曲线dBlurData
*/
void BlurCurve(LPSTR lpBmp, long lSrcWidth, long lSrcHeight, DWORD dLineBites)
{
if(lpBmp == NULL)
return;
int i,j;
LPSTR lpSrcPtr = NULL;
BYTE btTempValue = 0;
int nTistogram[256] = {0};
for (i = 0;i < lSrcHeight; ++i)
{
lpSrcPtr = lpBmp + i * dLineBites;
for (j = 0;j <lSrcWidth; ++j)
{
btTempValue = (BYTE)*(lpSrcPtr + j);
nTistogram[btTempValue]++;
}
}
//-----------------直方图平均-----------
int nSum = 0, nMaxTtg = 0;
const int c_nScop = 5;
int arrValue[c_nScop] = {0};
for (j = 0;j <256; ++j)
{
nSum += nTistogram[j];
if(j < c_nScop)
{
arrValue[j] = nTistogram[j];
nTistogram[j] = nSum/(j+1);
}
else
{
i = j%c_nScop;
nSum -= arrValue[i];
arrValue[i] = nTistogram[j];
nTistogram[j] = nSum/c_nScop;
}
if(nMaxTtg < nTistogram[j])
nMaxTtg = nTistogram[j];
outFile<<nTistogram[j]<<endl;
}//end
//--------------------------------------- vector <Peack> vctPeak;
GetPeaks(nTistogram,nMaxTtg/500,vctPeak);
int nNum = vctPeak.size();
if(nNum > 1)
{
int arrThesh[255] = {0};
int nSetScop = 255/(nNum -1);
for (i=0; i<nNum-1; i++)
{
int nWinWidth = int(0.3*(vctPeak[i+1].nValue-vctPeak[i].nValue));
double dGetMemship = 0,dMinData = 0xFFFFFFFF;
for (int nGrayPos = vctPeak[i].nValue;nGrayPos <= vctPeak[i+1].nValue; ++nGrayPos)
{
double dOutData = 0;
for (j = 0;j <256; ++j)
{
dGetMemship = MemshipFunc(j,nGrayPos,nWinWidth);
if(dGetMemship > 0.5)
dGetMemship = 1 - dGetMemship;
if(dGetMemship < 0)
dGetMemship = 0;//-dGetMemship;
dOutData += nTistogram[j]*dGetMemship;
}
//dOutData *= 2.0/(lSrcHeight*lSrcWidth);
if(dMinData > dOutData)
{
dMinData = dOutData;
arrThesh[i] = nGrayPos;
}
}
}
//---------
for (i = 0;i < lSrcHeight; ++i)
{
lpSrcPtr = lpSrcData + i * dLineBites;
lpDstPtr = lpDstData + i * dLineBites;
for (j = 0;j <lSrcWidth; ++j)
{
btTempValue = (BYTE)*(lpSrcPtr + j);
int k;
for ( k=0; k<nNum-1; k++)
{
if(btTempValue < arrThesh[k])
{
*(lpDstPtr + j)= (BYTE)(k*nSetScop);
break;
}
}
if(k == nNum-1)
*(lpDstPtr + j) = 255;
}
}//END FOR
}
else if(nNum == 1)//单峰,用峰值左侧点为分割点
{
for (i = 0;i < lSrcHeight; ++i)
{
lpSrcPtr = lpSrcData + i * dLineBites;
lpDstPtr = lpDstData + i * dLineBites;
for (j = 0;j <lSrcWidth; ++j)
{
btTempValue = (BYTE)*(lpSrcPtr + j);
if(btTempValue < vctPeak[0].nEnd)
*(lpDstPtr + j)= 0;
else
*(lpDstPtr + j) = 255;
}
}
}
else
{
MessageBox(_T("分割失败"),_T("提示"),MB_ICONWARNING);
}
}
/*
函数:GetPeaks
参数: pTistogram - 图像直方图
nLowValue - 谷底最小值
vctPeak - 返回的峰值列表
说明:函数主要对处理过的直方图查找谷峰
*/
void GetPeaks(const int pTistogram[256], const int nLowValue,vector<Peack> &vctPeak)
{
int arrPos[2] = {0};
int nSratMin = 0xFFFFFF,nContinueNum=0;
int nPreValue = pTistogram[0];
int nSetThres = 8;
for (int i=1; i<256; i++)
{
if(nSratMin > pTistogram[i])
{
nSratMin = pTistogram[i];
if(nSratMin <= nLowValue)
nSetThres = 5;
}
//-------------------------------
if(arrPos[0] != 0)
{
if(pTistogram[i] < nPreValue)
{
nContinueNum ++;
if(nContinueNum >= nSetThres)
arrPos[1] = i;
}
else// if(nTistogram[i] > nPreValue)
{
if(arrPos[1] != 0)//data out
{
Peack pk;
pk.nStat = arrPos[0];
pk.nEnd = arrPos[1];
vctPeak.push_back(pk);
//--------
arrPos[0] = arrPos[1] =0;
nSratMin = 0xFFFFFF;
nSetThres = 10;
}
nContinueNum = 0;
}
}
else
{
if(pTistogram[i] > nPreValue)
{
nContinueNum ++;
if(nContinueNum >= nSetThres)
arrPos[0] = i-nSetThres;
}
else //if(nTistogram[i] < nPreValue)
{
static int s_nLowNum = 0;
if(pTistogram[i] < nPreValue)
{
s_nLowNum ++;
if(s_nLowNum == i && s_nLowNum > 10)
arrPos[0] = 1;
}
else if(nContinueNum > 2)
nContinueNum ++;
else
nContinueNum = 0;
}
}
/
nPreValue = pTistogram[i];
}
//============峰值计算==============
int nSum = 0,nTistoSum = 0;
vector<Peack>::iterator it=vctPeak.begin();
while (it != vctPeak.end())
{
nSum = nTistoSum = 0;
for (int i=(*it).nStat; i<=(*it).nEnd; i++)
{
nSum += i*pTistogram[i];
nTistoSum += pTistogram[i];
}
(*it).nValue = nSum/nTistoSum;
it++;
}
}
下面是“模糊阈值分割代码(1)”中的瓶底图像获得的峰值信息:
三个阈值信息:
最终的分割结果:
从图上可以看出,该种方法还是比较准确的得到了分割的阈值,结果还是比较理想的。
当然,这是针对的多峰值的情况,对于单峰值的情况也是适用的。
其实这种算法在上世纪80年代就已经推出了,之所以没有推广开,问题是在求解的过程中有很多不确定性。比如峰值的确定,本身峰值怎么求的准确就是一个困难的问题,特别是在复杂的情况下,比如弱灰度、有噪音的情况,峰值往往并不明显。而要获得自私应的模糊阈值的,其窗宽又依赖于峰值选取的结果,结果可想而知,通用性就大大降低了!要解决这类算法的通用性问题,首先要减小不确定性,这也是现在依旧在探讨的问题。