RM机器视觉——图像处理、识别装甲板(ROBOMASTER)

声明:
本文案基于robomaster机甲大师;
本文案为个人视觉组初稿,仍有较多问题,远不及开源的大佬所作,但文本通俗易懂,为初学者提供入门思路;
代码注释行也会有相应讲解,非技术人员可以跳过;
部分技术可在博主其他博文中略知一二;
本算法最终测试帧率在150帧左右(单线程);

摘要:
环境配置
除去必要的相机库以外,还需要配置opencv库,方便图像预处理,获得更好的处理效果。
摄像头配置
使用海康威视摄像头可以配置摄像头内置文件,包括对抓取图像的方式选择(降采样)、曝光值调整(7000)、摄像头亮度调节(70)等,提高算法运行的稳定性。
图像处理
摄像机取流以及图像预处理,用最快的速度,将图像处理成只有黑色背景和清晰的白色线条组成的图案,方便装甲板位置解算。
装甲板位置解算
计算出图像中所有线条的位置、面积、倾斜角度、长宽比,与灯条的对应数据匹配、匹配成功(有一定的阈值)即筛选成功。
图像的分区
为了方便与下位机的交流,根据图像大小把图像差分成17块,除了4*4的16块标准分区以外还设置了视野正中间的一小块,即枪口对准的部分,方便兵种自主射击。
串口通讯
图像处理结束之后需要下位机做出相应的反应,需要串口通讯发送下位机需要的数据。

正文:

首先自行配置好适合自己使用的Ubuntu-arm系统,预装好自己觉得合适的opencv版本,安装好qt5。系统配置这里不多赘述。

接着进行qt5+opencv+C++的环境配置

Source文件所需如下:

SOURCES += \
    track_on.cpp \
    uart.cpp \
    grabimage-main.cpp \
    area.cpp

(根据个人需要自主配置,相匹配的文件链接https://download.csdn.net/download/qq_46046959/14939252)

header文件所需如下:

HEADERS += \
    ../../../../opt/MVS/include/uart.h \
    ../../../../opt/MVS/include/track_on.h \
    ../../../../opt/MVS/include/PixelType.h \
    ../../../../opt/MVS/include/MvErrorDefine.h \
    ../../../../opt/MVS/include/MvCameraControl.h \
    ../../../../opt/MVS/include/CameraParams.h \
    ../../../../opt/MVS/include/area.h

(使用的海康威视相机以及自己的封装)

使用海康威视摄像头,需要调用内置库如下:

INCLUDEPATH += $$PWD/../../../../opt/MVS/include

配置opencv3.4.0如下:

unix:!macx: LIBS += -L$$PWD/../../opencv-3.4.0/build/lib/ -lopencv_core
unix:!macx: LIBS += -L$$PWD/../../../../opt/MVS/lib/aarch64/ -lMvCameraControl

win32:CONFIG(release, debug|release): LIBS += -L$$PWD/../../opencv-3.4.0/build/lib/release/ -lopencv_highgui
else:win32:CONFIG(debug, debug|release): LIBS += -L$$PWD/../../opencv-3.4.0/build/lib/debug/ -lopencv_highgui
else:unix: LIBS += -L$$PWD/../../opencv-3.4.0/build/lib/ -lopencv_highgui

win32:CONFIG(release, debug|release): LIBS += -L$$PWD/../../opencv-3.4.0/build/lib/release/ -lopencv_imgproc
else:win32:CONFIG(debug, debug|release): LIBS += -L$$PWD/../../opencv-3.4.0/build/lib/debug/ -lopencv_imgproc
else:unix: LIBS += -L$$PWD/../../opencv-3.4.0/build/lib/ -lopencv_imgproc

opencv版本不同,配置也不同)
至此QT5环境配置结束,即pro文件的内容,该内容并非完全提前配置好,是在不断调试的过程中完善的。

下面开始算法开始的前提条件:摄像头取流,该部分功能只需要修改相机自带的取流例程适合自己使用即可,举例海康威视相机的取流如下:

