0x00 往期博文

OriginBot我的第一辆智能小车

使用(PS2)手柄控制OriginBot小车

《HelloOriginBot::使用Python编写Joy手柄功能包》

《深入Originbot:PID人脸追踪》

《App上位机控制OriginBot小车》 

BPU初体验::红绿灯 (精选)

[精选]《Originbot::find_object_2d书籍卡片识别》

[精选]旭日X3pi :: 三原色识别

0x01 实验环境

操作环境及软硬件配置如下:

  • OriginBot机器人(视觉版/导航版)
  • PC:Ubuntu (≥20.04) + ROS2 (≥Foxy)
  • 巡线场景:黑色路径线,背景有明显反差(黑白)

知识点:

  • 视觉巡线:使用自适应局部二值化方法处理图像找出黑线
  • 计算出黑线质心,根据质心控制机器人循迹。

0x02. adaptiveThreshold 自适应二值化

dst = cv.adaptiveThreshold(src, maxValue, adaptiveMethod, thresholdType, blockSize, C[, dst])

将自适应阈值应用于一个图像数据
Applies an adaptive threshold to an array.

该函数通过以下公式将灰度图转换为二值图:
The function transforms a grayscale image to a binary image according to the formulae:

  • THRESH_BINARYimage.png
  • THRESH_BINARY_INVimage-2.png


其中T(x,y)是针对每个像素单独计算的阈值(参见adaptiveMethod参数)。
where T(x,y) is a threshold calculated individually for each pixel (see adaptiveMethod parameter).

参数:

  • src: 8比特单通道的源图像,灰度图。
    Source 8-bit single-channel image.

  • dst: 处理结果,尺寸和类型和源图像一致。
    Destination image of the same size and the same type as src.

  • maxValue: 分配给满足条件的像素的非零值。
    Non-zero value assigned to the pixels for which the condition is satisfied.

  • adaptiveMethod: 要使用的自适应阈值算法,请参阅AdaptiveThresholdTypesBORDER_REPLICATE | BORDER_ISOLATE用于处理边界。
    Adaptive thresholding algorithm to use, see AdaptiveThresholdTypes. The BORDER_REPLICATE | BORDER_ISOLATED is used to process boundaries.

    • cv.ADAPTIVE_THRESH_MEAN_C | 均值
    • cv.ADAPTIVE_THRESH_GAUSSIAN_C | 高斯

  • thresholdType: 阈值类型必须是THRESH_BINARYTHRESH_MINARY_INV,请参阅ThresholdTypes或看一下上面的公式。
    Thresholding type that must be either THRESH_BINARY or THRESH_BINARY_INV, see ThresholdTypes.

  • blockSize: 用于计算像素阈值的像素邻域的大小:3、5、7等奇数。
    Size of a pixel neighborhood that is used to calculate a threshold value for the pixel: 3, 5, 7, and so on.

  • C: 从平均值或加权平均值中减去常数 C(详见下文)。通常,它是正的,但也可能是零或负的。
    Constant subtracted from the mean or weighted mean (see the details below). Normally, it is positive but may be zero or negative as well.

0x03 为什么使用 adaptiveThreshold?

参考

cv.AdaptiveThreshold 既可以做边缘提取,也可以实现二值化,这是由邻域blockSize参数所确定的。

如果邻域参数非常小(比如3×3),那么很显然阈值的“自适应程度”就非常高,这在结果图像中就表现为边缘检测的效果。
如果邻域参数较大(比如31×31),那么阈值的“自适应程度”就比较低,这在结果图像中就表现为二值化的效果。

一般情况下,滤波器宽度应该大于被识别物体的宽度。否则block_size太小,计算出的均值将无法代表背景。

对此灰度图使用自适应阈值函数进行局部二值化处理,该算法可以利用白底黑线的颜色对比度,更精确地找出黑线。
注意:该算法在白底黑线的图纸上实验最佳。

0x04 图像识别效果对比

原始图像

HSV、LAB 色彩空间调参

全局二值化穷举 cv2.threshold

这两种方式,效果均不理想,易受外接环境(光线)干扰。

自适应二值化代码实现

# Convert BGR to GRAY
gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)

# retval, dst = cv2.threshold(gray, 0, 255,  cv2.THRESH_BINARY | cv2.THRESH_OTSU)  # 全局二值化
ada_dst = cv2.adaptiveThreshold(gray, 255, cv2.ADAPTIVE_THRESH_MEAN_C, cv2.THRESH_BINARY_INV, 101, 77) # 局部二值化

_2show = cv2.cvtColor(ada_dst, cv2.COLOR_BGR2RGB)
plt.imshow(_2show)

可见‘自适应’二值化效果最佳;

0x05 图像分隔

核心思路:

  1. 机器人欲寻的线必定在其脚下,且机器人低头走路。
  2. 随切割图像于高1/2处取下半部分,以消除上半部分可能存在的外部干扰。
  3. 对下半部分,再做三次图像切割,通过权重计算出当前线段质心位置,根据该值控制机器人转向或前进。

