2023年电赛E题总结(openCV/c++)

1.2023电赛E题题目分析

在这里插入图片描述

本题的主要重点就是识别矩形框,和识别在矩形框上面的红色激光点,然后控制红色激光点。当然也可以不用识别激光点的坐标直接识别矩形框的位置和姿态后直接控制云台走相应的距离也行(虽然凭借这种方式侥幸获得国一),但为了更可加靠这里我还是介绍一下闭环的方案。方案我当初采用的是Sipeed M2dock(主云台)+K210(跟随云台)+stm32控制步进电机云台。

2.图像处理

1.当初比赛时采用的官方的micropython库可以直接识别矩形的边框,经过一番折腾后可以调到能用,但在非常极限的位置的时候仍然不可避免的会走偏。好在裁判没有放在极其偏的位置上面。
2.当然,光识别一个矩形时比赛的要求(几天内完成压力还算大了),如果是识别任意图形呢?

2.1 opencv(c++) 矩形框中心提取

  • openMV 或者 K210识别的都是矩形的外边框,而矩形框是有一个宽度的,虽然比赛要求的精度克以大致忽略,但真是的轨迹应是外边框向里面偏移几个像素的位置才是实际的位置。
  • 这里我参考了这位大佬的:
    opencv轮廓处理

在这里插入图片描述

  • 主要思路就是: 原始图像 -> 二值化 -> 轮廓提取->轮廓面积求取-> 轮廓角的个数获取 -> 轮廓偏移到矩形中心位置-> 输出轮廓的点集。

2.1.1 图像二值化处理

 cvtColor(image, imgGray, COLOR_BGR2GRAY);           
 threshold(imgGray, binImage, 100, 255, cv::THRESH_BINARY_INV); // 转换二值图,设置阈值,高于100认为255`
  • 先将原始图像转化为灰度图->再转换为二值化。

2.1.2 轮廓提取

/**
 * @brief : 检测出物体的轮廓
 * @param  Mat imgDil 输入图像
 * @param  contours contours,定义为“vector<vector<Point>> contours”,
 * 是一个向量,并且是一个双重向量,向量内每个元素保存了一组由连续的Point点构成的点的集合的向量,
 * 每一组Point点集就是一个轮廓。有多少轮廓,向量contours就有多少元素。
 * @param  hierarchy 定义为“vector<Vec4i> hierarchy”,先来看一下Vec4i的定义: typedef Vec<int, 4> Vec4i;
 * Vec4i是Vec<int,4>的别名,定义了一个“向量内每一个元素包含了4个int型变量”的向量。所以从定义上看,
 * hierarchy也是一个向量,向量内每个元素保存了一个包含4个int整型的数组。向量hiararchy内的元素和轮廓向量contours
 * 内的元素是一一对应的,向量的容量相同。hierarchy向量内每一个元素的4个int型变量——hierarchy[i][0] ~hierarchy[i][3],
 * 分别表示第 i个轮廓的后一个轮廓、前一个轮廓、父轮廓、内嵌轮廓的索引编号。如果当前轮廓没有对应的后一个轮廓、前一个轮廓、
 * 父轮廓或内嵌轮廓的话,则hierarchy[i][0] ~hierarchy[i][3]的相应位被设置为默认值-1。
 * @param  mode 定义轮廓的检索模式:
 * 取值一:CV_RETR_EXTERNAL只检测最外围轮廓,包含在外围轮廓内的内围轮廓被忽略
 * 取值二:CV_RETR_LIST   检测所有的轮廓,包括内围、外围轮廓,但是检测到的轮廓不建立等级关系,彼此之间独立,没有等级关系,
 * 这就意味着这个检索模式下不存在父轮廓或内嵌轮廓,所以hierarchy向量内所有元素的第3、第4个分量都会被置为-1,具体下文会讲到
 * 取值三:CV_RETR_CCOMP  检测所有的轮廓,但所有轮廓只建立两个等级关系,外围为顶层,若外围内的内围轮廓还包含了其他的轮廓信息,
 * 则内围内的所有轮廓均归属于顶层。
 * 取值四:CV_RETR_TREE, 检测所有轮廓,所有轮廓建立一个等级树结构。外层轮廓包含内层轮廓,内层轮廓还可以继续包含内嵌轮廓。
 * @param  method 定义轮廓的近似方法:
 * 取值一:CV_CHAIN_APPROX_NONE 保存物体边界上所有连续的轮廓点到contours向量内
 * 取值二:CV_CHAIN_APPROX_SIMPLE 仅保存轮廓的拐点信息,把所有轮廓拐点处的点保存入contours
 * 向量内,拐点与拐点之间直线段上的信息点不予保留
 * 取值三和四:CV_CHAIN_APPROX_TC89_L1,CV_CHAIN_APPROX_TC89_KCOS使用teh-Chinl chain 近似算法
 * @param offset Point偏移量:
 * 所有的轮廓信息相对于原始图像对应点的偏移量,相当于在每一个检测出的轮廓点上加上该偏移量,并且Point还可以是负值!
 * @retval: None
 */
