写在前面
按照上一篇约定,这次更新对于视觉的入坑心得。这个比赛需要视觉的参与,包括补给区视觉标签的识别判断,敌方车辆的判断。
视觉标签的识别比较好搞,使用现成的Apriltag就可以了,但是对于敌方车辆的识别则见仁见智,opencv特征、滤波类、深度学习,都是很好的选择,这里就不对算法说太多,着重说一下踩得坑。
同样的,以下仅代表个人观点,若有错误请批评指正。
视觉驱动
- 做这个比赛视觉模块肯定是必不可少的,那么就需要选择一个合适的摄像头。虽然有些摄像头可以做到即插即用,但是因为这个比赛的视觉数据有多个用处,所以不能简单的直接读,比如用opencv自带的Videocapture,这样会造成一个模块对设备的持续占用,也就不能多模块并行处理了。正确的做法是,使用驱动将摄像头的数据作为rostopic进行发送,接收模块接收数据自己处理,从而保证多模块并行处理。
- 我用的是uvc摄像机,可以使用USB驱动,最后选了usb_cam驱动,可以直接从github搜索下载使用,具体的介绍网上有很多博客都说过这个问题,这里就不赘述了。讲一下踩过的坑,比如usb_cam-test.launch
<launch>
<node name="usb_cam" pkg="usb_cam" type="usb_cam_node" output="screen" >
<param name="video_device" value="/dev/video0" />
<param name="image_width" value="640" />
<param name="image_height" value="480" />
<param name="pixel_format" value="yuyv" />
<param name="camera_frame_id" value="usb_cam" />
<param name="io_method" value="mmap"/>
</node>
<node name="image_view" pkg="image_view" type="image_view" respawn="false" output="screen">
<remap from="image" to="/usb_cam/image_raw"/>
<param name="autosize" value="true" />
</node>
</launch>
- 首先是node name,因为后续的包不一定是在哪里下载的,所以名字可能对不上,需要注意一下;
- 设备的数据地址,也就是video0,使用迷你主机的话,一般是0,如果是笔记本,可能就变成1了;
- 图像宽高,可以使用Python直接读一下输出shape数据查一下,如果没有明确说明的话。另外,有些网上卖的摄像头可能同时配套有修改摄像头内置参数的代码驱动,可以根据需要自己修改;
- pixel-format这个很重要,我使用的摄像头就是默认的格式,删掉就好了,如果知道确定格式的话一定要改好;
- 其他的都是些修改亮度、对比度之类的参数,自己看着改就好了;
- iamge-view是在屏幕上展示图像,所以如果想在其他模块接受数据的话就要订阅这个节点发布的rostopic;
视觉标定
直接用的话可能会出现数据不准的情况,所以需要进行视觉标定,关于这方面的说明就更多了,甚至ROS官网就有说明,使用的是image_pipeline这个包,标定一般不会出问题,如果参数出问题了,我的方法是——改代码,把参数写在代码中,就不用在命令行中输入这些参数,虽然看起来很玄学,但确实之前命令行跑不通的程序正确执行了。详情直接搜视觉标定就可以了。
视觉标签识别
这个也是很简单的,使用apriltags2_ros,包在GitHub上就有,教程也是一大堆,唯一要注意的问题是——名称,就像上文说的,因为各个包作者不一样,所以可能名称对不上,造成消息订阅不到,这个也是小问题,改一下就好了。
敌方装甲识别
- 准确度:当然是opencv特征<滤波类算法<深度学习算法;
- 识别速度:当然是opencv特征>滤波类算法>深度学习算法;我在TX2上实验这三个算法,分别是170帧起步,70帧左右和10~20帧,当然了,opencv基于几何特征的没有使用特别复杂的匹配,滤波类用的也是最简单的KCF,深度学习用的SSD;所以基于以上数据得出的结论就是——没有结论,选用什么算法自己满意就好了,顺便提醒一下,如果有不正常的卡顿,比如帧率明明很高但是图像延迟还是很高,可以考虑调整一下消息收发频率;
- 今年比赛过程中,因为装甲板上贴有数字,所以我还写了一套基于数字识别的,但是最后因为摄像头画面偏黑所以无疾而终。使用opencv自带的基于cascade的识别即可,网上有很多训练好的数据集,可以直接用,也不复杂。我的思路是先通过颜色通道将比较亮的数字区域选择出来,这个时候考虑不要切的太大,否则影响准确率,二值化以后进行判断就可以了;
- 其他问题,做比赛的时候遇到一些神奇的问题,在这里简单记一下,提醒下后来人,可能比较幼稚,但是当时确实卡住了些时间:
- 既然使用了cascade,那么opencv版本起码要330以上,在Ubuntu下一般选择源码编译,其他的都很简单,就是不明白为什么网上的教程非要在Cmake命令后面加一大堆参数也不解释,明明cmake … 就可以了
cmake ..
- 使用catkin_make的时候,遇到一大段白色的报错信息,一个红色的都没有,诸如 cv::Mat之类最简单的函数、数据类型都没有定义,“undefined referrence”这个时候不要怕,是“软伤”,一般是Cmakelist.txt错了,这个时候不要乱改,网上加这个添那个,还有各种神奇的修改方式。我当时就是改了下Cmakelist.txt就好了,改动贴在下面:
find_package( OpenCV REQUIRED )
include_directories(
# include
${catkin_INCLUDE_DIRS}
${OpenCV_INCLUDE_DIRS}
)
target_link_libraries(${PROJECT_NAME}_node
PUBLIC
${catkin_LIBRARIES}
${OpenCV_LIBRARIES}
)
顺便一提,为了使用自己定义的消息类型,还需要加上这么一句:
generate_messages()
具体原理迫于时间压力还没有细究,感兴趣的朋友可以私信讨论一下;
- 驱动发送的消息不能直接接收,需要通过cv_bridge转换,下面分别贴一下接收部分的代码:
Python
from cv_bridge import CvBridge, CvBridgeError
def listener():
rospy.init_node('armor_detection_fromusb',anonymous=True)
rospy.Subscriber('/usb_cam/image_raw',Image,callback)
rospy.spin()
def callback(data):
cb=CvBridge()
cv_image=cb.imgmsg_to_cv2(data,"bgr8")
cv2.imshow('receive',cv_image)
cv2.waitKey(5)
img=cv_image
res=recgnize(img)
#cv2.rectangle(img, (res[0], res[1]), (res[2], res[3]), (0, 0, 255), 2)
cv2.imshow('contours', img)
cv2.waitKey(5)
C++
#include <image_transport/image_transport.h>
#include <cv_bridge/cv_bridge.h>
#include <sensor_msgs/image_encodings.h>
void numberdeal(const sensor_msgs::ImageConstPtr& msg)
{
cv_bridge::CvImagePtr cv_ptr;
try
{
cv_ptr = cv_bridge::toCvCopy(msg, sensor_msgs::image_encodings::BGR8);
}
catch (cv_bridge::Exception& e)
{
ROS_ERROR("cv_bridge exception: %s", e.what());
return;
}
Mat frame;
cv_ptr->image.copyTo(frame);
//其他处理过程
std::vector<pr::PlateInfo> res = prc.RunPiplineAsImage(frame);
}
- 一个节点既发送消息又接受消息不是很好写,在这里放一个模板,也就是数字识别节点的代码,用以范例,具体处理细节不述;
#include "./lpr/include/Pipeline.h"
#include <iostream>
#include <opencv2/opencv.hpp>
#include <fstream>
#include <ros/ros.h>
#include <image_transport/image_transport.h>
#include <cv_bridge/cv_bridge.h>
#include <sensor_msgs/image_encodings.h>
#include <roborts_msgs/ArmorDetection.h>
using namespace pr;
using namespace std;
using namespace cv;
pr::PipelinePR prc("/home/dyson/roborts_ws/src/RoboRTS-ros/roborts_numberrecog/lpr/model/cascade.xml",
"/home/dyson/roborts_ws/src/RoboRTS-ros/roborts_numberrecog/lpr/model/HorizonalFinemapping.prototxt", "/home/dyson/roborts_ws/src/RoboRTS-ros/roborts_numberrecog/lpr/model/HorizonalFinemapping.caffemodel",
"/home/dyson/roborts_ws/src/RoboRTS-ros/roborts_numberrecog/lpr/model/Segmentation.prototxt", "/home/dyson/roborts_ws/src/RoboRTS-ros/roborts_numberrecog/lpr/model/Segmentation.caffemodel",
"/home/dyson/roborts_ws/src/RoboRTS-ros/roborts_numberrecog/lpr/model/CharacterRecognization.prototxt", "/home/dyson/roborts_ws/src/RoboRTS-ros/roborts_numberrecog/lpr/model/CharacterRecognization.caffemodel"
);
class NumberRecogize
{
public:
ros::NodeHandle nh_;
ros::Subscriber image_sub_;
ros::Publisher armor_pub_;
~NumberRecogize()
{
}
void numberdeal(const sensor_msgs::ImageConstPtr& msg)
{
cv_bridge::CvImagePtr cv_ptr;
try
{
cv_ptr = cv_bridge::toCvCopy(msg, sensor_msgs::image_encodings::BGR8);
}
catch (cv_bridge::Exception& e)
{
ROS_ERROR("cv_bridge exception: %s", e.what());
return;
}
Mat frame;
cv_ptr->image.copyTo(frame);
std::vector<pr::PlateInfo> res = prc.RunPiplineAsImage(frame);
roborts_msgs::ArmorDetection ar;
if(res.size()!=0){
ar.x1=res[0].ROI.x;
std::cout<<ar.x1<<" ";
ar.y1=res[0].ROI.y;
std::cout<<ar.y1<<" ";
ar.x2=res[0].ROI.x+res[0].ROI.width;
std::cout<<ar.x2<<" ";
ar.y2=res[0].ROI.y+res[0].ROI.height;
std::cout<<ar.y2<<" \n";
}
else
{
ar.x1=0;
ar.y1=0;
ar.x2=0;
ar.y2=0;
}
armor_pub_.publish(ar);
}
NumberRecogize()
{
image_sub_=nh_.subscribe("/usb_cam/image_raw",2,&NumberRecogize::numberdeal,this);
armor_pub_=nh_.advertise<roborts_msgs::ArmorDetection>("numberrecognize",2);
}
};
int main(int argc, char** argv){
ros::init(argc,argv,"numberrecognize");
NumberRecogize nr;
//ros::AsyncSpinner async_spinner(1);
//async_spinner.start();
//ros::waitForShutdown();
ros::spin();
return 0;
}
未完待续————————————————————————————————————————
欢迎批评指正!