# 任务2_Rune **Repository Path**: MrDreamQ/task-2--rune ## Basic Information - **Project Name**: 任务2_Rune - **Description**: 神符识别 - **Primary Language**: C++ - **License**: Not specified - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 1 - **Forks**: 0 - **Created**: 2021-09-27 - **Last Updated**: 2024-11-17 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # 任务2_Rune 学习与参考的[博客1](https://blog.csdn.net/qq_43667130/article/details/105276577),[博客2](https://blog.csdn.net/u010750137/article/details/100825793)。 长文警告!检查任务跳转至***使用说明***即可。 ## 项目介绍——神符识别算法逻辑 以下是本人的神符识别算法逻辑解释。 ### 图像预处理 图像预处理部分与任务1类似,由于目标板的灯光为红光或蓝光,因此可对红(蓝)通道的灰度值进行增强。 该任务中识别的是目标板为蓝光。 #### 蓝色通道的灰度值增强 将视频帧的三通道分离出来,然后用b通道的灰度值矩阵减去r通道的灰度值矩阵,对应代码如下: ```c++ if (_color) //判断是否为红光,是则执行以下代码块,不是则执行else的代码块 { gray_img = channels.at(2) - channels.at(0); //增强红色通道 } else { gray_img = channels.at(0) - channels.at(2); //增强蓝色通道 } ``` 程序执行的是else下的代码块。执行后,r通道和b通道灰度值相近的地方将会接近或等于0,而蓝色灯光部分的灰度值在其灰度图矩阵中将被较好地独立出来,从而更好地进一步处理。 #### 图像二值化和形态学操作 ```c++ threshold(gray_img, binBright_img, 80, 255, THRESH_BINARY); //二值化图像 ``` 与任务1类似,阈值设定为80。 得到二值化矩阵后,再对二值化图像进行闭操作,代码如下: ```c++ Mat element = getStructuringElement(MORPH_RECT, Size(5, 5)); morphologyEx(binBright_img, binBright_img, MORPH_CLOSE, element, Point(-1, -1)); //进行闭操作使得目标板下得箭头能相连 ``` 形态学操作前后效果如下: ![形态学操作前](https://s3.bmp.ovh/imgs/2021/10/213b081feb17d23f.png) ![形态学操作后](https://s3.bmp.ovh/imgs/2021/10/e65e61afac3e005c.png) 经过闭操作,连接中心与目标板的箭头相互连接在一块,为下一步寻找并建立父子轮廓关系树打下基础。 ### 寻找符合条件的轮廓及其像素点 寻找并建立父子轮廓关系树,并以此为基础筛选出目标板轮廓是关键所在。 ### 父子轮廓关系树 ```c++ vector> contours; //存储轮廓像素点的二维数组 vector hierarchy; //存储父轮廓及内嵌轮廓等上级、下级、同级信息的数组 findContours(binBright_img, contours, hierarchy, RETR_TREE, CHAIN_APPROX_NONE, Point(0, 0)); //以相距不超过1像素的距离寻找轮廓,并存储轮廓像素点坐标 ``` 利用以上代码,建立父子轮廓关系树,轮廓之间的相互关系存储进容器`hierarchy`,所有外层、内嵌轮廓存储进容器`contours`。 #### 关于hierarchy 学习与参考的[博客](https://blog.csdn.net/TanJingV/article/details/105818054),及书《OpenCV4快速入门》 ![](https://s3.bmp.ovh/imgs/2021/10/3a589dd4317be6f7.png) ### 目标板的筛选 旋转的神符中,有目标板和非目标板,其二值化特征如下: ![](https://s3.bmp.ovh/imgs/2021/10/3600946fc45c47ae.png) 进行形态学操作后的目标板轮廓含有一个内嵌轮廓,而非目标板含有三个,因此可利用父轮廓含有的内嵌轮廓数对所有轮廓进行筛选,筛选出只含有一个内嵌轮廓的父轮廓,代码如下: ```c++ int contour[25] = {0}; //存储某个父轮廓含有子轮廓数的数组 for (size_t i = 0; i < contours.size(); i++) //这里的i为所有轮廓(包括外层及内嵌轮廓) { if (hierarchy[i][3] != -1) //如果父轮廓的索引编号不为-1,即该轮廓为父轮廓,含有内嵌轮廓 { contour[hierarchy[i][3]]++; //某个轮廓上一层父轮廓索引号的位置自增,即contour[i]表示某个轮廓含有的子轮廓的数目 } } ``` 视频中还出现以下情况: ![](https://s3.bmp.ovh/imgs/2021/10/b5b09c4412569307.png) 五个非目标板旋转干扰时,板与中心之间的连接断裂——这会出现多个只含有一个内嵌轮廓的父轮廓,因此,本人在对只含有一个内嵌轮廓的父轮廓进行形状检测筛选和画线标定之前,会对所有含有一个内嵌轮廓的父轮廓进行计数,代码如下: ```c++ int cnt = 0; for (size_t j = 0; j < contours.size(); j++) { if (contour[j] == 1) { cnt++; //若遍历到一个含有一个内嵌轮廓的父轮廓,则cnt自增1 } } ``` 当该数大于1时,跳出形状检测筛选和画线标定,直接进行视频的输出。经测试,五个非目标板旋转干扰时,标定效果较好。 然后,再对只含有一个内嵌轮廓的父轮廓的子轮廓(即该内嵌轮廓)进行形状检测与筛选,代码如下: ````c++ if (contour[k] == 1) //判断当前轮廓含有的子轮廓数目是否为1 { int num = hierarchy[k][2]; //某个只含有一个内嵌轮廓的父轮廓的下一层子轮廓的索引编号 RotatedRect rrect = minAreaRect(contours[num]); //返回该内嵌轮廓的外接矩形 Point2f point[4]; //存储外接矩形的四个顶点的数组 rrect.points(point); //将外接矩形的四个顶点映射到数组中 /*剔除含有三个内嵌轮廓的非目标板的外接矩形(长短边长之比大于2.5)的影响*/ if (rrect.size.height > rrect.size.width && ((rrect.size.height / rrect.size.width) > 2.5)) { continue; //进行下一子轮廓为1的父轮廓外接矩形的判断与绘制 } else if (rrect.size.height < rrect.size.width && ((rrect.size.width / rrect.size.height) > 2.5)) { continue; } ```` 由于曝光、神符灯光变化、二值化阈值较高等原因,非目标板的轮廓可能会与其父轮廓或子轮廓脱离,以上的代码目的是通过轮廓外接矩形的长宽之比将可能出现的非目标板轮廓的外接矩形剔除。 经过上述步骤,`num`便为目标板轮廓的索引编号,`rrect`为其外接矩形,利用其外接矩形的参数对目标板进行画线标定。 ### 目标板出现时中心“R”的标定 经过形态学操作,可将神符中心的“R”转变成一个小白块,该白块轮廓的特征为——没有子轮廓和父轮廓。 **在对目标板进行画线与标定后**,再遍历所有轮廓,筛选出没有子轮廓和父轮廓的轮廓,输出其外接矩形,利用外接矩形的长宽之比和最长边长度,剔除其他轮廓的影响。 然后再对中心“R”进行画线标定: ```c++ line(src, Rpoint[i], Rpoint[(i + 1) % 4], Scalar(0, 255, 255), 2, 8, 0);//画出中间R的外接轮廓 ``` ### 目标打击点的简单预测 通过: ``` Mat rot_mat = getRotationMatrix2D(center, -30, 1.1); ``` 得到神符中心点(即中心R的外接矩形的中心)顺时针旋转30度并缩放1.1倍的旋转矩阵`rot_mat`,类型如图: ![](https://s3.bmp.ovh/imgs/2021/10/61f8cb92883a4d78.png) 目标点向量为`Point2f target`,用旋转矩阵与目标点矩阵点乘,即`rot_mat·(target.x, target.y, 1.0)`——(2,3)矩阵点乘(3,1)矩阵,得到的(2,1)矩阵即为预测点的坐标,在该坐标上做空心圆,对应代码如下: ``` void RuneDetect::target_predict(cv::Mat &src, cv::Point2f target, cv::Point2f center) { Mat rot_mat = getRotationMatrix2D(center, -30, 1.1); Mat target_xy = (Mat_(3,1)<< target.x,target.y,1.0); rot_mat.convertTo(rot_mat,CV_32F); target_xy.convertTo(target_xy,CV_32F); Mat resPoint = rot_mat*target_xy; circle(src, Point2f(resPoint.at(0,0), resPoint.at(1,0)), 15, Scalar(0,255,0), 2, 8, 0); // putText(src, "predict_point", Point2f(resPoint.at(0,0), resPoint.at(1,0)), FONT_HERSHEY_SIMPLEX, 0.4, Scalar(255, 255, 255), 1, 8, 0); } ``` 效果图如下: ![](https://s3.bmp.ovh/imgs/2021/10/db481557085c0f0c.png) ## 使用说明 ### 运行任务2可执行文件 在该项目文件夹下,运行命令行指令: ```bash $ mkdir build $ cd build/ $ cmake .. $ make $ ./task2 <能量机关_1.avi文件路径> ``` 即可查看神符识别效果视频。 ### 实例化对象的接口设置 与任务1和任务6的程序一样,该任务程序里有多种构造函数重载: ```c++ RuneDetect::RuneDetect() { _is_recorded = false; _color = BLUE; _is_continuous = true; } RuneDetect::RuneDetect(bool is_recorded) { _is_recorded = is_recorded; _color = BLUE; _is_continuous = true; } RuneDetect::RuneDetect(bool is_recorded, int color, bool is_continuous) { _is_recorded = is_recorded; _color = color; _is_continuous = is_continuous; } ``` 其中,参数1为视频写入判断参数,参数2为视频中灯条的颜色**(会影响图像预处理阶段中红蓝色通道增强过程)**,参数3为视频播放模式判断参数。 在`main.cpp`创建对象时,可选择多种创建方式。 如程序默认的创建对象方式为: ```c++ RuneDetect task2(false, BLUE, true); //此处修改写入视频判断、灯条颜色、播放模式判断参数的设置 ``` 则运行程序后不会写入并保存效果视频,处理默认灯条颜色——蓝色,连续播放模式。 若创建对象的方式为: ```c++ RuneDetect task2(true, RED, false); ``` 则运行程序后会写入视频,并在build文件夹下保存名为"代码效果.avi",编码格式为MPG2的视频文件,处理的灯条颜色为红色,逐帧播放视频(按下任意按键使视频前进,按下esc键退出) 注意:在连续播放模式中,按下空格键使视频暂停,暂停后按下除esc键的任意键使视频继续,暂停后按下esc键退出,该逻辑代码如下: ```c++ if (waitKey(30) == ' ') //按下空格暂停 { if (waitKey(0) == 27) //暂停后按下esc键退出 { break; } } ``` ## 注意事项 调用任务2可执行文件时,要附上能量机关\_1视频文件路径,该项目文件夹下已包含有,也可附上其他的能量机关\_1的视频文件路径。