findContours(Mat image, OutputArrayOfArrays contours,
             OutputArray hierarchy, int mode,
             int method, Point offset = Point());

2.1.3 轮廓面积/角的个数求取

// 该函数计算轮廓的长度,后面的bool值表面轮廓曲线是否闭合若为true 则轮廓曲线闭合
arcLength(contours[i], true); 
//计算轮廓面积
contourArea(contours[i]);
//  conpoly 同样为轮廓点集但它第二个数组中只有1-9个参数为了描述各个轮廓的拐角点
//  conpoly[i]是输出array  0.02*peri 这个参数理解不了就不要理解!!!最后一个参数仍然是询问是否闭合
approxPolyDP(contours[i], conpoly[i], 0.02f * peri, true); 
// 输出图像轮廓中的拐角点数和面积
cout << "conpoly:" << conpoly[i].size() << "area:" << area << endl;

2.1.4 轮廓偏移

判断是矩形的条件为 面积合适 && 角的个数==4

/**
 * @brief : 对cv::findContours(...,cv::CHAIN_APPROX_SIMPLE)找到的轮廓进行放大、缩小处理(注意最后的参数必须为cv::CHAIN_APPROX_SIMPLE)
 * @param  vector<cv::Point> &in 为输入轮廓
 * @param  vector<cv::Point> &out 为输出轮廓
 * @param  float scalar 负数为内缩,正数为外扩
 * @retval: None
 */
static void contours_handle(std::vector<cv::Point> &in, std::vector<cv::Point> &out, const float scalar)
{
    
    
    float SAFELINE = scalar;
    std::vector<cv::Point2f> dpList, ndpList;
    int count = in.size();

    for (int i = 0; i < count; ++i)
    {
    
    
        int next = (i == (count - 1) ? 0 : (i + 1));
        dpList.emplace_back(in.at(next) - in.at(i));
        float unitLen = 1.0f / sqrt(dpList.at(i).dot(dpList.at(i)));
        ndpList.emplace_back(dpList.at(i) * unitLen);
    }

    for (int i = 0; i < count; ++i)
    {
    
    
        int startIndex = (i == 0 ? (count - 1) : (i - 1));
        int endIndex = i;
        float sinTheta = ndpList.at(startIndex).cross(ndpList.at(endIndex));
        cv::Point2f orientVector = ndpList.at(endIndex) - ndpList.at(startIndex); // i.e. PV2-V1P=PV2+PV1
        if (std::isinf(SAFELINE / sinTheta * orientVector.x) || std::isinf(SAFELINE / sinTheta * orientVector.y))
        {
    
    
            continue; // 过滤掉离谱数据
        }
        out.emplace_back(cv::Point2f(in.at(i).x + SAFELINE / sinTheta * orientVector.x, in.at(i).y + SAFELINE / sinTheta * orientVector.y));
    }
}

然后就可以对识别到的轮廓进行偏移到中心位置了
实际矩形框中心的轨迹-红色框
至此,矩形的处理就算完成了,后面就是串口输出矩形的坐标,不同的开发板的方法也不一样.我这里使用的是OrangePi 5B CPU仅仅占用了5%. C++可以使用boost库来调用串口输出数据,网上有很多教程

