我是靠谱客的博主 开心枫叶,这篇文章主要介绍RM机器视觉——图像处理、识别装甲板(ROBOMASTER),现在分享给大家,希望可以做个参考。

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

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

正文:

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

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

Source文件所需如下:

复制代码
1
2
3
4
5
6
SOURCES += track_on.cpp uart.cpp grabimage-main.cpp area.cpp

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

header文件所需如下:

复制代码
1
2
3
4
5
6
7
8
9
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

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

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

复制代码
1
2
INCLUDEPATH += $$PWD/../../../../opt/MVS/include

配置opencv3.4.0如下:

复制代码
1
2
3
4
5
6
7
8
9
10
11
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文件的内容,该内容并非完全提前配置好,是在不断调试的过程中完善的。

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

复制代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
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);

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

复制代码
1
2
3
4
pthread_t nThreadID; nRet = pthread_create(&nThreadID, NULL ,WorkThread1 , handle); sleep(999999999999999999999999999999);//取流时间

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

复制代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
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循环里的处理部分如下:

复制代码
1
2
3
4
5
6
7
8
9
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. 图像预处理,第一步,降采样取图,提高算法运行的速度和稳定性
复制代码
1
2
pyrDown(src,src,Size(src.cols/2,src.rows/2));
  1. 转HSV图像,分离颜色通道,重新合并两通道,为了后面方便寻找图像中的所有轮廓
复制代码
1
2
3
4
5
cvtColor(img_rgb, hsvImage, COLOR_BGR2HSV); vector<Mat> hsvsplit;//hsv的分离通道 split(hsvImage, hsvsplit); merge(hsvsplit, hsvImage);//重新合并
  1. 图像二值化处理,摒弃环境光干扰
复制代码
1
2
3
Mat thresHold; threshold(hsvsplit[2], thresHold, 240, 245, THRESH_BINARY);
  1. 模糊、膨胀处理,让图像变得圆润
复制代码
1
2
3
4
blur(thresHold, thresHold, Size(3, 3)); Mat element = cv::getStructuringElement(cv::MORPH_ELLIPSE, cv::Size(3, 3)); //膨胀 dilate(thresHold, element, element);
  1. 开始寻找轮廓
复制代码
1
2
3
4
5
vector< RotatedRect> vc; vector< RotatedRect> vRec; vector<vector<Point>> Light_Contour; // 发现的轮廓 findContours(element.clone(),Light_Contour,CV_RETR_EXTERNAL,CV_CHAIN_APPROX_SIMPLE);
  1. 从面积上筛选轮廓
复制代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
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. 从灯条长宽比上来筛选轮廓
复制代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
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; }

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

复制代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
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("装甲板所在区域为:1n"); 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("装甲板所在区域为:2n"); 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("装甲板所在区域为:3n"); 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("装甲板所在区域为:4n"); 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("装甲板所在区域为:5n"); 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("装甲板所在区域为:6n"); 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("装甲板所在区域为:7n"); 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("装甲板所在区域为:8n"); 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("装甲板所在区域为:9n"); 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("装甲板所在区域为:10n"); 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("装甲板所在区域为:11n"); 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("装甲板所在区域为:12n"); 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("装甲板所在区域为:13n"); 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("装甲板所在区域为:14n"); 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("装甲板所在区域为:15n"); 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("装甲板所在区域为:16n"); buff[0] = 49; buff[1] = 54; buff[2] = 'r'; buff[3] = 'n'; int ret = write(fd, buff, strlen(buff)); }

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

  1. 为了防止有其他装甲板干扰,使得算法可以追踪,加入限幅滤波
复制代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#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. 为辅助筛选装甲板,提高算法运行速度,做一次筛选预处理
复制代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
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函数中用到的通讯部分:
分为两个部分:设置串口参数和打开串口
串口参数如下:

复制代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
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; }

打开串口如下:

复制代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
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=%dn", fcntl(fd, F_SETFL, 0)); } /*测试是否为终端设备*/ if (isatty(fd) == 0) { printf("standard input is not a terminal devicen"); } else { printf("isatty success!n"); } printf("fd-open=%dn", fd); return fd; }

串口设置完成

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

复制代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
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; }

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

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

最后

以上就是开心枫叶最近收集整理的关于RM机器视觉——图像处理、识别装甲板(ROBOMASTER)的全部内容,更多相关RM机器视觉——图像处理、识别装甲板(ROBOMASTER)内容请搜索靠谱客的其他文章。

本图文内容来源于网友提供,作为学习参考使用,或来自网络收集整理,版权属于原作者所有。
点赞(69)

评论列表共有 0 条评论

立即
投稿
返回
顶部