原始素材
需求描述
从有反光的黑色或者白色背景中分割出红蓝绿色的单色的前景。
问题分析
背景有反光,前景也有反光,而且三维的前景还会在背景下留下影子。目标是圆柱型的,因此在有光的情况下,总会有一个角度会出现反光的情况。对于这种情况,做图像增强是不可能的,前景和背景都横跨了颜色空间的大部分。
方法1:使用全局阈值的方法
目标最明显的特征是颜色,所以使用hsv或者lab颜色空间,根据人为指定的前景区域上的一个点,确定前景区域阈值,在光照条件较好的情况下可以分割。但是这种方法,人为指定前景区域的不同,分割结果不同。
方法2:使用深度神经网络的方法
目标的特征除了颜色,还有细长形状,走向不会突变等特征。深度模型学习到这些特征,可以直接进行图像的分割。该目标识别或者图像分割的情况和目前图像深度神经网络应用的情况有些差别,不能照搬已有模型。
方法3:使用滑动窗口的方法
滑动窗口的思想是在局部(一个窗口)进行前景的分割,然后利用目标细长的特性,沿着目标进行窗口的滑动,根据上次窗口和这次窗口交集中前景的颜色进行阈值的选取。可以应对使用全局阈值不能解决目标前景颜色沿着生长方向渐变的问题。本文主要介绍的是方法3:
实现算法
- 选择窗口大小(根据细长目标的直径确定,是正方形)
- 设置滑动起始点(由人为交互指定)
- 设置滑动次数
- 设置每次滑动步长(初步设为窗口边长的1/5)
- 根据初次窗口前景确定初始阈值
- 朝一个方向滑动窗口一步(滑动方向由目标最小包围矩形长边方向确定)
- 取这次窗口和上次窗口交集,由交集确定本次窗口分割阈值。
- 重复第6步和第7步,直到到达目标尽头或图像边界。
- 6,7,8步重新执行,第六步改为朝另一个方向滑动窗口一步。
关键代码
分割方法封装后调用
这里分割的方法进行了封装:
CableMeasure cm;
cm.setWindowSize(150, 150); //设置窗口大小
cm.setInitData(src, points[pointnum]); //设置初始数据
cm.setMaxIter(100); //设置向两边搜索的范围
cm.setdebug(true); //是否调试
cm.run();//开始分割
超两个不同方向搜索
run函数实现超两个不同方向搜索的逻辑:
void CableMeasure::run()
{
//朝正方向搜索
reInit(true);
while (once())
{
if (debug)
{
Rect rect = getCurrentWindow();
Mat rectshow = rawpic.clone();
rectangle(rectshow, rect, Scalar(0, 0, 255), 4);
imshow("rectshow", rectshow);
//Mat rectmin = rawpic(rect);
waitKey();
}
}
//朝相反方向搜索
reInit(false);
while (once())
{
if (debug)
{
Rect rect = getCurrentWindow();
Mat rectshow = rawpic.clone();
rectangle(rectshow, rect, Scalar(0, 0, 255), 4);
imshow("rectshow", rectshow);
waitKey();
}
}
}
如何知道超哪个方向滑动并保持
换方向前调用reInit函数,把初始窗口设为交互的点,并设置滑动方向。滑动方向根据两向量夹角的余弦值确定。
void CableMeasure::reInit(bool is)
{
searchdirection = is;
iter = 0;
currentwindow.x = rawpoint.x - windowSize.width / 2;
currentwindow.y = rawpoint.y - windowSize.height / 2;
currentwindow.width = windowSize.width;
currentwindow.height = windowSize.height;
if (!limitwindowrange(currentwindow)) //限制滑动窗口范围
cout << "初始点太靠近边界" << endl;
prewindow = currentwindow;
offset.xoffset = 0;
offset.yoffset = 0;
if (is)
{
preoffset.xoffset = 1;
preoffset.yoffset = 1;
}
else
{
preoffset.xoffset = -1;
preoffset.yoffset = -1;
}
}
单次滑动处理过程
内部调用的once函数代表窗口滑动一次,算法的主要部分在这儿实现
bool CableMeasure::once() //窗口滑动一次
{
iter++;
currentwindow.x += offset.xoffset;
currentwindow.y += offset.yoffset;
if (!limitwindowrange(currentwindow))
{
cout << "迭代过程达到边界" << endl;
return false;
}
if (prewindow == currentwindow && iter > 1) //结束条件1,矩形框不再滑动
{
cout << "窗口到达边界,不再继续滑动" << endl;
return false;
}
if (iter > maxiter)
{
cout << "滑动次数到达上限" << endl;
return false;
}
//记录最终窗口位置
if (!searchdirection)
startwindow = currentwindow;
else
stopwindow = currentwindow;
prewindow = currentwindow;
//=======================================================//
//对currentwindow进行分析,得到下次偏移位置
Mat roi = rawpic(currentwindow);
//=====================方法1:像素聚类===========================
Mat dst;
//Clustering(roi, dst); //当输入比较复杂时,聚类没有结果
//imshow("dst", dst);
//=====================方法2===========================
Mat lab;
cvtColor(roi, lab, COLOR_BGR2Lab);
//Vec3b a = lab.at<Vec3b>(currentwindow.width/2, currentwindow.height / 2);
//找到在线缆区域中的点
int L = 0, A = 0, B = 0;
int istart, istop;
int jstart, jstop;
int sum;
if (iter == 1) //第一次滑动,阈值使用手动点击的点计算
{
istart = -2; istop = 3; jstart = istart; jstop = istop;
sum = (istop - istart) * (jstop - jstart);
for (int i = istart; i < istop; i++)
{
for (int j = jstart; j < jstop; j++)
{
L += lab.at<Vec3b>(currentwindow.width / 2 + i, currentwindow.height / 2 + j)[0];
A += lab.at<Vec3b>(currentwindow.width / 2 + i, currentwindow.height / 2 + j)[1];
B += lab.at<Vec3b>(currentwindow.width / 2 + i, currentwindow.height / 2 + j)[2];
}
}
//自适应窗口大小
}
else //使用和上次相交区域中线缆区域确定
{
//prewindow
if (offset.xoffset > 0)
{
istart = offset.xoffset;
istop = windowSize.width;
}
else
{
istart = 0;
istop = windowSize.width + offset.xoffset;
}
if (offset.yoffset > 0)
{
jstart = offset.yoffset;
jstop = windowSize.height;
}
else
{
jstart = 0;
jstop = windowSize.height + offset.yoffset;
}
Mat temprect = rawpic.clone();
sum = 0;
for (int i = istart; i < istop; i++)
{
for (int j = jstart; j < jstop; j++)
{
//Vec3b v3b = prewindow.at<Vec3b>(i, j)
if (prewindowmask.at<Vec3b>(i,j)[0] == 255)
{
L += prewindowlab.at<Vec3b>(i, j)[0];
A += prewindowlab.at<Vec3b>(i, j)[1];
B += prewindowlab.at<Vec3b>(i, j)[2];
sum++;
}
}
}
}
Vec3b a = Vec3b(L / sum, A / sum, B / sum);
Vec3f low, high;
float s[3] = { 1,1,1 };
float range[3] = { 50,15,15 };
//阈值策略
for (int i = 0; i < 3; i++)
{
low[i] = (a[i] - range[i]) * s[i];// < 0 ? 0 : (a[i] - range[i]) * s[i];
high[i] = (a[i] + range[i]) * s[i];// > 255 ? 255 : (a[i] + range[i]) * s[i];
}
//cout << "low:" << low[0] << "," << low[1] << "," << low[2] << endl;
//cout << "high:" << high[0] << "," << high[1] << "," << high[2] << endl;
Mat mask;
inRange(lab, Scalar(low[0], low[1], low[2]), Scalar(high[0], high[1], high[2]), mask);
//imshow("mask", mask);
dst = Mat::zeros(lab.size(), CV_8UC3);
Vec3b ff = Vec3b(255, 255, 255);
Vec3b f0 = Vec3b(0, 0, 0);
int forecount = 0;
for (int r = 0; r < lab.rows; r++)
{
for (int c = 0; c < lab.cols; c++)
{
if (mask.at<uchar>(r, c) == 255) //前景
{
dst.at<Vec3b>(r, c) = ff;
forecount++;
}
else //背景
{
dst.at<Vec3b>(r, c) = f0;
}
}
}
if (forecount > windowSize.width * windowSize.height / 2)
{
cout << "超出线缆区域" << endl;
return false;
}
//========================结果处理===========================
Mat dst2;
cvtColor(dst, dst2, COLOR_BGR2GRAY);
threshold(dst2, dst2, 100, 255, THRESH_BINARY); //90以上
int dilation_size = 1;
Mat element = getStructuringElement(MORPH_RECT,
Size(2 * dilation_size + 1, 2 * dilation_size + 1),
Point(dilation_size, dilation_size));
//膨胀操作dilate //腐蚀操作erode
dilate(dst2, dst2, element);
erode(dst2, dst2, element);
//imshow("dst2", dst2);
//保存这次结果,下次计算阈值使用
prewindowmask = dst.clone();
prewindowlab = lab.clone();
//========================找前景轮廓===========================
vector<vector<cv::Point> > contours_all;
vector<cv::Vec4i> hierarchy_all;
cv::findContours(dst2, contours_all, hierarchy_all, RETR_TREE, CHAIN_APPROX_NONE, Point(0, 0));
Mat show(dst.size(), CV_8UC3);
drawContours(show, contours_all, -1, Scalar(0,0,255),4);
//imshow("show", show);
//========================前景轮廓增加到===========================
int maxindex = -1;
int max = 0;
for (int i = 0; i < contours_all.size(); i++)
{
if (contours_all[i].size() > max)
{
max = contours_all[i].size();
maxindex = i;
}
}
if (maxindex != -1)
{
vector<Point> ps;
ps = contours_all[maxindex];
foreground.push_back(ps);
foregroundpos.push_back(currentwindow);
string add;
if (searchdirection)
add = "0-";
else
add = "1-";
string filename = add + to_string(iter) + ".jpg";
//imwrite(filename, rawpic(currentwindow));
}
else
{
cout << "找不到前景,滑动结束" << endl;
return false;
}
//========================计算Correction===========================
Moments mo = moments(contours_all[maxindex]);
int correctionx = mo.m10 / mo.m00;
int correctiony = mo.m01 / mo.m00;
//Point center = Point(correctionx, correctiony);
//center.x += currentwindow.x;
//center.y += currentwindow.y;
怎么找到轮廓的中心
//if (searchdirection)
// centerpoints.push_back(center);
//else
// centerpoints.push_front(center);
correctionx = correctionx - currentwindow.width / 2;
correctiony = correctiony - currentwindow.height / 2;
//cout << "correctionx:" << correctionx << ",correctiony" << correctiony << endl;
//========================计算offset===========================
//offset赋值,使用最小包围矩形的角度
RotatedRect rotaterect = minAreaRect(contours_all[maxindex]);
float angle = 0;
if (rotaterect.size.width > rotaterect.size.height)
angle = rotaterect.angle;
else
angle = rotaterect.angle + 90;
//cout << "angle:" << angle << endl;
//======================怎么找到轮廓的中心======================
//1.得到线缆垂直方向角度
float vangle = angle + 90;
if (vangle > 90)
vangle -= 180;
if (vangle < -90)
vangle += 180;
kfLine line(vangle, rotaterect.center);
float dis = 0, pre_dis = 0;
vector<Point> crosspoint;
//距离小于一定值的点
for (int i = 0; i < contours_all[maxindex].size(); i++)
{
pre_dis = dis;
dis = line.DisFromPoint(contours_all[maxindex][i]);
if (dis < 1.4)
{
crosspoint.push_back(contours_all[maxindex][i]);
i += 10;
}
}
//确保有2个点
vector<Point> crosspointtemp;
crosspointtemp.push_back(crosspoint[0]);
int maxcrosspointindex = 0;
float maxdis = 0;
for (int i = 1; i < crosspoint.size(); i++)
{
float dis = getDistance(crosspoint[0], crosspoint[i]);
if (dis > maxdis)
{
maxdis = dis;
maxcrosspointindex = i;
}
}
crosspointtemp.push_back(crosspoint[maxcrosspointindex]);
crosspoint = crosspointtemp;
//添加到处理结果
Point center;
if (crosspoint.size() == 2) //如果数量不对,则这次结果不计入最后处理结果
{
center.x = (crosspoint[0].x + crosspoint[1].x) / 2 + currentwindow.x;
center.y = (crosspoint[0].y + crosspoint[1].y) / 2 + currentwindow.y;
Pointpair pp;
pp.p1.x = crosspoint[0].x + currentwindow.x;
pp.p1.y = crosspoint[0].y + currentwindow.y;
pp.p2.x = crosspoint[1].x + currentwindow.x;
pp.p2.y = crosspoint[1].y + currentwindow.y;
pp.dis = sqrt((pp.p2.x - pp.p1.x) * (pp.p2.x - pp.p1.x) + (pp.p2.y - pp.p1.y) * (pp.p2.y - pp.p1.y));
if (searchdirection)
{
centerpoints.push_back(center);
pointpairs.push_back(pp);
}
else
{
centerpoints.push_front(center);
pointpairs.push_front(pp);
}
}
//判断offset和preoffset的夹角,判断是否需要换符号,以此解决朝一个方向搜索问题
offset.yoffset = offsetdis * sin(angle * 3.1416 / 180);// +correctiony;
offset.xoffset = offsetdis * cos(angle * 3.1416 / 180);// +correctionx;
if (preoffset.xoffset * offset.xoffset + preoffset.yoffset * offset.yoffset > 0) //两向量夹角公式的分子
{
offset.yoffset += correctiony;
offset.xoffset += correctionx;
}
else
{
offset.yoffset = -offset.yoffset + correctiony;
offset.xoffset = -offset.xoffset + correctionx;
}
//保证移动固定的距离
float d = sqrt(offset.yoffset * offset.yoffset + offset.xoffset * offset.xoffset);
float k = offsetdis / d;
offset.yoffset *= k;
offset.xoffset *= k;
//cout << "距离:" << sqrt(offset.yoffset* offset.yoffset + offset.xoffset * offset.xoffset) << endl;
preoffset = offset;
//=======================================================//
return true;
}
当成功分割时返回为true,分割识别或到达边界返回为false。
是否越界判断
在滑动过程中需要时刻判断是否达到窗口边界并进行修正
//返回true,没有到达边界
bool CableMeasure::limitwindowrange(Rect& rect)
{
Rect rawrect = rect;
if (rect.x < 0)
rect.x = 0;
if (rect.y < 0)
rect.y = 0;
if (rect.x > rawpic.cols - windowSize.width)
rect.x = rawpic.cols - windowSize.width;
if (rect.y > rawpic.rows - windowSize.height)
rect.y = rawpic.rows - windowSize.height;
if (rawrect == rect)
return true;
else
return false;
}