-
图像分割是由图像处理进到图像分析的关键步骤。它是目标表达的基础,使得更高层的图像分析和理解成为可能。
-
主要难点:
- 多尺度问题
- 多视角问题(旋转)
- 光照问题
- 感受野引发的边缘震荡
- 类别不均衡问题
- 区域遮挡问题
-
基本依据
- 区域内一致性
- 区域间不一致性
阈值分割方法
阈值法的基本思想是基于图像的灰度特征来计算一个或多个灰度阈值,并将图像中每个像素的灰度与阈值相比较,最后将像素根据比较结果分到合适的类别中。因此,该类方法最为关键的一步是按照某个准则函数来求解最佳灰度阈值。
全局阈值
- 固定某像素值为分割点。
for (int i = 0; i < nWidth; ++i)
{
for (int j = 0; j < nHigh; ++j)
{
// threshold可由双峰法给出
if (Image[i][j] >= threshold)
{
Image[i][j] = 255;
}
else
{
Image[i][j] = 0;
}
}
}
- 直方图双峰法:Prewitt等人于六十年代中期提出的直方图双峰法(mode法)是典型的全局单阈值分割方法。该方法的基本思想是:假设图像中有明显的目标和背景,则其灰度直方图呈双峰分布,当灰度直方图具有双峰特性时,选取两峰之间的谷底对应的灰度值作为阈值。如果背景的灰度值在整个图像中可以合理地看作为恒定,而且所有物体与背景都具有几乎相同的对比度,那么选择一个正确的、固定的全局阈值会有较好的效果。
迭代阈值
- 迭代法是基于逼近的思想,其步骤如下
- 求出图像的最大最小灰度值,分别记为ZMAX和ZMIN,令初始阈值 T 0 = ( Z M A X + Z M I N ) / 2 T_0= (ZMAX+ZMIN)/2 T0=(ZMAX+ZMIN)/2;
- 根据阈值 T K T_K TK将图像分割为前景和背景,分别求出两者的平均灰度值 Z O Z_O ZO和 Z B Z_B ZB;
- 求出新阈值 T k + 1 = ( Z O + Z B ) / 2 T_{k+1}=(Z_O+Z_B)/2 Tk+1=(ZO+ZB)/2;
- 若 T k = = T k + 1 T_k==T_{k+1} Tk==Tk+1,则所得即为阈值;否则转2,迭代计算。
自适应阈值
- 前景物体和背景的对比度不能完全一致,可以对图像的局部特征采用不同的阈值进行分割。将图像分为几个区域分别选择阈值,或动态的根据一定领域范围选择没点处的阈值,从而进行图像分割。
- 均值法 :
把图像分成m*n块子图,求取每一块子图的灰度均值作为阈值。子图越多,分割效果越好,效率越低。 - 大津法OTSU(最大类间方差法):
日本学者大津在1979年提出的自适应阈值确定方法。按照图像的灰度特性,将图像分为背景和目标两部分。背景和目标之间的类间方差越大,说明构成图像的两部分的差别越大,当部分目标错分为背景或部分背景错分为目标都会导致两部分差别变小。因此,使类间方差最大的分割意味着错分概率最小。
对于图像 ( x , y ) (x,y) (x,y),前景(即目标)和背景的分割阈值记作T,属于前景的像素点数占整幅图像的比例记为 ω 0 \omega_0 ω0,其平均灰度 μ 0 \mu_0 μ0;背景像素点数占整幅图像的比例记为 ω 1 \omega_1 ω1,其平均灰度为 μ 1 \mu_1 μ1;图像的总平均灰度记为 μ \mu μ,类间方差记为 g = ω 0 ( μ 0 − μ ) 2 + ω 1 ( μ 1 − μ ) = ω 0 ω 1 ( μ 0 − μ 1 ) 2 g=\omega_0(\mu_0-\mu)^2+\omega_1(\mu_1-\mu)=\omega_0\omega_1(\mu_0-\mu_1)^2 g=ω0(μ0−μ)2+ω1(μ1−μ)=ω0ω1(μ0−μ1)2。
- 均值法 :
/*
parameter:
*image -- buffer for image
rows, cols -- size of image
x0, y0, dx, dy -- region of vector used for computing threshold
vvv -- debug option, is 0, no debug information outputed
OTSU global thresholding routine:
takes a 2D unsigned char array pointer, number of rows, and number of cols in the array. returns the value of the threshold
*/
int otsu(unsigned char * image, int rows, int cols, int x0, int y0, int dx, int dy)
{
unsigned char *np; // 图像指针
int thresholdValue = 1; // 阈值
int ihist[256]; // 图像直方图
int i, j, k; // various counters
int n, n1, n2, gmin, gmax;
double m1, m2, sum, csum, fmax, sb;
// 对直方图置零
memset(ihist, 0, sizeof(ihist));
gmin = 255; gmax = 0;
// 生成直方图
// 求出最大像素值和最小像素值
// 求出图像中各个灰度值的个数存与数组ihist中
for (i = y0+1; i < y0 + dy - 1; i++)
{
np = &image[i*cols+x0+1];
for (j = x0 + 1; j < x0 + dx - 1; j++)
{
ihisi[*np]++;
if(*np > gmax) gmax = *np;
if (*np < gmin) gmin = *np;
np++; /*next pixel*/
}
}
// set up everything
sum = csum = 0.0;
n = 0;
for (k =0; k <= 255; k++)
{
// 图像的总灰度值
sum += (double)k * (double)ihist[k]; /* x*f(x)质量矩*/
// 总像素点数
n += ihist[k]; /* f(x)质量 */
}
if (!n)
{
// if n has no value, there is problem...
fprintf(stderr, "NOT NORMAL thresholdValue = 160/n";
return (160);
}
// do the otsu global thresholding method
for (k = 0; k <= 255; k++)
{
n1 += ihist[k];
if (!n1)
{
continue;
}
n2 = n - n1;
if (n2 == 0)
{
break;
}
csum += (double) k * ihist[k];
m1 = csum / n1;
m2 = (sum - csum) / n2;
sb = (double)n1 * (double)n2 * (m1 - m2) * (m1 - m2);
/* bbg: note: can be optimized */
if (sb > fmax)
{
fmax = sb;
thresholdValue = k;
}
}
return(thresholdValue);
}
边缘分割方法
图像中两个不同区域的边界线上连续的像素点的集合,是图像局部特征不连续性的反映,体现了灰度、颜色、纹理等图像特性的突变。通常情况下,基于边缘的分割方法指的是基于灰度值的边缘检测,它是建立在边缘灰度值会呈现出阶跃型或屋顶型变化这一观测基础上的方法。阶跃型边缘两边像素点的灰度值存在明显的差异,而屋顶型边缘则位于灰度值上升或下降的转折处。正是基于这一特性,可以使用微分算子进行边缘检测,即使用一阶导数的极值与二阶导数的过零点来确定边缘,具体实现时可以使用图像与模版卷积来完成。
区域分割方法
按照图像的相似性准则划分为不同区域块,主要有种子区域生长法,区域分裂合并法、分水岭法等。
区域生长
- 区域生长的基本思想是将具有相似性质的像素集合起来构成区域。具体先对每个需要分割的区域找一个种子像素作为生长的起点,然后将种子像素周围邻域中与种子像素有相同或相似性质的像素(根据某种事先确定的生长或相似准则来判定)合并到种子像素所在的区域中。将这些新像素当作新的种子像素继续进行上面的过程,直到再没有满足条件的像素可被包括进来。这样一个区域就长成了。区域生长需要选择一组能正确代表所需区域的种子像素,确定在生长过程中的相似性准则,制定让生长停止的条件或准则。相似性准则可以是灰度级、彩色、纹理、梯度等特性。选取的种子像素可以是单个像素,也可以是包含若干个像素的小区域。大部分区域生长准则使用图像的局部性质。生长准则可根据不同原则制定,而使用不同的生长准则会影响区域生长的过程。区域生长法的优点是计算简单,对于较均匀的连通目标有较好的分割效果。它的缺点是需要人为确定种子点,对噪声敏感,可能导致区域内有空洞。另外,它是一种串行算法,当目标较大时,分割速度较慢,因此在设计算法时,要尽量提高效率。
//区域生长,4连通区域,基于一个种子点,种子点可以自选,种子点在seed图像内设置为255
void Grow(IplImage* src,IplImage* src1,IplImage* seed, int t1)//gray=255
{
stack <seedpoint> seedd;//定义一个堆栈
seedpoint point;//堆栈的元素为point
// 获取图像数据,保存种子区域
int height = seed->height;
int width = seed->width;
int step = seed->widthStep;
uchar* seed_data = (uchar *)seed->imageData;
uchar* src_data =(uchar *)src->imageData;
uchar* src1_data =(uchar*)src1->imageData;
int temp;//用于当前点的像素值
for(int i=0;i<height;i++)
{
for(int j=0;j<width;j++)
{
if(seed_data[i*step+j]==255)
{
point.x=i;
point.y=j;
temp = src1_data[point.x*step+point.y];
seedd.push(point);
}
}
}
seedpoint temppoint;//临时存放种子点的中间变量
while(!seedd.empty())
{
point=seedd.top(); //返回栈顶数据,不删除
seedd.pop(); //栈顶数据出栈,删除栈顶元素,但不返回其值
if((point.x>0)&&(point.x<(height-1))&&(point.y>0)&&(point.y<(width-1)))//如果种子点在图像范围内,分别检测其8个邻域点
{
//邻域一点像素为0,即不是种子点本身,并且该点与种子点之差小于设定的阈值
if((seed_data[(point.x-1)*step+point.y]==0)&&(abs(src1_data[(point.x-1)*step+point.y]-temp) < t1))
{
//cvSet2D(seed,(point.x-1),point.y,pixel);
seed_data[(point.x-1)*step+point.y]=255;
src_data[(point.x-1)*step+point.y]=src1_data[(point.x-1)*step+point.y];
temppoint.x=point.x-1;
temppoint.y=point.y;
seedd.push(temppoint);
}
if((seed_data[point.x*step+point.y+1]==0)&&(abs(src1_data[point.x*step+point.y+1]-temp) < t1))
{
//cvSet2D(seed,(point.x),(point.y+1),pixel);
seed_data[point.x*step+point.y+1]=255;
src_data[point.x*step+point.y+1]=src1_data[point.x*step+point.y+1];
temppoint.x=point.x;
temppoint.y=point.y+1;
seedd.push(temppoint);
}
if((seed_data[point.x*step+point.y-1]==0)&&(abs(src1_data[point.x*step+point.y-1]-temp) < t1))
{
//cvSet2D(seed,(point.x),(point.y-1),pixel);
seed_data[point.x*step+point.y-1]=255;
src_data[point.x*step+point.y-1]=src1_data[point.x*step+point.y-1];
temppoint.x=point.x;
temppoint.y=point.y-1;
seedd.push(temppoint);
}
if((seed_data[(point.x+1)*step+point.y]==0)&&(abs(src1_data[(point.x+1)*step+point.y]-temp) < t1))
{
//cvSet2D(seed,(point.x),(point.y+1),pixel);
seed_data[(point.x+1)*step+point.y]=255;
src_data[(point.x+1)*step+point.y]=src1_data[(point.x+1)*step+point.y];
temppoint.x=point.x+1;
temppoint.y=point.y;
seedd.push(temppoint);
}
/
if((seed_data[(point.x-1)*step+point.y-1]==0)&&(abs(src1_data[(point.x-1)*step+point.y-1]-temp) < t1))
{
//cvSet2D(seed,(point.x-1),point.y,pixel);
seed_data[(point.x-1)*step+point.y-1]=255;
src_data[(point.x-1)*step+point.y-1]=src1_data[(point.x-1)*step+point.y-1];
temppoint.x=point.x-1;
temppoint.y=point.y-1;
seedd.push(temppoint);
}
if((seed_data[(point.x-1)*step+point.y+1]==0)&&(abs(src1_data[(point.x-1)*step+point.y+1]-temp) < t1))
{
//cvSet2D(seed,(point.x-1),point.y,pixel);
seed_data[(point.x-1)*step+point.y+1]=255;
src_data[(point.x-1)*step+point.y+1]=src1_data[(point.x-1)*step+point.y+1];
temppoint.x=point.x-1;
temppoint.y=point.y+1;
seedd.push(temppoint);
}
if((seed_data[(point.x+1)*step+point.y-1]==0)&&(abs(src1_data[(point.x+1)*step+point.y-1]-temp) < t1))
{
//cvSet2D(seed,(point.x-1),point.y,pixel);
seed_data[(point.x+1)*step+point.y-1]=255;
src_data[(point.x+1)*step+point.y-1]=src1_data[(point.x+1)*step+point.y-1];
temppoint.x=point.x+1;
temppoint.y=point.y-1;
seedd.push(temppoint);
}
if((seed_data[(point.x+1)*step+point.y+1]==0)&&(abs(src1_data[(point.x+1)*step+point.y+1]-temp) < t1))
{
//cvSet2D(seed,(point.x-1),point.y,pixel);
seed_data[(point.x+1)*step+point.y+1]=255;
src_data[(point.x+1)*step+point.y+1]=src1_data[(point.x+1)*step+point.y+1];
temppoint.x=point.x+1;
temppoint.y=point.y+1;
seedd.push(temppoint);
}
}
}
}
区域分裂合并
- 区域分裂合并法(Gonzalez, 2002),区域生长是从某个或者某些像素点出发,最后得到整个区域,进而实现目标提取。分裂合并差不多是区域生长的逆过程:从整个图像出发,不断分裂得到各个子区域,然后再把前景区域合并,实现目标提取。分裂合并的假设是对于一幅图像,前景区域由一些相互连通的像素组成的,因此,如果把一幅图像分裂到像素级,那么就可以判定该像素是否为前景像素。当所有像素点或者子区域完成判断以后,把前景区域或者像素合并就可得到前景目标。在这类方法中,最常用的方法是四叉树分解法。设R代表整个正方形图像区域,P代表逻辑谓词。基本分裂合并算法步骤如下:
(1)对任一个区域,如 果H(Ri)=FALSE就将其分裂成不重叠的四等份;
(2)对相邻的两个区域Ri和Rj,它们也可以大小不同(即不在同一层),如果条件H(Ri∪Rj)=TRUE满足,就将它们合并起来。
(3)如果进一步的分裂或合并都不可能,则结束。
分裂合并法的关键是分裂合并准则的设计。这种方法对复杂图像的分割效果较好,但算法较复杂,计算量大,分裂还可能破坏区域的边界。
分水岭法
- 分水岭法(Meyer, 1990)是一种基于拓扑理论的数学形态学的分割方法,其基本思想是把图像看作测地学上的拓扑地貌,图像中每一点像素的灰度值表示该点的海拔高度,每一个局部极小值及其影响区域称为集水盆,而集水盆的边界则形成分水岭。该算法的实现可以模拟成洪水淹没的过程,图像的最低点首先被淹没,然后水逐渐淹没整个山谷。当水位到达一定高度的时候将会溢出,这时在水溢出的地方修建堤坝,重复这个过程直到整个图像上的点全部淹没,这时所建立的一些列堤坝就成为分开各个盆地的分水岭。分水岭算法对微弱边缘有着良好的响应,但图像中的噪声会使分水岭算法产生过分割的现象。
#include <opencv2/opencv.hpp>
#include <iostream>
using namespace cv;
using namespace std;
int main(int argc, char** argv)
{
// input
Mat src = imread("D:/vcprojects/images/coins_001.jpg");
if(src.empty()){
printf("could not load image...\n")
return -1;
}
namedWindow("input image", CV_WINDOW_AUTOSIZE);
imshow("input image", src);
// MeanShiftFiltering()
// 找到概率密度梯度为零的采样点,并以此作为特征空间聚类的模式点
// Non-Parametric Density Esitimation
Mat gray, binary, shifted;
pyrMeanShiftFiltering(src, shifted, 21, 51);
imshow("shifted:, shifted);
// gray
cvtColor(shifted, gray, COLOR_BGR2GRAY);
threshold(gray, binary, 0, 255, THRESH_BINARY|THRESH_OTSU);
imshow("binary", binary);
// distance transform
Mat dist;
distranceTransform(binary, dist, DistanceType::DIST_L2, 3, CV_32F);
normalize(dist, dist, 0, 1, NORM_MINMAX);
imshow("distance result", dist);
// binary
threshold(dist, dist, 0.4, 1, THRESH_BINARY);
imshow("distance binary", dist);
// markers
Mat dist_m;
dist.convertTo(dist_m, CV_8U);
vector<vector<Point>> contours;
findContours(dist_m, contours, RETR_EXTERNAL, CHAIN_APPROX_SIMPLE, Point(0, 0));
// create markers
Mat markers = Mat::zeros(src.size(), CV_32SC1)
for(size_t t = 0; t < contours.size(); t++)
{
drawContours(markers, contours, static_cast<int>(i), Scalar::all(static_cast<int>(i) + 1), -1);
}
circle(markers, Point(5, 5), 3, Scalar(255), -1);
imshow("markers", markers*10000);
// Morphological filtering
Mat k = getStructuringElement(MORPH_RECT, Size(3, 3), Point(-1, -1));
morphologyEx(src, src, MORPH_ERODE, k);
// watershed
watershed(src, markers);
Mat mark = Mat::zeros(markers.size(), CV_8UC1);
markers.convertTo(mark, CV_8UC1);
bitwise_not(mark, mark, Mat());
imshow("watershed result", mark);
// generate random color
vector<Vec3b> colors;
for(size_t i = 0; i < contours.size(), i++){
int r = theRNG().uniform(0, 255);
int g = theRNG().uniform(0, 255);
int b = theRNG().uniform(0, 255);
colors.push_back(Vec3b((uchar)b, (uchar)g, (uchar)r));
}
Mat dst = Mat::zeros(markers.size(), CV_8UC3);
int index = 0;
for(int row = 0; row < markers.rows; row++){
for(int col = 0; col < markers.clos; col++){
index = markers.at<int>(row, col);
if(index > 0 && index < contours.size()){
dst.at<Vec3b>(row, col) = colors[index - 1];
}else{
dst.at<Vec3b>(row,col) = Vec3(0, 0, 0);
}
}
}
imshow("Final Result", dst);
printf("number of objects:%d", contours.size());
waitKey(0);
return 0;
}
图论分割方法
- 基本思想:
- 将图像用图的方式表示,顶点表示像素,边表示像素之间的关系。图像分割对应图的割集。
- 确定图中边的权值,使图像分割目标(能量最小化)对应图的最小割。
- 用最大流算法求解最小割问题 (max flow ~ min cut)。
特定理论分割方法
聚类分析
特征空间聚类法进行图像分割是将图像空间中的像素用对应的特征空间点表示,根据它们在特征空间的聚集对特征空间进行分割,然后将它们映射回原图像空间,得到分割结果。其中,K均值、模糊C均值聚类(FCM)算法是最常用的聚类算法。K均值算法先选K个初始类均值,然后将每个像素归入均值离它最近的类并计算新的类均值。迭代执行前面的步骤直到新旧类均值之差小于某一阈值。模糊C均值算法是在模糊数学基础上对K均值算法的推广,是通过最优化一个模糊目标函数实现聚类,它不像K均值聚类那样认为每个点只能属于某一类,而是赋予每个点一个对各类的隶属度,用隶属度更好地描述边缘像素亦此亦彼的特点,适合处理事物内在的不确定性。利用模糊C均值(FCM)非监督模糊聚类标定的特点进行图像分割,可以减少人为的干预,且较适合图像中存在不确定性和模糊性的特点。FCM算法对初始参数极为敏感,有时需要人工干预参数的初始化以接近全局最优解,提高分割速度。另外,传统FCM算法没有考虑空间信息,对噪声和灰度不均匀敏感。
模糊集理论
模糊集理论具有描述事物不确定性的能力,适合于图像分割问题。1998年以来,出现了许多模糊分割技术,在图像分割中的应用日益广泛。模糊技术在图像分割中应用的一个显著特点就是它能和现有的许多图像分割方法相结合,形成一系列的集成模糊分割技术,例如模糊聚类、模糊阈值、模糊边缘检测技术等。模糊阈值技术利用不同的S型隶属函数来定义模糊目标,通过优化过程最后选择一个具有最小不确定性的S函数。用该函数增强目标及属于该目标的像素之间的关系,这样得到的S型函数的交叉点为阈值分割需要的阈值,这种方法的困难在于隶属函数的选择。基于模糊集合和逻辑的分割方法是以模糊数学为基础,利用隶属图像中由于信息不全面、不准确、含糊、矛盾等造成的不确定性问题。该方法在医学图像分析中有广泛的应用,如薛景浩等人提出的一种新的基于图像间模糊散度的阈值化算法以及它在多阈值选择中的推广算法,采用了模糊集合分别表达分割前后的图像,通过最小模糊散度准则来实现图像分割中最优阈值的自动提取。该算法针对图像阈值化分割的要求构造了一种新的模糊隶属度函数,克服了传统S函数带宽对分割效果的影响,有很好的通用性和有效性,方案能够快速正确地实现分割,且不需事先认定分割类数。实验结果令人满意。
基因编码
把图像背景和目标像素用不同的基因编码表示,通过区域性的划分,把图像背景和目标分离出来,具有处理速度快的优点,但算法实现起来比较难。
小波变换
基于小波变换的阈值图像分割方法的基本思想是首先由二进小波变换将图像的直方图分解为不同层次的小波系数,然后依据给定的分割准则和小波系数选择阈值门限,最后利用阈值标出图像分割的区域。整个分割过程是从粗到细,有尺度变化来控制,即起始分割由粗略的L2®子空间上投影的直方图来实现,如果分割不理想,则利用直方图在精细的子空间上的小波系数逐步细化图像分割。分割算法的计算馈与图像尺寸大小呈线性变化。