int nRet = MV_OK;
    void* handle = NULL;
    MV_CC_DEVICE_INFO_LIST stDeviceList;
    memset(&stDeviceList, 0, sizeof(MV_CC_DEVICE_INFO_LIST));

    nRet = MV_CC_EnumDevices(MV_GIGE_DEVICE | MV_USB_DEVICE, &stDeviceList); //枚举设备

    unsigned int nIndex = 0;//相机在系统中的设备号为0

    nRet = MV_CC_CreateHandle(&handle, stDeviceList.pDeviceInfo[nIndex]);
    nRet = MV_CC_OpenDevice(handle);
    nRet = MV_CC_SetEnumValue(handle, "TriggerMode", 0);
nRet = MV_CC_StartGrabbing(handle);

至此相机取流前的配置已经结束,因为取流是整个算法自始至终独立运行的过程,所以下面单独开启一个线程供其取流:

pthread_t nThreadID;
nRet = pthread_create(&nThreadID, NULL ,WorkThread1 , handle);
sleep(999999999999999999999999999999);//取流时间

目前还没用其他看起来舒服的方法解决,暂缓一会,后面会更新。
接着进入线程,开始取流:

static void* WorkThread1(void* pUser)
{
    int nRet = MV_OK;

    MVCC_INTVALUE stParam; // 获取数据包大小
    memset(&stParam, 0, sizeof(MVCC_INTVALUE));
    nRet = MV_CC_GetIntValue(pUser, "PayloadSize", &stParam);

    MV_FRAME_OUT_INFO_EX stImageInfo = {0};
    memset(&stImageInfo, 0, sizeof(MV_FRAME_OUT_INFO_EX));

    unsigned char * pData = (unsigned char *)malloc(sizeof(unsigned char) * stParam.nCurValue);

    unsigned int nDataSize = stParam.nCurValue;

    while(1)
    {
        nRet = MV_CC_GetOneFrameTimeout(pUser, pData, nDataSize, &stImageInfo, 1000);
        //取流结果存在nRet里,但是此时的数据类型是byte类型,需要装换成opencv的Mat类型
        if (nRet == MV_OK)
        {
            Mat image =Mat(stImageInfo.nHeight,stImageInfo.nWidth,CV_8UC1, pData);//转换为Mat类型
            //在这里就可以进行图像的处理了
        }
    }
}

在上面的图像处理的位置,为了方便阅读,体现C++优点,我们对需要用到的功能进行外部封装:用于识别装甲板的track_on函数、用于分区的draw_grad_line函数、(之后会在track_on函数中再次封装一个用于通讯的uart函数)
所以处于while循环里的处理部分如下:

start=clock();
            cvtColor(image,src,CV_BayerBG2BGR);
            track_on(src,0,0);
            draw_grad_line(src);
            imshow("检测结果",src);//将处理的最终结果展示出来
  finish=clock();
            printf("该图像处理帧率为: %.0f帧\n",1000/(double(finish-start)/CLOCKS_PER_SEC*1000));
            waitKey(1);

下面进入封装的部分:
track_on :

  1. 图像预处理,第一步,降采样取图,提高算法运行的速度和稳定性
pyrDown(src,src,Size(src.cols/2,src.rows/2));
  1. 转HSV图像,分离颜色通道,重新合并两通道,为了后面方便寻找图像中的所有轮廓
cvtColor(img_rgb, hsvImage, COLOR_BGR2HSV);
vector<Mat> hsvsplit;//hsv的分离通道
split(hsvImage, hsvsplit);
merge(hsvsplit, hsvImage);//重新合并
  1. 图像二值化处理,摒弃环境光干扰
Mat thresHold;
threshold(hsvsplit[2], thresHold, 240, 245, THRESH_BINARY);
  1. 模糊、膨胀处理,让图像变得圆润
blur(thresHold, thresHold, Size(3, 3));
Mat element = cv::getStructuringElement(cv::MORPH_ELLIPSE, cv::Size(3, 3)); //膨胀
dilate(thresHold, element, element);
  1. 开始寻找轮廓
vector< RotatedRect> vc;
vector< RotatedRect> vRec;
vector<vector<Point>> Light_Contour; // 发现的轮廓
findContours(element.clone(),Light_Contour,CV_RETR_EXTERNAL,CV_CHAIN_APPROX_SIMPLE);
  1. 从面积上筛选轮廓
