一、前言
最近在使用floodFill这个算法时,突然想起selective search里的基础图像分割算法 - 基于图的graphsementation。
两者是比较简单的算法,存在相似之处,即都使用了相邻像素之间像素强度关系。
同时又存在不同点,floodFill关注点是像素层面上,生成一个区域;
而graphsementation由像素逐步构建出图块,生成多个区域。
二、graphsementation源码分析
基于图的分割算法graphsementation没有在opencv主模块版本中实现,其代码位于opencv_contrib/modules/ximgproc模块
中的graphsegmentation.cpp。算法主体步骤如下:
void GraphSegmentationImpl::processImage(InputArray src, OutputArray dst) {
Mat img = src.getMat();
dst.create(img.rows, img.cols, CV_32SC1);
Mat output = dst.getMat();
output.setTo(0);
// Filter graph
Mat img_filtered;
filter(img, img_filtered);
// Build graph
Edge *edges;
int nb_edges;
buildGraph(&edges, nb_edges, img_filtered);
// Segment graph
PointSet *es;
segmentGraph(edges, nb_edges, img_filtered, &es);
// Remove small areas
filterSmallAreas(edges, nb_edges, es);
// Map to final output
finalMapping(es, output);
delete [] edges;
delete es;
}
1.高斯滤波降低噪声影响
void GraphSegmentationImpl::filter(const Mat &img, Mat &img_filtered) {
Mat img_converted;
// Switch to float
img.convertTo(img_converted, CV_32F);
// Apply gaussian filter
GaussianBlur(img_converted, img_filtered, Size(0, 0), sigma, sigma);
}
2.基于相邻像素强度差来构建图bulidGraph,计算权重(即相邻像素强度差)
void GraphSegmentationImpl::buildGraph(Edge **edges, int &nb_edges, const Mat &img_filtered) {
//1.相邻像素连线用Edge表示,每个像素均计算4个方向(四连通,上下左右)。
*edges = new Edge[img_filtered.rows * img_filtered.cols * 4];
nb_edges = 0;
int nb_channels = img_filtered.channels();
//2.遍历所有像素,计算Edge的权重weight值。
for (int i = 0; i < (int)img_filtered.rows; i++) {
const float* p = img_filtered.ptr(i);
for (int j = 0; j < (int)img_filtered.cols; j++) {
//Take the right, left, top and down pixel
for (int delta = -1; delta <= 1; delta += 2) {
for (int delta_j = 0, delta_i = 1; delta_j <= 1; delta_j++ || delta_i--) {
int i2 = i + delta * delta_i;
int j2 = j + delta * delta_j;
if (i2 >= 0 && i2 < img_filtered.rows && j2 >= 0 && j2 < img_filtered.cols) {
const float* p2 = img_filtered.ptr(i2);
float tmp_total = 0;
//3.权重简单地等于像素差值的平方再开根号,彩色3通道则全部累加
for ( int channel = 0; channel < nb_channels; channel++) {
tmp_total += pow(p[j * nb_channels + channel] - p2[j2 * nb_channels + channel], 2);
}
float diff = 0;
diff = sqrt(tmp_total);
//4.from、to存储像素坐标索引
(*edges)[nb_edges].weight = diff;
(*edges)[nb_edges].from = i * img_filtered.cols + j;
(*edges)[nb_edges].to = i2 * img_filtered.cols + j2;
nb_edges++;
}
}
}
}
}
}
3.对构建的图,按权重进行分割
void GraphSegmentationImpl::segmentGraph(Edge *edges, const int &nb_edges, const Mat &img_filtered, PointSet **es) {
int total_points = ( int)(img_filtered.rows * img_filtered.cols);
//1.按权重大小对edge排序
// Sort edges
std::sort(edges, edges + nb_edges);
//2.构建点集块,初始化时每个像素为独立一个点集块,后面逐步合并
// Create a set with all point (by default mapped to themselfs)
*es = new PointSet(img_filtered.cols * img_filtered.rows);
//3.判断是否合并的阈值,每个块(初始化时即像素)对应一个阈值
// Thresholds
float* thresholds = new float[total_points];
for (int i = 0; i < total_points; i++)
thresholds[i] = k;
for ( int i = 0; i < nb_edges; i++) {
//4.获取块的索引,即基点的坐标索引
int p_a = (*es)->getBasePoint(edges[i].from);
int p_b = (*es)->getBasePoint(edges[i].to);
//5.根据阈值,判断是否合并这两个块
if (p_a != p_b) {
if (edges[i].weight <= thresholds[p_a] && edges[i].weight <= thresholds[p_b]) {
(*es)->joinPoints(p_a, p_b);
p_a = (*es)->getBasePoint(p_a);
thresholds[p_a] = edges[i].weight + k / (*es)->size(p_a);//块的权重更新
edges[i].weight = 0;
}
}
}
delete [] thresholds;
}
4.合并过小的区域块filterSmallAreas
5.根据最终的点集块生成分割结果finalMapping
三、floodFill源码分析
opencv的floodFill主要以入栈方式和行扫描进行。
当loDiff、upDiff均为0时,由icvFloodFill_CnIR函数完成计算,这里会按分割结果直接对原图设置newVal,不生成mask。
其他情况均是走icvFloodFillGrad_CnIR函数,主要步骤如下:
1.从seed坐标所在的那行开始填充mask
L = R = seed.x;//L、R为每行的最左及最右x坐标
if( mask[L] )
return;
mask[L] = newMaskVal;//填充mask的值,默认为1,可通过flags设置
_Tp val0 = img[L];//seed点的像素值
if( fixedRange )//以seed点为基准
{
while( !mask[R + 1] && diff( img + (R+1), &val0 ))
mask[++R] = newMaskVal;
while( !mask[L - 1] && diff( img + (L-1), &val0 ))
mask[--L] = newMaskVal;
}
else//以相邻点为基准
{
while( !mask[R + 1] && diff( img + (R+1), img + R ))
mask[++R] = newMaskVal;
while( !mask[L - 1] && diff( img + (L-1), img + L ))
mask[--L] = newMaskVal;
}
2.循环扫描上下行
XMax = R;
XMin = L;
//seed点所在行入栈
ICV_PUSH( seed.y, L, R, R + 1, R, UP );
while( head != tail )
{
int k, YC, PL, PR, dir;
ICV_POP( YC, L, R, PL, PR, dir );//出栈
//扫描上下行
int data[][3] =
{
{-dir, L - _8_connectivity, R + _8_connectivity},
{dir, L - _8_connectivity, PL - 1},
{dir, PR + 1, R + _8_connectivity}
};
unsigned length = (unsigned)(R-L);
......
if( fillImage )//根据mask填充原图
for( i = L; i <= R; i++ )
img[i] = newVal;