在实际工程应用中,经常会有搜索直线边沿的需求,该功能可用于特征定位、计算交点等场景。搜索大部分文章博客,基本都是用sobel算子等进行梯度计算并提取直线特征,没有实现按照指定方向进行搜索边沿的功能,会引入大量噪音。
图1
具体实现中,一般按照以下步骤实现边沿搜索:
提取卡尺区域-合并成单像素profile-梯度计算-按要求搜索边沿点。
1. 沿卡尺方向提取搜索区域
搜索区域见图1,此时我们引入两个参数,分别叫做区域内卡尺数scanCount和每个卡尺的宽度scanWidth,那么每个卡尺就可以当作一个小矩形进行区域提取。
提取矩形区域见我的另一篇博文。另外,卡尺可以用一个有向线段来表示,线段描述方法:中点(Center),角度Angle和线段长度Length,同时线段长度也表示该矩形区域的宽。
-
剪切图像 旋转图像
得到scanwidth宽度的矩形图像如下
代码:
void Extract1DEdge::GetProfileMat(cv::Mat InputMat,cv::Point2d Center, float Angle)
{
if (InputMat.channels() > 1)//多通道像素合并
{
cvtColor(m_mInputMat, m_mInputMat, COLOR_BGR2GRAY);
}
Mat RotateMat = getRotationMatrix2D(Center, -Angle, 1);//
warpAffine(InputMat, InputMat, RotateMat, InputMat.size(), WARP_INVERSE_MAP | INTER_LINEAR);把图像绕向量角度进行旋转
Mat newCenter = RotateMat * (Mat_<double>(3, 1) << Center.x, Center.y, 1);
double x = newCenter.at<double>(0, 0);
double y = newCenter.at<double>(1, 0);
//Length是搜索向量长度
//Height是搜索区域宽度
Mat M = (Mat_<double>(2, 3) << 1, 0, x - Length * 0.5, 0, 1, y - Height * 0.5);
warpAffine(InputMat, InputMat, M, Size2d(Length, Height), WARP_INVERSE_MAP | INTER_LINEAR);//把图像矩形区域提取出来
}
2. 对图像滤波
为了查找边沿时减少无效点的影响,需要把提取后的图像高斯滤波以过滤噪音。
void Extract1DEdge::FilterMat(cv::Mat& InputMat, float sigma)
{
GaussianBlur(InputMat, InputMat, Size(1, 3), sigma);
}
3. 提取图像滤波求梯度
把滤波后的图像合并为单像素Profile,然后使用滤波算子计算梯度。
单像素图像 梯度图像
void Extract1DEdge::GetGradientMat(cv::Mat& InputMat, cv::Mat& sobelMat)
{
cv::Mat reduceMat;
reduce(InputMat, reduceMat, 0, REDUCE_AVG, -1);//沿宽度方向合并成单像素
Sobel(reduceMat, sobelMat, CV_32FC1, 1, 0, 1);//计算梯度
}
4. 按照选择方式输出点
得到梯度图像后,其实每个像素都是沿搜索方向的梯度值,此时可以根据设定方式进行边沿点的选取。一般来说,选择边沿点的方式有First(第一个有效点),Last(最后一个有效点),Best(最大梯度点)三种;同时边沿点的类型也有上升沿Rising,下降沿Falling两种。
对于上升沿,只需要把梯度值中正值取出进行选取,而下降沿处理的是负值。
if (m_mInputMat.type() == CV_8SC1)
{
signed char* ptr = InputMat.ptr<signed char>(0);//复制内存块;
for (int i = 0; i < InputMat.cols; i++)
{
double Gradient = (double)abs(ptr[i]);
if (Gradient >= Threshold)//提取出梯度非0的点
{
Candidate.push_back(Point2d(i, ptr[i]));
}
}
}
if (edgeType == EdgeType::Rising)// from dark to light: f'(x)>0
{
for (vector<Point2d>::iterator iter = Candidate.begin(); iter != Candidate.end();)
{
if ((*iter).y <= 0)
{
iter = Candidate.erase(iter);//搜索上升边沿时需要过滤梯度负值点
}
else
{
iter++;
}
}
}
else if (edgeType == EdgeType::Falling)
{
for (vector<Point2d>::iterator iter = Candidate.begin(); iter != Candidate.end();)
{
if ((*iter).y > 0)
{
iter = Candidate.erase(iter);//搜索下降沿时需要过滤梯度正值点
}
else
{
iter++;
}
}
}
//The selection condition is met
if (selection == Selection::First)//只保留第一个点
{
Candidate.erase(Candidate.begin() + 1, Candidate.end());
}
else if (selection == Selection::Last)//只保留最后一点
{
Candidate.erase(Candidate.begin(), Candidate.end() - 1);
}
else if (selection == Selection::Best)//搜索梯度最大值点
{
Point2d pdMax(0, 0);
double dGradientMax = 0;
for(Point2d item: m_vpCandidate)
{
if (abs(item.y) >= dGradientMax)
{
pdMax = item;
dGradientMax = abs(item.y);
}
}
Candidate.clear();
Candidate.push_back(pdMax);
}
5. 使用Ransac方法过滤异常点
该方法就不赘述了,可以随机选择两个得到的边沿点拟合直线,然后计算其他点到该直线距离。
迭代多次后得到最优直线,然后按照百分比过滤异常点,把保留点再次拟合直线。
具体内容可以参考这一篇Ransac算法实现直线拟合
6. 拟合直线
直接调用cv::fitLine()方法即可
处理结果: