ROS智能车(一):视觉感知
实验一 视觉感知:红绿灯与锥桶检测
实验目的:
- 掌握相机标定的基本方法,了解相机畸变的影响;
- 掌握传统图像处理的基本方法,使用 OpenCV 进行图像处理;
实验内容:
- 使用相机标定源码标定相机;
- 运用OpenCV处理锥桶和红绿灯信息;
实验仪器:
ROS智能车、雷达、红色蓝色锥桶、路由器、工控机。
实验原理:
ROS智能车上配置的是USB串口摄像头。原厂代码不包含驱动,我们需要在ROS.org上获取usb相机驱动usb_cam功能包,功能包在
1 | Header header |
通过相机获取图像信息,首先需要对相机进行标定以消除畸变,否则无法正常驱动相机。相机标定的目的是为了确定相机内部参数和外部参数,以便修正图像并将图像坐标映射到世界坐标,或者从世界坐标映射到图像坐标。这个过程是计算机视觉和三维感知任务的关键步骤之一。相机标定可以通过matlab、OpenCV、ROS三种方式进行,本实验我们采用camera_calibration功能包,通过国际棋盘计算参数,实现相机标定。
近年来,随着计算机技术不断发展和深度学习的兴起,图像处理领域也得到了广泛的关注和研究。当前主流的图像处理算法可以分为传统算法和深度学习算法两类。
传统算法主要是基于人工设计的特征提取和分类模型,包括SIFT、HOG、SURF等。这些算法在一些场景下仍然具有一定的优势,例如对于目标检测任务,传统算法通常能够更好地处理尺度变化和视角变化等问题,而且相对于深度学习算法来说,传统算法的计算速度也更快。但是,传统算法需要大量的手工调参和特征设计,而这些过程通常需要专业知识和经验,成本较高且不易复用。
深度学习算法则是基于神经网络的方法,通过大规模数据训练神经网络,实现自动特征提取和分类。AlexNet、VGG、GoogLeNet、ResNet等深度学习模型在图像处理领域具有重要的作用。与传统算法相比,深度学习算法能够更好地处理复杂的非线性特征,且无需手动设计特征,更加便于工程实现。同时,深度学习算法的泛化能力比传统算法更强,在大规模数据下表现更好。
然而,深度学习算法也存在一些问题,例如需要大量的数据和计算资源进行训练,且网络结构和参数调整较为复杂,容易出现过拟合等问题。此外,在一些场景下,深度学习算法的计算开销较大,对计算资源的要求较高。
考虑到四代i5工控机计算资源有限,且室外专项赛道元素较为单一 ,不需要对各种场景进行泛,选择传统算法可能是更合适的选择。
传统图像处理 | 深度学习网络 | |
---|---|---|
关键技术 | 信号处理方法,如直方图,灰度化,滤波,差值,采样等 | CNN,RNN,FC,Yolov5s,LSTM,etc. |
优点 | 特点场景效果较好 | 数据驱动,泛化能力好 |
缺点 | 1.泛化能力弱 2.需要人为处理图像,寻找特征 |
1.可解释性差 2.需要大量数据集训练 |
算力要求 | 低,CPU单线程处理 | 高,GPU 并行处理 |
通过降低图像中的噪声来提高图像质量。常见的方法包括中值滤波、高斯滤波和锐化滤波。
中值滤波是一种常见的非线性图像滤波方法,它使用像素点邻域内的中值来取代中心像素的灰度值,从而达到去除噪声的效果。中值滤波的公式比较简单,就是将像素点邻域内的像素值进行排序,然后取中间值作为当前像素的值。
$$
I_{\text{filtered}}(x, y) = \text{median}\left(I(x+i, y+j) ,|, i = -k,\ldots,k, j = -k,\ldots,k\right)
$$
中值滤波器对于去除椒盐噪声等概率型噪声非常有效,因为它不受噪声干扰,能够更好地保留图像的边缘信息。在OpenCV
中,可以使用cv2.medianBlur()
函数来应用中值滤波。
1 | blurred_image = cv2.medianBlur(image, ksize) |
其中关键参数如下:
image
是输入的图像ksize
是中值滤波核的大小,通常设置为一个大于1的奇数,表示邻域的大小
$$
H(i, j) = \frac{1}{2\pi\sigma^2} e^{-\frac{i^2+j^2}{2\sigma^2}}
$$
其中,i
和j
是滤波器的坐标,$\sigma$是高斯函数的标准差。在OpenCV
中,可以使用cv2.GaussianBlur()
函数来应用高斯滤波。
1 | blurred_image = cv2.GaussianBlur(image, (ksize_x, ksize_y), sigmaX, sigmaY) |
其中关键参数如下:
image
是输入的图像(ksize_x, ksize_y)
是高斯核的大小,通常设置为奇数,表示在x
方向和y
方向上的标准差sigmaX
和sigmaY
是高斯核函数在x
方向和y
方向上的标准差
锐化滤波器可以通过不同的算子来实现,其中一个常见的算子是拉普拉斯算子。其公式如下:
$$
\nabla^2 f = \frac{\partial^2 f}{\partial x^2} + \frac{\partial^2 f}{\partial y^2}
$$
在OpenCV中,可以使用 cv2.Laplacian 函数来应用拉普拉斯算子。
1 | laplacian_image = cv2.Laplacian(image, ddepth, ksize) |
其中关键参数如下:
image
是输入的图像ddepth
是输出图像的深度,通常设置为cv2.CV_8U
或-1
表示与输入图像相同的深度ksize
是拉普拉斯算子的核大小
形态学滤波是基于形态的滤波方法,基本的形态操作包含膨胀、腐蚀,以及这两者的组合运算,如形态学开运算,闭运算,黑帽运算等等。
膨胀: 膨胀是一种形态学操作,用于求取局部最大值。在膨胀过程中,我们使用一个卷积核(也称为结构元素),将其与图像进行卷积操作。对于每个卷积核覆盖区域内的像素,将选择其中的最大值作为该区域内所有像素点的输出。膨胀操作可以使边缘变得更加明显,并将对象之间的间隙填充。
$$
\text{dilateAB}\ A(x,y) = \max_{(i,j) \in B}(B(i,j) + A(x+i, y+j))
$$
其中,$A$为原始图像,$B$为卷积核(也称为结构元素),$(x,y)$为当前像素坐标,$(i,j)$为卷积核中心点到当前像素点的距离。
1 | cv2.dilate(src, kernel[, dst[, anchor[, iterations[, borderType[, borderValue]]]]]) |
其中关键参数如下:
src
:原始图像kernel
:卷积核/结构元素dst
:输出图像(可选)anchor
:锚点,指示卷积核中心位置(默认为(-1, -1))iterations
:迭代次数,表示膨胀操作的次数(默认为1)borderType
:边界扩充类型(默认为cv2.BORDER_CONSTANT
),可选的值还有cv2.BORDER_REPLICATE
等borderValue
:边界填充值(默认为0)
腐蚀: 腐蚀是一种形态学操作,用于求取局部最小值。在腐蚀过程中,我们同样使用一个卷积核与图像进行卷积操作。对于每个卷积核覆盖区域内的像素,将选择其中的最小值作为该区域内所有像素点的输出。腐蚀操作可以使边缘变得更加细化,并消除图像中的小的孔洞或细小的连通区域。
$$
\text{erodeAB}\ A(x,y) = \min_{(i,j) \in B}(B(i,j) + A(x+i, y+j))
$$
其中,$A$为原始图像,$B$为卷积核(也称为结构元素),$(x,y)$为当前像素坐标,$(i,j)$为卷积核中心点到当前像素点的距离。
1 | cv2.erode(src, kernel[, dst[, anchor[, iterations[, borderType[, borderValue]]]]]) |
其中关键参数如下:
src
:原始图像kernel
:卷积核/结构元素dst
:输出图像(可选)anchor
:锚点,指示卷积核中心位置(默认为(-1, -1))iterations
:迭代次数,表示腐蚀操作的次数(默认为1)borderType
:边界扩充类型(默认为cv2.BORDER_CONSTANT),可选的值还有cv2.BORDER_REPLICATE
等borderValue
:边界填充值(默认为0)
边缘检测是数字图像处理中常用的技术,用于识别图像中物体边缘的位置。常见的边缘检测算法包括Sobel算子、Prewitt算子、Roberts算子和Canny边缘检测算法等。其中,Sobel算子和Canny算法是应用最为广泛的两种方法。
Sobel算子:是一种基于梯度的边缘检测算子,它通过计算图像的水平和竖直方向上的梯度来识别边缘。具体来说,Sobel算子使用卷积操作来对图像进行梯度计算,从而找到像素值变化最为剧烈的位置,即边缘。Sobel算子的计算简单高效,适用于许多实际场景。在OpenCV
中,可以使用cv2.Sobel()
函数来应用Sobel边缘检测。
1 | cv2.Sobel(src, ddepth, dx, dy, ksize) |
其中关键参数如下:
src
:输入图像。ddepth
:输出图像的深度,通常设置为-1,表示输出图像与输入图像有相同的深度。dx
和dy
:分别表示在x和y方向上的导数的阶数。dx和dy都可以是0、1或者-1。ksize
:Sobel算子的孔径大小,可以取1、3、5或者7。
Canny算法:它是一种多阶段的边缘检测方法。Canny算法首先对图像进行高斯滤波以去除噪声,然后计算图像梯度的幅值和方向,接着利用非极大值抑制来细化边缘,最后通过双阈值处理来提取强边缘并抑制弱边缘,从而得到最终的边缘图像。Canny算法在实际应用中表现出较好的性能,尤其在复杂背景和噪声较多的图像中效果明显。本次试验中,我们使用 canny 计算锥桶边缘获得连接线,继而确定锥桶位置。在OpenCV
中,可以使用cv2.Canny()
函数来应用Canny边缘检测。
1 | cv2.Canny(image, threshold1, threshold2) |
其中关键参数如下:
image
:输入图像。threshold1
和threshold2
:两个阈值,用于控制边缘检测的灵敏度。通常情况下,threshold2
会被用于边缘连接,而threshold1
被用来控制强边缘的初始检测。推荐的threshold2/threshold1
比值在2:1到3:1之间。
实验步骤:
在ubuntu中查看摄像头需要安装cheese:
1 | sudo apt-get install cheese |
在终端输入cheese查看摄像头
1 | cheese |
如果能如下图显示摄像头画面则表示摄像头正常
在ROS下获取usb_cam功能包,usb_cam功能包支持源码下载和本地下载:
1 | sudo apt-get install ros-noetic-usb-cam # 本地下载 |
智能车首次使用单目相机时,需要对其进行标定,首先获取camera_calibration功能包:
1 | rosdep install camera_calibration |
将一个9X6的国际棋盘格27mmX27mm打印到A4纸上,在终端启动如下指令:
1 | rosrun camera_calibration cameracalibrator.py --size 9x6 --square 0.027 image:=/camera/image_raw camera:=/camera |
将棋盘在相机前方上、下、左、右平移、翻转棋盘格,直到条形变为绿色。当calibration按钮亮起时,代表已经有足够的数据进行摄像头的标定,此时请按下calibration并等待一分钟左右,标定界面会变成灰色,单击“save”和“commit”将标定数据保存至本地,下次启动相机时会自动加载标定数据。
标定结束后,可以通过launch文件启动usb_cam:
1 | roslaunch usb_cam usb_cam.launch |
launch文件中加载的配置文件及其含义注释如下:
1 | start_service_name: "start_capture" # 定义重新启动暂停的流媒体的std_srvs::Empty服务的名称后缀 |
此时在终端订阅话题
1 | rostopic echo <camera_name>/raw |
上文我们在ROS中获取到的图像为sensor_msgs/Image格式,我们希望使用OpenCV处理图像,ROS中提供了CvBridge库,用于连接ROS格式数据与OpenCV格式数据:
首先定义一个类,用于订阅ROS中摄像头传递回来的图像,self.bridge为CvBridge()的成员变量,用于将ROS格式信息转换为OpenCV格式信息:
1 | class image_converter: |
定义callback函数作为回调函数,首先使用self.bridge.imgmsg_to_cv2(data, “bgr8”)转换图像格式为BGR8比特图像,每FPS取出一次图像,传入self.detectLocation函数用于检测锥桶:
1 | def callback(self, data): |
接下来我们详细介绍self.detectLocation中检测锥桶的代码实现流程:
定义红色和蓝色锥桶的HSV颜色区间,通过inRange()函数分别将红、蓝锥桶从图像中分割出来并形成二值化图像,注意红色在HSV中有两个区间,使用inRange()二值化之后需要使用cv2.bitwise_or()融合两张图片。
1 | # 红色颜色区间 |
然后进行形态学图像处理,主要使用腐蚀和膨胀平滑和处理图像,以便更容易检测图像中的锥桶。先通过腐蚀后膨胀消除小的伪影,腐蚀操作会使图像中的白色区域减小,它通过内核在图像上滑动并将内核下的像素值设置为内核中的最小值,膨胀操作会使图像中的白色区域扩大,它通过内核在图像上滑动并将内核下的像素值设置为内核中的最大值。再次进行膨胀操作,此时iterations=11
表示膨胀操作将重复执行11次。这一步的目的是填补可能由于反光带而导致的橙色边缘之间的间隙,将交通锥桶还原到标准大小。最后,再次进行腐蚀操作,iterations=7
表示腐蚀操作将重复执行7次。这一步的目的是对图像进行最后的平滑处理,以确保交通锥桶的形状较为均匀和规则,以便进行后续的边缘检测。
1 | # 通过先腐蚀然后膨胀来消除小的伪影 |
然后使用Canny
边缘检测算法来检测图像中的边缘,并使用cv2.findContours
查找图像中的轮廓,并获得这些轮廓的信息。
1 | # 检测图像中的所有边缘 |
最后,使用道格拉斯-佩克算法平滑曲线,对锥桶轮廓点进行多边形拟合,判断轮廓如果是三角形,就在周围绘制一个边界框,标明识别到锥桶:
1 | for cnt in contours_red: |
检测红绿灯的代码框架与上文检测锥桶类似,主要区别为两者的callback()
函数,接下来我们详细介绍self.RedAndGreen()
函数,以下是总体设计的流程图:
首先我们先指定红绿灯设备中红色的HSV颜色阈值上下界,通过cv2.cvtColor
函数将图片从BGR空间转换至HSV空间,然后通过cv2.inRange()
函数过滤出红色部分的二值化函数
1 | # 指定红色值的范围 |
我们无法直接对二值化后的图片进行圆形检测,主要原因有两个:
- 由于红绿灯上檐会产生反光,我们需要尽可能消除这部分,否则会对圆形检测产生许多干扰
- 红绿灯圆形范围边界模糊、残缺,我们需要尽可能填满这部分,让其形成一个便于检测的圆形
因此我们对二值化后的图像先膨胀,再腐蚀,然后再膨胀:
- 先膨胀:补充完整红绿灯圆形边界
- 再腐蚀:去除上檐的反光部分
- 最后膨胀:对红绿灯边界进行平滑处理,以确保红绿灯边界的形状较为均匀和规则
1 | # 膨胀 |
实现的效果图分别如下,可见可以得到一个比较好的圆形,因此后续的圆形检测能够实现比较好的效果:
然后我们对处理完毕之后的图片使用cv2.HoughCircles()
函数进行圆形检测,设定圆心距20,canny阈值50,投票数20,最小半径1,最大半径400
1 | # 使用HoughCircles函数检测圆形 |
由于可能会检测到多个圆形,我们检测到圆形后只画出最大的圆:
1 | if circles is not None: |
在检测到圆形的基础上,检测红色二值化后的圆形边界,如果检测到圆形边界的$宽\times 高>150\times 150$,则判断检测到了红灯并且框选出红灯。
1 | # 查找轮廓 |
参考资料:
- wiki.ros.org/usb_cam
- ROS下采用camera_calibration进行单目相机标定_rosrun camera_calibration cameracalibrator.py
- cv_bridge/Tutorials - ROS Wiki
- camera_calibration - ROS Wiki
实验总结:
本次实验旨在通过相机标定和图像处理技术,实现对锥桶和红绿灯信息的准确提取。采用了相机标定源码和OpenCV等工具库,我们成功地完成了这一目标,取得了一系列令人满意的成果。
首先,相机标定是我们实验的重要一环。通过使用先进的相机标定源码,我们准确地调整了相机的内外参数,确保了图像的准确性和可靠性。这个步骤为后续的图像处理提供了可靠的基础,使得我们的算法能够更好地映射到实际场景。在锥桶信息的处理过程中,我们应用了中值滤波和形态学滤波。中值滤波的使用帮助我们有效地去除了图像中的噪声,提高了图像质量。形态学滤波则在处理锥桶的形态和数量时发挥了关键作用,使得我们能够精准地识别和定位锥桶。红绿灯信息的提取则涉及到边缘检测算法的应用。通过合理选择算法参数,我们成功地捕捉到交通信号的位置,确保了对交通灯状态的准确判断。这一步骤对于交通场景的智能识别具有重要的实际意义。
整个实验过程中,我们团队注重技术细节的处理,追求精益求精。通过灵活运用图像处理算法,我们建立了高效、稳健的处理流程。对工具库的熟练应用使我们能够在实际场景中更加灵活地解决问题。在未来的工作中,我们将继续优化算法,提高处理的速度和准确度。同时,我们也将探索更多的图像处理技术,以适应不同场景和环境的需求。
总体来说,这次实验为智能车的运行提供了有效的数据决策,也为我们在今后的学习生涯里学习图像处理技术奠定了坚实的基础。