提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档
最近在做毕设,内容是交通信号灯和交通标识识别,并作出相应的移动。需要用到ROS以及Opencv的一些相关知识,用CSDN也两三年了,但是没有自己发过帖子,于是突发奇想开个帖子分享一下学习进度,记录一下学习过程有利最后的论文写作的同时,遇到问题也可以向大佬们寻求一下帮助。也欢迎朋友在评论区进行一些讨论,毕竟相互学习才能进步更快。
我也是第一次用ROS做开发,在虚拟机上用Ubuntu20.04学习的.作为一个编程基础差的,在ros入门阶段就遇到很多问题。目前对于ROS的了解也不多,只是自学了一下官网的第一章基本教程和在B站看了古月居的ROS21讲,了解了一下基本的一些ROS通讯原理,自己执行了一下官网的python节点和话题定义,服务和客户定义。中文ROS官网的链接是这个ros中文入门教程。
下面的学习笔记全是基于古月居的机器人视觉入门的教程来进行展开的。
首先就是安装Opencv库到ros中,只需要在终端中输入以下命令,将其中的noetic换成自己所用ros的版本名就可以安装函数库了。
sudo apt-get install ros-noetic-vision-opencv libopencv-dev python3-opencv
这里我用的是古月的机器人视觉入门的教程,github的链接在这里古月居机器人视觉入门
下面是百度百科对于HSV颜色通道的介绍:HSV颜色通道。
我对于它的理解是一个更加符合人眼对颜色描述的通道,不同于RGB或则其他通道,HSV通道是直接面向用户,能更好的直接描述颜色,比如什么颜色,饱和度是多少,亮度是多少。HSV通道也有三个值,分别为色调(H)、饱和度(S)和明度(V)。
色调(H)使用的HSV锥形模型进行定义,取值范围是0°~360°。开始0°为红色,逆时针旋转计算,绿色为120°,蓝色为240°。
饱和度(S)取值范围为0-100,值越大,颜色饱和度越高。
亮度(V)取值范围也是0-100,值越大,颜色越明亮。
首先安装ROS的usb驱动,在终端输入以下命令
sudo apt-get install ros-noetic-usb-cam
显示安装成功就可以运行usb摄像头驱动了。因为我目前没有拿到实物的小车,所以摄像头用的是笔记本自带的。这里运行一下自带的测试文件。
roslaunch usb_cam usb_cam-test.launch
运行后就可以看到摄像头被打开,框里的图像就是目前ros发布的图像话题内的内容。
然后就可以用ros中的小工具Image_view(在一个新的终端输入rqt_image_view)来做进行话题的订阅和显示。
可以看到有五个不同的话题,针对不同的需求可以选择订阅。到此ros摄像头驱动安装成功,其他节点可以通过订阅该节点来获取摄像头的实时数据。
下一步要学习的就是怎么获得目标物体的HSV值,这里用的目标物的图片直接是教程里给到的一张图,我们的目的是提取图中红色部分的HSV值。这样在后面进行目标物体阈值设置的时候就可以将所有含有目标物体颜色像素点提取出来。这里提取目标HSV值的方法的是用一个叫做hsv_test.py的脚本,代码如下:
#!/usr/bin/env python3
# -*- coding: UTF-8 -*-
import cv2
img = cv2.imread('image.png')
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)# 转化为灰度通道
hsv = cv2.cvtColor(img, cv2.COLOR_BGR2HSV)# 转化为HSV通道
def mouse_click(event, x, y, flags, para):
if event == cv2.EVENT_LBUTTONDOWN: # 左边鼠标点击,可以根据自己的需求来选择想要的通道值
#print('PIX:', x, y)
#print("BGR:", img[y, x])
#print("GRAY:", gray[y, x])
print("HSV:", hsv[y, x])
if __name__ == '__main__':
cv2.namedWindow("img")# 创建一个名为img的图片窗口
cv2.setMouseCallback("img", mouse_click)
while True:
cv2.imshow('img', img)
if cv2.waitKey() == ord('q'):
break
cv2.destroyAllWindows()
使用以下命令行即可将limo_visions/scripts目录下的图像读取出来。
roscd limo_visions/scripts/&& python hsv_test.py
这个代码运行不了可以试试将python改成python3. 运行后的效果如下
可以看到通过点击图片窗口可以得到任意像素点的HSV通道值。可以看到,虽然肉眼看到的颜色差不多,但是HSV值还是存在一定的差异,所以在后面的代码中设置阈值是会设置一个范围而不是单纯的设置一个值。
现在得到目标的颜色阈值以后就可以代码来实现目标的检测并返回目标物的位置信息了。整体的思路是根据目标物体的颜色设置一个HSV的阈值范围。第一步通过高斯滤波将图片平滑化,在转化到HSV通道,这时将阈值范围内的像素点保留,将阈值范围外的像素点去除。然后就是找到所有保留下的像素点的轮框。这时还会存在除主要目标物轮框外的其他轮框,可以选择将较小的方框剔除,保留最大的,就是我们需要目标物体的轮框。
这里使用到关于ROS的内容是,这个节点会发布一个名叫object_detect_image的话题,话题的内容有两个,self.image_pub 和self.target_pub,分别代表着检测到目标物后的图像和检测到的目标物的位置信息。通过该节点还会订阅一个话题/camera/rgb/image_raw,就是上面提到的摄像头发布的话题数据。需要注意的是,从摄像头直接得到的话题数据并不能直接用来进行Opencv的一些操作,需要用到一个cv_bridge的功能包进行转化。所以需要在订阅到图片时和完成图像处理后分别进行两次转换。
具体的代码如下所示:
`#!/usr/bin/env python
# -*- coding: utf-8 -*-
#
# Copyright (c) 2021 PS-Micro, Co. Ltd.
#
# SPDX-License-Identifier: Apache-2.0
#
import time
import rospy
import cv2
from cv_bridge import CvBridge, CvBridgeError
from sensor_msgs.msg import Image
import numpy as np
from math import *
from geometry_msgs.msg import Pose
HUE_LOW = 170
HUE_HIGH = 180
SATURATION_LOW = 170
SATURATION_HIGH = 190
VALUE_LOW = 70
VALUE_HIGH = 100
class ImageConverter:
def __init__(self):
# 创建图像缓存相关的变量
self.cv_image = None
self.get_image = False
# 创建cv_bridge
self.bridge = CvBridge()
# 声明图像的发布者和订阅者
self.image_pub = rospy.Publisher("object_detect_image",
Image,
queue_size=1)
# 发布一个话题,话题类型为Image,就是检测到的目标图像
self.target_pub = rospy.Publisher("object_detect_pose",
Pose,
queue_size=1)
# 发布一个话题,话题类型为Pose,就是目标物体的位姿
self.image_sub = rospy.Subscriber("/camera/rgb/image_raw",
Image,
self.callback,
queue_size=1)
# 订阅camera来的图像并执行callback函数
def callback(self, data):
# 判断当前图像是否处理完
if not self.get_image:
# 使用cv_bridge将ROS的图像数据转换成OpenCV的图像格式
try:
self.cv_image = self.bridge.imgmsg_to_cv2(data, "bgr8")
except CvBridgeError as e:
print e
# 设置标志,表示收到图像
self.get_image = True
def detect_object(self):
# 创建HSV阈值列表
boundaries = [([HUE_LOW, SATURATION_LOW,
VALUE_LOW], [HUE_HIGH, SATURATION_HIGH, VALUE_HIGH])]
# 遍历HSV阈值列表
for (lower, upper) in boundaries:
# 创建HSV上下限位的阈值数组
lower = np.array(lower, dtype="uint8")
upper = np.array(upper, dtype="uint8")
# 高斯滤波,对图像邻域内像素进行平滑
hsv_image = cv2.GaussianBlur(self.cv_image, (5, 5), 0)
# 颜色空间转换,将RGB图像转换成HSV图像
hsv_image = cv2.cvtColor(hsv_image, cv2.COLOR_BGR2HSV)
# 根据阈值,去除背景
mask = cv2.inRange(hsv_image, lower, upper)
output = cv2.bitwise_and(self.cv_image, self.cv_image, mask=mask)
# 将彩色图像转换成灰度图像
cvImg = cv2.cvtColor(output, 6) # cv2.COLOR_BGR2GRAY
npImg = np.asarray(cvImg)
thresh = cv2.threshold(npImg, 1, 255, cv2.THRESH_BINARY)[1]
# 检测目标物体的轮廓
cnts, hierarchy = cv2.findContours(thresh, cv2.RETR_LIST,
cv2.CHAIN_APPROX_NONE)
# 遍历找到的所有轮廓线
for c in cnts:
# 去除一些面积太小的噪声
if c.shape[0] < 150:
continue
# 提取轮廓的特征
M = cv2.moments(c)
if int(M["m00"]) not in range(500, 22500):
continue
cX = int(M["m10"] / M["m00"])
cY = int(M["m01"] / M["m00"])
print("x: {}, y: {}, size: {}".format(cX, cY, M["m00"]))
# 把轮廓描绘出来,并绘制中心点
cv2.drawContours(self.cv_image, [c], -1, (0, 0, 255), 2)
cv2.circle(self.cv_image, (cX, cY), 1, (0, 0, 255), -1)
# 将目标位置通过话题发布
objPose = Pose()
objPose.position.x = cX
objPose.position.y = cY
objPose.position.z = M["m00"]
self.target_pub.publish(objPose)
# 再将opencv格式额数据转换成ros image格式的数据发布
try:
self.image_pub.publish(
self.bridge.cv2_to_imgmsg(self.cv_image, "bgr8"))
except CvBridgeError as e:
print e
def loop(self):
if self.get_image:
self.detect_object()
self.get_image = False
if __name__ == '__main__':
try:
# 初始化ros节点
rospy.init_node("object_detect")
rospy.loginfo("Starting detect object")
image_converter = ImageConverter()
rate = rospy.Rate(100)
while not rospy.is_shutdown():
image_converter.loop()
rate.sleep()
except KeyboardInterrupt:
print "Shutting down object_detect node."
cv2.destroyAllWindows()`
接下来就是根据上个节点得到的目标物的位置信息让小车做出相应的运动完成对目标物体的跟踪。整体的思路比较好理解,通过订阅上个节点发来的目标位置信息X和Z,给出x和z方向的速度(x代表直线方向速度,z代表垂直方向速度即控制左右转向),使得小车完成避障操作。首先是根据目标物体轮框大小Z来判断距离目标的距离,过大时说明距离太近,这是小车停止。当Z介于一个固定区间时,根据距离大小定义x方向的速度。然后就是判断目标是在左边还是右边,根据X的信息,可以判断目标是偏左还是偏右,然后做出对应的速度就可以了。
目标跟踪的代码如下:
!/usr/bin/env python
# -*- coding: utf-8 -*-
import rospy
import cv2
from cv_bridge import CvBridge, CvBridgeError
from sensor_msgs.msg import Image
import numpy as np
from math import *
from geometry_msgs.msg import Twist
from geometry_msgs.msg import Pose
objPose = Pose()
objPose.position.x = 0
objPose.position.y = 0
objPose.position.z = 0
#初始化Pose
vel = Twist()
vel.linear.x = 0.0
vel.linear.y = 0.0
vel.linear.z = 0.0
vel.angular.x = 0.0
vel.angular.y = 0.0
vel.angular.z = 0.0
#初始化Twist
class follow_object:
def __init__(self):
#订阅位姿信息
self.Pose_sub = rospy.Subscriber("object_detect_pose", Pose, self.poseCallback)
#发布速度指令
self.vel_pub = rospy.Publisher('cmd_vel', Twist, queue_size=5)
def poseCallback(self,Pose):
X = Pose.position.x;
Y = Pose.position.y;
Z = Pose.position.z;
if Z >=14500 and Z <= 15500 :
vel = Twist()
elif Z < 14500 :
vel = Twist()
vel.linear.x = (1.0 - Z/14000) * 0.8
vel = Twist()
vel.linear.x = (1.0 - Z/15000) * 0.8
else:
print("No Z,cannot control!")
self.vel_pub.publish(vel)
rospy.loginfo(
"Publsh velocity command[{} m/s, {} rad/s]".format(
vel.linear.x, vel.angular.z))
if X > 310 and X < 330 :
vel = Twist()
elif X < 310 :
vel = Twist()
vel.angular.z = (1.0 - X/320)
elif X > 330 :
vel = Twist()
vel.angular.z = (1.0 - X/320)
else:
print("No X,cannot control!")
self.vel_pub.publish(vel)
rospy.loginfo(
"Publsh velocity command[{} m/s, {} rad/s]".format(
vel.linear.x, vel.angular.z))
if __name__ == '__main__':
try:
# 初始化ros节点
rospy.init_node("follow_object")
rospy.loginfo("Starting follow object")
follow_object()
rospy.spin()
except KeyboardInterrupt:
print "Shutting down follow_object node."
cv2.destroyAllWindows()
这就是一个简单的ROS小车避障的实例,整体是利用了ROS的话题发布与订阅和OpenCV的一些函数调用。因为也是刚开始做ROS,对于代码的写法方面还是有很多没有完全理解,只能先比葫芦画瓢着来,相信把这个搞完全明白不管是对ROS开发,还是对简单的机器视觉有一个更深的了解。同时也感谢网上提供各种资源的平台,对于新手来说遇到基本都可以网上找到解决办法。
第一次发文章,相信文章中有很多问题或则解释不清楚的地方,希望大家看到问题或则对我的描述有异议的,积极在评论区讨论。我看到后一定第一时间进行订正和完善。