注意
本项目创建时间:2024-7-17
本文标记为OpenCV入门参考项目以及电赛典型范例,由题主长期维护 —2024-10-2
一、图像处理方案
Opencv: 开源计算机视觉处理库,使用嵌入式SOC板卡作为摄像头模块,提供相对可靠的性能。
由于激光点的亮度远高于其他位置,所以采用HSV格式的色彩空间,对图像H,S,V域进行识别。
若要区分红绿激光,那么就对图像的RGB空间进行处理
2. 电工胶带的识别
-
使用原图像->灰度图像->设置阈值后进行二值化的方法,使得黑胶带区域识别为纯白色,其余部分全部为纯黑色区域。关于调参:建议使用创建滑块窗口的方法,进行动态调参,减少调参上浪费的时间。
-
摄像头容易受到外接环境光的干扰,可以通过调节分辨率,设置摄像头的图像获取范围,也有一种更为实用的方法:ROI(感兴趣区),可以通过创建掩膜的方式,只对ROI区域进行处理,这样就排除了外界环境的干扰。当然也可以使用切片的方法,定义一个变量用于读取图像的指定范围,然后再进行图像处理,需要显示参数时,再将一些特征点、辅助线、坐标点、数据显示在想要观看的图像上,使用opencv中的imshow进行可视化显示(注意:imshow是非常耗时的操作,在调试时可以使用,在最终运行时建议注释掉,提升程序执行效率,或者采取图像编解码的方法,使用TCP/UDP协议,ROS通信等方式进行远程图传,降低软件资源的使用)
-
ROI区域也不一定是适宜的图像处理区域,也可能有一些噪声信号的干扰,这可能就要需要用到一些滤波处理的算法,如:高斯平滑滤波(高斯模糊)。
-
胶带容易受激光点的影响,导致二值化图像被“分割”,所以需要对胶带的二值化图像先膨胀,后进行图形学闭运算缝合胶带二值化图像漏洞
3. 串口通信的关键问题
- 数据帧:一开始采用字符串发送地方法。结果单片机很容易接收不到一些数据帧
- 通信速率:19200
3. 激光“巡线”的路径规划
-
第一种方法:程序不断对图像进行处理,边缘检测,封闭图形检测,然后激光沿着检测出来的图形边沿进行巡线
-
第二种方法:采用“路径规划的方法”,进行一定时间的图像采集后,确定好激光的运行路径,这样,即避免了激光对矩形框识别的强光干扰,又大大减少了程序运行的资源损耗,只需要执行激光跟随部分的程序,不需要再对图像进行边沿检测。
二、程序设计
视觉部分
1. 摄像头处理部分
1.1 摄像头的类封装
GuideLine 的作用:绘制辅助线
思考:Python中函数对形参的调用,会不会影响到实参?
# 摄像头类创建class pi_Camera(): # 类的初始化 def __init__(self): # 图像初始化配置 self.Video = cv2.VideoCapture(8, cv2.CAP_V4L2)# 使能摄像头8的驱动
# 检查摄像头是否打开 ret = self.Video.isOpened() if ret: print("The video is opened.") else: print("No video.")
codec = cv2.VideoWriter_fourcc('M', 'J', 'P', 'G') self.Video.set(cv2.CAP_PROP_FOURCC, codec) self.Video.set(cv2.CAP_PROP_FPS, 60) # 帧数 self.Video.set(cv2.CAP_PROP_FRAME_WIDTH, 640) # 列 宽度 self.Video.set(cv2.CAP_PROP_FRAME_HEIGHT, 480) # 行 高度
# 绘制辅助线 def GuideLine(self, c1, c2): ret, image = self.Video.read()# 注意:read返回一个bull值和图像数据list!,需要用两个变量获取 if ret: cv2.line(image, (0, 360), (640, 360), color=(0, 0, 255), thickness=3)# 红色的线 cv2.line(image, (0, 240), (640, 240), color=(0, 0, 255), thickness=3)# 红色的线 cv2.line(image, (int(c1), 360), (int(c2), 240), color=(0, 255, 0), thickness=2)# 绘出倾角线 cv2.imshow("GuideLine", image)1.2 摄像头的图像获取
ret, frame = Watch.Video.read() #获取摄像头图像数据1.3 相机资源的释放
思考:Python中不对窗口,或者使用Python语言的硬件资源不释放会怎么样?
Watch.Video.release()2. 激光追踪相关
要想实现激光点的巡线,以及红绿激光的相互跟随,首先要做到单个激光点的精确识别与控制移动,这里我们使用了二维舵机云台,精度在能够完成基本要求之内。使用USB 1080P摄像头进行画面捕获。
激光点在图像中的亮度远远大于其他图像像素点,不难想到在HSV空间中对图像进行操作更容易识别到激光点。
2.1 激光点识别的图形预处理
通过调用v2.cvtColor()函数的方法,将图像颜色空间转换到 HSV中,然后通过设定阈值,进行图像的二值化(阈值调节建议采用滑块创建的方法,并且函数需要赋初值)
# 寻找激光点def detect_lasers(image): # imgae:用于处理的画布图像,frame:原图像,且是最终数据展示的图像 global load_process# 声明使用外部定义的 load_process
hsv = cv2.cvtColor(image, cv2.COLOR_BGR2HSV)# 转换颜色空间为HSV
# 定义红色激光点hsv色彩阈值范围,用作二值化 lower_laser = np.array([0, 100, 180]) upper_laser = np.array([179, 255, 255])
# 创建二值化图像 mask_laser = cv2.inRange(hsv, lower_laser, upper_laser) # cv2.imshow("mask_laser1", mask_laser)
# 闭运算 kernel = np.ones((5, 5), np.uint8) mask_laser = cv2.morphologyEx(mask_laser, cv2.MORPH_CLOSE, kernel)2. 2 激光点的轮廓识别
调用cv2.findContours()函数,对封闭图形进行检测,并且绘制出激光点的图形识别轮廓
函数解释:
mask_laser:这是输入的二值图像,也就是说,它应该是经过阈值处理或其他形式的图像处理后得到的黑白图像(通常是单通道的,只有 0 和 255 两个像素值)。cv2.RETR_EXTERNAL:这是轮廓检索模式(Contour Retrieval Mode),指定了轮廓的检索模式。RETR_EXTERNAL表示只检索最外层的轮廓,忽略内部的轮廓。还有其他的检索模式,比如RETR_LIST、RETR_TREE等,可以根据需要选择不同的模式。cv2.CHAIN_APPROX_SIMPLE:这是轮廓的逼近方法(Contour Approximation Method),指定了轮廓的表示方法。CHAIN_APPROX_SIMPLE表示压缩水平、垂直和对- 第一个返回值
contours_laser是一个列表,其中每个元素是一个 numpy 数组,表示一个检测到的轮廓
cv2.minAreaRect()
函数解释:
contour:这是一个轮廓,通常是通过findContours函数找到的其中一个轮廓对象,它是一个包含轮廓点的 numpy 数组。cv2.minAreaRect(contour):这个函数接收一个轮廓作为输入,并返回一个RotatedRect对象,它表示包围轮廓的最小旋转矩形框。
具体来说,返回的 RotatedRect 对象包含以下信息:
center:矩形框的中心点坐标(cx, cy)。size:矩形框的宽度和高度(width, height)。angle:矩形框相对于水平方向的旋转角度(逆时针为正)
获取矩形框角点:
box = cv2.boxPoints(rect):cv2.boxPoints函数接收一个RotatedRect对象作为输入,并返回该旋转矩形框的四个顶点坐标。- 这些顶点坐标是浮点型数据,表示为一个包含四个点的 numpy 数组。
box = np.int0(box):- 这一步将上一步得到的四个顶点坐标
box转换为整数类型的 numpy 数组。 np.int0()函数会将浮点数数组四舍五入为最接近的整数,并返回一个整数类型的 numpy 数组。
- 这一步将上一步得到的四个顶点坐标
# 寻找轮廓contours_laser, _ = cv2.findContours(mask_laser, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)laser_coords = Nonefor contour in contours_laser: rect = cv2.minAreaRect(contour)# 获取最小矩形框 laser_coords = tuple(map(int, rect[0]))# 矩形框的中心坐标 box = cv2.boxPoints(rect)# 矩形框的四个角点 box = np.int0(box) cv2.drawContours(frame, [box], 0, (0, 0, 0), 2)# 绘制矩形框(原图像中)2.3激光点的颜色区分
imge.shape[]
image.shape:这是一个 numpy 数组属性,用于获取图像的尺寸和通道数信息。image.shape[:2]:这是对shape属性进行切片操作,[:2]表示取前两个元素,即图像的高度和宽度。
具体来说,如果 image 是一个图像的 numpy 数组,例如形状为 (height, width, channels),那么 image.shape[:2] 将返回一个包含图像高度和宽度的元组 (height, width)。
这种操作通常用于获取图像的空间尺寸信息,以便在处理图像时进行尺寸相关的计算或操作
方圆坐标确定:
- 获取输入坐标:
x, y = coords:这行代码将变量coords解构为x和y,分别表示中心点的横纵坐标。
- 确定左上角坐标:
x_start = max(0, x - radius):计算方形区域的左上角横坐标。x - radius表示以中心点x为基础,向左偏移半径radius的距离。max(0, ...)确保左上角横坐标不会小于 0,即不会超出图像左边界。y_start = max(0, y - radius):计算方形区域的左上角纵坐标。同理,y - radius表示以中心点y为基础,向上偏移半径radius的距离。max(0, ...)确保左上角纵坐标不会小于 0,即不会超出图像上边界。
- 确定右下角坐标:
x_end = min(width - 1, x + radius):计算方形区域的右下角横坐标。x + radius表示以中心点x为基础,向右偏移半径radius的距离。min(width - 1, ...)确保右下角横坐标不会超过图像的右边界。y_end = min(height - 1, y + radius):计算方形区域的右下角纵坐标。同理,y + radius表示以中心点y为基础,向下偏移半径radius的距离。min(height - 1, ...)确保右下角纵坐标不会超过图像的下边界。
get_pixel_sum
这段代码用于从给定的区域中提取红色(R)和绿色(G)颜色通道的值。让我来解释一下这段代码的含义:
roi[:, :, 2]:roi应该是图像的一个区域,即感兴趣的区域(Region of Interest,ROI)。:是一个表示所有行或所有列的切片操作。[ : , : , 2]表示选取所有行的所有列的第三个颜色通道。在 BGR 颜色空间中,第三个通道对应的是红色通道,所以这行代码获取了 ROI 中所有的红色通道值。
roi[:, :, 1]:- 与上面的代码类似,这行代码选取了所有行的所有列的第二个颜色通道。在 BGR 颜色空间中,第二个通道对应的是绿色通道,所以这行代码获取了 ROI 中所有的绿色通道值。
在大多数图像处理库中(如 OpenCV),图像默认是以 BGR 顺序存储的,而不是 RGB。这意味着在获取颜色通道时需要使用 [ : , : , 2] 来获取红色通道,[ : , : , 1] 来获取绿色通道,[ : , : , 0] 来获取蓝色通道。
# 激光点RGB值获取def get_pixel_sum(image, coords): # 获取图像宽度和高度 height, width = image.shape[:2] radius = 3
# 确定方圆的左上角和右下角坐标 x, y = coords x_start = max(0, x - radius) y_start = max(0, y - radius) x_end = min(width - 1, x + radius) y_end = min(height - 1, y + radius)
# 提取方圆区域 roi = image[y_start:y_end, x_start:x_end]
# 计算 R 和 G 通道总值 r_channel = roi[:, :, 2] g_channel = roi[:, :, 1] r_sum = int(r_channel.sum()) g_sum = int(g_channel.sum()) return r_sum, g_sumcv2.circle(frame, laser_coords, 4, (0, 0, 255), -1)
绘制实心圆
cv2.putText()
- 参数解释:
frame:这是目标图像帧,即要在其上添加文本的图像。"RED":要写入的文本内容,这里是字符串 “RED”。(laser_coords[0] - 10, laser_coords[1] - 10):文本放置的起始位置,由laser_coords提供,向左上方偏移了(10, 10)的像素值。cv2.FONT_HERSHEY_SIMPLEX:字体类型,这里使用了简单的字体类型。0.5:字体大小因子,控制文本大小相对于基础字体的比例。(0, 0, 255):文本的颜色,这里是红色,对应 BGR 颜色空间中的(0, 0, 255)。2:文本的线宽,即文本轮廓的粗细。
- 作用说明:
- 这行代码的主要作用是在图像帧的指定位置绘制红色的文本 “RED”。通常用于在图像或视频中标记特定的对象、位置或状态信息。
- 注意事项:
- 如果
laser_coords提供的坐标(x, y)位于图像的边缘,确保(x - 10, y - 10)不会超出图像边界,以避免出现越界错误。
- 如果
# 是否获取到激光点矩形框的中心坐标,防止读取值为空if laser_coords is not None: # 获取激光点的RGB数据 color_vel = get_pixel_sum(image, laser_coords)
# 在红色激光点中心坐标出绘制圆点图像(原图像中) cv2.circle(frame, laser_coords, 4, (0, 0, 255), -1) # 打印文本数据“RED”在红色激光点上 cv2.putText(frame ,"RED", (laser_coords[0] - 10, laser_coords[1] - 10), cv2.FONT_HERSHEY_SIMPLEX, 0.5, (0, 0, 255), 2)2.4差值计算与串口通信
PS:上位机与下位机通信,建议采用十六进制:帧头+数据类型+数据内容 的单帧数据包格式,这样才能保证下位机能够同步反应上位机发送的特征值,减小通信的复杂度,同时可靠性和速率较高,不建议采用发送字符串类型的通信。
if mode3.胶带矩形框识别
3.1 ROI区域的创建
PS: 使用ROI区域后,又想还原到原图像上对图像进行处理,就需要对坐标进行还原操作,保证图形每个像素点的索引和原图像对应
class ROIPixelProcessor: def __init__(self): pass
def process_roi(self, image, roi): """ 取得 ROI 区域的像素值进行处理,但不改变索引号 :param image: 输入图像 :param roi: ROI 参数 (x, y, width, height) :return: ROI 区域的像素值 """ x, y, w, h = roi
# 确保 ROI 在图像范围内 if x < 0: w += x x = 0 if y < 0: h += y y = 0 if x + w > image.shape[1]: w = image.shape[1] - x if y + h > image.shape[0]: h = image.shape[0] - y
# 提取 ROI 区域的像素值 roi_pixels = image[y:y+h, x:x+w] return roi_pixels3.2 图像的预处理
通过BGR转灰度,自设定阈值进行二值化地方法,将胶带部分转换为白色单值,其他部分全为黑色单值,并且通过滤波、腐蚀、闭运算地方法对图形进行处理,降低噪点干扰,提高图形识别完整性。
if time.time()-start_time <= 5:
# 将图像转换为灰度图 gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY) #cv2.imshow('gray',gray)
gray = cv2.GaussianBlur(gray, (9, 9), 0) #高斯平滑滤波,(9,9)是卷积核,0 代表函数自动计算标准差
#cv2.imshow('gauss',gray) # 对灰度图进行阈值处理,将黑色部分变为白色,其他部分变为黑色 _, threshold = cv2.threshold(gray, 120, 255, cv2.THRESH_BINARY_INV)
# 进行形态学闭操作,填充内部空洞 kernel = np.ones((5,5),np.uint8) closing = cv2.morphologyEx(threshold, cv2.MORPH_CLOSE, kernel, iterations=3)
# 进行形态学腐蚀操作,缩小黑框大小,减少噪点信号干扰 kernel = np.ones((3,3),np.uint8) erosion = cv2.erode(closing,kernel,iterations = 1) #dilation = cv2.dilate(threshold, kernel, iterations=1)3.3 矩形框轮廓的识别
同样地,使用封闭图形检测算法,检测矩形框轮廓,这里使用 ROI处理,要特别注意像素点索引(坐标)的复原的操作。
即:
# 将轮廓绘制在原始图像上for contour in contours: # 将轮廓点的坐标映射回原始图像的坐标系 contour += (x, y) # 将ROI的偏移加回来近似曲线
注意:这里approx_contour得到的是最终近似矩形的四个角点的坐标!
epsilon = 0.01 * cv2.arcLength(max_contour, True)approx_contour = cv2.approxPolyDP(max_contour, epsilon, True)cv2.arcLength用于计算轮廓的周长或弧长。epsilon是一个近似精度参数,通常是轮廓周长的某个百分比,这里设定为总周长的1%。cv2.approxPolyDP用来对轮廓进行多边形逼近,通过减少顶点的数量来近似表示轮廓。approx_contour是近似后的多边形轮廓。
找到左右轮廓的边界点(列表)
leftmost[0]就是左轮廓第一个点
leftmost = min(path, key=lambda x: x[0]) # 左轮廓边界坐标点rightmost = max(path, key=lambda x: x[0]) # 右轮廓边界坐标点path是一组坐标点的集合,通常表示为一个列表或数组。min(path, key=lambda x: x[0])和max(path, key=lambda x: x[0])分别用于找到path中横坐标x最小和最大的点,即左轮廓和右轮廓的边界点。
参数说明:
lambda x: x[0]是一个匿名函数,用于指定以每个点的横坐标x作为比较的关键字。因此,min函数找到具有最小横坐标的点,而max函数找到具有最大横坐标的点。
计算轮廓的中心点
center_point_x = int(sum([point[0] for point in path]) / len(path))center_point_y = int(sum([point[1] for point in path]) / len(path))center_point = (center_point_x, center_point_y)path是一组轮廓的坐标点集合,通常表示为一个列表或数组。sum([point[0] for point in path])对path中所有点的横坐标进行求和。sum([point[1] for point in path])对path中所有点的纵坐标进行求和。len(path)是path中点的数量,即轮廓的长度或大小。
参数说明:
center_point_x和center_point_y分别是计算得到的轮廓中心点的横坐标和纵坐标。center_point是由center_point_x和center_point_y组成的元组,表示轮廓的中心点坐标(x, y)。
作用:
- 这段代码的作用是计算轮廓
path的几何中心点,通过对所有点的坐标进行平均值计算得出
#cv2.imshow('dilation',erosion)
roi = closing[center_y-120:center_y+120, center_x-120:center_x+120]#创建ROI图像
#cv2.imshow('roi',roi)x,y,w,h = center_x-120,center_y-120,240,240 #ROI左上角点为220,140,ROI检测框高度为200,宽度为200
#绘制ROI矩形cv2.rectangle(frame,(x,y),(x+w,y+h),(0,255,0),1)
#cv2.imshow('erosion',erosion)# 利用轮廓检测函数找到黑色框的轮廓contours, hierarchy = cv2.findContours(roi, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)# 轮廓 层级 轮廓检索模式 轮廓逼近方法
# 将轮廓绘制在原始图像上for contour in contours: # 将轮廓点的坐标映射回原始图像的坐标系 contour += (x, y) # 将ROI的偏移加回来 #cv2.drawContours(frame, [contour], -1, (0, 0, 255), 2) 原始轮廓,这里用近似法得到更平滑的外围轮廓
# 如果找到了轮廓if len(contours) > 0: # 提取最大的轮廓 max_contour = max(contours, key=cv2.contourArea)
# 近似曲线 epsilon = 0.01 * cv2.arcLength(max_contour, True) approx_contour = cv2.approxPolyDP(max_contour, epsilon, True)
# 获取路径坐标 path = [] for point in approx_contour: path.append(tuple(point[0]))
# 找到左右轮廓的边界点 leftmost = min(path, key=lambda x: x[0])# 左轮廓边界坐标点 rightmost = max(path, key=lambda x: x[0])# 右轮廓边界坐标点
# 计算左右轮廓的中间点 middle_x = int((leftmost[0] + rightmost[0]) / 2)# x中心坐标 middle_y = int((leftmost[1] + rightmost[1]) / 2)# y中心坐标 middle_point = (middle_x, middle_y)
# 绘制左右轮廓中心点 #cv2.circle(frame, middle_point, 5, (0, 255, 0), -1)
# 计算轮廓的中心点 center_point_x = int(sum([point[0] for point in path]) / len(path)) center_point_y = int(sum([point[1] for point in path]) / len(path)) center_point = (center_point_x, center_point_y)
cv2.drawContours(frame, [np.array(path)], -1, (125, 255, 125), 1) #近似后的轮廓 # 轮廓 第几个(默认-1:所有) 颜色 线条厚度 # 绘制轮廓中心点 cv2.circle(frame, center_point, 3, (0, 255, 0), -1)
resize = 0.92
resize_path = []
for point in path: resize_x = int(center_point_x + resize*(point[0]-center_point_x)) resize_y = int(center_point_y + resize*(point[1]-center_point_y)) resize_path.append((resize_x,resize_y))
cv2.drawContours(frame, [np.array(resize_path)], -1, (125, 255, 125), 1) # 内缩后的轮廓 long_path = interpolate_points(resize_path)
for point in long_path: cv2.circle(frame, point, 4, (0, 0, 255), -1)3-4 矩形框的路径规划-插值算法
如果让激光点沿着轮廓一个一个坐标点去走,这样计算量会相当大,不如将矩形轮廓通过近似得到四个角点,再通过插值法,设置相邻点的插值参数,得到12个路径点,这样就既保证了巡线的稳定性,也大大减少了巡线的计算量。同时,通过调用time库来计时,程序启动时定时5s后,这段时间提供给我们确认摄像机状态,移动ROI区域,完成路径规划,实现“动态建图”的效果。
#插值算法def interpolate_points(points): interpolated_points = [] for i in range(len(points)): start_point = points[i] end_point = points[(i+1) % len(points)] # 计算两点之间的距离 dx = end_point[0] - start_point[0] dy = end_point[1] - start_point[1] distance = 3 # 根据距离进行插值 for j in range(distance): x = start_point[0] + int(dx * j / distance) y = start_point[1] + int(dy * j / distance) interpolated_points.append((x, y)) return interpolated_points