在实际工程应用中,经常会有搜索直线边沿的需求,该功能可用于特征定位、计算交点等场景。搜索大部分文章博客,基本都是用sobel算子等进行梯度计算并提取直线特征,没有实现按照指定方向进行搜索边沿的功能,会引入大量噪音。

opencv 查找区域的中线_算法

图1

具体实现中,一般按照以下步骤实现边沿搜索:

提取卡尺区域-合并成单像素profile-梯度计算-按要求搜索边沿点。

1. 沿卡尺方向提取搜索区域

搜索区域见图1,此时我们引入两个参数,分别叫做区域内卡尺数scanCount和每个卡尺的宽度scanWidth,那么每个卡尺就可以当作一个小矩形进行区域提取。

opencv 查找区域的中线_计算机视觉_02

提取矩形区域见我的另一篇博文。另外,卡尺可以用一个有向线段来表示,线段描述方法:中点(Center),角度Angle和线段长度Length,同时线段长度也表示该矩形区域的宽。

opencv 查找区域的中线_搜索_03

-

opencv 查找区域的中线_计算机视觉_04

                  剪切图像                                                  旋转图像

得到scanwidth宽度的矩形图像如下

opencv 查找区域的中线_opencv_05

代码:

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. 对图像滤波

为了查找边沿时减少无效点的影响,需要把提取后的图像高斯滤波以过滤噪音。

opencv 查找区域的中线_opencv 查找区域的中线_06

void Extract1DEdge::FilterMat(cv::Mat& InputMat, float sigma)
{
    GaussianBlur(InputMat, InputMat, Size(1, 3), sigma);
}

3. 提取图像滤波求梯度

把滤波后的图像合并为单像素Profile,然后使用滤波算子计算梯度。

opencv 查找区域的中线_算法_07

opencv 查找区域的中线_算法_08

                   单像素图像                                                           梯度图像

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()方法即可

处理结果:

opencv 查找区域的中线_opencv_09