树莓派 Python + OpenCV 颜色识别、跟随、巡线小车技术分析

在创客空间和高校实验室里,你可能见过这样的场景:一辆小巧的智能小车正“专注”地盯着前方一块红色积木,亦步亦趋地跟着它移动;又或者,它沿着地面上一条弯弯曲曲的黑线稳稳前行,仿佛有双无形的眼睛在引导。这些看似简单的自主行为背后,其实是嵌入式系统与计算机视觉融合的典型体现。

而实现这一切的核心硬件组合——树莓派 + 摄像头 + 电机驱动模块,配上一段用 Python 编写的 OpenCV 程序,成本不过几百元,却能完成从图像采集到运动控制的完整闭环。这种低门槛、高集成的技术路径,正在让越来越多的学生、爱好者和开发者踏入机器人视觉的世界。


要让一个小车“看得懂”颜色、“跟得上”目标、“走得准”路线,关键在于如何高效处理摄像头传来的每一帧画面,并将视觉信息转化为精确的电机指令。整个过程虽然涉及多个技术模块,但它们之间的协作逻辑其实非常清晰。

以最常见的颜色识别任务为例,真正决定成败的并不是代码行数多少,而是对色彩空间的理解是否到位。很多人一开始会直接使用 BGR(或 RGB)颜色空间进行阈值分割,结果发现只要光照稍有变化,原本稳定的红色区域就消失了。问题出在哪?BGR 是设备相关的颜色表示方式,对亮度极其敏感。相比之下, HSV 色彩空间 把颜色拆解为色调(Hue)、饱和度(Saturation)和明度(Value),我们只需要关注 H 分量来锁定特定颜色,再通过 S 和 V 设置合理的范围,就能大幅增强系统对环境光变化的鲁棒性。

下面这段代码就是典型的 HSV 处理流程:

import cv2
import numpy as np

cap = cv2.VideoCapture(0)
cap.set(cv2.CAP_PROP_FRAME_WIDTH, 320)
cap.set(cv2.CAP_PROP_FRAME_HEIGHT, 240)

lower_red = np.array([0, 120, 70])
upper_red = np.array([10, 255, 255])

while True:
    ret, frame = cap.read()
    if not ret:
        break

    hsv = cv2.cvtColor(frame, cv2.COLOR_BGR2HSV)
    mask = cv2.inRange(hsv, lower_red, upper_red)
    mask = cv2.erode(mask, None, iterations=2)
    mask = cv2.dilate(mask, None, iterations=2)

    contours, _ = cv2.findContours(mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)

    if len(contours) > 0:
        c = max(contours, key=cv2.contourArea)
        M = cv2.moments(c)
        if M["m00"] != 0:
            cx = int(M["m10"] / M["m00"])
            cy = int(M["m01"] / M["m00"])
            cv2.circle(frame, (cx, cy), 5, (0, 255, 0), -1)

    cv2.imshow("Frame", frame)
    cv2.imshow("Mask", mask)

    if cv2.waitKey(1) == ord('q'):
        break

cap.release()
cv2.destroyAllWindows()

这里有几个工程实践中容易忽略的细节:

  • 形态学操作的作用 erode dilate 并非可有可无。前者可以消除细小噪点,后者则填补目标内部空洞,两者结合形成“开运算”,能有效提升轮廓质量。
  • 只取最大轮廓 :当场景中存在多个同色物体时,选择面积最大的那个作为主目标,是一种简单有效的抗干扰策略。
  • 动态调试 HSV 阈值 :建议写一个滑动条程序实时调整 lower/upper 值,避免靠猜参数浪费时间。

一旦拿到了目标的中心坐标 (cx, cy) ,下一步就是让它成为控制系统的输入信号。这时候就得靠树莓派的 GPIO 接口来驱动电机了。

树莓派本身不能直接驱动大电流负载,所以必须通过 H 桥芯片(如 L298N 或 TB6612FNG)做中间隔离。GPIO 引脚负责发送方向电平和 PWM 波形,由驱动模块调节电机转速和转向。以下是一个典型的双电机控制函数:

import RPi.GPIO as GPIO

ENA = 18  # 左电机PWM
IN1 = 23
IN2 = 24
ENB = 19  # 右电机PWM
IN3 = 6
IN4 = 5

GPIO.setmode(GPIO.BCM)
GPIO.setup([ENA, IN1, IN2, ENB, IN3, IN4], GPIO.OUT)

pwm_left = GPIO.PWM(ENA, 1000)
pwm_right = GPIO.PWM(ENB, 1000)
pwm_left.start(0)
pwm_right.start(0)

def set_motor(left_speed, right_speed):
    if left_speed > 0:
        GPIO.output(IN1, GPIO.HIGH)
        GPIO.output(IN2, GPIO.LOW)
        pwm_left.ChangeDutyCycle(min(abs(left_speed), 100))
    else:
        GPIO.output(IN1, GPIO.LOW)
        GPIO.output(IN2, GPIO.HIGH)
        pwm_left.ChangeDutyCycle(min(abs(left_speed), 100))

    if right_speed > 0:
        GPIO.output(IN3, GPIO.HIGH)
        GPIO.output(IN4, GPIO.LOW)
        pwm_right.ChangeDutyCycle(min(abs(right_speed), 100))
    else:
        GPIO.output(IN3, GPIO.LOW)
        GPIO.output(IN4, GPIO.HIGH)
        pwm_right.ChangeDutyCycle(min(abs(right_speed), 100))

