网站菜单

OpenCV实战练习3——轻量物体识别

工作原因,需要在开发板上对物体进行识别。一开始的思路是使用模板匹配对物体进行识别,但是这么做有几个缺点:

  • 模板匹配过于昂贵,在开发板上以1080P分辨率匹配5次就要耗时约1秒
  • 使用图像金字塔虽然可以提升速度,但是丢失的信息使准确率显著降低
  • 模板匹配对角度不敏感,物体旋转角度以后则很难识别出来(可以旋转模型,但是速度又慢了)
  • 匹配结果受光线影响较大
  • 采图麻烦(懒

因此需要转换思路重新规划如何识别物体。因为开发板性能有限,而任务要求需要在半秒内识别出物体,并获取物体的中心点与角度。因此越轻量的解决办法越优先。

重新观察了一下物体,发现需要识别的物体有如下的特征。(敏感物体,不放图)物体为深绿色长方形,内部正中央的位置有一个圆,材质为塑料。(实际情况需要同时判别,并识别其它物体,这里仅以此具有代表性的物体举例)

遂放弃模板匹配,从形状入手规划一下新识别的流程——即识别内部的圆与物体的外接矩形轮廓。内部圆的圆心则为物体的圆心,外接矩形的角度则为物体的角度。

第一步:找圆心

OpenCV内部是有一个现成的识别圆形的方法的。

函数形式:
void HoughCircles(InputArray image, OutputArray circles, int method, double dp, double minDist, double param1 = 100, double param2 = 100, int minRadius = 0, int maxRadius = 0)

参数说明:
InputArray: 输入图像,数据类型一般用Mat型即可,需要是8位单通道灰度图像
OutputArray:存储检测到的圆的输出矢量
method:使用的检测方法,目前opencv只有霍夫梯度法一种方法可用,该参数填HOUGH_GRADIENT即可(opencv 4.1.0下)
dp:double类型的dp,用来检测圆心的累加器图像的分辨率于输入图像之比的倒数,且此参数允许创建一个比输入图像分辨率低的累加器。上述文字不好理解的话,来看例子吧。例如,如果dp= 1时,累加器和输入图像具有相同的分辨率。如果dp=2,累加器便有输入图像一半那么大的宽度和高度。
minDist:为霍夫变换检测到的圆的圆心之间的最小距离
param1:它是第三个参数method设置的检测方法的对应的参数。对当前唯一的方法霍夫梯度法CV_HOUGH_GRADIENT,它表示传递给canny边缘检测算子的高阈值,而低阈值为高阈值的一半。
param2:也是第三个参数method设置的检测方法的对应的参数,对当前唯一的方法霍夫梯度法HOUGH_GRADIENT,它表示在检测阶段圆心的累加器阈值。它越小的话,就可以检测到更多根本不存在的圆,而它越大的话,能通过检测的圆就更加接近完美的圆形了。
minRadius:表示圆半径的最小值
maxRadius:表示圆半径的最大值

以下为实际方法:(笔者的需求是找到最小的圆)

void LeftFrameAnalysisThread::findCircleCenter()
{
    Mat ref;
    vector<Vec3f> circles;
    cvtColor(leftFrame, ref, CV_BGR2GRAY);

    //return value: x, y, radius
    HoughCircles(ref, circles, HOUGH_GRADIENT, 1, ref.rows/5, 150, 100, 0, 0);           
    int minimum;
    int count;

    if (circles.empty())
    {
        cout << "NO CIRCLE FOUND" << endl;
    }
    else
    {
        //find the smallest circle which is the slot
        for (const auto& circle : circles)        
        {
            int radius = cvRound(circle[2]);
            if (radius < minimum)
            {
                minimum = radius;
            }
        }
        Point center(cvRound(circles[count][0]), cvRound(circles[count][1]));
        leftLocation = Point(center.x , center.y);
    }
}

第二步:找矩形

2.1 过滤背景

在找矩形以前,我们首先需要过滤以下背景,最好的情况是物体为黑色,其他全为白色(这样后期可以用findContours找轮廓点)。因此我们需要首先对帧进行处理。众所周知过滤颜色的最好办法是将图像转化为HSV色彩空间,然后使用inrange方法生成针对特定颜色的掩膜,然后使用这个掩膜对图像的特定颜色进行保留以达到过滤的效果。

因此整体流程如下:

  1. 将采集图像从BGR空间转化为HSV色彩空间
  2. 使用inrange生成目标颜色的掩膜
  3. 使用bitwise_and将掩膜与原图像进行合并,获取只有目标颜色的图像
  4. 最后将图像转化为GRAY,并二值化,为后续的findContours做好准备

整体代码如下:

Mat LeftFrameAnalysisThread::extractColour(const cv::Mat& frame)
{
    Mat ref = frame.clone();
    cvtColor(ref, ref, COLOR_BGR2HSV);      //to HSV

    //extract colour area
    Mat mask, result;
    if (detectionMode == 0)         //extract red colour
    {
        inRange(ref, Scalar(0, 143, 146), Scalar(180, 255, 255), mask);
    }
    else if (detectionMode == 1)    //extract green colour
    {
        inRange(ref, Scalar(0, 127, 0), Scalar(120, 255, 120), mask);
    }
    bitwise_and(leftFrame, leftFrame, result, mask);

    //convert into black and white
    cvtColor(result, result, COLOR_BGR2GRAY);

    //clean area
    threshold(result, result, 20, 255, THRESH_BINARY);       // to binary
    return result;
}

2.2 颜色翻转

这里需要注意的是,上一步处理完的图像,目标物体为白色,而背景为黑色,我们需要将它反过来——即目标物体为黑色,背景为白色。方法也很简单,遍历所有像素点,将255变成0,0变成255即可。

代码如下:

Mat LeftFrameAnalysisThread::reverseBlackWhite(cv::Mat frame)
{
    Mat ref = frame.clone();
    int height = ref.rows;
    int width = ref.cols;

    for(int i= 0; i< height; i++)
    {
        for(int j=0; j< width; j++)
        {
            ref.at<uchar>(i, j)= 255- ref.at<uchar>(i, j);   // 每一个像素反转
        }
    }
    return ref;
}

2.3 找矩形

OpenCV没有直接识别矩形的方法,但是有通过点,画出矩形的方法。与一般的Rect不一样的是,通过RotateRect创建的矩形是带有角度的,正好符合此项目的需求。

关于RotateRect对于角度的判定,下面这张图解释的很清楚。

Opencv采用通用的图像坐标系,左上角为原点O(0,0),X轴向右递增,Y轴向下递增,单位为像素。
矩形4个顶点位置的确定,是理解其它各变量的基础,其中p[0]点是关键。
顶点p[0]的位置可以这样理解:
如果没有对边与Y轴平行,则Y坐标最大的点为p[0]点,如矩形(2)(3)(4);
如果有对边与Y轴平行,则有两个Y坐标最大的点。此时,左侧的点为p[0]点。如矩形(1)。
通俗的说就是RotatedRect的坐标点,Y轴最大的为P[0],p[0]围着center顺时针旋转, 旋转角度为负的话即是P[0]在左下角,为正P[0]是右下角

因此思路也很明确了:

  1. 首先进行闭操作,将之前没有连接到的边缘连接到一起
  2. 寻找轮廓
  3. 对获取到的轮廓,画最小内接矩形。通过面积大小/坐标位置过滤掉一些干扰结果
  4. 对长宽进行判断,默认为长大于宽,若不是则说明矩形为竖直状态,角度需要修正
  5. 获得最终的矩形的角度

代码如下:

void LeftFrameAnalysisThread::findRect()
{
    ///preprocess and store all the contours
    vector<vector<Point> > contours;
    vector<Vec4i> hierarchy;
    RNG rng(12345);
    blur(leftROI, leftROI, Size(3, 3));
    Mat element = getStructuringElement(MORPH_RECT, Size(128, 64));
    Mat element2 = getStructuringElement(MORPH_RECT, Size(32, 32));
    dilate(leftROI, leftROI, element);
    erode(leftROI, leftROI, element2);
    findContours(leftROI, contours, hierarchy, CV_RETR_TREE, CHAIN_APPROX_NONE);

    //if it is cash box, reverse the black and white pixels
    if (detectionMode == 1)
    {
        leftROI = reverseBlackWhite(leftROI);
    }
    threshold(leftROI, leftROI, 20, 255, THRESH_BINARY);       // to binary
    imwrite("./Temp/afterProcessed.jpg",leftROI);

    /// store rectangles based on all the contours
    vector<RotatedRect> minRect( contours.size() );
    int minArea, maxArea;
    // different sizes for cash bag and cash box
    if (detectionMode == 0)
    {
        minArea = 2000;
        maxArea = 42000;
    }else if (detectionMode == 1)
    {
        minArea = 10000;
        maxArea = 160000;
    }

    for( int i = 0; i < contours.size(); i++ )
    {
        RotatedRect rect = minAreaRect(Mat(contours[i]));
        cout << "area: ";
        cout << rect.size.area() << endl;
        if (rect.size.area() < minArea)
        {
            continue;
        }
        if (rect.size.area() > maxArea)
        {
            continue;
        }
        else if ((rect.center.x > 200 && rect.center.x < 800) && (rect.center.y > 200 && rect.center.y < 800))
        {
            if(rect.size.height > rect.size.width)      //if the rectangle is placed vertically, adjust the degree
            {
                leftDegree = rect.angle;
                leftDegree -= 90;
                cout << "leftDegree: ";
                cout << leftDegree << endl;
            }
            if (detectionMode == 0)                     // cash bags uses the rectangle to determine the center and degree
            {
                leftDegree = rect.angle;
                leftLocation = rect.center;
                cout << rect.center.x<<endl;
                cout << rect.center.y<<endl;
                minRect[i] = minAreaRect( Mat(contours[i]) );
                break;
            }
            else if (detectionMode == 1)                // cash box only uses the rectangle to determine the angle
            {
                leftDegree = rect.angle;
                minRect[i] = minAreaRect( Mat(contours[i]) );
                break;
            }

        }
    }
    /// draw rotatable rectangles
    Mat drawing = Mat::zeros( leftROI.size(), CV_8UC3 );
    for( int i = 0; i< contours.size(); i++ )
    {
        Scalar color = Scalar( rng.uniform(0, 255), rng.uniform(0,255), rng.uniform(0,255) );
        // rotated rectangle
        Point2f rect_points[4];
        minRect[i].points( rect_points );
        for( int j = 0; j < 4; j++ )
        {
            line( drawing, rect_points[j], rect_points[(j+1)%4], color, 1, 8 );
        }
    }
    imwrite("./Temp/contours.jpg",drawing);
}

至此,物体的中心点与物体的角度都获取到了。

显示评论 (0)

文章评论

相关推荐

Yolov5_Seg输出解析

通过矩阵乘法(在代码中称为“matmul”)来计算分割掩码的原因,主要与实例分割网络(例如 YOLOv5 Segmentation)的实现方式有关。这种方法实际上是一种高效的特征图与目标分割系数组合的…

Ubuntu交叉编译Python

在 Ubuntu 上交叉编译 Python 的流程通常用于为不同平台生成可执行文件(如 ARM、MIPS 等)。以下是一般的操作步骤: 1. 安装必要的依赖工具 首先,确保已经安装了编译所需的工具和依…

此文章禁止复制~