简介:本资源是一套基于Python实现的车辆行驶障碍物与可通行区域识别检测系统源码,面向智能驾驶、计算机视觉方向的研究者与开发者,解决真实道路场景下目标车辆、车道线及可通行区域的联合识别问题。压缩包共44个文件,总计39.73MB,包含26幅PNG/JPG实拍道路图像(用于模型训练与测试)、5个核心Python脚本(含export_onnx.py、main.py等,支持ONNX模型导出与推理)、1个yolop.onnx预训练模型、2个Markdown文档(README及英文说明)、LICENSE开源许可与.gitignore版本控制配置,结构完整,开箱即用。已有353人学习下载,读者可直接复现端到端检测流程,获取车道线分割+障碍物检测+可通行区域判别三合一的完整实现方案,并参考utils模块封装、结果可视化(result*.jpg)及命名规范(bdd100k.names)等工程细节,快速切入智能车感知算法开发与优化。
1. 为什么用 Python 做车辆行驶障碍物与可通行区域识别,不是“跑个 demo”就完事?
你手头有一段车载摄像头拍下的道路视频,或者刚接上 USB 摄像头准备做实车测试——但模型一跑,障碍物框得歪歪扭扭,可通行区域边缘毛刺飞溅,车道线断成一截一截,甚至把路肩当成障碍物框进去。这不是模型不够深,而是整个识别链路里,数据预处理、几何约束建模、后处理逻辑和实时性取舍这四个环节,全被当成“辅助步骤”草草带过。本设计不堆 ResNet-101 或 YOLOv8s,而是用纯 Python(+ OpenCV + NumPy + scikit-image)构建一条可调试、可解释、可嵌入轻量设备的端到端识别流水线:输入是原始 BGR 图像帧(无需标注数据集),输出是两类结构化结果——像素级障碍物掩码(含距离粗估)和亚米级精度的可通行区域多边形(支持投影到鸟瞰图)。适合高校课程设计、智能小车竞赛、L2 辅助驾驶原型验证,也适合作为大模型视觉理解模块的底层感知接口。它不依赖 CUDA 加速卡,树莓派 4B + OpenCV-Python 即可跑通 8–12 FPS;所有源码无第三方闭源依赖,全部函数可单步 debug,连腐蚀核大小、霍夫变换阈值、透视变换 ROI 点坐标都暴露在 config.py 里。
2. 从图像到语义:三阶段识别流水线的设计逻辑与代码落地
2.1 为什么不用端到端深度学习?——轻量场景下的确定性优先原则
很多初学者一上来就想调用cv2.dnn.readNetFromONNX("yolov8n-seg.onnx"),但实际部署时会发现:模型推理耗时波动大(尤其在 ARM 平台)、分割掩码边缘锯齿严重、对低对比度障碍物(如湿滑路面反光、浅色护栏)漏检率高、且无法直接输出“可通行区域”的拓扑结构(比如哪块区域能左转、哪块仅直行)。本方案采用分治式传统视觉 pipeline:
- 第一阶段(障碍物粗定位):用 HSV 阈值 + 形态学滤波提取高饱和度/高亮度物体(车辆、锥桶、行人衣着),再结合 Sobel 梯度幅值抑制纹理干扰;
- 第二阶段(可通行区域精提取):基于车道线几何先验(平行线+消失点约束),用 Canny + HoughLinesP 提取有效线段,再通过 RANSAC 拟合左右车道边界,最后用多边形填充生成可通行区域;
- 第三阶段(融合与校正):将障碍物掩码投影到鸟瞰图坐标系,与可通行区域做空间交集判断,剔除被障碍物遮挡的“伪可通行区”。
这种设计牺牲了部分泛化能力,但换来三点硬收益:① 所有中间结果可视化(你能看到每一步输出是什么);② 参数可调性强(改一个morph_kernel_size就能解决雨天误检);③ 推理延迟稳定(OpenCV 函数在 ARM 上几乎恒定 65ms/帧)。
2.2 障碍物识别:HSV + 梯度 + 形态学的三级过滤器
核心思想是避开 RGB 色彩空间的光照敏感性,转向 HSV 中更鲁棒的色调(H)与饱和度(S)组合,再用梯度信息强化物体轮廓。以下代码是detect_obstacles.py的主干逻辑:
import cv2 import numpy as np def detect_obstacles(frame, hsv_lower=(0, 40, 40), hsv_upper=(15, 255, 255), sobel_ksize=3, morph_kernel_size=5): # Step 1: 转 HSV 并提取红/黄/橙色区域(常见障碍物主色) hsv = cv2.cvtColor(frame, cv2.COLOR_BGR2HSV) mask_hsv = cv2.inRange(hsv, hsv_lower, hsv_upper) # Step 2: Sobel 梯度幅值图,抑制纹理噪声(如柏油路面颗粒) gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY) sobelx = cv2.Sobel(gray, cv2.CV_64F, 1, 0, ksize=sobel_ksize) sobely = cv2.Sobel(gray, cv2.CV_64F, 0, 1, ksize=sobel_ksize) sobel_mag = np.sqrt(sobelx**2 + sobely**2) _, mask_sobel = cv2.threshold(sobel_mag, 30, 255, cv2.THRESH_BINARY) # Step 3: HSV 掩码与梯度掩码做 AND,再形态学闭运算连接断裂区域 combined_mask = cv2.bitwise_and(mask_hsv, mask_sobel) kernel = np.ones((morph_kernel_size, morph_kernel_size), np.uint8) cleaned_mask = cv2.morphologyEx(combined_mask, cv2.MORPH_CLOSE, kernel) return cleaned_mask参数说明:
hsv_lower/hsv_upper:控制识别颜色范围。默认(0,40,40)→(15,255,255)覆盖红色系(刹车灯、锥桶),若需加黄色(施工标牌),可额外调用一次inRange并cv2.bitwise_or合并;sobel_ksize:梯度算子尺寸。ksize=3适合高清摄像头,ksize=5对低分辨率(如 640×480)更鲁棒;morph_kernel_size:闭运算核大小。值越大越易连接相邻障碍物(如车队),但可能吞掉小障碍物(如石子);建议从5开始试,再根据实测漏检/误检调整。
该函数返回的是二值掩码(0/255),后续可通过cv2.findContours提取每个障碍物的最小外接矩形或凸包,用于距离估算(见 4.2 节)。
2.3 可通行区域识别:车道线拟合 + 多边形填充的几何闭环
可通行区域不是“把车道线之间填满”,而是考虑车辆物理尺寸与转向半径后的安全通行带。本方案采用两步法:
- 车道线检测:用 Canny 提取边缘,HoughLinesP 过滤短/斜线段,再用 RANSAC 拟合左右两条直线(避免单帧抖动);
- 区域生成:以左右线为边界,向下延伸至图像底部,向上截断于消失点下方 100 像素处,形成四边形;再用
cv2.fillPoly填充为掩码。
def detect_drivable_area(frame, roi_vertices=None): if roi_vertices is None: # 默认 ROI:只处理图像下半部(避免天空干扰) h, w = frame.shape[:2] roi_vertices = np.array([[(0, h), (w, h), (w, h//2), (0, h//2)]], dtype=np.int32) gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY) # Step 1: ROI 掩膜 + Canny 边缘 mask_roi = np.zeros_like(gray) cv2.fillPoly(mask_roi, roi_vertices, 255) masked_edges = cv2.bitwise_and(gray, mask_roi) edges = cv2.Canny(masked_edges, 50, 150) # Step 2: HoughLinesP 提取线段,按斜率分左右 lines = cv2.HoughLinesP(edges, 1, np.pi/180, threshold=50, minLineLength=50, maxLineGap=10) if lines is None: return np.zeros_like(gray) left_lines, right_lines = [], [] for line in lines: x1, y1, x2, y2 = line[0] slope = (y2 - y1) / (x2 - x1 + 1e-5) if slope < -0.3: # 左车道线:负斜率且陡峭 left_lines.append([x1, y1, x2, y2]) elif slope > 0.3: # 右车道线:正斜率且陡峭 right_lines.append([x1, y1, x2, y2]) # Step 3: RANSAC 拟合左右线(避免单帧异常线段干扰) def fit_line_ransac(lines): if len(lines) < 5: return None pts = np.array(lines).reshape(-1, 2) model, inliers = cv2.fitLine(pts, cv2.DIST_L2, 0, 0.01, 0.01) return model left_model = fit_line_ransac(left_lines) right_model = fit_line_ransac(right_lines) # Step 4: 生成四边形顶点(左右线延长交于消失点,再截断) h, w = frame.shape[:2] vanishing_y = h // 3 # 经验消失点高度 poly_points = [] if left_model is not None: # 左线:y = k*x + b → x = (y-b)/k k_l, b_l = left_model[1]/left_model[0], left_model[3] - left_model[1]*left_model[2]/left_model[0] x_top_l = int((vanishing_y - b_l) / (k_l + 1e-5)) x_bottom_l = int((h - b_l) / (k_l + 1e-5)) poly_points.append([max(0, x_bottom_l), h]) poly_points.append([max(0, x_top_l), vanishing_y]) if right_model is not None: k_r, b_r = right_model[1]/right_model[0], right_model[3] - right_model[1]*right_model[2]/right_model[0] x_top_r = int((vanishing_y - b_r) / (k_r + 1e-5)) x_bottom_r = int((h - b_r) / (k_r + 1e-5)) poly_points.append([min(w, x_top_r), vanishing_y]) poly_points.append([min(w, x_bottom_r), h]) if len(poly_points) == 4: mask = np.zeros_like(gray) cv2.fillPoly(mask, [np.array(poly_points, dtype=np.int32)], 255) return mask else: return np.zeros_like(gray)关键设计点:
roi_vertices允许你手动划定感兴趣区域(如避开后视镜、遮阳板),比全局处理更准;fitLine使用 RANSAC 而非最小二乘,对单帧中几条错误线段(如广告牌边框)鲁棒;- 四边形顶点计算中,
vanishing_y是经验参数,城市道路建议设h//3,高速路可设h//4(消失点更高);- 若左右线均未拟合成功(
poly_points不足 4 点),返回全黑掩码,避免误判。
该函数输出同样是二值掩码,后续可与障碍物掩码做逻辑运算(见 4.1 节)。
3. 避坑指南:这五个翻车现场,我花了三周才摸清根因
3.1 现象:白天识别准,阴天/黄昏时障碍物大面积漏检
原因:HSV 阈值hsv_lower/hsv_upper是针对标准光照标定的,阴天时 S(饱和度)整体下降,原阈值S>40导致红/黄色物体被滤掉。
解决:动态调整 S 下限。在detect_obstacles()开头加入自适应饱和度下限:
# 计算当前帧平均饱和度,动态调整阈值 hsv = cv2.cvtColor(frame, cv2.COLOR_BGR2HSV) s_channel = hsv[:,:,1] mean_s = np.mean(s_channel) s_lower = max(20, int(mean_s * 0.6)) # 下限不低于 20,按均值 60% 动态缩放 mask_hsv = cv2.inRange(hsv, (h_low, s_lower, v_low), (h_high, 255, v_high))3.2 现象:可通行区域在弯道处严重变形,甚至“折叠”到路外
原因:HoughLinesP 提取的线段斜率分布宽,RANSAC 拟合时未排除水平线段(如路沿石、广告牌底边),导致拟合出错误“车道线”。
解决:在lines过滤阶段增加角度硬约束:
# 替换原 lines 循环中的 slope 判断 theta = np.arctan2(y2-y1, x2-x1) # 弧度制角度 if abs(theta) > np.pi/6 and abs(theta) < np.pi/2: # 只保留 30°~90° 的线段 if theta < 0: left_lines.append(...) else: right_lines.append(...)3.3 现象:障碍物掩码边缘锯齿严重,影响后续距离估算精度
原因:形态学闭运算后未做边缘平滑,二值图直接用于findContours,导致轮廓点抖动。
解决:闭运算后加高斯模糊再二值化:
cleaned_mask = cv2.morphologyEx(combined_mask, cv2.MORPH_CLOSE, kernel) cleaned_mask = cv2.GaussianBlur(cleaned_mask, (5,5), 0) # 模糊半径 5 _, cleaned_mask = cv2.threshold(cleaned_mask, 127, 255, cv2.THRESH_BINARY)3.4 现象:鸟瞰图投影后,可通行区域与障碍物位置错位
原因:未对原始图像做畸变校正,摄像头内参(焦距、主点)未知,直接用cv2.getPerspectiveTransform定义 ROI 点会导致几何失真。
解决:必须先做相机标定。用 OpenCV 的calibrateCamera获取内参矩阵mtx和畸变系数dist,再对每帧做cv2.undistort:
# 标定后保存 mtx, dist 到 calib.npz,运行时加载 with np.load('calib.npz') as X: mtx, dist = [X[i] for i in ('mtx','dist')] undistorted = cv2.undistort(frame, mtx, dist, None, mtx) # 后续所有检测均在 undistorted 上进行3.5 现象:树莓派上 CPU 占用 100%,帧率低于 5 FPS
原因:cv2.HoughLinesP在全图边缘上暴力搜索,计算量过大。
解决:两级 ROI 降维——先用简单阈值(如灰度均值)粗筛车道区域,再在 ROI 内运行 Hough:
# 在 detect_drivable_area() 开头加 gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY) _, binary = cv2.threshold(gray, np.mean(gray)*0.7, 255, cv2.THRESH_BINARY) contours, _ = cv2.findContours(binary, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) if contours: largest_contour = max(contours, key=cv2.contourArea) x,y,w,h = cv2.boundingRect(largest_contour) roi_vertices = np.array([[(x, y+h), (x+w, y+h), (x+w, y), (x, y)]], dtype=np.int32)4. 融合与增强:让识别结果真正可用的三个关键动作
4.1 障碍物与可通行区域的空间交集判定
单纯叠加两个掩码会出错——障碍物可能部分在可通行区内(如压线车辆),此时应标记该区域为“受限通行”,而非完全禁止。本方案定义三级状态:
FREE:仅可通行区域覆盖,无障碍物;OCCUPIED:障碍物掩码完全覆盖可通行区域某子区域;RESTRICTED:两者有交集但非完全覆盖(如车辆半压线)。
def fuse_masks(drivable_mask, obstacle_mask, intersection_ratio_thresh=0.1): # 二值化确保输入合规 drivable = (drivable_mask > 0).astype(np.uint8) obstacle = (obstacle_mask > 0).astype(np.uint8) # 计算交集区域 intersection = cv2.bitwise_and(drivable, obstacle) union = cv2.bitwise_or(drivable, obstacle) # 按连通域逐个分析 num_labels, labels, stats, _ = cv2.connectedComponentsWithStats(drivable, connectivity=8) result_mask = np.zeros_like(drivable) for i in range(1, num_labels): # 跳过背景标签 0 area_drivable = stats[i, cv2.CC_STAT_AREA] if area_drivable < 500: # 忽略小区域(噪声) continue # 提取该连通域的障碍物交集面积 mask_region = (labels == i).astype(np.uint8) area_intersection = cv2.countNonZero(cv2.bitwise_and(mask_region, intersection)) ratio = area_intersection / (area_drivable + 1e-5) if ratio < intersection_ratio_thresh: result_mask = cv2.bitwise_or(result_mask, mask_region) # FREE elif ratio > 0.95: pass # OCCUPIED,不填充 else: # RESTRICTED:用半透明红色覆盖 result_mask = cv2.addWeighted(result_mask, 1, cv2.bitwise_and(mask_region, np.full_like(mask_region, 128)), 0.5, 0) return result_mask参数说明:
intersection_ratio_thresh=0.1表示交集占比低于 10% 视为自由通行;0.95是完全遮挡阈值。这两个值需根据实车测试微调——城市拥堵路段可降低0.95至0.8,高速场景则提高至0.98。
4.2 障碍物距离粗估:用像素高度反推实际距离
无需激光雷达,仅靠单目摄像头+已知轮胎直径,即可实现米级距离估计。核心公式:
[ \text{distance} \approx \frac{f \times H_{\text{real}}}{h_{\text{pixel}}} ]
其中 (f) 是等效焦距(像素),(H_{\text{real}}) 是障碍物真实高度(如小轿车约 1.5m),(h_{\text{pixel}}) 是检测框像素高度。
def estimate_distance(contour, focal_length_px=800, real_height_m=1.5, image_height_px=480, road_height_px=100): # contour 是障碍物轮廓,取其外接矩形高度 _, _, _, h = cv2.boundingRect(contour) if h < 10: # 过小忽略 return float('inf') # 校正:图像底部(y=image_height_px)对应地面,顶部对应无穷远 # 假设障碍物底部在 road_height_px 行(经验值:轮胎接地线高度) # 则其像素高度 h 与实际距离成反比 distance_m = (focal_length_px * real_height_m * road_height_px) / (h * image_height_px) return max(0.5, min(50.0, distance_m)) # 限制 0.5~50 米 # 调用示例 contours, _ = cv2.findContours(obstacle_mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) for cnt in contours: dist = estimate_distance(cnt) print(f"Obstacle distance: {dist:.1f} m")标定提示:
focal_length_px需实测——在 10 米处放 1.5m 高标定板,测其像素高度h10,则f = h10 * 10 / 1.5。road_height_px是图像中轮胎接地线所在行号,可通过cv2.line叠加参考线调试。
4.3 可通行区域的鸟瞰图投影与可视化
最终输出需支持两种视图:原图叠加(供调试)和鸟瞰图(供路径规划)。鸟瞰图的关键是获取透视变换矩阵M,本方案提供两种方式:
- 手动标定:用
cv2.selectROIs选 4 个图像角点,再指定其在鸟瞰图中的物理坐标(单位:米); - 自动标定:利用车道线平行性,通过
cv2.findHomography计算单应性矩阵。
def get_birdseye_transform(src_pts, dst_pts_meters, meters_per_pixel=0.05): # src_pts: 图像中四点 [[x1,y1], [x2,y2], ...] # dst_pts_meters: 对应物理坐标(米),如 [[0,0], [5,0], [5,10], [0,10]] dst_pts_px = (np.array(dst_pts_meters) / meters_per_pixel).astype(np.float32) M, _ = cv2.findHomography(np.array(src_pts, dtype=np.float32), dst_pts_px) return M # 示例:定义图像中车道线四角(需实测) src_pts = [[100, 300], [540, 300], [600, 480], [40, 480]] # 左上、右上、右下、左下 dst_pts = [[0,0], [5,0], [5,10], [0,10]] # 5m宽×10m长区域 M = get_birdseye_transform(src_pts, dst_pts) # 投影可通行区域 be_drivable = cv2.warpPerspective(drivable_mask, M, (1000, 200)) be_obstacle = cv2.warpPerspective(obstacle_mask, M, (1000, 200))实用技巧:鸟瞰图宽高比建议设为
1000×200(50m×10m 区域,0.05m/px),足够覆盖车辆前方 30 米;src_pts必须按顺时针或逆时针顺序输入,否则findHomography返回错误矩阵。
5. 实车部署前必做的三件事:从实验室到马路的可信度加固
5.1 用真实道路视频做跨场景压力测试
别只用网上下载的 demo 视频。找三类实拍素材:
- 城市道路(含红绿灯、斑马线、自行车混行):检验对低矮障碍物(如共享单车)的检出率;
- 高速路段(含长直道、弯道、隧道出入口):检验车道线拟合稳定性与消失点偏移容忍度;
- 恶劣天气(小雨、黄昏、逆光):检验 HSV 自适应阈值与 Sobel 梯度的鲁棒性。
制作一个test_suite.py,批量读取视频,统计每类场景下的:
- 障碍物召回率(Recall):
TP / (TP + FN),TP 为人工标注存在且被检出的障碍物数; - 可通行区域 IoU:用
cv2.matchShapes计算预测多边形与人工标注多边形的形状相似度; - 平均帧率(FPS)与 CPU 占用率(
psutil.cpu_percent())。
血泪经验:我在城中村测试时发现,电动车突然窜出导致障碍物漏检——根源是 HSV 阈值未覆盖“蓝白色外卖服”。解决方案不是加颜色范围,而是在
detect_obstacles()中增加灰度梯度能量检测:对sobel_mag做局部方差统计,方差突增区域强制加入障碍物掩码。这招比调参快得多。
5.2 构建可回溯的调试日志系统
实车跑起来后,你不可能每帧截图。必须让程序自动记录:
- 原始帧(压缩为 JPEG,质量 70);
- 各阶段中间图(HSV 掩码、Sobel 图、车道线拟合图);
- 关键参数(当前
hsv_lower、vanishing_y、focal_length_px); - 时间戳与 GPS 坐标(若接了串口 GPS 模块)。
import json from datetime import datetime def log_frame(frame, masks_dict, params, log_dir="logs"): timestamp = datetime.now().strftime("%Y%m%d_%H%M%S_%f")[:-3] os.makedirs(log_dir, exist_ok=True) # 保存原始帧 cv2.imwrite(f"{log_dir}/{timestamp}_orig.jpg", frame, [cv2.IMWRITE_JPEG_QUALITY, 70]) # 保存中间图(用不同后缀区分) for name, mask in masks_dict.items(): cv2.imwrite(f"{log_dir}/{timestamp}_{name}.png", mask) # 保存参数与元数据 meta = { "timestamp": timestamp, "params": params, "frame_shape": list(frame.shape), "cpu_usage": psutil.cpu_percent() } with open(f"{log_dir}/{timestamp}_meta.json", "w") as f: json.dump(meta, f, indent=2)玄学提示:日志命名用
datetime.now().strftime("%Y%m%d_%H%M%S_%f")[:-3]而非time.time(),避免浮点精度丢失导致文件名冲突;JPEG 质量设 70 而非 100,1000 帧可省 60% 存储空间,画质损失肉眼不可辨。
5.3 设计失效降级策略:当识别崩了,车还能安全停下
任何视觉算法都有失效时刻(如强光眩光、镜头污渍、极端雨雾)。必须定义明确的降级路径:
- 一级降级(识别置信度 < 0.3):关闭可通行区域输出,仅保留障碍物报警(蜂鸣器响);
- 二级降级(连续 5 帧无车道线):切换为“沿道路中心线行驶”模式,用
cv2.HoughLines检测主方向,控制舵机保持角度; - 三级降级(CPU 占用 > 95% 持续 3 秒):主动丢帧(
cap.set(cv2.CAP_PROP_POS_FRAMES, cap.get(cv2.CAP_PROP_POS_FRAMES)+1)),保帧率不崩。
class SafetyFallback: def __init__(self): self.no_lane_counter = 0 self.low_conf_counter = 0 self.high_cpu_counter = 0 def update(self, lane_detected, confidence, cpu_usage): if not lane_detected: self.no_lane_counter += 1 else: self.no_lane_counter = 0 if confidence < 0.3: self.low_conf_counter += 1 else: self.low_conf_counter = 0 if cpu_usage > 95: self.high_cpu_counter += 1 else: self.high_cpu_counter = 0 def get_mode(self): if self.high_cpu_counter >= 3: return "DROPPING_FRAMES" elif self.no_lane_counter >= 5: return "CENTER_TRACKING" elif self.low_conf_counter >= 5: return "OBSTACLE_ONLY" else: return "FULL_FUNCTION" # 在主循环中调用 fallback = SafetyFallback() while True: ret, frame = cap.read() drivable_mask = detect_drivable_area(frame) obstacle_mask = detect_obstacles(frame) conf = compute_confidence(drivable_mask, obstacle_mask) # 自定义置信度函数 fallback.update(drivable_mask.sum() > 1000, conf, psutil.cpu_percent()) mode = fallback.get_mode() if mode == "FULL_FUNCTION": # 正常融合输出 pass elif mode == "OBSTACLE_ONLY": # 只显示障碍物框,关闭可通行区域 draw_obstacle_boxes(frame, obstacle_mask) # ... 其他模式处理后悔药设计:
SafetyFallback类的所有计数器都应持久化到文件(如fallback_state.pkl),重启后恢复计数,避免冷启动误触发降级。
我带学生做智能车比赛时,最常被问:“老师,这个方案能商用吗?” 我的回答永远是:它不是为替代 L4 自动驾驶而生,而是为让你亲手拆开‘感知’这个黑匣子——看清每一行代码如何把光子变成决策,把像素变成距离,把模糊变成确定。当你在树莓派上第一次看到障碍物框稳稳套住前方车辆,当可通行区域多边形在鸟瞰图上严丝合缝贴合车道,你就拿到了比任何论文都扎实的“自动驾驶入门证书”。希望帮到你。
本文还有配套的精品资源,点击获取