声明:
本文案基于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
:
pyrDown(src,src,Size(src.cols/2,src.rows/2));
cvtColor(img_rgb, hsvImage, COLOR_BGR2HSV);
vector<Mat> hsvsplit;//hsv的分离通道
split(hsvImage, hsvsplit);
merge(hsvsplit, hsvImage);//重新合并
Mat thresHold;
threshold(hsvsplit[2], thresHold, 240, 245, THRESH_BINARY);
blur(thresHold, thresHold, Size(3, 3));
Mat element = cv::getStructuringElement(cv::MORPH_ELLIPSE, cv::Size(3, 3)); //膨胀
dilate(thresHold, element, element);
vector< RotatedRect> vc;
vector< RotatedRect> vRec;
vector<vector<Point>> Light_Contour; // 发现的轮廓
findContours(element.clone(),Light_Contour,CV_RETR_EXTERNAL,CV_CHAIN_APPROX_SIMPLE);
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);
}
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));
}
这里为了方便数据输出,直接在该模块下面输出数据,后期会更新一个封装,美化程序。
#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;
}
}
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;
}
至此,本次算法讲解结束。
感悟:
表面看来,这一套算法貌似简简单单,轻轻松松,实际上对于一个初入门槛的人来说,需要付出太多的努力与艰辛,要想真正理解每步蕴含的原理,目前还远远不够,甚至未来几年的时间都不够,需要学习太多的东西、了解太多的原理。不是每行代码都能毫无挫折的,有的算法你需要几天到几个月才能让他跑起来,没错,只是跑起来,至于里面的方法、原理更是目前阶段远远无法企及。知识真的是无尽的,哪怕是一个新兴行业。加油!入门人。