简介面向树莓派与机器人初学者的实战项目资源演示如何基于Python与OpenCV实现颜色识别、巡线行驶与物体跟随一体的智能小车控制方案。适合具备基础Python语法、希望将视觉算法落地到硬件控制的开发者参考学习。压缩包仅2个文件包含一个python脚本与一张示例图片整体约109KB、结构精简核心逻辑集中在xun.py中便于直接阅读与二次改造。当前已有10623人学习下载属于同类型中关注度较高的小型入门资源。内容虽短但覆盖了从USB摄像头取流、cv2.inRange颜色阈值提取、线条边缘检测到根据目标位置控制小车运动方向的完整闭环可帮助读者理解视觉识别与运动控制结合的基本思路也适合作为课程设计或DIY项目的起步模板。1. 树莓派小车用视觉做大脑这套代码把颜色识别、跟随、巡线一次讲完如果你手里有一块吃灰的树莓派想让它干点“看得见”的活那这套小车项目绝对是最合适的起点。传统的小车循迹靠红外对管、靠灰度传感器识别规则写死、泛化能力差地面颜色一变就翻车。而用 OpenCV 做颜色识别等于把摄像头的画面变成决策依据小车能“看”着目标走能判断红绿蓝能追着一个色块跑也能沿着一条线自己走完整个赛道。这套代码把三件事打包在一起红绿蓝颜色识别与定位、目标跟随控制、巡线行驶核心逻辑全部用 Python 实现依赖少、上手门槛低适合有基础编程经验、想接触计算机视觉和运动控制的开发者。拿来跑一遍你就知道视觉小车的链路是怎么从一帧图像走到电机转动的。2. 硬件选型与环境搭建这套代码跑起来需要哪些基础2.1 树莓派型号与摄像头选型参考拿到这份代码第一件事不是打开编辑器而是确认手里的硬件能不能撑起 OpenCV 这套计算负载。代码主仓库的测试环境是树莓派 4B 搭配 500 万像素 USB 摄像头Python 版本为 3.7 及以上OpenCV 为 4.x 系列。树莓派 4B 的内存建议 2GB 以上因为 OpenCV 读取画面、做形态学运算、算质心这套流程CPU 占用并不低内存太小容易在处理 640x480 分辨率时出现卡顿。如果用的是 3B也不是不能跑但帧率会掉到 10fps 左右实时性受影响控制效果会变得迟钝。摄像头方面USB 摄像头是首选驱动免安装插上就能被 V4L2 识别。如果用 CSI 排线摄像头代码中读取画面的方式要改成 picamera 或 libcamera这与 USB 摄像头的 cv2.VideoCapture 读取方式不兼容需要额外改驱动层逻辑。我一般建议新手用 USB 摄像头因为调试时拔插方便出问题容易排查。L298N 电机驱动模块或者双 H 桥驱动板都可以代码里对 GPIO 的控制接口只依赖 RPi.GPIO 库不挑具体驱动板型号。2.2 系统镜像与 Python 依赖安装系统层推荐使用官方 Raspberry Pi OS Lite或带桌面版因为需要安装 OpenCV 的编译依赖。这里有两条路可以走一条是直接用 pip 安装 opencv-python 预编译包另一条是源码编译。对于多数人来说预编译包是省事的选择但有一个坑——在树莓派这种 ARM 架构上opencv-python 的预编译包可能缺少某些扩展模块比如 xfeatures2d 这类但对于颜色识别项目来说基础模块已经够用。# 更新系统源 sudo apt update sudo apt upgrade -y # 安装编译 OpenCV 所需的基础依赖 sudo apt install -y python3-pip python3-dev libatlas-base-dev libjasper-dev sudo apt install -y libhdf5-dev libhdf5-serial-dev libqtgui4 libqt4-test # 通过 pip 安装 opencv-python 与相关科学计算库 pip3 install opencv-python4.5.5.64 pip3 install numpy逻辑说明第一行更新系统包索引是安装任何依赖前的标准操作第二行安装的 libatlas-base-dev 是 OpenCV 在 ARM 平台上做矩阵运算的底层加速库没有它会出现无法定位的错误或运行时性能骤降libjasper-dev 是处理 JPEG 编码所需的库OpenCV 读图写图会用到。参数说明opencv-python 的版本号 4.5.5.64 是经过社区验证在树莓派上运行较稳的版本如果你在后续安装其他依赖时出现版本冲突可以把版本号去掉直接装最新版但要做好行为差异的心理准备。2.3 代码目录结构与运行入口把压缩包解压到树莓派的 /home/pi/ 目录下整个项目的主目录名是 color_car/打开之后你能看到以下核心文件排列方式。color_car/ ├── main.py # 主程序入口包含模式选择与主循环 ├── config.py # 全局配置HSV范围、串口参数、GPIO引脚号 ├── color_detect.py # 颜色识别模块负责过滤、降噪、定位 ├── pid_control.py # PID控制类用于速度与转向闭环 ├── motor.py # 电机驱动封装前进后退左右转 ├── track_mode.py # 巡线模式实现 ├── follow_mode.py # 跟随模式实现 ├── requirements.txt # 依赖清单 └── README.md # 项目说明与接线表主程序 main.py 是唯一需要启动的文件它在初始化阶段加载 config.py 中的配置参数创建电机控制对象和摄像头对象然后通过终端输入的数字键切换运行模式。输入 1 进入颜色识别模式屏幕上会实时框出目标颜色的位置和中心点坐标输入 2 进入跟随模式小车会自动调整方向追着目标色块跑输入 3 进入巡线模式小车沿着地面上的特定颜色轨迹线行驶。这种模式独立拆分的结构后期想加功能很方便直接在 main.py 的 mode 分支里注册新模块就行。3. 颜色识别核心模块拆解一帧图像如何变成小车能懂的动作指令3.1 HSV 颜色空间与 inRange 过滤为什么不用 RGB颜色识别这部分的代码集中在 color_detect.py 中。一般来说新手第一次做颜色识别都会直接用 RGB 三通道的阈值判断但实际调试会发现极度不稳定光线稍微变暗红色就可能被识别成紫色。这套代码选择的是 HSV 颜色空间因为 H色相通道对光照变化相对不敏感S饱和度和 V明度可以分别控制颜色的鲜艳程度和明暗范围这样在自然光环境下识别稳定性会高很多。import cv2 import numpy as np class ColorDetector: def __init__(self, hsv_lower, hsv_upper): # 存储目标颜色的HSV下限与上限 self.hsv_lower np.array(hsv_lower) # 例如红色: [0, 80, 60] self.hsv_upper np.array(hsv_upper) # 例如红色: [10, 255, 255] def detect(self, frame): # 将BGR帧转成HSV空间 hsv cv2.cvtColor(frame, cv2.COLOR_BGR2HSV) # 根据阈值生成二值掩码目标区域为白色其余为黑色 mask cv2.inRange(hsv, self.hsv_lower, self.hsv_upper) # 开运算去除噪点 mask cv2.morphologyEx(mask, cv2.MORPH_OPEN, np.ones((5, 5), np.uint8)) # 闭运算填充空洞 mask cv2.morphologyEx(mask, cv2.MORPH_CLOSE, np.ones((5, 5), np.uint8)) return mask逻辑说明cvtColor 把一帧 BGR 画面转换成 HSV 格式这是 OpenCV 读取摄像头帧的默认色彩空间转换后才能用 HSV 阈值做范围过滤。inRange 会遍历每个像素点判断是否落在 lower 和 upper 定义的区间内满足则置为 255不满足置为 0输出一张二值图。MORPH_OPEN 是先腐蚀后膨胀作用是去掉孤立噪点MORPH_CLOSE 是先膨胀后腐蚀用于填补目标内部的空洞。参数说明核大小 5x5 是经验值如果识别出的目标边缘噪点明显可以加大到 7x7但代价是目标边缘会变圆润、质心位置略微偏移在巡线场景中会导致转向延迟所以不是越大越好。HSV 的低阈值和高阈值是一组六元组不同颜色的分界值差异很大红色因为色相范围绕了 0 度一圈往往需要两组阈值拼接这点在参数调优章节再细说。3.2 轮廓查找与质心计算从掩码到坐标点的关键一步拿到二值掩码后需要从里面提取目标的几何信息最常见的手段是查找轮廓并计算质心。cv2.findContours 的返回值在不同版本的 OpenCV 里有差异4.x 版本返回两个值3.x 返回三个所以代码里一般会做兼容处理。def locate(self, mask): # 查找轮廓只保留外层轮廓使用简化近似算法 contours, _ cv2.findContours(mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) if not contours: return None, None, 0 # 选出面积最大的轮廓作为主要跟踪目标 max_contour max(contours, keycv2.contourArea) area cv2.contourArea(max_contour) # 如果面积太小认为是噪点而非真实目标 if area 500: return None, None, 0 # 通过图像矩计算质心坐标 moments cv2.moments(max_contour) if moments[m00] 0: return None, None, 0 c_x int(moments[m10] / moments[m00]) c_y int(moments[m01] / moments[m00]) return (c_x, c_y), max_contour, area逻辑说明RETR_EXTERNAL 只取最外层轮廓避免目标内部纹理产生子轮廓干扰面积排序CHAIN_APPROX_SIMPLE 压缩水平方向、垂直方向和对角方向的冗余点减少轮廓点数量。取面积最大轮廓是为了滤除小色块噪声在跟随场景中如果有多个同样颜色的目标小车只追最大的那一个。面积阈值 500 对应的是 640x480 画面下约 2% 的占比如果摄像头放得远、目标偏小这个值要下调否则永远找不到目标。参数说明moments 里的 m00 是零阶矩代表轮廓面积m10 和 m01 是一阶矩分别代表 x 方向和 y 方向的像素累计和除以面积就是质心的加权平均位置。3.3 主循环中的帧率控制与模式切换主程序 main.py 的逻辑并不复杂但有一个关键细节是帧率控制。树莓派的 CPU 性能有限如果不做控制OpenCV 的视频读取会以最高速度跑导致 CPU 占用率 100%并且画面延迟越来越严重。代码里通过时间戳来控制每帧处理间隔固定 20fps。import time import cv2 from color_detect import ColorDetector from pid_control import PIDController cap cv2.VideoCapture(0) cap.set(cv2.CAP_PROP_FRAME_WIDTH, 640) cap.set(cv2.CAP_PROP_FRAME_HEIGHT, 480) # 以红色目标为例初始化检测器 detector ColorDetector(hsv_lower[0, 80, 60], hsv_upper[10, 255, 255]) pid PIDController(kp0.4, ki0.01, kd0.15) prev_time 0 while True: now time.time() # 控制帧率不超过20fps避免CPU过载 if now - prev_time 0.05: continue prev_time now ret, frame cap.read() if not ret: break mask detector.detect(frame) center, contour, area detector.locate(mask) if center is not None: # 在画面上绘制十字准星与轮廓框 cv2.drawContours(frame, [contour], -1, (0, 255, 0), 2) cv2.circle(frame, center, 5, (0, 0, 255), -1) # 计算目标偏移量并送入PID控制器 offset_x center[0] - 320 # 画面中心x坐标为320 turn_speed pid.compute(offset_x, 0) print(f目标坐标: {center}, 偏移量: {offset_x}, 转向值: {turn_speed})逻辑说明cap.set 设置画面分辨率640x480 是平衡清晰度和性能的中间值再往上调 1280x720 会让树莓派的 CPU 开销翻倍实时性明显劣化。每帧处理前先检查距离上一帧的时间差差小于 0.05 秒就跳过这个睡帧方式比 time.sleep 更高效不会阻塞摄像头缓冲区的读取画面延迟会更低。PID 控制器在这里的输入是目标质心与画面中心点的像素偏差输出是转向速度值送到后续的电机控制模块。参数说明320 是与分辨率宽度 640 对应的中心点横坐标如果你把分辨率改了这里要同步调整否则小车会一直往一个方向偏。4. 跟随与巡线的控制逻辑PID 参数和转向策略怎么配合4.1 电机控制封装与差速转向原理电机控制是整套系统的执行层config.py 中定义了 GPIO 引脚映射关系motor.py 对这些引脚做了封装。差速转向的物理原理不复杂左轮转速大于右轮车身右转右轮转速大于左轮车身左转。代码里通过 PWM 控制电机驱动板的使能引脚来实现速度调节。import RPi.GPIO as GPIO import time class Motor: def __init__(self, in1, in2, ena, in3, in4, enb): # 初始化GPIO模式为BCM编码 GPIO.setmode(GPIO.BCM) # 配置左右轮电机控制引脚 self.pins {in1: in1, in2: in2, ena: ena, in3: in3, in4: in4, enb: enb} for pin in [in1, in2, ena, in3, in4, enb]: GPIO.setup(pin, GPIO.OUT) # 使能引脚使用PWM频率100Hz self.pwm_a GPIO.PWM(ena, 100) self.pwm_b GPIO.PWM(enb, 100) self.pwm_a.start(0) self.pwm_b.start(0) def set_speed(self, left_speed, right_speed): # 左轮速度 GPIO.output(self.pins[in1], GPIO.HIGH) GPIO.output(self.pins[in2], GPIO.LOW) self.pwm_a.ChangeDutyCycle(max(0, min(100, left_speed))) # 右轮速度 GPIO.output(self.pins[in3], GPIO.HIGH) GPIO.output(self.pins[in4], GPIO.LOW) self.pwm_b.ChangeDutyCycle(max(0, min(100, right_speed)))逻辑说明BCM 编码使用芯片引脚编号而非物理排针编号写代码时候不容易被板子的布局绕晕。in1 和 in2 控制左轮电机旋转方向HIGH/LOW 组合决定正转还是反转ena 是左轮使能引脚输入 PWM 占空比控制转速。ChangeDutyCycle 的范围是 0 到 100对应占空比 0% 到 100%max(0, min(100, value)) 是防止 PID 输出值超范围导致 GPIO 芯片报错。参数说明PWM 频率选 100Hz 是常见做法电机驱动板在这个频率下噪音较小、扭矩输出较线性。如果你用的是带减速箱的直流电机频率超过 500Hz 会显著降低电机效率这一点不用刻意调高。4.2 PID 控制器实现与跟随模式的调试经验PID 是跟随模式的核心pid_control.py 里实现了一个最简单的增量式 PID。代码不复杂真正重要的是调参经验。我用过这套代码在室内光照条件下跑红色目标跟随最稳的一组参数是 kp0.4、ki0.01、kd0.15。class PIDController: def __init__(self, kp, ki, kd): self.kp kp self.ki ki self.kd kd self.integral 0 self.last_error 0 def compute(self, setpoint, current_value): # 偏差值期望值减去当前值 error setpoint - current_value self.integral error # 抑制积分饱和防止长时间偏差积累导致超调 if self.integral 200: self.integral 200 elif self.integral -200: self.integral -200 derivative error - self.last_error self.last_error error # PID输出 比例项 积分项 微分项 output self.kp * error self.ki * self.integral self.kd * derivative return output逻辑说明setpoint 和 current_value 的传参方式需要注意在 main.py 中调用 pid.compute(offset_x, 0)把实际偏移作为第一个参数、目标值 0 作为第二个参数这与常规的 compute(setpoint, actual) 顺序相反我一般在代码注释里特别标注防止二次开发时传参顺序搞混。积分饱和限制的 200 值对应的是电机 PWM 的满量程范围这个值是跟着电机控制量走的改成 50 会导致转向响应迟缓改成 500 会导致靠近目标时明显震荡。参数说明kd 项在跟随模式中作用很大因为视觉帧率只有 20fps相邻帧的目标位置变化本身就带有噪声抖动微分项可以提前抑制过大波动。4.3 巡线模式边缘检测与转向指令生成巡线模式不是用颜色识别整条线而是只取画面底部一行的像素做判断这是典型的视觉巡线做法。画面底部区域代表小车当前所处位置附近的地面取这一行的目标颜色像素分布就能判断车身相对轨迹线的偏移方向。实现上先做颜色掩码提取然后按列统计白色像素的分布。def track_line(self, mask): # 取画面底部1/4区域作为检测带减少计算量 height, width mask.shape roi mask[int(height * 0.75):, :] # 按列求和得到每列的目标像素数量 col_sum roi.sum(axis0) # 找到所有有目标的列索引 indices np.where(col_sum 0)[0] if len(indices) 0: # 丢线状态返回急转指令 return None # 计算当前轨迹中心的列坐标 line_center int(np.mean(indices)) # 计算与画面中心的偏差超过阈值则转向 error line_center - int(width / 2) if abs(error) 20: return forward elif error 0: return right, min(80, 30 abs(error)) else: return left, min(80, 30 abs(error))逻辑说明画面底部 1/4 区域是车身正前方贴近地面的位置选取这个区域能提前感知轨迹的弯曲方向同时又不会被远处复杂背景干扰。col_sum 对 ROI 区域按列求和每一列的值表示该列有多少白像素值越大的列说明轨迹越可能经过那里。取所有非零列的均值得到轨迹几何中心的列坐标再用它与画面中心的差值作为偏航误差。20 像素的阈值是死区在这个范围内不调整方向避免小车因地面纹理或光照噪声做无意义的修正。转向输出使用线性映射偏移越大转向 PWM 值越大但最大不超过 80防止急转弯时后轮打滑失去抓地力。丢线状态返回 None主循环里会让小车原地小角度旋转寻找轨迹线这是一个简单但有效的恢复策略。5. 避坑指南树莓派跑 OpenCV 小车常见的五个坑与排查方法5.1 HSV 阈值记反导致目标识别反向现象代码跑起来了但是小车的目标识别区域恰好是背景区域不该动的时候猛追该追的时候原地不动。原因HSV 的阈值范围写反了lower 写成了 [0, 80, 60]upper 写成了 [10, 255, 255]看起来没错但其实你是在用 0 到 10 的色相范围匹配红色这个范围只覆盖了正红色的一小段区间。更隐蔽的问题是红色色相在 OpenCV 中绕 0 度一圈实际上要匹配完整红色需要把 0 到 10 和 170 到 180 两段分别做 inRange再用 bitwise_or 合并。解决写一个鼠标回调脚本直接在画面窗口点击目标颜色让程序自动打印当前像素的 HSV 值然后根据打印结果设置阈值范围。这是所有做视觉识别项目的通用调试手段比肉眼猜阈值高效得多。5.2 摄像头缓冲延迟导致画面越来越卡现象程序运行 30 秒后显示画面明显卡顿目标位置总是滞后实际位置一大截小车跟随不停过冲。原因摄像头缓冲区积累了大量未读取的帧而主循环通过 time.sleep 控制处理频率时没有清理缓冲区导致每帧读到的是几十秒前缓存的旧画面。解决在 cap.read() 之前连续丢弃缓冲区的残留帧或者把 time.sleep 的方式改为检测时间差、立即退出当前循环保证每一轮重新从最新帧开始处理。主循环里加上一个清缓冲的操作在进入下一轮处理前把缓冲区里的帧全部读出丢弃只保留最新帧实测延迟能降到一帧以内。5.3 电机 PWM 值过大导致转向失灵现象PID 输出值在 80 到 100 之间时小车转向反应反而不如 60 到 80 区间灵敏有时甚至出现原地抖动。原因驱动板进入 PWM 饱和区后电机实际转速受电压钳制左右轮速度差消失转向力矩反而下降。加上电池电压不足或 USB 供电带不动两个电机同时高转速PWM 超过 85 后驱动模块直接进入欠压保护。解决把 PID 输出做饱和限制到最大 80电机的基准速度设定在 45 到 60 区间让左右轮在基准速度之上叠加转向偏差。这样转向偏差即使达到 30也不会让高速轮进入饱和区。5.4 OpenCV 版本差异导致 findContours 返回值报错现象运行 color_detect.py 时抛异常错误信息提示函数返回值数量不对。原因OpenCV 3.x 中 cv2.findContours 返回三个值分别是 image、contours、hierarchyOpenCV 4.x 返回两个值contours 和 hierarchy。你在树莓派环境装的版本与项目开发环境不一致直接解包就崩。解决代码里写一个版本判断用 len 判断返回值的数量兼容两种写法。实际上这种兼容写在所有视觉项目里都值得保留因为你永远不知道用户会在哪台机器上跑你的代码。5.5 开机自启运行但摄像头初始化失败现象把程序写进了 rc.local 或 systemd 服务开机后进程显示在运行但画面全黑或者直接退出。原因开机自启脚本执行时USB 摄像头设备还没有完成枚举设备节点 /dev/video0 尚未生成程序过早地执行 VideoCapture 导致失败。解决写一个等待摄像头节点的循环在启动脚本中持续检查 /dev/video0 是否存在超时时间设 10 秒或者直接在代码里加一个 try-except 重试机制VideoCapture 失败后延时 2 秒重试。我一般推荐后者因为不依赖服务管理器配置。6. 二次开发方向从演示小车改成可工程化的视觉平台想把这套小车项目改造成真正能用的机器人按优先级来看第一件值得做的事是增加多目标颜色识别能力。当前 ColorDetector 类一次只能传入一组 HSV 阈值意味着只能跟踪一种颜色。你可以修改init接收一个阈值列表每个元素对应一种颜色然后在 detect 方法中遍历所有阈值分别生成掩码最后用 cv2.bitwise_or 合成总掩码。这样小车就能同时识别红绿蓝三色配合不同颜色对应不同转向策略可以做出按颜色走不同路径的复杂巡线任务。第二件推荐做的是把 PID 参数放到一个 JSON 配置文件里运行时通过终端动态修改避免每次调参都要编辑代码重启。调试中我最大的体会是PID 参数在纸面上看起来怎么调都有道理但实际跑起来光照变化、地面摩擦差异都会影响最优参数动态调参能力比参数本身更有价值。摄像头从 USB 升级到 CSI 相机时只需重写读取帧的部分把一个读取函数抽象为 CameraInterface 基类USB 摄像头和 CSI 摄像头各写一个子类主循环完全不需要改动。加超声波测距模块也一样在 config.py 中新增两个 GPIO 引脚在跟随模式中叠加一个防碰撞中断逻辑比在小车里硬编码距离判断要优雅得多。最后可以给程序加上远程控制能力用 Flask 起一个简单 Web Server把摄像头画面以 MJPEG 流推送到浏览器同时接收控制指令。这样在 PC 上就能看到小车视角不用每次调试都蹲在地上盯着 HDMI 屏幕。代码改造走完这几步这套视觉小车的可玩性会提升一个台阶也基本具备小型移动机器人底盘的雏形。回看我最初跑通这套代码时也是被 HSV 阈值的颜色玄学折腾了一整晚后来把调试脚本写进去从那以后我每次接手视觉相关项目都强制自己先把取色回调函数写完再写业务逻辑。这个习惯帮我省掉了太多靠猜的调试时间希望帮到你。本文还有配套的精品资源点击获取