2.2 红色激光点的识别

  • 老办法 ,还是对图像进行阈值处理,这里采用RGB通道。与openMV调参一样 需要设定LAB 的阀值区间,这里也一样,需要设置RGB颜色区间。为了方便调参,可以调用opencv自带的控件进行滑动调参。
 // 设定彩色图像的阈值范围
 inRange(image, Scalar(ColorB, ColorG, ColorR), Scalar(ColorBM, ColorGM, ColorRM), RGB_binImage);

在这里插入图片描述
可以看到微调阈值就只剩下红色光斑的地方了。然后重复之前的识别轮廓的操作就可以获取红色激光点的坐标了。

  • 好像是激光点光照太强导致摄像头识别到的颜色显示到的是偏白色的现象, 这种情况可以调节摄像头的曝光时间: 原理就类似与控制摄像头的进光量,光照进来的越少了,自然就不会因外太强而偏白色了,工业相机有专门的调节尽管的镜头,而普通的镜头没有的话就只能通过算法曝光来达到同样的效果了,同时,调节激光的大小使得激光点的大小越大也会使得识别准确性越高。

3.步进电机的控制

  • 采用方案为 :stm32 + 步进电机 +陀螺仪 Pitch轴闭环(第一问定位用) Yaw轴限位开关。
    控制方式就不赘述了。
/**
* @brief : 步进电机脉冲控制
* @param  float yaw_step_Freq YAW轴脉冲频率控制
* @param  float pitch_step_Freq PITCH轴脉冲频率控制
* @retval: None
*/
static void Step_Motor_Server(float yaw_step_Freq, float pitch_step_Freq)
{
    
    
   // Yaw
   static uint32_t count = 0;
   uint32_t step_Cycle;
   if (yaw_step_Freq >= 0)
   {
    
    
       step_Cycle = (uint32_t)(yaw_step_Freq);
       HAL_GPIO_WritePin(DIR_GPIO_Port, DIR_Pin, GPIO_PIN_SET);
   }
   else if (yaw_step_Freq < 0)
   {
    
    
       step_Cycle = (uint32_t)(-yaw_step_Freq);
       HAL_GPIO_WritePin(DIR_GPIO_Port, DIR_Pin, GPIO_PIN_RESET);
   }
   count++;
   if ((count % (10000 / step_Cycle)) == 0)
   {
    
    
       count = 0;
       HAL_GPIO_TogglePin(PUL_GPIO_Port, PUL_Pin);
   }
   // Pitch
   static uint32_t count1;
   static uint32_t step_Cycle1;
   if (pitch_step_Freq >= 0)
   {
    
    
       step_Cycle1 = (uint32_t)(pitch_step_Freq);
       HAL_GPIO_WritePin(DIR1_GPIO_Port, DIR1_Pin, GPIO_PIN_SET);
   }
   else if (pitch_step_Freq < 0)
   {
    
    
       step_Cycle1 = (uint32_t)(-pitch_step_Freq);
       HAL_GPIO_WritePin(DIR1_GPIO_Port, DIR1_Pin, GPIO_PIN_RESET);
   }
   count1++;
   if ((count1 % (10000 / step_Cycle1)) == 0)
   {
    
    
       count1 = 0;
       HAL_GPIO_TogglePin(PUL1_GPIO_Port, PUL1_Pin);
   }
}

总结和思考

  • findContours() 可以输出轮廓的所有点集,当然也包括任意形状,一般会用于(我接触到的)零件的边缘毛刺去除。
  • 如果发挥部分是不规则的图形的话,请问阁下又该如何应对?
  • openmv本就是入门的,被micropython 封装死了,很多底层操作都无法进行,但对比赛来说够用了,如果想要提升的话,还是的从opencv开始入门。

源码

