最近工作中需要对二值图进行图像分割,然而书上网上大多讲的是对灰度图的图像分割,思来想去觉得图像轮廓兴许是个突破口,利用轮廓点集中点与点之间的距离关系决定是否分割,事实证明分割效果可以达到不错的效果。
具体思路:待分割区域为下图中白色部分,黑色部分是背景。该图片包括3个轮廓,分别是1个外侧轮廓和2个内侧轮廓(事实上我的项目场景中最多只会存在两层轮廓的嵌套关系),理想的分割方式是把白色连通区域中狭窄的通道切割开,保留相对独立的白色区域。如下图中点a和点b,这两点连线的距离很短,且ab轮廓距离较大(如红色带箭头的线段所示),这时可以考虑将ab连接起来对整个区域进行一次图像分割。另外对于点c和点d,这两点连线的距离也很短,但是点c处于外侧轮廓上,点d处于内侧轮廓2上,无法计算轮廓距离,直接连接起来进行图像分割即可。
代码如下:
#include <opencv2/highgui/highgui.hpp>
#include <opencv2/imgproc/imgproc.hpp>
#include <map>
using namespace std;
using namespace cv;
class CPoint
{
public:
CPoint(int x, int y)
{
x_ = x;
y_ = y;
}
CPoint(cv::Point pt)
{
x_ = pt.x;
y_ = pt.y;
}
bool operator<(const CPoint& other) const
{
if(x_ == other.x_){
return y_ < other.y_;
}
return x_ < other.x_;
}
public:
int x_;
int y_;
};
// 判断在图片binary_img中start到end是否直线可达(连接直线的灰度值必须全是255)
bool isValidLinePath(Mat& binary_img, Point start, Point end)
{
int cols = binary_img.cols;
int min_x = min(start.x, end.x);
int max_x = max(start.x, end.x);
int min_y = min(start.y, end.y);
int max_y = max(start.y, end.y);
if(min_y != max_y && min_x != max_x)
{
float x_slope = float(end.y - start.y) / (end.x - start.x);
int y_off = x_slope>0? min_y : max_y;
for(int i=min_x; i<=max_x; i++)
{
int yoffset = (i-min_x)*x_slope + y_off;
if(*(binary_img.data+yoffset*cols+i) == 0)
return false;
}
float y_slope = float(end.x - start.x) / (end.y - start.y);
int x_off = y_slope>0? min_x : max_x;
for(int j=min_y; j<=max_y; j++)
{
int xoffset = (j-min_y)*y_slope + x_off;
if(*(binary_img.data+j*cols+xoffset) == 0)
return false;
}
}
else
{
for(int i=min_y; i<=max_y; i++)
{
for(int j=min_x; j<=max_x; j++)
{
if(*(binary_img.data+i*cols+j) == 0)
return false;
}
}
}
return true;
}
vector<Point> getOffsetTable(int distance, const int theta=0)
{
vector<Point> offsets;
double minSlope = tan((double(theta)/180)*CV_PI);
double maxSlope = tan((double(90-theta)/180)*CV_PI);
for(int n=1; n<distance; n++)
{
for(int i=-n+1; i<n; i++)
{
if(theta != 0)
{
double slope = abs(double(i) / n);
if(slope > minSlope && slope < maxSlope)
continue;
}
offsets.push_back(Point(i,-n));
offsets.push_back(Point(i,n));
offsets.push_back(Point(-n,i));
offsets.push_back(Point(n,i));
}
if(theta == 0)
{
offsets.push_back(Point(-n,-n));
offsets.push_back(Point(n,-n));
offsets.push_back(Point(-n,n));
offsets.push_back(Point(n,n));
}
}
return offsets;
}
void fillContour(Mat& image, const vector<Point>& contour, int val)
{
Rect rect = boundingRect(contour);
for(int i=rect.y; i<rect.y+rect.height; i++)
{
for(int j=rect.x; j<rect.x+rect.width; j++)
{
if(*(image.data+i*image.cols+j) == 255 &&
pointPolygonTest(contour, Point(j,i), false) >= 0)
{
*(image.data+i*image.cols+j) = val;
}
}
}
}
bool segment(Mat& image, Mat& markImg, double lenThresh)
{
// 分割线的两端点若在同一条轮廓,则它们的轮廓距离须大于thresh2
const double thresh2 = 6*lenThresh;
vector<vector<Point>> contours;
vector<Vec4i> hierarchy;
findContours(markImg, contours, hierarchy, CV_RETR_TREE, CHAIN_APPROX_NONE);
if(contours.size() == 0)
return false;
// 找出面积最小的外侧轮廓进行处理
int minContourId = -1;
double minArea = static_cast<double>(INT_MAX);
Mat contour_img = Mat::zeros(image.size(), CV_8U);
for(int i=0; i<contours.size(); i++)
{
drawContours(contour_img, contours, i, Scalar(255));
double contour_area = contourArea(contours[i]);
if(hierarchy[i][3] == -1 && contour_area < minArea)
{
minContourId = i;
minArea = contour_area;
}
}
// 获取图像轮廓集合,并标明每个轮廓点集中每个坐标点的顺序索引
map<CPoint,int> contourMap;
int pointIndex = 0;
for(auto point : contours[minContourId]){
contourMap.insert(pair<CPoint,int>(CPoint(point),pointIndex++));
}
//获取阈值内的邻域偏移,临近点与中心点连线的角度限制在-5~5、85~95
vector<Point> offsets = getOffsetTable(lenThresh, 5);
// 遍历轮廓点集,将满足分割条件的分割点对保存
Point pt1,pt2;
double minDistance = static_cast<double>(INT_MAX);
for(auto curPoint : contourMap)
{
for(auto offset : offsets)
{
int x_off = curPoint.first.x_ + offset.x;
int y_off = curPoint.first.y_ + offset.y;
// 忽略非轮廓点
if(*(contour_img.data+y_off*image.cols+x_off) == 0)
continue;
auto findIter = contourMap.find(CPoint(x_off,y_off));
// 该轮廓点的邻近轮廓点位于本轮廓内增加轮廓顺序距离判断
if(findIter != contourMap.end())
{
// 若邻近点与该点的顺序索引距离小于thresh2则无效
int distance = abs(findIter->second - curPoint.second);
if(distance < thresh2 || distance > contourMap.size()-thresh2){
continue;
}
}
Point tempPt1 = Point(curPoint.first.x_,curPoint.first.y_);
Point tempPt2 = Point(x_off,y_off);
if(!isValidLinePath(markImg, tempPt1, tempPt2))
continue;
double distance = pow(double(tempPt1.x-tempPt2.x),2) + pow(double(tempPt1.y-tempPt2.y),2);
if(distance < minDistance)
{
pt1 = tempPt1;
pt2 = tempPt2;
minDistance = distance;
}
}
}
if(minDistance != static_cast<double>(INT_MAX))
{
// 满足分割条件
line(image, pt1, pt2, Scalar(0), 1);
line(markImg, pt1, pt2, Scalar(0), 1);
return true;
}
else
{
// 若该区域无法分割则将该区域设置为背景色
fillContour(markImg, contours[minContourId], 0);
return true;
}
return false;
}
void regionSegment(Mat& binary_img, int lenThresh)
{
Mat image = binary_img.clone();
Mat markImg = binary_img.clone();
while (segment(image, markImg, lenThresh));
imshow("分割效果", image);
}
int main()
{
Mat src_img = imread("/home/gk/program/C++/regionSegmentation/house.jpg", IMREAD_GRAYSCALE);
// 二值化
Mat binary_img;
threshold(src_img, binary_img, 128, 255, CV_THRESH_BINARY);
// 区域分割
regionSegment(binary_img, 30);
waitKey();
return 0;
}
原图和分割效果图如下所示: