之前做的博客《玩机器人,先从一名军火商做起》,讲述了如何设计一个电磁炮,当时的视频给大家演示到了OpenCV的单目标锁定追踪的效果。

但是那个锁定追踪不是用ROS开发的,而只是简单的Python OpenCV + PID + 舵机控制组成的。最近看了古月老师的《ROS机器视觉开发入门》教程,也尝试着使用ROS的方式来开发一下单目标锁定追踪的Demo程序。

果然,古月老师YYDS!实在是强大,调试好了!效果如下。需要功能包的小伙伴可以私信古月居公众号哦~

嗯,步骤呢,先启动usb_cam功能包,调用摄像头然后发布个图像的话题,然后启动object_tracker功能包的object_tracker.py文件,定于图像话题转成OpenCV的数据来操作即可,简直简单的不要不要的~

嗯,看到这里可能有人要骂了,没错,就是这么的凡尔赛~

但是真的很简单,不信,你看下去!

我们先来聊聊关于Python + OpenCV实现单目标锁定追踪是咋搞的!话不多说,上链接!传送门

没猜错,单目标锁定追踪的代码抄这里的,它的代码是这样的!

#!usr/bin/env python3
#coding:utf-8

from imutils.video import VideoStream
from imutils.video import FPS
import argparse
import imutils
import time
import cv2

ap = argparse.ArgumentParser()
ap.add_argument("-v", "--video", type=str, help="path to input video file")
ap.add_argument("-t", "--tracker", type=str, default="kcf", help="OpenCV object tracker type")
args = vars(ap.parse_args())
 
# My opencv:4.1.0
(major, minor) = cv2.__version__.split(".")[:2]
 
if int(major) == 3 and int(minor) < 3:
    tracker = cv2.Tracker_create(args["tracker"].upper())
else:
    OPENCV_OBJECT_TRACKERS = {
        "csrt": cv2.TrackerCSRT_create,
        "kcf": cv2.TrackerKCF_create,
        #"boosting": cv2.TrackerBoosting_create,
        "mil": cv2.TrackerMIL_create,
        #"tld": cv2.TrackerTLD_create,
        #"medianflow": cv2.TrackerMedianFlow_create,
        #"mosse": cv2.TrackerMOSSE_create
    }
    tracker = OPENCV_OBJECT_TRACKERS[args["tracker"]]()
    
initBB = None
 
if not args.get("video", False):
    print("[INFO] starting video stream...")
    vs = VideoStream(src=0).start()
    time.sleep(1.0)
else:
    vs = cv2.VideoCapture(args["video"])
 
fps = None

while True:
    frame = vs.read()
    frame = frame[1] if args.get("video", False) else frame
 
    if frame is None:
        break
 
    #frame = imutils.resize(frame, height = 1280, width=720)
    (H, W) = frame.shape[:2]
 
    if initBB is not None:
        (success, box) = tracker.update(frame)
 
        if success:
            (x, y, w, h) = [int(v) for v in box]
            cv2.rectangle(frame, (x, y), (x + w, y + h), (0, 255, 0), 2)
        fps.update()
        fps.stop()

        info = [
            ("Tracker", args["tracker"]),
            ("Success", "Yes" if success else "No"),
            ("FPS", "{:.2f}".format(fps.fps())),
        ]
 
        for (i, (k, v)) in enumerate(info):
            text = "{}: {}".format(k, v)
            cv2.putText(frame, text, (10, H - ((i * 20) + 20)), cv2.FONT_HERSHEY_SIMPLEX, 0.6, (0, 0, 255), 2)
                        
    cv2.imshow("Frame", frame)
    key = cv2.waitKey(1) & 0xFF
    if key == ord("s"):
        initBB = cv2.selectROI("Frame", frame, fromCenter=False, showCrosshair=True)
        tracker.init(frame, initBB)
        fps = FPS().start()
    elif key == ord("q"):
        break
 
if not args.get("video", False):
    vs.stop()

cv2.destroyAllWindows()

这里我就不加注释了,在后面的ROS代码里面会有超级详细的注释。

实现的原理也比较简单,主要是通过调用Python OpenCV内置的ROI感兴趣区域提取算法来实现。以前的OpenCV支持七个计算模型,现在不知道什么情况,只有三个能用。

呃,具体的原理,大家看下这篇博文吧,我也不想写了,主要是写的不一定有人家写的好。https://blog.csdn.net/weixin_38907560/article/details/82292091

他这个博文讲的代码和我在上面抄的一样的,有着很详细的解释,特别是每个计算模型之间的异同都有介绍~

不过这段代码要运行,你需要安装一下opencv-python、opencv-contrib-python、imutils三个功能包,使用Python3的环境,最后是Python3.6以上的。(往下我没测试过,我的是Python3.8)

pip3 install opencv-python
pip3 install opencv-contrib-python
pip3 install imutils

我们pip在安装的时候,会跳到pypi.org这个网站进行下载,如果pip显示链接超时的情况下,可以去这个网站根据自己的Python版本和名称来下载对应的安装包,离线安装即可。

嗯,其实Python + OpenCV的方式是完全可以开发的,为啥非把这玩意在ROS下再开发一次呢~

我的想法是这样的。假如我现在有台具备摄像头的机器人,我如果使用Python + OpenCV的方式进行开发的话,摄像头的资源只能在这一个程序里面使用,调用摄像头、获取图像、处理图像、输出图像,貌似也没啥问题。

前端时间接了个开发兼职,甲方爸爸提了个项目需求,我现在只有一个摄像头,但是现在有两个程序需要同时使用这个摄像头的数据,不可能两个程序同时调用摄像头来搞,难搞难搞~

最后的解决方案是“跨线程”,开了三条线程,第一个线程专门用来读取摄像头的数据,第二个线程从线程1获取摄像头数据让程序1来使用,第三个线程从线程1获取摄像头数据让程序2来执行,算是勉勉强强的完成了项目需求。功能实现了,后续有优化需求再说,你电脑跑不了是你环境的问题。毕竟程序和程序猿总要有一个得跑的吧(PS:玩笑话)

这是个很有意思的需要,可以在扩充下,我现在是在两个程序,如果有三个四个甚至多个呢?开线程这玩意“治标不治本”,而且自己做的diamagnetic比较烂,安全不安全的根本就没考虑;如果有两个电脑需要这个数据的,那我岂不是还要做个网络通讯吗,实在是不划算。

其实这个问题在ROS下可以得到很好的解决。我们通过usb_cam来获取摄像头数据,将这个数据通过话题的方式发布出去,你们谁要用直接来订阅就好了,和我无关。就像我们这里做的Demo,usb_cam发布一个图像话题之后,我的image_view做了订阅显示,我的object_tracker做了订阅并通过ROS的cv_bridge功能包转成OpenCV的数据类型,直接使用OpenCV来处理数据。

这个rqt_graph是很简洁清晰的,一个名叫usb_cam的节点发布了一个叫做/usb_cam/image_raw的话题,分别被image_view节点和object_tarcker节点订阅。至于这俩节点订阅后干了啥,自己瞅代码去!

这里有个很重要的地方,ROS的图像数据格式和OpenCV的图像数据格式之间的互转,这里用到了cv_bridge功能包,咋实现的我也不知道,在Python语言下能用起来就好,这里抄一份古月老师的代码。

#!/usr/bin/env python
# -*- coding: utf-8 -*-

import rospy
import cv2
from cv_bridge import CvBridge, CvBridgeError
from sensor_msgs.msg import Image

class image_converter:
    def __init__(self):    
        # 创建cv_bridge,声明图像的发布者和订阅者
        self.image_pub = rospy.Publisher("cv_bridge_image", Image, queue_size=1)
        self.bridge = CvBridge()
        self.image_sub = rospy.Subscriber("/usb_cam/image_raw", Image, self.callback)

    def callback(self,data):
        # 使用cv_bridge将ROS的图像数据转换成OpenCV的图像格式
        try:
            cv_image = self.bridge.imgmsg_to_cv2(data, "bgr8")
        except CvBridgeError as e:
            print(str(e))

        # 在opencv的显示窗口中绘制一个圆,作为标记
        (rows,cols,channels) = cv_image.shape
        if cols > 60 and rows > 60 :
            cv2.circle(cv_image, (60, 60), 30, (0,0,255), -1)

        # 显示Opencv格式的图像
        cv2.imshow("Image window", cv_image)
        cv2.waitKey(3)

        # 再将opencv格式额数据转换成ros image格式的数据发布
        try:
            self.image_pub.publish(self.bridge.cv2_to_imgmsg(cv_image, "bgr8"))
        except CvBridgeError as e:
            print(str(e))

if __name__ == '__main__':
    try:
        # 初始化ros节点
        rospy.init_node("cv_bridge_test")
        rospy.loginfo("Starting cv_bridge_test node")
        image_converter()
        rospy.spin()
    except KeyboardInterrupt:
        print("Shutting down cv_bridge_test node.")
        cv2.destroyAllWindows()

这个是古月老的cv_bridge_test的代码,功能方面实现了订阅/usb_cam/image_raw话题转成了OpenCV的格式,并通过Python OpenCV进行处理给加了个红色的圈显示处理并显示出来,显示之后又将处理过的图像转成ROS下的图像话题cv_bridge_image给发布到了ROS系统下。

而追踪的ROS代码呢,其实就是这两段代码的结合体,它是这样的!

#!/usr/bin/env python
# -*- coding: utf-8 -*-

import rospy	#导入rospy
import cv2		#导入cv2
from cv_bridge import CvBridge, CvBridgeError	#导入cv_bridge
from sensor_msgs.msg import Image	#导入sensor_msgs

import argparse	#导入argparse

ap = argparse.ArgumentParser()	#读取后缀参数
ap.add_argument("-t", "--tracker", type=str, default="kcf", help="OpenCV object tracker type")	#后缀参数t用于指定模型
args = vars(ap.parse_args())	#获取参赛

(major, minor) = cv2.__version__.split(".")[:2]	#检测当前OpenCV版本