/**
 * @brief           : 轮廓的识别处理
 * @FilePath        : example.cpp
 * @Author          : fubingyan
 * @Date            : 2023-09-16 19:51:02
 * @LastEditTime    : 2023-09-18 14:39:29
 * @version         : V1.0.0
 * @Copyright (c) 2023 by fubingyan, All Rights Reserved.
 */

#include <iostream>
#include <opencv2/highgui.hpp>   // 说是说gui 具体什么gui 不清楚
#include <opencv2/imgcodecs.hpp> // 图像头文件
#include <opencv2/imgproc.hpp>   // 图像处理头文件
#include <opencv2/imgproc/types_c.h>

using namespace std;
using namespace cv;

// 今天又是美好的一天,我到底在干什么呢?
/*要进行图像形貌检测之前
 *首先要二值化,阈值处理,再进行滤波处理,再进行Canny边缘检测
 *最后才能检测出图形轮廓
 */

Mat imgGray;      // 灰度图像
Mat binImage;     // 灰度图二值化的图像
Mat RGB_binImage; // RGB图像阈值后的图像

static void getContours(Mat imgDil, Mat &img); // 获取轮廓
static void morphTreat(Mat &binImg);           // 形态学处理
static void contours_handle(std::vector<cv::Point> &in,
                            std::vector<cv::Point> &out, const float scalar); // 缩放轮廓
static void RGB_adjust(void);                                                 // 控件调参

// 帧率数据
static float t, lt;
uint16_t fps;

// RGB颜色阈值(小) 只有在最小值和最大值区间的才会被保留
static int ColorR = 0;
static int ColorG = 0;
static int ColorB = 0;
// RGB颜色阈值(大)
static int ColorRM = 255;
static int ColorGM = 255;
static int ColorBM = 240;
uint8_t Step = 0;
uint16_t step_cnt = 0;
int main()
{
    
    
    Mat image; // 在opencv 中所有的图像信息都使用Mat
    VideoCapture capture;
    namedWindow("Sample", WINDOW_AUTOSIZE);
    capture.open(0);
    RGB_adjust();
    if (capture.isOpened())
    {
    
    
        // 更改图像的捕获像素 这里为320*240大小
        capture.set(CAP_PROP_FRAME_WIDTH, 320);
        capture.set(CAP_PROP_FRAME_HEIGHT, 240);
        capture.set(CAP_PROP_FPS, 30);
        // capture.set(cv::CAP_PROP_AUTO_EXPOSURE, 0.25); //关闭相机的自动曝光 启用是-1
        // capture.set(cv::CAP_PROP_EXPOSURE, -3); //设置相机的曝光值
        cout << "Capture is opened" << endl;
        for (;;)
        {
    
    
            capture >> image;
            if (image.empty())
                break;
            // 显示帧率
            t = ((float)cv::getTickCount() - lt) / cv::getTickFrequency();
            lt = (float)cv::getTickCount();
            fps = 1.0f / t;
            cout << "FPS:" << fps << endl;

            if (step == 0)
            {
    
    
                step_cnt++;
                if (step_cnt > 100) // 识别100桢矩形框
                {
    
    
                    step == 1;
                }
                cvtColor(image, imgGray, COLOR_BGR2GRAY);                      // 将图像转化为灰度图像
                threshold(imgGray, binImage, 100, 255, cv::THRESH_BINARY_INV); // 转换二值图,设置阈值,高于100认为255
                getContours(binImage, image);                                  // 第一个参数 是寻找轮廓的参数, 第二个参数是显示图案的参数
                /* code */
            }
            else if (step == 1)
            {
    
    

                inRange(image, Scalar(ColorB, ColorG, ColorR), Scalar(ColorBM, ColorGM, ColorRM), RGB_binImage); // 设定彩色图像的阈值范围
                getContours(RGB_binImage, image);
            }

            imshow("Sample", image);        // 在原始图像上显示处理识别到的轮廓
            imshow("BinIMG", RGB_binImage); // 显示阈值后的图像

            if (waitKey(5) >= 0)
                break;
        }
    }
    else
    {
    
    
        cout << "No capture" << endl;
        image = Mat::zeros(240, 320, CV_8UC1);

        imshow("Sample", image);
        waitKey(0);
    }
    return 0;
    // pre-processing image  图像预处理
}


