机器视觉处理之Opencv实现简单的目标跟踪
1 资料
本文是机器视觉处理系列的第三篇,我们将使用Opencv实现简单的目标跟踪。上一篇文章ROS高效进阶第四章 – 机器视觉处理之基于haaz特征的人脸检测,并控制机器人移动 我们实现了人脸检测,其属于单帧操作。本文讨论的目标跟踪,涉及帧间操作,即针对连续视频画面中单个或多个目标进行跟踪,跟踪的结果是关联视频前后帧中的同一物体(目标),并赋予唯一TrackID。本文依然基于本系列第一篇 ROS高效进阶第四章 – 机器视觉处理之图像格式,usb_cam,摄像头标定,opencv和cv_bridge引入 的 robot_vision 进行扩充。
本文参考资料如下:
(1)《ROS机器人开发实践》胡春旭 第7章的第四节
(2)opencv学习小结(1)–高斯模糊与卷积核、模糊半径
(3)高斯模糊的算法
(4)一文搞懂图像二值化算法
(5)图像的腐蚀与膨胀
(6)基于深度学习的目标跟踪
本系列博客汇总:ROS高效进阶系列。
2 正文
2.1 目标跟踪涉及的图像处理知识
(1)高斯模糊:微信打电话时,提供了背景模糊功能,相当于加了一层“模糊”滤镜。针对灰度图像,最简单的模糊算法,是把一个中心像素周围的八个像素的灰度值求平均,作为中心像素的灰度值。
但这种方式太简单了,效果也不好,因为图像都是连续的,越靠近的点关系越密切,越远离的点关系越疏远。因此,加权平均更合理,距离越近的点权重越大,距离越远的点权重越小。
高斯模糊就是加权平均,其将高斯分布(又名"正太分布")用于图像处理。通过创建一个卷积核,核内每个像素内是符合二维高斯分布的权重值,通过与原图像进行卷积,每次得到中心像素的灰度值。
高斯模糊能有效降低图像的详细信息,只保留其主要特征,在接下来的物体检测或边缘检测中,能够得到更好的结果。
更具体的细节,包括效果图,这里推荐两个博客:
opencv学习小结(1)–高斯模糊与卷积核、模糊半径
高斯模糊的算法
(2)图像二值化:简单理解二值图就是图片里只有黑或白,不存在中间灰度。一般称白色为前景色,黑色为背景色。
二值图像经常被用作图像处理和计算机视觉中的中间步骤,因为它们可以大大简化对图像的分析和解释。例如,在边缘检测、形态学操作、轮廓检测、目标跟踪等任务中,都会使用到二值图像。
具体细节,这里推荐一篇博客:一文搞懂图像二值化算法
(3)图像膨胀:一般图像二值化后,会进行图像膨胀或腐蚀。所谓膨胀或腐蚀,都是针对图像前景物的操作,膨胀是增大前景物,腐蚀是缩小前景物。
膨胀操作的过程实际上是将图像中的每一个像素点替换为其周围邻域内的最大值,腐蚀是取最小值。
具体细节,这里推荐一篇博客:图像的腐蚀与膨胀
2.2 motion_detector 样例
(1)在 robot_vision 中创建motion_detector
cd ~/catkin_ws/src/robot_vision
mkdir scripts
touch scripts/motion_detector.py launch/motion_detector.launch
(2)motion_detector 解析
图像处理流程:
motion_detector.py
#! /usr/bin/env python3
# -*- coding: utf-8 -*-
import rospy
import cv2
import numpy as np
from functools import partial
from cv_bridge import CvBridge, CvBridgeError
from sensor_msgs.msg import Image, RegionOfInterest
class DetectorParam:
def __init__(self):
self.minArea = rospy.get_param("~minArea", 500)
self.threshold = rospy.get_param("~threshold", 25)
self.firstFrame = None
self.text = "Unoccupied"
def image_cb(msg, cv_bridge, detector_param, image_pub):
# 使用cv_bridge将ROS的图像数据转换成OpenCV的图像格式
try:
cv_image = cv_bridge.imgmsg_to_cv2(msg, "bgr8")
# 将Opencv图像转换numpy数组形式,数据类型是uint8(0~255)
# numpy提供了大量的操作数组的函数,可以方便高效地进行图像处理
frame = np.array(cv_image, dtype=np.uint8)
except (CvBridgeError, e):
print(e)
# 创建灰度图像
gray_image = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)
cv2.imshow("ycao: gray image", gray_image)
cv2.waitKey(3)
# 将灰度图高斯模糊化,去除具体细节,高斯核是21*21,标准方差为0
gray_image = cv2.GaussianBlur(gray_image, (21,21), 0)
cv2.imshow("ycao: Gaussian blur image", gray_image)
cv2.waitKey(3)
# 使用两帧图像做比较,检测移动物体的区域
# firstFrame 用于存储第一帧,后面的图像都与第一帧进行比对
if detector_param.firstFrame is None:
detector_param.firstFrame = gray_image
return
# cv2.absdiff()用于计算两个数组(在这里是两帧图像)之间的绝对差值,
# absdiff返回值也是一个NumPy数组,每个元素表示像素点在这两个图像之间的亮度差值,从0~255
frameDelta = cv2.absdiff(detector_param.firstFrame, gray_image)
# 图像二值化,灰度差大于detector_param.threshold(25),则为白色(前景),否则为黑色(背景)
# cv2.threshold()函数返回的是一个元组(tuple),第一个元素为255,第二个才是二值化的图像数据
threshold = cv2.threshold(frameDelta, detector_param.threshold, 255, cv2.THRESH_BINARY)[1]
cv2.imshow("ycao: THRESH BINARY image", threshold)
cv2.waitKey(3)
# 进行两次图像膨胀
threshold = cv2.dilate(threshold, None, iterations=2)
cv2.imshow("ycao: dilate image", threshold)
cv2.waitKey(3)
# 寻找轮廓,cv2.RETR_EXTERNAL表示只寻找最外层的轮廓。
# cv2.CHAIN_APPROX_SIMPLE表示只保存轮廓的端点信息
# cnts是找到的轮廓,hierarchy描述了这些轮廓之间的嵌套关系,我们分析前者就行
cnts, hierarchy = cv2.findContours(threshold.copy(), cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
for c in cnts:
# cv2.contourArea用于计算轮廓面积,太小的轮廓,直接忽略
if cv2.contourArea(c) < detector_param.minArea:
continue
# 在输出画面上框出识别到的物体
# boundingRect是计算每个轮廓的外接矩形
(x,y,w,h) = cv2.boundingRect(c)
# 给轮廓画边框,并标识Occupied
cv2.rectangle(frame, (x,y), (x+w, y+h), (50,255,50), 2)
detector_param.text = "Occupied"
# 在输出画面上打当前状态和时间戳信息
# 在图像的(10,20)位置上,用红色的字体显示状态信息,0.5号字体,线条宽度为2
cv2.putText(frame, "Status: {}".format(detector_param.text), (10,20), cv2.FONT_HERSHEY_SIMPLEX, 0.5, (0,0,255), 2)
# 将识别后的图像转换成ROS消息并发布
image_pub.publish(cv_bridge.cv2_to_imgmsg(frame, "bgr8"))
def main():
rospy.init_node("motion_detector")
rospy.loginfo("starting motion_detector node")
bridge = CvBridge()
detector_param = DetectorParam()
image_pub = rospy.Publisher("/cv_bridge_image", Image, queue_size=1)
bind_image_cb = partial(image_cb, cv_bridge=bridge, detector_param=detector_param, image_pub=image_pub)
rospy.Subscriber("/usb_cam/image_raw", Image, bind_image_cb)
rospy.spin()
cv2.destroyAllWindows()
if __name__ == "__main__":
main()
motion_detector.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 pkg="robot_vision" name="motion_detector" type="motion_detector.py" output="screen">
<rosparam>
minArea: 500
threshold: 20
</rosparam>
</node>
<node
pkg="rqt_image_view"
type="rqt_image_view"
name="rqt_image_view"
output="screen"
/>
</launch>
(3)编译并运行
cd ~/catkin_ws/
catkin_make --source src/robot_vision
source devel/setup.bash
roslaunch robot_vision motion_detector.launch
3 总结
本文使用Opencv实现了简单的目标跟踪,由于这个算法太简单了,连物体TrackID都给不了,对于复杂的场景,识别度也很差,测试时请找一个背景比较干净的环境,必要时调整那两个参数。更优秀的目标跟踪算法,这里推荐一篇综述文章,写的很好:基于深度学习的目标跟踪
本文的样例托管在本人的github上:robot_vision