基于opencv的视觉巡线实现

点击上方“小白学视觉”,选择加"星标"或“置顶

重磅干货,第一时间送达1fc577c097f61fff6053ea9110f45f46.jpeg

前言

这段时间在和学弟打软件杯的比赛,有项任务就是机器人的视觉巡线,这虽然不是什么稀奇的事情,但是对于一开始不了解视觉的我来说可以说是很懵了,所以现在就想着和大家分享一下,来看看是如何基于opencv来实现巡线的。我这里以ubuntu20.04为例了

正文

1.查看相机设备

首先要完成视觉巡线那必不可少的就是相机了,使用

ll /dev/video*

来查看相机。

faeccc65b87673122778e51c4a3444af.png

这里可以看到我有两个相机设备,一个是我电脑自带的相机video0,另一个是我的usb相机video1。

2.显示实时图像

新建一个工作空间,然后新建一个cpp文件,然后进行相机的初始化,以及调用窗口实时显示图像

#include <opencv2/opencv.hpp>
#include <iostream>
#include <chrono>


using namespace std;


int camera_width = 640;
int camera_height = 480;


int main(int argc, char const *argv[])
{
    // 初始化变量和对象
    cv::VideoCapture cap(1);
    cap.set(CAP_PROP_FRAME_WIDTH, camera_width);
    cap.set(CAP_PROP_FRAME_HEIGHT, camera_height);
    // 循环处理每一帧图像
    while (true) {
        cv::Mat color_image;
        cap.read(color_image);
        if (color_image.empty()) {
            cerr << "Failed to capture image" << endl;
            break;
        }
     imshow("Color Image", color_image);
     char key = waitKey(1);
        if (key == 'q') {
            break;
        }
    }


    // 释放资源
    cap.release();
    destroyAllWindows();
    return 0;
}

这里初始化cv::VideoCapture cap(1)传入的参数就是上面查看到的设备,如果想要调用系统自带相机,那就改为cap(0)。

3.巡线函数

我这里函数声明如下:

tuple<cv::Mat, float, bool, bool, bool> followBlindPath(cv::Mat color_image)

由于我想要多个返回值所以就采用了tuple模版,后面采用tie函数进行解包,其中输入参数为要识别的图片,输出参数分别为经识别后标记的图片,以及水平方向上偏差(后面会具体解释是什么偏差),后面三个布尔值表示三个状态,分别为巡线,转弯和停止。

在识别开始之前,由于图片在opencv保存的格式默认为BGR格式图片,我们要转为HSV格式,因为后面的操作都是基于HSV图片进行的。

cv::cvtColor(color_image, hsvFrame, COLOR_BGR2HSV);

效果如下:

d6a48d30a15a7deb9220d4d07f9773e2.png

a854a5f00ba5a9655744bf682b8fc3be.png

然后指定HSV的色域,scalar函数三个参数分别为色调(Hue)、饱和度(Saturation)和亮度(Value),我这里设置的值为黄色的色域。

cv::Scalar color_lower =  cv::Scalar(10, 40, 120);
cv::Scalar color_upper =  cv::Scalar(40, 255, 255);
cv::inRange(hsvFrame, color_lower, color_upper,color_mask);

inRange函数用于判断一个像素或像素矩阵是否在指定的范围内,hsvFrame是输入图像,返回图像color_mask是一个二值图像,即在色域内的为白色,色域外为黑色。

处理效果如下:

96229c7ab0b79136d7aec425db1fc217.png

然后进行滤波,过滤掉一些噪声然后进行膨胀和腐蚀的操作使得图片识别效果更好

//矩形结构
cv::Mat dilate_kernel = cv::getStructuringElement(MORPH_RECT, Size(10, 10));
cv::Mat erode_kernel = cv::getStructuringElement(MORPH_RECT, Size(5, 5));
cv::medianBlur(color_mask, color_mask, 9);  // 中值滤波
cv::dilate(color_mask, color_mask, dilate_kernel);  // 膨胀 
cv::erode(color_mask, color_mask, erode_kernel);  // 腐蚀

上面定义了用于膨胀和腐蚀的矩形结构,然后进行了滤波、膨胀和腐蚀。

效果如下:

7ce129c1e07405c5ac4647234e73fd43.png

接着就要划定ROI区域了,也就是要识别的区域:

cv::Mat mask_roi = cv::color_mask(Rect(0, 0, camera_width, 20));  // 划定ROI区域

我这里ROI区域为上面高20的区域,因为我实际的相机在下方,具体的ROI区域,还是要和相机的位置进行相应的设置

区域如下:

20f3ffd2fb960a8f8afdc084e66cbe29.png

然后进行识别操作:

vector<vector<cv::Point>> contours_roi;
    //cv::Vec4i是二维向量,
    vector<Vec4i> hierarchy;
    cv::findContours(mask_roi, contours_roi, hierarchy, RETR_EXTERNAL, CHAIN_APPROX_SIMPLE);
    if (!contours_roi.empty()) {
        //内联函数,找到面积最大值
        vector<Point> c = *max_element(contours_roi.begin(), contours_roi.end(), [](vector<Point> a, vector<Point> b) {
            return contourArea(a) < contourArea(b);
        });
        //cv::boundingRect用于计算轮廓的最小边界矩形。
        //它返回值是一个轮廓所包围的最小矩形,该矩形的边界与轮廓的边界平行,并且完全包含轮廓。
        //cv::Rect 表示矩形
        cv::Rect bound_rect = boundingRect(c);
        //用于在图像上绘制矩形。color_image输入输出图像,bound_rect是标记的矩形
        cv::rectangle(color_image, bound_rect, Scalar(255, 255, 255), 2);


        int center_x = bound_rect.x + bound_rect.width / 2;
        int center_y = bound_rect.y + bound_rect.height / 2;
        cv::circle(color_image, Point(center_x, center_y), 5, Scalar(0, 0, 255), -1);
        cv::imshow("Path3", color_image);
        last_center_x = center_x;
        
        return make_tuple(color_image, ((center_x / (float)camera_width) * 2 - 1), true, false, false);
    }

findContours用于在二值图像中查找轮廓,其中:

  • contours:输出的轮廓向量,每个轮廓表示为一个 std::vector对象

  • hierarchy:可选的输出向量,包含了轮廓的层次结构信息。默认情况下,不输出层次结构信息,可以传入一个空的 cv::OutputArray 对象。

  • 轮廓数据存储在 contours 和 hierarchy 两个输出参数中。

执行findContours过后,判断contours_roi.empty()是否为空,如果为空说明所划的区域未识别到黄线,转为了下一个状态,如果识别到了就找到面积最大的区域划矩形框。

其中采用rectangle()函数绘制矩形框,然后找到中心点(center_x,center_y),使用circle绘制点,同时返回值第一个bool值置为true,其他为false。

最终结果如下:

fe03091880b826e3f78e8a9f86c7e762.png

如果上面contours_roi.empty()为空,则说明所划的区域未识别到黄线,那么就需要重新划定剩下ROI区域

else {
        // TurnState
        mask_roi = cv::color_mask(Rect(0, 20, camera_width, 80));  // 划定ROI区域
        vector<vector<Point>> contours_roi;
        vector<Vec4i> hierarchy;
        cv::findContours(mask_roi, contours_roi, hierarchy, RETR_EXTERNAL, CHAIN_APPROX_SIMPLE);
        if (!contours_roi.empty()) {
            vector<Point> c = *max_element(contours_roi.begin(), contours_roi.end(), [](vector<Point> a, vector<Point> b) {
                return contourArea(a) < contourArea(b);
            });
            cv::Rect bound_rect = boundingRect(c);
            cv::rectangle(color_image, bound_rect, Scalar(255, 255, 255), 2);


            return make_tuple(color_image, ((last_center_x / (float)camera_width) * 2 - 1), false, true, false);
        }
        else{
            // StopState
            return make_tuple(color_image, 0.0, false, false, true);
        }
    }

如果这部分contours_roi.empty()不为空说明识别到了黄线,该执行转弯动作,返回值第二个bool置为true,其他为false,如果这部分contours_roi.empty()仍为空,说明完成了全部线的跟随,进入了停止状态,返回值最后一个置为true,其他为false。

4.偏差计算

那返回的偏差是如何计算的呢,这里是用2倍的中心点x坐标减去图片宽度最后比上图片宽度实现的即:

last_center_x / (float)camera_width) * 2 - 1

这样的意义可以使得中心点在左边时,值为负,在右边时值为正,更为方便判断转向,然后将偏差以相应的比例赋给相应的机器人便可以啦。

这个偏差也是可以进行PID的,这样控制效果会更好,后续我看看要不要也分享一下,这次就到此结束啦!

下载1:OpenCV-Contrib扩展模块中文版教程

在「小白学视觉」公众号后台回复:扩展模块中文教程,即可下载全网第一份OpenCV扩展模块教程中文版,涵盖扩展模块安装、SFM算法、立体视觉、目标跟踪、生物视觉、超分辨率处理等二十多章内容。


下载2:Python视觉实战项目52讲
在「小白学视觉」公众号后台回复:Python视觉实战项目,即可下载包括图像分割、口罩检测、车道线检测、车辆计数、添加眼线、车牌识别、字符识别、情绪检测、文本内容提取、面部识别等31个视觉实战项目,助力快速学校计算机视觉。


下载3:OpenCV实战项目20讲
在「小白学视觉」公众号后台回复:OpenCV实战项目20讲,即可下载含有20个基于OpenCV实现20个实战项目,实现OpenCV学习进阶。


交流群

欢迎加入公众号读者群一起和同行交流,目前有SLAM、三维视觉、传感器、自动驾驶、计算摄影、检测、分割、识别、医学影像、GAN、算法竞赛等微信群(以后会逐渐细分),请扫描下面微信号加群,备注:”昵称+学校/公司+研究方向“,例如:”张三 + 上海交大 + 视觉SLAM“。请按照格式备注,否则不予通过。添加成功后会根据研究方向邀请进入相关微信群。请勿在群内发送广告,否则会请出群,谢谢理解~