这个函数的设计体现了差速转向的基本思想:左右轮速度差产生转弯动作。比如左轮慢右轮快,小车就会向左偏转。但如果你只是简单地根据目标偏移方向做“左转/右转”的开关式控制,车子很可能会出现剧烈抖动——刚转过去又发现偏了另一边,来回震荡。

解决办法是引入比例控制(P 控制)。假设图像宽度为 320,则中心 x 坐标是 160。当前目标质心位置为 cx ,偏差 error = cx - 160 。我们可以把这个误差乘以一个比例系数 Kp ,得到转向修正量:

Kp = 0.5
base_speed = 50

if len(contours) > 0 and area > 100:
    M = cv2.moments(c)
    cx = int(M["m10"] / M["m00"])
    error = cx - 160
    turn = Kp * error
    left_speed = base_speed - turn
    right_speed = base_speed + turn
    set_motor(left_speed, right_speed)
else:
    set_motor(-30, 30)  # 未检测到目标时原地旋转搜索

这种方式实现了“越偏越多转”的平滑响应,大大减少了锯齿状轨迹。当然,实际调参时你会发现 Kp 太大会导致过冲,太小又反应迟钝。更进一步的做法是加入积分项(I)抑制静态误差,甚至微分项(D)预测趋势,构成完整的 PID 控制器。但在大多数室内环境下,一个调得好的 P 控制已经足够稳定。

相比目标跟随,巡线任务略有不同。它不需要识别大面积色块,而是要在复杂背景中提取出一条狭长的线条。常见的做法是截取图像底部的一块 ROI(Region of Interest),比如只处理最后 60 行像素,这样既能减少计算量,又能聚焦于地面路径。

接着进行灰度化处理,并采用 Otsu 自适应阈值法自动确定最佳二值化分界点:

roi = frame[180:240, :]
gray = cv2.cvtColor(roi, cv2.COLOR_BGR2GRAY)
_, thresh = cv2.threshold(gray, 0, 255, cv2.THRESH_BINARY + cv2.THRESH_OTSU)

Otsu 方法的优势在于无需手动设定阈值,在光照不均的情况下也能较好地区分黑白线。之后通过查找轮廓并筛选面积适中的连通域,即可定位有效线段:

contours, _ = cv2.findContours(thresh, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
line_found = False

for cnt in contours:
    area = cv2.contourArea(cnt)
    if 100 < area < 5000:
        x, y, w, h = cv2.boundingRect(cnt)
        cx_line = x + w // 2
        error = cx_line - 160
        turn = 0.4 * error
        set_motor(40 - turn, 40 + turn)
        line_found = True
        break

if not line_found:
    set_motor(0, 0)  # 可改为回退或小幅旋转尝试找回线路

这种方法适用于直线或缓弯路径。如果遇到急弯或十字交叉口,建议升级为基于滑动窗口的多项式拟合方法,或者干脆使用霍夫变换提取线段角度,从而获得更精确的方向估计。

整个系统的稳定性不仅取决于算法本身,还与硬件布局密切相关。例如摄像头安装高度应在 15~20cm 左右,确保视野覆盖前方 0.5 米范围内的路径;镜头尽量保持垂直向下,避免透视畸变影响判断;电机供电需独立于树莓派,最好使用 LM2596 等降压模块分别稳压,防止电机启停时电压波动导致树莓派重启。

另外,性能优化也不容忽视。OpenCV 虽然功能强大,但在树莓派上运行高分辨率视频流仍会造成 CPU 占用过高。经验做法包括:
- 将分辨率降至 320×240 或 480×360;
- 关闭不必要的图像显示(尤其是远程 VNC 时);
- 使用 time.sleep(0.05) 限制主循环频率,给 CPU 留出喘息空间;
- 替换 RPi.GPIO pigpio 库,获得更高精度的硬件 PWM 输出。

值得一提的是,这套架构的扩展潜力远超初学者预期。当你熟练掌握了基础的颜色识别与路径跟踪后,完全可以在此基础上叠加更多高级功能:
- 加入 Haar 分类器或 CNN 实现人脸检测,让小车主动追随人脸;
- 集成 QR 码识别模块,用于导航点标记;
- 使用 YOLO 或 MobileNet-SSD 实现多类别物体识别;
- 结合 IMU 数据做传感器融合,提升定位精度;
- 进一步发展为轻量级 SLAM 系统,构建局部地图并规划路径。

更重要的是,这种“感知—决策—执行”的闭环模式,正是现代服务机器人、AGV 小车乃至自动驾驶系统的缩影。尽管目前的功能还停留在原型阶段,但它所训练的工程思维——如何在资源受限条件下平衡实时性与准确性,如何设计容错机制应对现实干扰——却是通往复杂系统开发的必经之路。

最终你会发现,这辆小车的价值不仅仅在于它能做什么,而在于它教会你如何去构建一个真正的智能体。哪怕只是一个简单的颜色追踪任务,背后也藏着对光照、噪声、延迟、反馈等真实世界问题的深刻理解。而这,才是开源硬件与计算机视觉教育最迷人的地方。

这种高度集成且开放的设计思路,正引领着下一代智能移动设备向更可靠、更高效的方向演进。

更多推荐