ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

基于Django的车道线检测系统:从模型接入到Web部署实战

基于Django的车道线检测系统:从模型接入到Web部署实战 简介这是一套面向深度学习与计算机视觉方向学习者、毕业设计或课程设计需求者的车道线检测系统完整项目包基于Python 3.8、Django与MySQL 5.7搭建采用YOLOv5作为核心检测算法可实现对车道线图像的自动识别与提取为自动驾驶环境感知提供参考方案。资源包共约2000个文件以xml标注数据、txt说明与配置、yaml参数文件、py源码为主另含sql数据库脚本、md说明文档及pdf、docx等论文与报告材料压缩包整体约977.99MB目录结构完整便于按模块查阅与二次开发。系统功能覆盖首页、图片检测、图片管理、视频检测、个人信息与用户管理配套可运行源码、数据库文件及LW文档能帮助读者快速理解YOLOv5训练流程、Django后端组织方式与前后端交互逻辑适合作为毕设、大作业或工程实训的起步项目。目前已有89人学习下载。1. 车道线检测系统为什么值得用 Django 落地很多人第一次接触车道线检测注意力全在模型上觉得只要把 U-Net 或者 LaneNet 跑通就万事大吉。真到要交付一个能给别人用的系统时才发现模型只是中间一环前端要传图、后端要调度推理、结果要可视化、历史记录要能查、不同用户上传的图片不能互相污染。这时候一个稳的后端框架比模型精度更影响交付体验。Django 在这里的价值不是潮而是它自带 ORM、Admin、用户体系和路由分层能把车道线检测从一段脚本变成一个可维护的 Web 服务。这篇笔记面向的是手里有 Python 基础、想做一个完整车道线检测系统的人从环境搭建、模型接入、接口设计到部署踩坑按我实际做过的路径讲一遍能照着复现也能看清边界。2. 车道线检测与 Django 的分工谁负责算谁负责管2.1 车道线检测这条链路到底在算什么车道线检测的本质是逐像素或逐实例地判断这个位置是不是车道线再把这些像素拟合成有几何意义的线。主流做法分两派一派是语义分割思路把车道线当成一类前景输出二值掩码代表是 U-Net、ENet 这类编码器-解码器结构另一派是实例/锚点思路直接回归出若干条车道线的参数或点序列代表是 LaneNet、Ultra-Fast-Lane-Detection。选哪派取决于你的场景如果只是做演示系统、图片来自固定视角语义分割足够训练数据好标注掩码可视化也直观如果要处理弯道、变道、多线并行的复杂路况实例方法更合适但标注成本和调参难度都上一个台阶。对绝大多数做课程设计或中小型项目的同学我一般建议先走语义分割 后处理拟合这条路。原因是模型结构简单、开源权重多、推理速度快而且后处理把掩码变成直线或曲线这一步本身就是很好的工程练习。深度学习在这里承担的是像素级分类Django 承担的是把这张分类结果变成用户能看懂的东西。两者边界清晰调试时不会互相甩锅。需要提醒的是车道线检测对输入分辨率比较敏感。分辨率太低细线直接消失太高显存和推理时间吃不消。常见做法是把输入缩放到 512×256 或 800×288 这类宽高比接近 3:1 的尺寸既保留横向细节又控制计算量。这个尺寸不是拍脑袋定的而是和你的相机内参、裁剪区域强相关后面讲预处理时会再展开。2.2 为什么用 Django 而不是 Flask 或 FastAPI这不是框架优劣之争而是项目形态决定的。车道线检测系统通常需要用户注册登录、上传图片、查看历史检测记录、管理自己的数据集、可能还要一个后台让管理员看统计。这些需求里用户体系和后台管理占了大头。Django 自带 auth 和 admin开箱即用省掉大量重复代码Flask 更轻但这些都要自己搭FastAPI 异步性能好适合高并发推理接口但它的强项在 API 层不在带页面的完整系统。如果你的系统只是给内部几个人用、只暴露一个推理接口FastAPI 确实更利落。但只要涉及多用户 页面 记录管理Django 的 MTV 分层会让代码组织清楚很多。我做过的一个对比是同样一个上传-推理-展示的流程Django 版本在用户隔离和记录查询上少写了将近一半的代码代价是启动稍重、学习曲线略陡。对新手来说Django 的一站式教程生态也更友好遇到问题搜得到答案。2.3 项目骨架怎么切分才不返工我习惯把整个系统切成四层从下往上依次是推理层纯模型不依赖 Web、服务层封装推理处理图片预处理和后处理、接口层Django 的 view 和 url、展示层模板或前端页面。这样切的好处是推理层可以单独用脚本测试不启动 Django 也能跑服务层可以被接口层和后台任务共用接口层只做参数校验和结果组装。目录结构上我会建一个独立的inference包放模型和权重一个detectorapp 放业务逻辑config放 settings 拆分。权重文件不要塞进 git用环境变量或本地路径配置。下面是一个最小可跑的骨架先建工程再建 app# 创建虚拟环境Python 3.9 以上都行我常用 3.10 python -m venv venv source venv/bin/activate # Windows 用 venv\Scripts\activate # 安装核心依赖torch 按你的 CUDA 版本去官网选对应命令 pip install django opencv-python numpy pillow pip install torch torchvision # 有 GPU 的话装对应 cuda 版本 # 建工程和 app django-admin startproject lane_system cd lane_system python manage.py startapp detectorstartproject生成的是工程配置startapp生成的是业务模块。这里有个新手常踩的点app 建完必须去settings.py的INSTALLED_APPS里注册否则迁移和模板都找不到。注册时写detector或detector.apps.DetectorConfig都行后者更规范。数据库先用默认的 SQLite 跑通等要上线再换 PostgreSQL迁移命令不用改。提示模型权重文件.pth 或 .onnx动辄几十上百 MB别放进代码仓库。用.gitignore排除部署时单独传或用对象存储挂载。3. 把训练好的车道线模型接进 Django从权重到接口3.1 模型加载放在哪才不会每次请求都重载这是 Django 接深度学习模型最经典的坑如果把torch.load()写在 view 函数里每来一个请求就加载一次权重几秒的延迟直接把体验拖垮。正确做法是在 Django 启动时加载一次全局复用。有两种常见方式一是用 AppConfig 的ready()方法二是用模块级单例。我更推荐 AppConfig因为它和 Django 生命周期绑定启动即加载且只执行一次。# detector/apps.py import os import torch from django.apps import AppConfig class DetectorConfig(AppConfig): default_auto_field django.db.models.BigAutoField name detector model None # 类属性全局持有 def ready(self): # 只在真正启动服务时加载避免迁移命令也触发 if os.environ.get(RUN_MAIN) ! true and not os.environ.get(SKIP_MODEL): from .inference.loader import load_model DetectorConfig.model load_model()ready()里那个RUN_MAIN判断很关键。Django 开发服务器默认开自动重载会启动两个进程不加判断模型会被加载两次显存直接翻倍。SKIP_MODEL环境变量是给迁移、测试这类不需要模型的命令留的后门。加载函数本身要处理设备选择# detector/inference/loader.py import torch def load_model(weight_pathweights/lane_unet.pth, deviceNone): if device is None: device cuda if torch.cuda.is_available() else cpu # 先建结构再加载权重map_location 保证 CPU 上也能读 GPU 存的权重 from .model import LaneUNet net LaneUNet(num_classes2) state torch.load(weight_path, map_locationdevice) net.load_state_dict(state) net.to(device) net.eval() # 推理模式关掉 dropout 和 batchnorm 更新 return neteval()这行千万别漏。训练时用的 BatchNorm 和 Dropout 在推理时行为不同忘了切模式同一张图两次推理结果都不一样排查起来非常玄学。map_location也是血泪经验在 GPU 机器上训练的权重拿到只有 CPU 的服务器上加载不加这个参数直接报错。3.2 图片预处理尺寸、归一化和通道顺序模型吃的是张量用户传的是各种尺寸、各种格式的图片中间这层预处理决定了推理稳不稳。标准流程是读图 → 转 RGB → 缩放到固定尺寸 → 转张量 → 归一化 → 加 batch 维度。每一步都有坑。# detector/inference/preprocess.py import cv2 import numpy as np import torch MEAN np.array([0.485, 0.456, 0.406], dtypenp.float32) STD np.array([0.229, 0.224, 0.225], dtypenp.float32) def preprocess(image_bytes, input_size(800, 288)): # 从字节流解码避免落盘cv2 默认 BGR arr np.frombuffer(image_bytes, np.uint8) img cv2.imdecode(arr, cv2.IMREAD_COLOR) if img is None: raise ValueError(图片解码失败可能不是有效图像) img cv2.cvtColor(img, cv2.COLOR_BGR2RGB) # 模型按 RGB 训练就必须转 img cv2.resize(img, input_size, interpolationcv2.INTER_LINEAR) img img.astype(np.float32) / 255.0 img (img - MEAN) / STD # 和训练时保持一致 tensor torch.from_numpy(img).permute(2, 0, 1).unsqueeze(0) return tensor, (img.shape[1], img.shape[0])cv2.imdecode直接从内存解码省去临时文件也避免并发时的文件名冲突。通道顺序是最容易翻车的地方OpenCV 读进来是 BGRPyTorch 生态的预训练权重基本都按 RGB 训练不转的话颜色通道错位模型输出会莫名其妙地差。归一化的均值和标准差必须和训练时完全一致训练用了 ImageNet 统计量就用上面这组训练时自己算过就换成自己的对不上精度会掉一截。permute(2,0,1)是把 HWC 转成 CHWunsqueeze(0)加 batch 维度这两步顺序别搞反。3.3 后处理把掩码变成能画出来的车道线模型输出的是概率图直接展示没意义要变成线。语义分割的后处理一般是取 argmax 得到二值掩码 → 按行扫描找每行的车道线中心点 → 用最小二乘或 RANSAC 拟合直线/曲线。这一步的鲁棒性直接决定最终效果。# detector/inference/postprocess.py import numpy as np def mask_to_lanes(mask, num_offsets20, min_pixels50): mask: HxW 的 0/1 数组返回左右车道线的点集 h, w mask.shape lanes [] # 从下往上按行采样底部车道线更可靠 rows np.linspace(h - 1, int(h * 0.4), num_offsets).astype(int) left_pts, right_pts [], [] for r in rows: cols np.where(mask[r] 0)[0] if len(cols) min_pixels: continue mid w / 2 left cols[cols mid] right cols[cols mid] if len(left) 0: left_pts.append((left.mean(), r)) if len(right) 0: right_pts.append((right.mean(), r)) if len(left_pts) 2: lanes.append(np.array(left_pts)) if len(right_pts) 2: lanes.append(np.array(right_pts)) return lanesnum_offsets控制采样行数太少拟合不稳太多引入噪声20 左右是个经验值。min_pixels是过滤掉那些只有零星几个像素的干扰行防止把噪点当车道线。按图像中线分左右是个简化假设适合单车道场景多车道并行时这套逻辑会失效需要改成聚类或实例分割。拟合阶段用np.polyfit做二次拟合能处理弯道直线场景一次就够。这些参数没有标准答案要拿你自己的测试图反复调调的时候把中间掩码也存下来看比盯着最终结果猜要快得多。3.4 接口设计一个上传接口要处理哪些边界接口层要做的事比想象中多校验文件类型、限制大小、处理并发、返回结构化结果。下面是一个基于 Django 函数视图的最小实现够用且好懂。# detector/views.py import json import numpy as np from django.http import JsonResponse from django.views.decorators.http import require_POST from django.core.files.uploadedfile import UploadedFile from .apps import DetectorConfig from .inference.preprocess import preprocess from .inference.postprocess import mask_to_lanes MAX_SIZE 10 * 1024 * 1024 # 10MB require_POST def detect(request): file: UploadedFile request.FILES.get(image) if file is None: return JsonResponse({code: 400, msg: 缺少 image 字段}, status400) if file.size MAX_SIZE: return JsonResponse({code: 413, msg: 图片超过 10MB}, status413) if file.content_type not in (image/jpeg, image/png): return JsonResponse({code: 415, msg: 仅支持 jpg/png}, status415) try: tensor, _ preprocess(file.read()) model DetectorConfig.model with torch.no_grad(): # 推理不建计算图省显存 logits model(tensor) mask logits.argmax(1).squeeze().cpu().numpy() lanes mask_to_lanes(mask) result [{points: pts.tolist()} for pts in lanes] return JsonResponse({code: 0, lanes: result}) except Exception as e: return JsonResponse({code: 500, msg: str(e)}, status500)torch.no_grad()是必须的否则每次推理都保留计算图显存会一路涨到 OOM。文件大小和类型校验放在最前面别等解码失败才报错那样用户看到的是 500 而不是明确的提示。异常捕获要返回结构化错误前端好处理。并发方面Django 开发服务器是单线程的生产环境用 gunicorn 多 worker 时要注意每个 worker 会各自加载一份模型显存占用是 worker 数乘以单份大小。GPU 显存紧张时要么减少 worker要么把推理拆成独立服务用队列调度。注意file.read()读的是整个文件到内存大图并发上传时内存压力不小。生产环境建议加限流或者用流式读取配合尺寸预检。4. 避坑与排查车道线检测系统上线前必须过的坎4.1 现象本地跑得好好的部署后推理结果全黑原因通常是设备不一致。本地有 GPU模型和输入张量都在 cuda 上服务器只有 CPU模型加载时map_location没设或设错张量还在 cuda运算直接报错或返回空。解决加载时统一用map_locationdevice预处理产出的张量也要.to(device)设备变量从一处取别在多个文件里各写各的。4.2 现象同一张图两次请求结果不一样原因基本是模型没切eval()模式BatchNorm 在推理时还在用当前 batch 的统计量Dropout 还在随机丢。解决加载后立刻net.eval()并且整个推理过程包在torch.no_grad()里。如果确认切了模式还不一致检查是不是有随机数据增强混进了推理路径。4.3 现象Django 启动时报显存不足但明明只加载了一个模型原因是开发服务器的自动重载起了两个进程ready()执行了两次。解决在ready()里判断os.environ.get(RUN_MAIN) true才加载或者干脆关掉自动重载--noreload。生产环境用 gunicorn 时worker 数乘以单份显存要算清楚别盲目开多 worker。4.4 现象上传中文文件名的图片报编码错误原因是部分环境下文件名编码处理不一致。解决不要用原始文件名做存储路径用 uuid 或时间戳重命名原始名只存数据库字段。这样既避开编码问题也防止路径穿越攻击。4.5 现象车道线在弯道处拟合出诡异的折线原因是采样行数太少或拟合阶数不匹配。直线场景用一次拟合弯道用二次或三次采样行数从 20 提到 40 试试。如果还是抖检查掩码本身是不是断断续续那说明模型精度不够得回去补训练数据而不是在后处理上硬凑。5. 让检测结果可复现一套自检脚本和参数固化习惯系统能跑起来只是及格能稳定复现才算过关。我一般会写一个脱离 Django 的自检脚本直接喂几张固定测试图把掩码、拟合线、耗时都打出来。这样模型换版本、参数调整时有个客观对照不至于感觉好像变好了。# scripts/selfcheck.py import time import numpy as np from detector.inference.loader import load_model from detector.inference.preprocess import preprocess from detector.inference.postprocess import mask_to_lanes CASES [test/straight.jpg, test/curve.jpg, test/night.jpg] def run(): model load_model() for path in CASES: with open(path, rb) as f: tensor, _ preprocess(f.read()) t0 time.time() with torch.no_grad(): mask model(tensor).argmax(1).squeeze().cpu().numpy() lanes mask_to_lanes(mask) dt (time.time() - t0) * 1000 print(f{path}: lanes{len(lanes)}, mask_ratio{mask.mean():.4f}, {dt:.1f}ms) if __name__ __main__: run()mask_ratio是掩码里前景像素占比这个值突然变大或变小往往意味着预处理或模型出了问题比肉眼看结果更早发现异常。耗时打出来是为了盯性能回归模型换版本后如果从 30ms 涨到 200ms得查原因。测试图要覆盖直道、弯道、夜间或逆光这三类是最容易暴露问题的场景。参数固化方面我的习惯是把所有可调参数集中到一个配置文件而不是散落在各个函数里。下面这张表是我常用的参数清单和取值参考具体数值要按你的数据和硬件调参数含义常用取值调整方向input_size模型输入尺寸(800, 288)显存不够就降宽num_offsets后处理采样行数20~40弯道多就加大min_pixels单行最小像素数30~80噪声多就加大poly_degree拟合阶数1 或 2直道用 1弯道用 2conf_threshold掩码二值化阈值0.5漏检多就降到 0.4这套自检加参数固化的习惯是我踩过改了后处理忘了改预处理的坑之后养成的。当时一个归一化均值写错模型输出整体偏移排查了大半天才发现是两处配置不一致。现在所有参数一处定义、多处引用改一个地方全局生效省心很多。希望帮到你。本文还有配套的精品资源点击获取
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进