for (int i = 0; i < Light_Contour.size(); i++)
    {
        // 求轮廓面积
        float Light_Contour_Area = contourArea(Light_Contour[i]);
        // 去除较小轮廓&fitEllipse的限制条件
        if (Light_Contour_Area < 15 || Light_Contour[i].size() <= 10)
            continue;
        // 用椭圆拟合区域得到外接矩形
        RotatedRect Light_Rec = fitEllipse(Light_Contour[i]);
        Light_Rec = adjustRec(Light_Rec, ANGLE_TO_UP);
        if (Light_Rec.angle > 10 )
            continue;
        // 长宽比和轮廓面积比限制
        if (Light_Rec.size.width / Light_Rec.size.height > 1.5
                || Light_Contour_Area / Light_Rec.size.area() < 0.5)
            continue;
        // 扩大灯柱的面积
        Light_Rec. size.height *= 1.1;
        Light_Rec.size.width *= 1.1;
        vc.push_back(Light_Rec);
}
  1. 从灯条长宽比上来筛选轮廓
for (size_t i = 0; i < vc.size(); i++)
    {
        for (size_t j = i + 1; (j < vc.size()); j++)
        {
            //判断是否为相同灯条
            float Contour_angle = abs(vc[i].angle - vc[j].angle); //角度差
            if (Contour_angle >= 7)
                continue;
            //长度差比率
            float Contour_Len1 = abs(vc[i].size.height - vc[j].size.height) / max(vc[i].size.height, vc[j].size.height);
            //宽度差比率
            float Contour_Len2 = abs(vc[i].size.width - vc[j].size.width) / max(vc[i].size.width, vc[j].size.width);
            if (Contour_Len1 > 0.25 || Contour_Len2 > 0.25)
                continue;


            RotatedRect ZJB;
            ZJB.center.x = (vc[i].center.x + vc[j].center.x) / 2.; //x坐标
            ZJB.center.y = (vc[i].center.y + vc[j].center.y) / 2.; //y坐标
            ZJB.angle = (vc[i].angle + vc[j].angle) / 2.; //角度
            float nh, nw, yDiff, xDiff;
            nh = (vc[i].size.height + vc[j].size.height) / 2; //高度
            // 宽度
            nw = sqrt((vc[i].center.x - vc[j].center.x) * (vc[i].center.x - vc[j].center.x) + (vc[i].center.y - vc[j].center.y) * (vc[i].center.y - vc[j].center.y));
            float ratio = nw / nh; //匹配到的装甲板的长宽比
            xDiff = abs(vc[i].center.x - vc[j].center.x) / nh; //x差比率
            yDiff = abs(vc[i].center.y - vc[j].center.y) / nh; //y差比率
            if (ratio < 1.0 || ratio > 5.0 || xDiff < 0.5 || yDiff > 2.0)
                continue;
            ZJB.size.height = nh;
            ZJB.size.width = nw;
            vRec.push_back(ZJB);
            Point2f point1;
            Point2f point2;
            point1.x=vc[i].center.x;point1.y=vc[i].center.y+20;
            point2.x=vc[j].center.x;point2.y=vc[j].center.y-20;
			//此时轮廓已筛选完毕,为了方便输出,我们将得到的数据就此输出处理
			ZJB.center.x = filter(ZJB.center.x,xmidnum, DELAT_MAX);
			ZJB.center.y = filter(ZJB.center.y,ymidnum, DELAT_MAX);
			rectangle(yuantu, point1,point2, (0, 120, 255), 2);//将装甲板框起来
			circle(yuantu,ZJB.center,10,CV_RGB(0,120,255));//在装甲板中心画一个圆
			//装甲板已经筛选结束,我们借着此循环,将分区和通讯直接完成
			if(!a)//打开串口
			{
				fd = open_uart("/dev/ttyTHS2");
				ret = set_uart_attr(fd, 115200, 8, 'N', 1);
				a = true;
			}
			
			

下面进行分区块的数据输出(此行文字上下代码块处于同一个{ }中):

			int lie=768;
			int hang =880;//采集图像的分辨率,这里注意图像降采样之后对像素点的影响
if(ZJB.center.x>(3*hang/8)&&ZJB.center.x<(5*hang/8)&&ZJB.center.y<(5*lie/8)&&ZJB.center.y>(3*hang/8))
            {
                printf("装甲板位置完美,可以射击\n");
                buff[0] = 48;
                buff[1] = '\r';
                buff[2] = '\n';
                int ret = write(fd, buff, strlen(buff));
            }
            //图像分区
            else if(ZJB.center.y<(lie/4))//1~4区域
            {
                if(ZJB.center.x<(hang/4))
                {
                    printf("装甲板所在区域为:1\n");
                    buff[0] = 49;
                    buff[1] = '\r';
                    buff[2] = '\n';
                    int ret = write(fd, buff, strlen(buff));
                }
                else if (ZJB.center.x<(2*hang/4)&&ZJB.center.x>(hang/4))
                {
                    printf("装甲板所在区域为:2\n");
                    buff[0] = 50;
                    buff[1] = '\r';
                    buff[2] = '\n';
                    int ret = write(fd, buff, strlen(buff));
                }
                else if (ZJB.center.x<(3*hang/4)&&ZJB.center.x>(2*hang/4))
                {
                    printf("装甲板所在区域为:3\n");
                    buff[0] = 51;
                    buff[1] = '\r';
                    buff[2] = '\n';
                    int ret = write(fd, buff, strlen(buff));
                }
                else if (ZJB.center.x<hang&&ZJB.center.x>(3*hang/4))
                {
                    printf("装甲板所在区域为:4\n");
                    buff[0] = 52;
                    buff[1] = '\r';
                    buff[2] = '\n';
                    int ret = write(fd, buff, strlen(buff));
                }
            }
            else if(ZJB.center.y>(lie/4)&&ZJB.center.y<(lie/2))//5~8区域
            {
                if(ZJB.center.x<(hang/4))
                {
                    printf("装甲板所在区域为:5\n");
                    buff[0] = 53;
                    buff[1] = '\r';
                    buff[2] = '\n';
                    int ret = write(fd, buff, strlen(buff));
                }
                else if (ZJB.center.x<(2*hang/4)&&ZJB.center.x>(hang/4))
                {
                    printf("装甲板所在区域为:6\n");
                    buff[0] = 54;
                    buff[1] = '\r';
                    buff[2] = '\n';
                    int ret = write(fd, buff, strlen(buff));
                }
                else if (ZJB.center.x<(3*hang/4)&&ZJB.center.x>(2*hang/4))
                {
                    printf("装甲板所在区域为:7\n");
                    buff[0] = 55;
                    buff[1] = '\r';
                    buff[2] = '\n';
                    int ret = write(fd, buff, strlen(buff));
                }
                else if (ZJB.center.x<hang&&ZJB.center.x>(3*hang/4))
                {
                    printf("装甲板所在区域为:8\n");
                    buff[0] = 56;
                    buff[1] = '\r';
                    buff[2] = '\n';
                    int ret = write(fd, buff, strlen(buff));
                }
            }
            else if(ZJB.center.y>(lie/2)&&ZJB.center.y<(3*lie/4))//9~12区域
            {
                if(ZJB.center.x<(hang/4))
                {
                    printf("装甲板所在区域为:9\n");
                    buff[0] = 57;
                    buff[1] = '\r';
                    buff[2] = '\n';
                    int ret = write(fd, buff, strlen(buff));
                }
                else if (ZJB.center.x<(2*hang/4)&&ZJB.center.x>(hang/4))
                {
                    printf("装甲板所在区域为:10\n");
                    buff[0] = 49;
                    buff[1] = 48;
                    buff[2] = '\r';
                    buff[3] = '\n';
                    int ret = write(fd, buff, strlen(buff));
                }
                else if (ZJB.center.x<(3*hang/4)&&ZJB.center.x>(2*hang/4))
                {
                    printf("装甲板所在区域为:11\n");
                    buff[0] = 49;
                    buff[1] = 49;
                    buff[2] = '\r';
                    buff[3] = '\n';
                    int ret = write(fd, buff, strlen(buff));
                }
                else if (ZJB.center.x<hang&&ZJB.center.x>(3*hang/4))
                {
                    printf("装甲板所在区域为:12\n");
                    buff[0] = 49;
                    buff[1] = 50;
                    buff[2] = '\r';
                    buff[3] = '\n';
                    int ret = write(fd, buff, strlen(buff));
                }
            }
            else if(ZJB.center.y>(3*lie/4)&&ZJB.center.y<lie)//13~16区域
            {
                if(ZJB.center.x<(hang/4))
                {
                    printf("装甲板所在区域为:13\n");
                    buff[0] = 49;
                    buff[1] = 51;
                    buff[2] = '\r';
                    buff[3] = '\n';
                    int ret = write(fd, buff, strlen(buff));
                }
                else if (ZJB.center.x<(2*hang/4)&&ZJB.center.x>(hang/4))
                {
                    printf("装甲板所在区域为:14\n");
                    buff[0] = 49;
                    buff[1] = 52;
                    buff[2] = '\r';
                    buff[3] = '\n';
                    int ret = write(fd, buff, strlen(buff));
                }
                else if (ZJB.center.x<(3*hang/4)&&ZJB.center.x>(2*hang/4))
                {
                    printf("装甲板所在区域为:15\n");
                    buff[0] = 49;
                    buff[1] = 53;
                    buff[2] = '\r';
                    buff[3] = '\n';
                    int ret = write(fd, buff, strlen(buff));
                }
                else if (ZJB.center.x<hang&&ZJB.center.x>(3*hang/4))
                {
                    printf("装甲板所在区域为:16\n");
                    buff[0] = 49;
                    buff[1] = 54;
                    buff[2] = '\r';
                    buff[3] = '\n';
                    int ret = write(fd, buff, strlen(buff));
                }

这里为了方便数据输出,直接在该模块下面输出数据,后期会更新一个封装,美化程序。

  1. 为了防止有其他装甲板干扰,使得算法可以追踪,加入限幅滤波
#define DELAT_MAX 30//定义限幅滤波误差最大值
typedef	int filter_type;//定义限幅滤波数据类型
filter_type filter(filter_type effective_value, filter_type new_value, filter_type delat_max);
filter_type filter(filter_type effective_value, filter_type new_value, filter_type delat_max)
{
    if ( ( new_value - effective_value > delat_max ) || ( effective_value - new_value > delat_max ))
    {
        new_value=effective_value;
        return effective_value;
    }
    else
    {
        new_value=effective_value;
        return new_value;
    }
}
  1. 为辅助筛选装甲板,提高算法运行速度,做一次筛选预处理
 cv::RotatedRect& adjustRec(cv::RotatedRect& rec, const int mode)
    {
        using std::swap;

        float& width = rec.size.width;
        float& height = rec.size.height;
        float& angle = rec.angle;

        if (mode == WIDTH_GREATER_THAN_HEIGHT)
        {
            if (width < height)
            {
                swap(width, height);
                angle += 90.0;
            }
        }

        while (angle >= 90.0) angle -= 180.0;
        while (angle < -90.0) angle += 180.0;

        if (mode == ANGLE_TO_UP)
        {
            if (angle >= 45.0)
            {
                swap(width, height);
                angle -= 90.0;
            }
            else if (angle < -45.0)
            {
                swap(width, height);
                angle += 90.0;
            }
        }
    return rec;
}//由于灯条是竖着的,借此纠正不是竖着的轮廓,方便算法查找

至此track_on函数结束

下面说明track_on函数中用到的通讯部分:
分为两个部分:设置串口参数和打开串口
串口参数如下:

int set_uart_attr(int fd, int nSpeed, int nBits, char nEvent, int nStop)
{
    struct termios newtio, oldtio;
    /*保存测试现有串口参数设置*/
    if (tcgetattr(fd, &oldtio) != 0)
    {
        perror("SetupSerial 1");
        return -1;
    }
    bzero(&newtio, sizeof(newtio));
    /*设置字符大小*/
    newtio.c_cflag |= CLOCAL | CREAD;
    newtio.c_cflag &= ~CSIZE;
    /*设置数据位*/
    nBits=8;
    newtio.c_cflag |= CS8;

    /*设置奇偶校验位*/
    nEvent='N';//无奇偶校验位
    newtio.c_cflag &= ~PARENB;

    /*设置波特率*/
    nSpeed=115200;
    cfsetispeed(&newtio, B115200);
    cfsetospeed(&newtio, B115200);

    /*设置停止位*/
     nStop == 1;
     newtio.c_cflag &= ~CSTOPB;

    /*设置等待时间和最小接收字符*/
    newtio.c_cc[VTIME] = 0;
    newtio.c_cc[VMIN]  = 0;

    /*处理未接收字符*/
    tcflush(fd, TCIFLUSH);

    /*激活新配置*/
    if ((tcsetattr(fd, TCSANOW, &newtio)) != 0)
    {
        perror("com set error");
        return -1;
    }
   printf("set done!\n");
    return 0;
}

打开串口如下:

int open_uart(const char* device_name)
{
    int fd;

    fd = open(device_name, O_RDWR | O_NOCTTY | O_NDELAY);
    if (-1 == fd)
    {
        perror("Can't Open Serial Port");
        return (-1);
    }

    /*恢复串口为阻塞状态*/
    if (fcntl(fd, F_SETFL, 0) < 0)
    {
        printf("fcntl failed!\n");
    }
    else
    {
        printf("fcntl=%d\n", fcntl(fd, F_SETFL, 0));
    }

    /*测试是否为终端设备*/
    if (isatty(fd) == 0)
    {
        printf("standard input is not a terminal device\n");
    }
    else
    {
        printf("isatty success!\n");
    }

    printf("fd-open=%d\n", fd);
    return fd;
}

串口设置完成

最后进行测试的可视化处理,将分区画在输出显示的图像上以便于调试代码,用到draw_grad_line函数,封装如下:

Mat draw_grad_line(Mat src)
{
   int Row=src.cols;
   int Col =src.rows;//自动获取图像分辨率
   int thickness = 1;//区域分割线宽

   Point p1 = Point(int(Row / 4), 0);
   Point p4 = Point(int(Row / 4), Col);
   Point p2 = Point(int(Row*2 / 4), 0);
   Point p5 = Point(int(Row*2 / 4), Col);
   Point p3 = Point(int(Row * 3 / 4), 0);
   Point p6 = Point(int(Row * 3 / 4), Col);

   Point p7 = Point(0, int(Col/4));
   Point p8 = Point(0, int(Col*2/4));
   Point p9 = Point(0, int(Col*3/4));

   Point p10 = Point(Row, int(Col/4));
   Point p11 = Point(Row, int(Col*2/4));
   Point p12 = Point(Row, int(Col*3/4));

   Point p13 = Point(int(Row * 3 / 8), int(Col * 3/8));
   Point p14 = Point(int(Row * 5 / 8), int(Col * 3/8));
   Point p15 = Point(int(Row * 3 / 8), int(Col * 5/8));
   Point p16 = Point(int(Row * 5 / 8), int(Col * 5/8));

   Scalar color = Scalar(255, 255, 0);//设置线条颜色

   line(src, p1, p4, color, thickness, LINE_8);
   line(src, p2, p5, color, thickness, LINE_8);
   line(src, p3, p6, color, thickness, LINE_8);
   line(src, p7, p10, color, thickness, LINE_8);
   line(src, p8, p11, color, thickness, LINE_8);
   line(src, p9, p12, color, thickness, LINE_8);

   //中心区域
   line(src, p13, p14, color, thickness, LINE_8);
   line(src, p13, p15, color, thickness, LINE_8);
   line(src, p14, p16, color, thickness, LINE_8);
   line(src, p15, p16, color, thickness, LINE_8);

   return src;
}

至此,本次算法讲解结束。

感悟:
表面看来,这一套算法貌似简简单单,轻轻松松,实际上对于一个初入门槛的人来说,需要付出太多的努力与艰辛,要想真正理解每步蕴含的原理,目前还远远不够,甚至未来几年的时间都不够,需要学习太多的东西、了解太多的原理。不是每行代码都能毫无挫折的,有的算法你需要几天到几个月才能让他跑起来,没错,只是跑起来,至于里面的方法、原理更是目前阶段远远无法企及。知识真的是无尽的,哪怕是一个新兴行业。加油!入门人。

你可能感兴趣的:(jetson,tx2,Robomaster,opencv,计算机视觉)