*提取直线、轮廓和区域
之前的二值边缘分布图有两个缺点。首先,检测到的边缘过厚,这导致更加难以识别物体的边界;第二,通常不能找到这样的阈值:低到足以检测到图像中的所有重要的边缘同时又避免产生太多无关紧要的边缘。Canny算法试图解决这样的问题。
使用cv::Canny()函数需要给出低阈值和高阈值两个阈值。canny算子通常是基于sobel算子,低阈值是宽松阈值,很多不需要的也被检测出来了;高阈值则界定重要轮廓边缘,canny算法结合这两种边缘分布图生成最优的轮廓分布图。
基于现在无人驾驶的火爆,我下载了一张道路图:
使用canny算子检测到的轮廓:
代码:
int main()
{
cv::Mat image = cv::imread("road.jpg");
cv::imshow("original image", image);
cv::Mat contours;
cv::Canny(image, contours, 350, 400);
cv::imshow("Canny Image", contours);
cvWaitKey();
}
在霍夫变换中,用这个方程式表示直线:
基础霍夫变化:CV::HoughLines,它输入一个二值分布图,通常是一个已经生成的边缘分布图,例如用Canny算子生成的分布图,输出的是一个cv::Vec2f的向量,每个元素是一对浮点数,表示检测到直线的( rho, theta )
这个算法检测的是图像中的直线而不是线段,不会给出直线的端点。
检测结果:
代码:
int main()
{
cv::Mat image = cv::imread("road2.jpg");
cv::imshow("original image", image);
cv::Mat contours;
cv::Canny(image, contours, 350, 400);
//用霍夫变换检测直线
std::vector<cv::Vec2f> lines;
cv::HoughLines(contours, lines,
1, PI / 180,//步长
60);//最小投票数
std::vector<cv::Vec2f>::const_iterator it = lines.begin();
while (it!=lines.end())
{
float rho = (*it)[0];
float theta = (*it)[1];
if (theta<PI / 4.0 || theta>3.0*PI / 4.0)
{
//直线与第一行的交叉点
cv::Point pt1(static_cast<int>(rho / cos(theta)), 0.0);
//直线与最后一行的交叉点
cv::Point pt2(rho / cos(theta) - image.rows*sin(theta) / cos(theta), image.rows);
cv::line(image, pt1, pt2, cv::Scalar(255,255,255), 1);
}
else
{
cv::Point pt1(0, rho / sin(theta));
cv::Point pt2(image.cols, rho / sin(theta) - image.cols*cos(theta) / sin(theta));
cv::line(image, pt1, pt2, cv::Scalar(255,255,255), 1);
}
it++;
}
cv::imshow("result", image);
cvWaitKey();
}
可以看出,
基础霍夫变换只是寻找图像中边缘像素的对齐区域。因为有些像素只是碰巧拍成了直线,这种方式可能产生错误的检测结果,也可能因为多条直线穿过了同一个像素对齐区域,而检测出重复的结果。
于是,基础霍夫变换的改进版出现了,那就是概率霍夫变换。
效果:
代码:
LineFinder类:
class LineFinder
{
private:
cv::Mat img;
//包含被检测直线的端点的向量
std::vector<cv::Vec4i> lines;
//累加器分辨率参数
double deltaRho;
double deltaTheta;
//确认直线之前必须受到的最小投票数
int minVote;
//直线的最小长度
double minLength;
//直线上允许的最大空隙
double maxGap;
public:
LineFinder() :deltaRho(1), deltaTheta(PI / 180), minVote(10), minLength(0.0), maxGap(0.0) {}
void setAccResolution(double dRho, double dTheta)
{
deltaRho = dRho;
deltaTheta = dTheta;
}
void setminVote(int minv)
{
minVote = minv;
}
void setLengthAndGap(double length, double gap)
{
minLength = length;
maxGap = gap;
}
std::vector<cv::Vec4i> findLines(cv::Mat& binary)
{
lines.clear();
cv::HoughLinesP(binary, lines, deltaRho, deltaTheta, minVote, minLength, maxGap);
return lines;
}
void drawDetectedLines(cv::Mat& image, cv::Scalar color = cv::Scalar(255, 255, 255))
{
std::vector<cv::Vec4i>::const_iterator it = lines.begin();
while (it!=lines.end())
{
cv::Point pt1((*it)[0], (*it)[1]);
cv::Point pt2((*it)[2], (*it)[3]);
cv::line(image, pt1, pt2, color);
it++;
}
}
};
int main()
{
cv::Mat image = cv::imread("road2.jpg");
LineFinder cFinder;
cFinder.setLengthAndGap(80, 20);
cFinder.setminVote(60);
cv::Mat contours;
cv::Canny(image, contours, 350, 400);
std::vector<cv::Vec4i> lines = cFinder.findLines(contours);
cFinder.drawDetectedLines(image);
cv::imshow("detected result", image);
cvWaitKey();
}
*点线的直线拟合
为了提取靠近第一条直线的点集,可以继续一下步骤:
1、在黑色图像上画一条白色直线,并且穿过用于检测直线的Canny轮廓图,进行相与操作,结果是一个只包含了与指定直线相关的点的图像;
2、把这些点的坐标插入到cv::Point对象的std::vector类型中;
3、用OpenCV函数cv::fitLine得到最优的拟合直线。
结果:
这里,cv::fitLine函数返回的是一个单位方向向量(cvVec4f的前两个数值)和直线上一个点的坐标(cvVec4f的后两个数值)
故拟合出的直线的两个定点的坐标为:
x0 = line[2]
y0 = line[3]
x1 = x0 + length*line[0]
y1 = y0 + length*line[1]
代码:
int main()
{
cv::Mat image = cv::imread("road2.jpg");
LineFinder cFinder;
cFinder.setLengthAndGap(80, 20);
cFinder.setminVote(60);
cv::Mat contours;
cv::Canny(image, contours, 350, 400);
std::vector<cv::Vec4i> lines = cFinder.findLines(contours);
cFinder.drawDetectedLines(image);
cv::imshow("detected result", image);
int n = 4;
cv::Mat oneLine(contours.size(), CV_8U, cv::Scalar(0));
cv::line(oneLine, cv::Point(lines[n][0], lines[n][1]),cv::Point(lines[n][2],lines[n][3]),cv::Scalar(255),3);
cv::bitwise_and(contours, oneLine, oneLine);
std::vector<cv::Point> points;
for (int y = 0; y < oneLine.rows; y++)
{
uchar* rowPtr = oneLine.ptr<uchar>(y);
for (int x = 0; x < oneLine.cols; x++)
{
if (rowPtr[x])
{
points.push_back(cv::Point(x, y));
}
}
}
cv::Vec4f line;
cv::fitLine(points, line, CV_DIST_L2, 0, 0.01, 0.01);
int x0 = line[2];
int y0 = line[3];
int x1 = x0 + 300 * line[0];
int y1 = y0 + 300 * line[1];
cv::line(image, cv::Point(x0, y0), cv::Point(x1, y1), 0, 3);
cv::imshow("4th detected line", image);
cvWaitKey();
}
*提取物体轮廓
在物体检测和识别程序中,第一步通常是生成二值图像,下一个步骤是从1和0组成的像素中图取出物体。
生成二值图像:
轮廓检测结果:
代码:
int main()
{
cv::Mat image = cv::imread("cow.jpg");
cv::cvtColor(image, image, CV_BGR2GRAY);
cv::threshold(image, image, 80, 255, CV_THRESH_BINARY_INV);
cv::Mat element7(7, 7, CV_8U, cv::Scalar(1));
cv::morphologyEx(image, image, cv::MORPH_OPEN, element7);
cv::morphologyEx(image, image, cv::MORPH_CLOSE, element7);
cv::imshow("binary image", image);
std::vector<std::vector<cv::Point>> contours;
cv::findContours(image, contours, CV_RETR_EXTERNAL,//检索外部轮廓
CV_CHAIN_APPROX_NONE);//每个轮廓的全部像素
cv::Mat result(image.size(), CV_8U, cv::Scalar(255));
cv::drawContours(result, contours,
-1,//画全部轮廓
0,//用黑色画
2);//宽度为2
cv::imshow("contours", result);
cvWaitKey();
}
*计算区域的形状描述子
连通区域通常代表着场景中的某个物体,为了识别出该物体,或将其与其它图像元素比较,需要对此区域进行测量,以提取出部分特征。
OpenCV中的几种形状描述子函数的输入都是轮廓(由cv::findContours生成),边界框的计算结果为:
代码:
cv::Rect rect = cv::boundingRect(contours[0]);
cv::rectangle(result, rect, cv::Scalar(0), 2);
cv::imshow("detected rectangle", result);
最小覆盖圆的计算结果:
代码:
float radius;
cv::Point2f center;
cv::minEnclosingCircle(contours[0], center, radius);
cv::circle(result, center, static_cast<int>(radius), cv::Scalar(0), 2);
cv::imshow("detected minEnclosingCircle", result);
多边形逼近:
代码:
std::vector<cv::Point> poly;
cv::approxPolyDP(contours[0], poly, 5, true);
cv::polylines(result, poly, true, 0, 2);
cv::imshow("detected polyLines", result);
代码:
std::vector<cv::Point> hull;
cv::convexHull(contours[0], hull);
cv::polylines(result, hull, true, 0, 2);
cv::imshow("detected hull", result);
计算轮廓矩是另一种功能强大的描述子(在所有区域内部画出重心):
代码:
std::vector<std::vector<cv::Point>>::iterator it = contours.begin();
while (it != contours.end())
{
cv::Moments moment = cv::moments(cv::Mat(*it++));
cv::circle(result, cv::Point(moment.m10 / moment.m00, moment.m01 / moment.m00), 2, 0, 2);
}
可以用OpenCV函数计算得到其他结构化属性。
cv::minAreaRect计算最小覆盖自由举行
cv::contourArea估算轮廓的面积
cv::pointPloygonTest判断一个点在轮廓的内部还是外部
cv::matchShapes度量两个轮廓之间的相似度
这些度量属性的方法可以都有效地结合起来,用于更高级地结构分析
*四边形检测
首先MSER检测,获得二值图像:
然后对图像进行开启和闭合操作,获得较为规整的二值图像:
在此基础上进行反转,前景为白色,背景为黑色:
使用cv::approxPolyDP()函数进行多边形逼近,并将四边形显示出来:
代码:
int main()
{
cv::Mat image = cv::imread("old_library.jpg");
cv::Ptr<cv::MSER> mser = cv::MSER::create(5, 200, 1500);
std::vector<std::vector<cv::Point>> points;
std::vector<cv::Rect> bboxes;
cv::Mat output(image.size(), CV_8UC3, cv::Scalar(255,255,255));
mser->detectRegions(image, points, bboxes);
for (std::vector<std::vector<cv::Point>>::iterator it = points.begin(); it != points.end(); it++)
{
for (std::vector<cv::Point>::iterator itPtr = it->begin(); itPtr != it->end(); itPtr++)
{
if (output.at<cv::Vec3b>(*itPtr)[0] = 255)
output.at<cv::Vec3b>(*itPtr) = cv::Vec3b(0, 0, 0);
}
}
cv::imshow("result", output);
cv::cvtColor(output, output, CV_BGR2GRAY);
output = output == 255;
cv::morphologyEx(output, output, cv::MORPH_OPEN, cv::Mat(), cv::Point(-1, -1), 3);
cv::morphologyEx(output, output, cv::MORPH_CLOSE, cv::Mat(), cv::Point(-1, -1), 3);
cv::imshow("opened result", output);
cv::Mat outputInv = 255 - output;
cv::imshow("outputInv", outputInv);
std::vector<std::vector<cv::Point>> contours;
cv::findContours(outputInv, contours, CV_RETR_EXTERNAL, CV_CHAIN_APPROX_NONE);
cv::Mat quardri(output.size(), CV_8U, 255);
std::vector<std::vector<cv::Point>>::iterator it = contours.begin();
std::vector<std::vector<cv::Point>> poly;
std::vector<std::vector<cv::Point>>::iterator polyit = poly.begin();
poly.resize(contours.size());
for (int i = 0; i < contours.size(); i++)
{
cv::approxPolyDP(contours[i], poly[i], 10, true);
if(poly[i].size() == 4)
cv::polylines(quardri, poly[i], true, 0, 2);
}
cv::imshow("detected quadrilateral", quardri);
cvWaitKey();
}
在运行
cv::approxPolyDP(contours[i], poly[i], 10, true);
这句时,VS报错:
_DEBUG_ERROR("vector subscript out of range");
后加上poly.resize(contours.size())
程序跑通