/**
 * @brief : 因为一开始参数不同,所以电脑直接将其视为重载函数
 * @param  Mat imgDil
 * @param  Mat &img
 * @retval: None
 */
static void getContours(Mat imgDil, Mat &img)
{
    
    
    /* contour is a vector inside that vector there is more vector
     * {
    
    {Point(20,30),Point(50,60)},{},{}} each vector like a contour and each contour have some points
     *
     **/
    vector<vector<Point>> contours;
    vector<Vec4i> hierarchy;                                                       // Vec4i 即代表该向量内有4个 int 变量typedef    Vec<int, 4>   Vec4i;   这四个向量每一层级代表一个轮廓
    findContours(imgDil, contours, hierarchy, CV_RETR_TREE, CV_CHAIN_APPROX_NONE); // CV_CHAIN_APPROX_SIMPLE - 简单的链式接近法

    vector<vector<Point>> conpoly(contours.size()); // conpoly(paprameter1) ,paprameter1便代表vector对象的行数,而其列数中的vector 是使用了point点集但其只包含图形的拐角点集
    vector<Rect> boundRect(contours.size());        // 记录各图形的拟合矩形
    string objType;                                 // 记录物体形状
    // 为了滤除微小噪声,因此计算area 的面积
    // 关于contours.size()为什么是返回二维数组的行,因为 vector::size()函数只接受vector 对象的调用而contours的所有行(不管列)均为其对象
    for (int i = 0; i < contours.size(); i++)
    {
    
    
        int area = contourArea(contours[i]);
        if (area > 1000)
        {
    
    
            float peri = arcLength(contours[i], true); // 该函数计算轮廓的长度,后面的bool值表面轮廓曲线是否闭合若为true 则轮廓曲线闭合
            // 寻找角点
            //  conpoly 同样为轮廓点集但它第二个数组中只有1-9个参数为了描述各个轮廓的拐角点
            //  conpoly[i]是输出array   0.02*peri 这个参数理解不了就不要理解!!! 最后一个参数仍然是询问是否闭合
            approxPolyDP(contours[i], conpoly[i], 0.02f * peri, true);
            //  通过conpoly 而绘制的轮廓中只存在程序认为应该存在的点
            cout << "conpoly:" << conpoly[i].size() << "area:" << area << endl; // 输出图像轮廓中的拐角点

            boundRect[i] = boundingRect(conpoly[i]); // 针对conpoly[i] 进行boundingRect 以便拟合相切矩形

            if ((int)conpoly[i].size() > 3 && (int)conpoly[i].size() < 5) // 寻找四边形
            {
    
    
                float aspRatio = (float)boundRect[i].width / (float)boundRect[i].height; // 计算float对象,一定要记得使用 float 强转符号
                if (aspRatio < 1.05f && aspRatio > 0.95f)
                    objType = "Square";
                else
                    objType = "Rectangle";
                std::vector<std::vector<cv::Point>> Hulls(contours.size()); // 保存凸包
                cv::convexHull(contours[0], Hulls[0], true, true);          // 寻找轮廓的凸包,输出点集为顺时针

                std::vector<std::vector<cv::Point>> outs(contours.size()); // 保存放大、缩小后的轮廓
                contours_handle(Hulls[i], outs[0], -4.0f);                 // 将轮廓缩小15个像素
                putText(img, objType, Point(boundRect[i].x, boundRect[i].y - 5), FONT_HERSHEY_PLAIN, 1, Scalar(255, 255, 255), 1);

                cout << "PointNUM:" << outs[0].size() << "Pointx[0]:" << outs[0][0].x << "Pointy[0]:" << outs[0][0].y << endl; // 输出图像轮廓中的拐角点
                drawContours(img, contours, i, Scalar(0, 0, 255), 1, 8, hierarchy, 3);
                drawContours(img, outs, 0, Scalar(255, 255, 255), 1);

                // 使用圆圈圈出矩形的四个点的坐标
                //  cv::circle(img,Point(conpoly[i][0].x,conpoly[i][0].y),2,Scalar(0, 0, 255),2);
                //  cv::circle(img,Point(conpoly[i][1].x,conpoly[i][1].y),2,Scalar(0, 255, 0),2);
                //  cv::circle(img,Point(conpoly[i][2].x,conpoly[i][2].y),2,Scalar(255, 0, 0),2);
                //  cv::circle(img,Point(conpoly[i][3].x,conpoly[i][3].y),2,Scalar(255, 255, 255),2);
                break;
            }
        }
    }
}