5.1 定义局部变量

line_centroid_x   # 质心 x 坐标, 最终结果

centroid_x_sum = 0        # 质心 x 权重和
weight_sum = 0            # sum(ROI[:, 0:4])
# line_centroid_x =  centroid_x_sum / weight_sum

# region of interest 感兴区域
# y0, y1, x0, x1, 权重
roi = [(300, 360, 0, 640, 0.1),  # (y1:y1), (x1:x2), 权重
       (360, 420, 0, 640, 0.3),  # 每层高度 60
       (420, 480, 0, 640, 0.6)]  # 将图像分割成三个部分

n = 0  # 用于遍历ROI, 计数

5.2 切割可视化

for r in roi:
    img_block = ada_dst[r[0]:r[1], r[2]:r[3]]
    
    # Jupyter 显示图像代码
    _2jpg = cv2.imencode(".jpg", img_block)[1].tobytes()
    display(widgets.Image(value=_2jpg, format='jpg', width=320))

5.3 标记路线

发现轮廓,处理噪声

 r in roi:
    img_block = ada_dst[r[0]:r[1], r[2]:r[3]]
    
    # 找出所有外轮廓
    contours, _ = cv2.findContours(img_block, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_NONE)
    
    # 该块未发现轮廓
    if len(contours) == 0:
        continue
    
    # 对轮廓对象按面积进行降序, 面积最大的为序号为0
    c_sorted = sorted(contours, key=lambda x:
                      math.fabs(
                          cv2.contourArea(x)
                      ),  reverse=True)
    
    # 如果面积小于 20, 认为是噪声, 下一个块处理
    if math.fabs(cv2.contourArea(c_sorted[0])) <= 20:
        continue
        
    # 否则识别到黑线, 这个程序目前只能处理黑线, 其他颜色的线不能用灰度色彩空间处理;
    # 在原图像上绘制轮廓, offset 为 roi 左上角坐标, 用于偏移
    cv2.drawContours(img_debug, c_sorted, 0, (0, 0, 255), 2, offset=(r[2], r[0]))
    
    # 取轮廓最小外接矩形
    rect = cv2.minAreaRect(c_sorted[0])  
    
    # 取最小外接矩形的四个顶点
    box = np.int0(cv2.boxPoints(rect))
    
    # 获取矩形的对角点, 再根据对角线求出中心点, 1,3 2,4 对角
    pt1_x, pt1_y = box[0, 0], box[0, 1]
    pt3_x, pt3_y = box[2, 0], box[2, 1]
    
    center_x, center_y = (pt1_x + pt3_x) / 2, (pt1_y + pt3_y) / 2  # 中心点
    cv2.circle(img_debug, (int(center_x), int(center_y) + r[0]), 3, (0, 0, 255), -1)  # 画出中心点,图中红点, -1填充
    # +r[0]是block ROI y起始位置;
    
    # 按权重不同对上、中、下三个中心点进行求和
    centroid_x_sum += center_x * r[4]       # 执行了三次,中心坐标 × 权重
    weight_sum += r[4]  # 0.1 + 0.3 + 0.6   # 三个区域, 的权重之和, 越靠后, 权重越大

# 权重值为0未找到黑线, 不为0, 某个块识别到了黑线; 或者说面积大于 20(根据实际情况修改)
if weight_sum != 0:
    # 求得加权平均中心点
    line_centroid_x = int(centroid_x_sum / weight_sum)  # 根据加权平均, 计算出的中心点,图中黄点
    cv2.circle(img_debug, (line_centroid_x, 360), 7, (0, 255, 255), -1)  # 画出中心, 这里y坐标是固定的; 巡线不关心y坐标;
else:
    line_centroid_x = -1  # 错误值, 未寻找到黑线
  
# Jupyter 显示图像
plt.imshow(img_debug[:,:,::-1])

5.5 计算质心

上图中

  • 红点 为 每一段黑线 的 中心点
  • 黄点 为 加权平均后 的 中心点, 可认为是整条黑线的质心

5.6 什么是加权平均?

例子:学校学期末成绩,期中考试占30%,期末考试占50%,作业占20%,假如某人期中考试得了84,期末92,作业分91。
如果是算数平均,那么就是
(84+92+91)/3=89;

加权后的,那么加权处理后就是
84x30%+92x50%+91x20%=89.4

在本例中,线段位置靠上距离机器人较远,权重值较小;反之,线段位置靠下距离机器人较近, 权重值较大。
距离与权重值之间呈反比例关系。

0x06 运动控制

: 机器人获取的图像像素默认为(640, 480)。

根据当前线段质心位置,设置阈值对转向或前进控制。

设 x 为当前线段质心位置,t为转向阈值可调。320为图像横坐标中心位置,即常量x_center。那么,

当 abs(x - 320) > t  时机器人偏离黑线,需左、右转向。
    x - 320 > t 时,机器人右转
    x - 320 < -t 时,机器人左转
否则
    机器人直线行走

参考:

    视觉巡线(OpenCV) - OriginBot智能机器人开源套件