简介:一套基于树莓派与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或者5,CSI摄像头或者免驱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, dtype=np.uint8) self.upper = np.array(upper, dtype=np.uint8) self.kernel = cv2.getStructuringElement(cv2.MORPH_RECT, (5, 5)) def find(self, bgr_frame, min_area=500): 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帧转成HSV,inRange一次性筛出落在阈值内的像素区域。开闭运算两个形态学操作是经验值,开运算去孤立亮点,闭运算填内部空洞,否则红色球体中间的高光点会把色块切碎。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_width=320, dead_zone=15, max_turn=0.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, width=320, roi_height=60): 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内存调大是因为摄像头编码和图像处理都吃GPU,64MB在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帧以上,转向响应快得多。
现象:运行时终端刷一堆RuntimeWarning,GPIO电平行为也偶发不对。 原因:进程没拿到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, mask=mask)) if cv2.waitKey(1) == ord('q'): break逻辑说明:这个工具把HSV上下限的六个值全部丢到trackbar上,调试时一边拖动一边看mask效果,直到目标变白、背景变黑。标定出来的数值可以直接写回color_track/params.py,整个流程基本靠视觉反馈,不用反复启动小车来回试。
参数说明:H通道范围是0到179,S和V是0到255,trackbar最大值要分别设成179/255/255,别统一用255,否则H的180到255区间永远选不中——这是新手标定最容易懵逼的地方。
转向控制方面,原来的FollowController只有静态的比例控制,小车在高速下会钟摆式振荡。我通常会改成带D项的PD控制,抑制摆动:
class PDSteer: def __init__(self, kp=0.35, kd=0.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),让控制逻辑算出来的转向量打印到终端,观察是否合理。确认控制方向正确再上电机,否则一次测试消耗的是好几颗螺丝和电池盒的寿命。我在这台上翻过几次车之后,再也没省过这一步。
希望帮到你。
本文还有配套的精品资源,点击获取