/**
 * @brief : 形态学处理
 * @param  Mat &binImg
 * @retval: None
 */
static void morphTreat(Mat &binImg)
{
    
    
    Mat BinOriImg;                                               // 形态学处理结果图像
    Mat element = getStructuringElement(MORPH_RECT, Size(5, 5)); // 设置形态学处理窗的大小
    GaussianBlur(binImg, binImg, Size(3, 3), 3, 0);
    dilate(binImg, binImg, element); // 进行多次膨胀操作
    dilate(binImg, binImg, element);

    erode(binImg, binImg, element); // 进行多次腐蚀操作
    erode(binImg, binImg, element);

    // imshow("形态学处理后", BinOriImg);        //显示形态学处理之后的图像
    cvtColor(binImg, binImg, CV_BGR2GRAY);              // 将形态学处理之后的图像转化为灰度图像
    threshold(binImg, binImg, 100, 255, THRESH_BINARY); // 灰度图像二值化
}

/**
 * @brief : 控件调参
 * @param  Mat &image
 * @retval: None
 */
static void RGB_adjust(void)
{
    
    
    createTrackbar("R:", "Sample", &ColorR, 255, 0);
    createTrackbar("G:", "Sample", &ColorG, 255, 0);
    createTrackbar("B:", "Sample", &ColorB, 255, 0);
    createTrackbar("RM:", "Sample", &ColorRM, 255, 0);
    createTrackbar("GM:", "Sample", &ColorGM, 255, 0);
    createTrackbar("BM:", "Sample", &ColorBM, 255, 0);
}

/**
 * @brief : 对cv::findContours(...,cv::CHAIN_APPROX_SIMPLE)找到的轮廓进行放大、缩小处理(注意最后的参数必须为cv::CHAIN_APPROX_SIMPLE)
 * @param  vector<cv::Point> &in 为输入轮廓
 * @param  vector<cv::Point> &out 为输出轮廓
 * @param  float scalar 负数为内缩,正数为外扩
 * @retval: None
 */
static void contours_handle(std::vector<cv::Point> &in, std::vector<cv::Point> &out, const float scalar)
{
    
    
    float SAFELINE = scalar;
    std::vector<cv::Point2f> dpList, ndpList;
    int count = in.size();

    for (int i = 0; i < count; ++i)
    {
    
    
        int next = (i == (count - 1) ? 0 : (i + 1));
        dpList.emplace_back(in.at(next) - in.at(i));
        float unitLen = 1.0f / sqrt(dpList.at(i).dot(dpList.at(i)));
        ndpList.emplace_back(dpList.at(i) * unitLen);
    }

    for (int i = 0; i < count; ++i)
    {
    
    
        int startIndex = (i == 0 ? (count - 1) : (i - 1));
        int endIndex = i;
        float sinTheta = ndpList.at(startIndex).cross(ndpList.at(endIndex));
        cv::Point2f orientVector = ndpList.at(endIndex) - ndpList.at(startIndex); // i.e. PV2-V1P=PV2+PV1
        if (std::isinf(SAFELINE / sinTheta * orientVector.x) || std::isinf(SAFELINE / sinTheta * orientVector.y))
        {
    
    
            continue; // 过滤掉离谱数据
        }
        out.emplace_back(cv::Point2f(in.at(i).x + SAFELINE / sinTheta * orientVector.x, in.at(i).y + SAFELINE / sinTheta * orientVector.y));
    }
}

  • over

猜你喜欢

转载自blog.csdn.net/qq_43037878/article/details/132974045