#版本小于等于3的和大于3的部分参数不一样
if int(major) == 3 and int(minor) < 3:
	tracker = cv2.Tracker_create(args["tracker"].upper())
else:
	OPENCV_OBJECT_TRACKERS = {
		"csrt": cv2.TrackerCSRT_create,					#CSRT求解器
		"kcf": cv2.TrackerKCF_create,					#KCF求解器
		#"boosting": cv2.TrackerBoosting_create,		#BOOSTING求解器,已弃用
		"mil": cv2.TrackerMIL_create,					#MIL求解器
		#"tld": cv2.TrackerTLD_create,					#TLD求解器,已弃用
		#"medianflow": cv2.TrackerMedianFlow_create,	#MedianFlow求解器,已弃用
		#"mosse": cv2.TrackerMOSSE_create				#MOSSE求解器,已弃用
	}
	tracker = OPENCV_OBJECT_TRACKERS[args["tracker"]]()	#读取参数加载模型
   
initBB = None	#初始化比对数据空间(ROI数据空间)

class image_converter:
	def __init__(self):    
		# 创建cv_bridge,声明图像的发布者和订阅者
		self.image_pub = rospy.Publisher("cv_bridge_image", Image, queue_size=1)
		self.bridge = CvBridge()
		self.image_sub = rospy.Subscriber("/usb_cam/image_raw", Image, self.callback)

	def callback(self,data):
		# 使用cv_bridge将ROS的图像数据转换成OpenCV的图像格式
		try:
			cv_image = self.bridge.imgmsg_to_cv2(data, "bgr8")
		except CvBridgeError as e:
			print(str(e))

		(H, W) = cv_image.shape[:2]	#获取图像的Height和Weight
		
		global initBB	
		
		#如果ROI数据不为空,说明有需要追踪的目标
		if initBB is not None:
			(success, box) = tracker.update(cv_image)	#将ROI数据和当前帧进行比对,返回比对结果和ROI区域值
			
			#如果比对结果为真,说明在当前帧图像内检测到需要追踪的目标
			if success:
				(x, y, w, h) = [int(v) for v in box]	#从ROI区域值提取x、y、w、h四个数值
				cv2.rectangle(cv_image, (x, y), (x + w, y + h), (0, 255, 0), 2)	#画框
			
			#制作提示信息	
			info = [
				("Tracker", args["tracker"]),
				("Success", "Yes" if success else "No"),
			]
			
			#将提示信息显示在图像上面
			for (i, (k, v)) in enumerate(info):
				text = "{}: {}".format(k, v)
				cv2.putText(cv_image, text, (10, H - ((i * 20) + 20)), cv2.FONT_HERSHEY_SIMPLEX, 0.6, (0, 0, 255), 2)
				
		cv2.imshow("cv_image", cv_image)	#使用OpenCV进行显示
			
		key = cv2.waitKey(1) & 0xFF	#做个按键检测
		#如果按下了's',通过鼠标选择要追踪的目标
		if key == ord("s"):
			initBB = cv2.selectROI("cv_image", cv_image, fromCenter=False, showCrosshair=True)
			tracker.init(cv_image, initBB)	#将选中的内容传入到initBB

		# 再将opencv格式额数据转换成ros image格式的数据发布
		try:
			self.image_pub.publish(self.bridge.cv2_to_imgmsg(cv_image, "bgr8"))
		except CvBridgeError as e:
			print(str(e))

if __name__ == '__main__':
	try:
		#初始化ros节点
		rospy.init_node("object_tracker")
		rospy.loginfo("object_tracking...")
		image_converter()
		rospy.spin()
	except KeyboardInterrupt:
		print("Shutting down object_tracker node.")
		cv2.destroyAllWindows()

唔,代码注释写的比较全(自认为),有问题大家可以留言评论,我经常逛社区,看到的一定回复。然后贴个rostopic list给大家看一下~

大家也可以自己跑一下,算是一个很简单的Demo吧~

编程的话,主要还是Python + OpenCV,然后使用了ROS的通讯机制——话题通讯、ROS的功能包——cv_bridge和usb_cam、ROS的工具——image_view,当然这个过程肯定是没有离开ROS社区的帮助,毕竟调试不出bug的代码是很罕见的。

最后吧,我还是说一句话~

ROS = 通讯机制+工具箱+功能包+社区

ROS只是一个工具,它不是核心。你的思想、你的代码才是项目的核心,一定一定一定不要本末倒置。

初入门者,切记!

后续开发的话,我打算将被追踪目标的像素坐标以话题的形式的发布,通过PID+舵机实现一下锁定追踪的效果。也考虑使用水晶弹枪做个炮台,然后使用ROS的服务通讯机制,搞搞Python的多目标锁定追踪,炮台每瞄准一个打一个,就做下服务的反馈。唔,效果的话,就像是《喜羊羊与灰太狼之羊羊运动会》第58集——绝地大反击当中的,喜羊羊操控超大机械羊灭杀狼群的那种效果(可以从6分30秒的时候开始看20秒左右即可)。很nice!

慢慢搞吧,毕竟当代大学生最不缺的就是时间!