
简介一套基于树莓派与Python/OpenCV的智能小车视觉控制项目围绕颜色识别、物体跟随与巡线三个核心功能展开适合机器人、嵌入式及计算机视觉初学者作为实战入门参考。压缩包共2个文件含一个.py主程序脚本与一张png效果示意图整体仅109KB代码精炼易读配合图片可快速理解摄像头实时取流、颜色阈值提取、目标定位与车辆运动控制的整体流程项目涉及cv2.inRange()颜色筛选、边缘检测、目标跟踪以及PID方向调节等关键技术并结合树莓派GPIO与USB摄像头实现从图像输入到电机驱动的闭环控制资源虽小但覆盖视觉、控制与硬件协同的常见实践要点。目前页面已有10623人浏览学习是比较热门的低成本视觉小车案例通过学习这份代码可以掌握OpenCV在树莓派上的基本调用方式也可将代码作为二次开发基础迁移应用到更多智能车、物联网或自动控制项目中。1. 树莓派颜色识别小车最大的坑是HSV阈值而不是电机如果照着默认HSV参数去跑这台树莓派小车大概率会在第一次室外试车时翻车——电机和GPIO的问题反而不大我最初栽在这里。这份代码做的是三件事颜色识别、色块跟随、巡线。三个玩法共用一套摄像头采集和电机控制链路用启动参数切换模式换玩法不用改主程序。它更像一个可以直接拿去改的竞赛小车底座而不是一次性玩具Demo。适合刚接触树莓派和OpenCV的开发者也适合给老小车做底盘控制的人。简单说它替你省掉了从摄像头到电机的骨架代码剩下的就是标定颜色、调PID、试跑。2. 先看懂整车设计硬件拓扑、目录结构与三模式切换2.1 三个模式共用一套处理链路拿到代码第一件事别急着跑先看清楚它怎么组织。整车结构是车头装摄像头底盘两个直流减速电机加一个万向轮L298N驱动板控制电机正反转。摄像头不断抓帧每帧先缩小到320x240再按启动参数进入颜色识别、目标跟随或者巡线模式。不管是哪个模式最后都输出一个“当前该往哪转”的值换算成左右轮差速后给到GPIO。也就是说图像处理和运动控制是解耦的改识别逻辑不会动电机代码改转向手感也不碰识别。链路大致是摄像头采集一帧 → 缩放成320x240 → 按模式做像素处理 → 得到目标中心或线偏差 → 比例换算成转向量 → 左右轮PWM输出。模式切换只发生在入口处后面的控制逻辑完全一样。2.2 硬件接线参考接线这块代码帮不了你只能看硬件。CSI摄像头插树莓派排线注意金手指朝外L298N的IN1到IN4分别接树莓派GPIO引脚ENA和ENB接PWM引脚用于调速。电源最关键的教训是电机驱动板不能从树莓派取电要单独一组电池或者稳压模块供电并和树莓派共地否则电机一启动就会把整板电压拉垮。硬件清单我习惯这样配树莓派4B或者5CSI摄像头或者免驱USB摄像头都可以L298N驱动板一块两个直流减速电机最好带编码器但不带也能跑18650电池组给驱动板供电NPN三极管模块给树莓派供电或者用降压模块降到5V——这部分看板子选型代码本身不关心电机怎么接只要GPIO电平能送到驱动板就行。2.3 目录结构与文件职责tree_pi_car/ ├── main.py # 入口解析启动参数选模式 ├── camera.py # 摄像头采集封装分辨率/帧率设置 ├── hsv_tool.py # HSV标定工具拖滑块找颜色阈值 ├── color_track/ │ ├── identify.py # 纯颜色识别输出色块中心 │ └── params.py # 默认HSV上下限集中管理 ├── follow/ │ └── chase.py # 目标跟随控制偏差转差速 ├── line/ │ └── line_follow.py # 巡线偏差提取 └── motor/ ├── driver.py # L298N GPIO初始化、PWM输出 └── pid.py # 简单PID/比例控制器main.py只做分发不写逻辑camera.py单独封装采集后续换USB摄像头只改文件头motor/driver.py把GPIO位定义集中在一起接线变了只改这一个文件。这样的分层对新手最友好排查问题也快——车不动先看driver颜色找不到先看params里的阈值。3. 核心模块拆解从一帧画面到一轮转向的完整链路3.1 颜色识别为什么必须用HSV而不是BGR颜色识别部分是整个项目的地基跟随和巡线都在这上面搭。代码里最终把RGB转成HSV再做筛选原因是BGR对光照太敏感同样一个红球室内灯光和太阳底下读出来的RGB差值非常大HSV把色相和亮度分开把亮度因素弱化后颜色判断会稳定得多。项目里默认写了一个ColorFinder类import cv2 import numpy as np class ColorFinder: def __init__(self, lower, upper): # lower/upper 是 HSV 三元组例如 ([0, 100, 80], [8, 255, 255]) self.lower np.array(lower, dtypenp.uint8) self.upper np.array(upper, dtypenp.uint8) self.kernel cv2.getStructuringElement(cv2.MORPH_RECT, (5, 5)) def find(self, bgr_frame, min_area500): hsv cv2.cvtColor(bgr_frame, cv2.COLOR_BGR2HSV) mask cv2.inRange(hsv, self.lower, self.upper) # 开运算去掉离散噪点闭运算把碎块拼成一个整体 mask cv2.morphologyEx(mask, cv2.MORPH_OPEN, self.kernel) mask cv2.morphologyEx(mask, cv2.MORPH_CLOSE, self.kernel) contours, _ cv2.findContours(mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) best None for c in contours: area cv2.contourArea(c) if area min_area: continue x, y, w, h cv2.boundingRect(c) cx x w // 2 cy y h // 2 if best is None or area best[0]: best (area, (cx, cy), (x, y, w, h)) return best逻辑说明先把BGR帧转成HSVinRange一次性筛出落在阈值内的像素区域。开闭运算两个形态学操作是经验值开运算去孤立亮点闭运算填内部空洞否则红色球体中间的高光点会把色块切碎。findContours配RETR_EXTERNAL只取外轮廓不会把内部高光轮廓也算进来。best里保存的是最大面积色块这样画面里出现多个相同颜色物体时小车追最近的目标。参数说明lower和upper是HSV的下限和上限H分量一般是0到179不是0到255标定时容易踩S和V的范围受光照影响最大室外调低S限制反而更稳。min_area是过滤小噪点的最小像素面积距离远目标变小这个值设太大会跟丢。3.2 目标跟随把偏差换算成左右轮差速识别到目标中心后跟随逻辑要做的事就是把它拉回画面正中间。代码里算的是像素偏差再把偏差归一化到-1到1直接叠加到左右轮速度上。这是最简单也最容易上手的做法先跑通再谈PID。class FollowController: def __init__(self, frame_width320, dead_zone15, max_turn0.6): self.center frame_width / 2 self.dead_zone dead_zone self.max_turn max_turn def steer(self, target_cx, target_area): if target_cx is None: return 0.0, 0.0 error target_cx - self.center if abs(error) self.dead_zone: turn 0.0 else: turn min(self.max_turn, max(-self.max_turn, error / self.center)) base_speed 1.0 left base_speed - turn right base_speed turn return left, right逻辑说明error为正表示目标在画面偏右turn为正时右轮减速左轮加速小车右转把目标拉回中间。error除以画面中心点宽度把像素差压到-1到1这样帧率或分辨率变了转向幅度的含义不变。dead_zone是死区目标偏差小于这个像素值时不动方向防止小车在目标静止时左右画龙。target_area这个参数目前还没用在速度上但预留了位置后续可以用面积估算距离、控制车速。参数说明dead_zone太小会转向抖动太大会让小车走S形我用15像素在320宽画面下效果还行max_turn限定最大转向量避免差速过猛导致小车原地打转。这种固定比例控制是简化版如果路线上目标一直在动可以把这里的turn反馈换成后面第5章的PD控制器。3.3 巡线模式ROI区域裁剪与中线偏移量计算巡线和颜色识别最大的不同是处理区域。整幅图里只有底部一小块是有效信息机器视觉里这叫ROI只处理这部分能大幅省CPU。代码默认取画面底部60像素高的长条巡线时车身面对的是正前方偏下的轨迹这个区域刚好覆盖车头位置。class LineFollower: def __init__(self, width320, roi_height60): self.width width self.roi_height roi_height def find_line_offset(self, frame): h, w frame.shape[:2] roi frame[h - self.roi_height:, :] gray cv2.cvtColor(roi, cv2.COLOR_BGR2GRAY) # 自适应阈值弱光环境比固定阈值稳 _, binary cv2.threshold(gray, 0, 255, cv2.THRESH_BINARY cv2.THRESH_OTSU) # 如果巡的是黑线需要反色让线变成255 # binary cv2.bitwise_not(binary) # 按列求平均亮度得到每一列被点亮了多少 col_mask cv2.reduce(binary, 0, cv2.REDUCE_AVG).flatten() cols np.where(col_mask 128)[0] if len(cols) 0: return None, None line_center int((cols[0] cols[-1]) / 2) offset line_center - w // 2 return line_center, offset逻辑说明取ROI后转灰度用OTSU自动阈值二值化这一步关键是不用手调亮度阈值黑线白线环境下都能自适应。col_mask是把二值图按列方向压缩成一行每一列的值是这一列的亮度均值线条所在列会因为轮廓被大面积点亮而超出128这个判定阈值。cols数组里第一个和最后一个元素分别对应线条左右边缘取它们的均值就是整条线在当前视野里的中心offset则是这个中心与画面中线的偏差。参数说明roi_height决定前瞻距离调大会提前看到弯道但远端线条会受地板杂色干扰调小更适应近距离反应32是小车低速时的稳妥起步值。OTSU如果环境光太乱也可以改回固定阈值但大多数场景下OTSU自动算出来的值比手调靠谱。4. 树莓派上跑起来环境搭建、启动参数与避坑记录4.1 系统装好、摄像头点亮、OpenCV就位树莓派上跑OpenCV最省心的方式是直接用系统包而不是pip硬装系统包带硬件加速和Raspberry Pi的摄像头驱动适配。先更新系统再拍两个包然后打开摄像头接口sudo apt update sudo apt install -y python3-pip python3-opencv sudo raspi-config # 在 Interface Options 里启用 Camera / Legacy Camera如果用的是CSI摄像头 # 启用后把GPU内存调到128MB以上否则大分辨率下容易卡 sudo reboot python3 -c import cv2; print(cv2.__version__)逻辑说明raspi-config这一步很多人跳过去CSI摄像头在树莓派上默认是关闭的不打开的话/dev/video0都不会出现。GPU内存调大是因为摄像头编码和图像处理都吃GPU64MB在320x240下勉强够但一旦接HDMI输出再加实时处理就可能抖动。如果坚持用pip装建议装opencv-python-headless而不是opencv-python后者会拉GUI依赖在树莓派上经常和系统包冲突。参数说明系统包版本一般低一点但稳定对这份代码足够。USB摄像头则不需要开Camera接口插上就能识别但顺序可能在video0和video1之间跳代码里camera.py加载的时候注意下索引。4.2 三种模式的启动方式主程序用命令行参数控制模式调试时比改代码快得多。搞清楚每个参数含义换颜色换环境就不用动文件# 颜色识别模式带HSV上下限 python3 main.py --mode color --lower 0 120 90 --upper 8 255 255 # 目标跟随模式 python3 main.py --mode follow # 巡线模式带ROI高度和线条颜色方向 python3 main.py --mode line --roi-height 60 --line-black逻辑说明--lower和--upper各带三个整数分别对应HSV的H、S、V这个值不是随便填的要用hsv_tool.py的滑动条先标定。--line-black表示巡的是黑色线条代码里会自动做一次反色默认是白线。帧率由camera.py里的分辨率决定如果跑起来转向一顿一顿优先把分辨率从640x480降到320x240不要急着优化算法。参数说明--mode三个值分别是color、follow、line对应前两章的三个模块。每次换镜头或换场地第一件事重新标定HSV而不是沿用上次的值这已经是我拆这类小车的习惯。4.3 避坑记录跑车一小时内最容易踩的五个坑现象CSI摄像头报了Camera module not enabled或者程序找不到/dev/video0。 原因树莓派的Camera接口默认没打开或者排线金手指方向插反了。 解决运行sudo raspi-config在Interface里启用Camera并重启插排线时金手指朝HDMI口方向。如果还是没有video0大概率是排线没插紧重插一次。这个坑几乎每个树莓派新手都会遇到不是代码问题。现象室内灯光下标定好的红色阈值到了室外全丢连目标都识别不出来。 原因HSV里的饱和度S和明度V受光照影响剧烈室内暖光和室外日光差异非常大。 解决别在室内标定室外用拿hsv_tool.py到实际场地当场标定。室外场景S下限可以往下降白天阳光强时V上限适当调低压制高光。从那以后我每次换场地都强制走一遍现场标定不再直接改阈值。现象电机一转整台树莓派掉电重启。 原因电机瞬间启动电流很大驱动板和树莓派共用电源时把电压拉垮到4.5V以下。 解决驱动板用独立电池组供电并且把驱动板的GND和树莓派GND连在一起共地才能保证GPIO控制信号电平一致。千万别从树莓派5V引脚直接给L298N供电那是给传感器留的电流余量不够电机塞牙缝的。现象识别帧率只有5帧每秒小车转向动作像慢放。 原因在640x480的整幅图上做中值滤波CPU被耗完了树莓派那块小芯片还得赶着跑Python。 解决camera.py里把分辨率压到320x240识别区域限定到ROI跟随模式只对目标可能出现的下半幅画面做处理。实测这三级跳水能把帧率从5帧拉到25帧以上转向响应快得多。现象运行时终端刷一堆RuntimeWarningGPIO电平行为也偶发不对。 原因进程没拿到root权限访问GPIO或者旧库对树莓派新内核有警告。 解决直接用sudo python3 main.py --mode follow跑一劳永逸。也可以在motor/driver.py初始化时加GPIO.setwarnings(False)但root才解决根本问题。5. 二次开发把颜色追踪和巡线改成你自己的节奏读代码阶段结束进入改造环节。这份代码最容易下手优化的两个位置是HSV阈值的管理方式以及固定转向控制升级成PD控制。先看HSV标定工具它直接决定你切换颜色时花多少调试时间import cv2 import numpy as np def nothing(x): pass cv2.namedWindow(setting) cv2.createTrackbar(H_min, setting, 0, 179, nothing) cv2.createTrackbar(S_min, setting, 0, 255, nothing) cv2.createTrackbar(V_min, setting, 0, 255, nothing) cv2.createTrackbar(H_max, setting, 179, 179, nothing) cv2.createTrackbar(S_max, setting, 255, 255, nothing) cv2.createTrackbar(V_max, setting, 255, 255, nothing) cap cv2.VideoCapture(0) # 摄像头索引按实际调整 while True: ret, frame cap.read() hsv cv2.cvtColor(frame, cv2.COLOR_BGR2HSV) h_min cv2.getTrackbarPos(H_min, setting) s_min cv2.getTrackbarPos(S_min, setting) v_min cv2.getTrackbarPos(V_min, setting) h_max cv2.getTrackbarPos(H_max, setting) s_max cv2.getTrackbarPos(S_max, setting) v_max cv2.getTrackbarPos(V_max, setting) lower np.array([h_min, s_min, v_min]) upper np.array([h_max, s_max, v_max]) mask cv2.inRange(hsv, lower, upper) cv2.imshow(mask, cv2.bitwise_and(frame, frame, maskmask)) if cv2.waitKey(1) ord(q): break逻辑说明这个工具把HSV上下限的六个值全部丢到trackbar上调试时一边拖动一边看mask效果直到目标变白、背景变黑。标定出来的数值可以直接写回color_track/params.py整个流程基本靠视觉反馈不用反复启动小车来回试。参数说明H通道范围是0到179S和V是0到255trackbar最大值要分别设成179/255/255别统一用255否则H的180到255区间永远选不中——这是新手标定最容易懵逼的地方。转向控制方面原来的FollowController只有静态的比例控制小车在高速下会钟摆式振荡。我通常会改成带D项的PD控制抑制摆动class PDSteer: def __init__(self, kp0.35, kd0.12): self.kp kp self.kd kd self.last_error 0.0 def update(self, error): derivative error - self.last_error self.last_error error turn self.kp * error self.kd * derivative return max(-0.8, min(0.8, turn))逻辑说明P项负责“往哪转”D项负责“转得多快”导数项对偏差变化进行阻尼小车在弯道里车身摆动的幅度会明显收敛。修改时只需要把FollowController里turn计算替换成PDSteer().update()motor/driver.py不用动。参数说明kp太大车子会左右窜kd太大会让转向反应迟钝。改造后验证方法我强烈建议先用视频回放而不是直接上真车。摄像头录一段目标移动的画面把读视频的代码暂时替换VideoCapture(0)让控制逻辑算出来的转向量打印到终端观察是否合理。确认控制方向正确再上电机否则一次测试消耗的是好几颗螺丝和电池盒的寿命。我在这台上翻过几次车之后再也没省过这一步。希望帮到你。本文还有配套的精品资源点击获取