☰
车道线检测系统实战:U-Net分割与Django部署
2026/10/2 10:05:16 网站建设 项目流程

简介:这是一份基于深度学习的车道线检测系统毕业设计项目,面向需要完成毕设、课程设计或工程实训的初学者与进阶开发者。系统采用Python 3.8、Django框架与MySQL 5.7构建,以YOLOv5为核心算法,通过卷积神经网络对车道线图像进行特征提取与识别,实现首页、图片检测、图片管理、视频检测、个人信息、用户管理等完整功能模块。资源包共2000个文件,整体约978MB,内容主要包括Python源码、YOLOv5模型配置、图像训练与标注数据、Shell脚本、SQL数据库文件以及开题报告和设计文档等,目录结构清晰,便于直接运行与二次开发。目前已有89人学习下载。项目附带可运行源码、SQL文件、开题报告、任务书和设计文档,材料完整,能帮助读者快速理解系统架构与实现思路,是一套适合用于毕业设计答辩和实际项目起步的完整参考方案。

1. 拿到这个“车道线检测系统”压缩包,先别急着跑训练

做自动驾驶和ADAS相关毕设、课题的人,大概率都下过类似“基于深度学习的车道线检测系统”的压缩包。解压之后通常是一个Django工程、一个训练脚本、一堆权重文件。很多人第一反应是把它当目标检测任务,拿YOLO直接去画框,跑出来的效果惨不忍睹。这不是模型不行,而是任务定错了。车道线检测的本质是像素级分割,框住一条线是没意义的,关键是把每个像素准确地分类成“车道线”或“背景”,再在分割结果基础上拟合出车道线方程。这套系统的核心价值就在这条pipeline上:用深度学习模型做分割,用Django做Web封装,用户上传一张车道图片,页面直接返回画好线的结果。它适合两类人:一类是深度学习入门者想做完整的图像分割落地项目,另一类是Django开发者想了解怎么把一个PyTorch模型接到Web后端里。

我基于标题和行业里最常见的工程实践,把这个系统的设计思路、关键代码、参数配置和踩坑记录完整拆一遍,你可以直接照着复现到自己的项目里。

2. 模型选型与训练:车道线检测为什么选分割网络而不是目标检测

2.1 分割网络对比:U-Net、SegNet还是轻量自定义模型

车道线的视觉特征很特殊:细长、连续、颜色单一、背景复杂。拿检测框去框一条细线,框的宽高比极端,回归框参数很容易抖动。所以主流方案里,车道线检测基本都用分割网络。U-Net是这类任务里最稳的选择,编码器下采样提取语义特征,解码器上采样恢复空间分辨率,加上skip connection把底层细节传回来,对小目标、细长结构的还原能力好,显存占用也不高。SegNet也行,但它保留的是池化索引,细节恢复能力比skip connection弱一点,车道线这种细结构容易断。

常见做法是在U-Net基础上做轻量化改造。编码器用预训练的ResNet34或者MobileNetV3,解码器保持U-Net结构但通道数减半。模型输出层用1x1卷积把通道压成1,经过sigmoid得到每个像素属于车道线的概率。用ResNet34做backbone,输入分辨率512x288(车道图片一般是宽幅),训练显存占用在8GB左右,推理一张大约30ms,这个量级放在Django后端完全够用。

训练数据一般用TuSimple数据集,它有3626张训练图片和2782张测试图片,标注是车道线坐标点。处理方式是先按标注点把线画成掩膜,生成对应的分割标签,再交给模型做二分类训练。自己采集数据的话,标注用LabelMe或CVAT画多边形,比逐点标注高效很多。数据量建议至少2000张原始图,每张再做平移、旋转、亮度扰动增强。

2.2 损失函数与训练参数:Dice Loss和BCE结合,学习率用余弦退火

训练分割模型时,正负样本极度不平衡——车道线像素占整张图比例不到5%。只用BCE(Binary Cross Entropy)会让模型倾向把所有像素预测成背景,因为这样loss已经很低了。解决方案是把Dice Loss和BCE按比例叠加。Dice Loss直接优化分割结果的区域重叠度,对小目标非常友好,但单独用容易震荡,和BCE混着用刚好互补。我自己实验下来,权重配比Dice:BCE = 1:1效果最稳,类别极不平衡时可以调到2:1。

训练脚本核心参数如下:

# train_seg.py import torch from torch.utils.data import DataLoader from torch.optim.lr_scheduler import CosineAnnealingLR model = LaneSegModel(backbone="resnet34") # 编码器用ResNet34,解码器自建 dataset = LaneDataset(img_dir="tu_simple/train", mask_dir="tu_simple/annotations", img_size=(512, 288)) # 宽512,高288,保留车道线的横向长条特征 dataloader = DataLoader(dataset, batch_size=8, # batch size按显存8GB量级开的 shuffle=True, num_workers=4) optimizer = torch.optim.AdamW(model.parameters(), lr=1e-3, weight_decay=1e-4) scheduler = CosineAnnealingLR(optimizer, T_max=60) # 60个epoch内余弦退火到最低点 def total_loss(pred, target): bce = torch.nn.functional.binary_cross_entropy_with_logits(pred, target) dice = dice_loss(torch.sigmoid(pred), target) # dice_loss需要自己实现或引用现成库 return bce + dice # 默认1:1,不平衡可调成2:1 for epoch in range(60): model.train() for imgs, masks in dataloader: pred = model(imgs) loss = total_loss(pred, masks) optimizer.zero_grad() loss.backward() optimizer.step() scheduler.step()

这段代码里有几个关键设计。第一,输入尺寸设成512x288而不是正方形,是因为车道图片都是横向宽幅的,强行resize成方形会让车道线被压扁,细节丢失。第二,优化器选了AdamW而不是SGD,AdamW学习率不用精调,收敛快,适合快速验证模型有效性的阶段;如果追求极致精度再换SGD加动量。第三,余弦退火调度器把学习率从1e-3平滑降到接近0,比固定学习率在分割任务上能多提升2到3个点mIoU。

训练结束后只保存state_dict,不要整个模型对象一起存,这样模型文件小,加载也快,后面接Django时也方便。

torch.save(model.state_dict(), "lane_seg_resnet34.pth")

推理时加载也要保持同样的键名结构。

2.3 置信度阈值:预测结果里最容易乱调的那个参数

分割模型的输出会经过sigmoid函数压缩到0到1之间,每个像素的值代表该像素属于车道线的概率。要把它转成二值掩膜,需要设一个阈值。这个阈值非常敏感,设高了线会断成虚线,设低了会把路面裂缝、轮胎印也误判成车道线。我一般设在0.4到0.5之间,具体要看模型训练的收敛程度。如果训练充分、验证集mIoU在85以上,阈值0.4就够了。如果模型欠拟合,把阈值提到0.6反而能过滤掉很多低置信度的误检。

3. Django后端集成:把PyTorch模型接到Web端的完整流程

3.1 Django项目初始化:创建app和设计的目录结构

Django后端接模型不是直接把推理代码扔进views.py就完事了。模型加载一次要几百毫秒到几秒,每次请求都重新加载模型,Web服务直接卡死。正确做法是在Django启动时加载模型到内存,请求来了只做forward传播,整个过程要处理好模型常驻、图片上传、结果返回三条链路。

先按标准流程创建项目和应用:

django-admin startproject lane_detection_project cd lane_detection_project python manage.py startapp detector

目录规划上有两个点要注意。第一,模型权重文件放到detector/weights/目录下,用settings配置路径,不要硬编码在代码里。第二,前端模板里用的静态资源走Django的static机制,上传的图片放到media/目录。这样的结构在本地开发和部署到服务器时都不会乱。

3.2 推理模块封装:模型加载、预处理、后处理三段式

推理逻辑要独立成模块,不要直接写在视图函数里,方便单元测试,也方便以后换ONNX或其他推理引擎时只改一个文件。

# detector/inference.py import torch import numpy as np import cv2 class LaneDetector: def __init__(self, weights_path, device="cuda" if torch.cuda.is_available() else "cpu"): self.device = device self.model = LaneSegModel(backbone="resnet34") self.model.load_state_dict(torch.load(weights_path, map_location=device)) self.model.to(device) self.model.eval() def preprocess(self, image): # opencv读进来是BGR,模型训练用的是RGB,这一步漏了颜色全乱 rgb = cv2.cvtColor(image, cv2.COLOR_BGR2RGB) resized = cv2.resize(rgb, (512, 288), interpolation=cv2.INTER_LINEAR) # 归一化到0-1区间,和训练时的数据预处理保持一致 normalized = resized.astype(np.float32) / 255.0 # HWC转CHW,加batch维度 tensor = torch.from_numpy(normalized).permute(2, 0, 1).unsqueeze(0) return tensor.to(self.device) def postprocess(self, output_tensor, original_shape, threshold=0.4): # 把模型输出经过sigmoid和二值化 prob = torch.sigmoid(output_tensor).squeeze().cpu().numpy() mask = (prob > threshold).astype(np.uint8) * 255 # 把mask从288x512还原回原图尺寸 mask_resized = cv2.resize(mask, (original_shape[1], original_shape[0]), interpolation=cv2.INTER_NEAREST) # 膨胀操作把断裂的车道线接起来,kernel大小根据分辨率调整 kernel = np.ones((5, 5), np.uint8) mask_dilated = cv2.dilate(mask_resized, kernel, iterations=1) return mask_dilated def detect(self, image): original_shape = image.shape tensor = self.preprocess(image) with torch.no_grad(): output = self.model(tensor) mask = self.postprocess(output, original_shape) return mask

这个封装有三个关键细节。第一,图像颜色空间转换必须写,OpenCV默认读成BGR,直接丢给PyTorch模型,模型输入和训练分布不一致,推理效果会断崖式下降,这是新手最容易踩的坑。第二,后处理里mask还原尺寸用INTER_NEAREST,不要用双线性插值。二值图用线性插值会产生灰色过渡值,后续处理都要再设一遍阈值,纯属自找麻烦。第三,膨胀操作只在推理端做,训练数据不要膨胀。训练时膨胀会让车道线变粗,模型学到的就是粗线,推理时再膨胀一次会过拟合到错误形态。

3.3 视图函数与路由:同步请求和异步任务的选择

视图层的设计要区分单张图片和批量图片两种场景。单张图片是同步请求,上传后直接返回结果;批量场景应该用Celery异步任务,把请求先扔进队列,前端轮询任务状态。标题里的系统一般指的是单张图同步处理的轻量方案,代码也按这个写。

# detector/views.py import cv2 from django.shortcuts import render from django.http import JsonResponse from django.conf import settings from .inference import LaneDetector # 模块加载时实例化一次,整个进程复用同一个对象 detector = LaneDetector(weights_path=settings.LANE_WEIGHTS_PATH) def index(request): return render(request, "detector/index.html") def detect_lane(request): if request.method != "POST": return JsonResponse({"code": 400, "msg": "only POST allowed"}) image_file = request.FILES.get("image") if not image_file: return JsonResponse({"code": 400, "msg": "image field required"}) # Django的File对象转成opencv能读的numpy数组 file_bytes = np.frombuffer(image_file.read(), np.uint8) image = cv2.imdecode(file_bytes, cv2.IMREAD_COLOR) if image is None: return JsonResponse({"code": 400, "msg": "invalid image"}) mask = detector.detect(image) # 把mask叠加到原图上,生成可视化结果 overlay = image.copy() overlay[mask > 0] = (0, 0, 255) # BGR顺序,纯红色车道线 result_path = save_overlay_result(overlay) # 保存到media目录,返回相对路径 return JsonResponse({"code": 200, "data": {"result_url": result_path}})

对应的路由:

# detector/urls.py from django.urls import path from . import views urlpatterns = [ path("", views.index, name="index"), path("detect/", views.detect_lane, name="detect_lane"), ]

这其中的关键点是:Django接收到的图片文件是一个Django的UploadedFile对象,不能直接喂给OpenCV。要先读成bytes,再用cv2.imdecode解码成numpy数组。反过来,OpenCV处理完的结果也不能直接写回数据库,要保存成文件后用URL暴露给前端。日常做法是保存在media/detected/下,文件名加个时间戳避免缓存冲突。input标签里写<img>标签加载这个URL时,Django的static配置要确保media路由正确,否则img标签显示不了——这个问题在网上一搜一大片。

4. 关键参数调优:车道线检测系统的阈值、膨胀核与输入尺寸

4.1 置信度阈值的选择:二分搜索每张图的最优值再取均值

置信度阈值是整个系统里最受玄学影响的一个参数。有人直接定0.5,有人试几个数看效果好就放着不管了,这样线上跑起来遇到阴影、雨雪天就翻车。靠谱做法是拿一张验证集图片做实验,在一个范围内遍历阈值,每跑一次在后处理后的mask上计算一次车道线像素的准确率和召回率,找到F1分数最高的值。一次遍历70张左右的图片,阈值从0.1到0.8步长0.05,取每张最优阈值的均值,作为系统的默认值。这样定出来的阈值比拍脑袋定的稳定得多。缺点是耗时,但这是离线操作,不影响线上推理速度。

4.2 膨胀核大小:影响通车线连续性的细节参数

膨胀操作是把车道线断裂处填补的关键手段,核大小直接决定填补力度。核太小时断裂处补不上,线看起来像虚线;核太大时会把相邻的并行车道线融合成一块,后续做车道线方程拟合时数据就废了。按512x288的输入分辨率,5x5的核迭代一次是稳妥起点。如果输入分辨率提高到640x360,核跟着放大到7x7。判断标准是看输出结果中车道线上下连通的情况:统计每条车道线在垂直方向的最大断裂间隙,间隙小于2个像素说明膨胀量够了,大于5个像素就需要加大核或增加迭代次数。注意这个判断要在二值化之后做。

4.3 输入尺寸与推理速度的权衡

模型的输入尺寸直接把推理速度和精度绑在一起。512x288是平衡点,分辨率降到320x180,推理速度提升约60%,但车道线消耗的像素太少,断裂严重。分辨率提到640x360,mIoU能涨2个点,但Django后端并发处理能力直线下降。如果Web应用要支撑几十个人同时测试,512x288是唯一能扛住的选择。推理瓶颈不在Django,而在GPU显存和带宽,一张图推理30ms,但所有用户共享同一个GPU,并发上来之后排队时间会明显变长。限流或队列是这类系统的刚需。

5. 部署与运行避坑:Django静态文件、模型路径、CPU与GPU环境不一致

5.1 踩坑记录:static目录下img标签显示不了

现象:Django模板里写了<img src="{% static 'images/result.jpg' %}">,本地能显示,部署到服务器上就裂开了。原因:Django默认的static文件处理在开发模式是自动找的,生产模式需要collectstatic把所有app的静态文件收集到一个目录,再由Nginx托管。解决:项目settings.py里加一行STATIC_ROOT = os.path.join(BASE_DIR, 'staticfiles'),部署前手动执行:

python manage.py collectstatic --noinput

Nginx端的配置把/static/路径指向这个目录,保证静态文件由Nginx直接返回,不经过Django。

5.2 踩坑记录:模型权重路径写死在views.py里,换环境就跑不了

现象:代码在Windows上跑通了,提交到Linux服务器后报FileNotFoundError。原因:有人把权重路径写死成D:/project/lane_seg.pth,换到服务器上路径不存在。解决:权重路径配置进settings.py,用相对BASE_DIR的方式拼接,并且服务器和开发机的目录结构保持一致。

# settings.py LANE_WEIGHTS_PATH = os.path.join(BASE_DIR, "detector", "weights", "lane_seg_resnet34.pth")

5.3 踩坑记录:训练用GPU,部署用CPU,推理结果对不上

现象:GPU上验证效果不错,部署到只有CPU的服务器上,同样的图片结果变差很多,车道线断断续续。原因:两个因素叠加。一是CPU推理时数据精度和GPU有差异,PyTorch在CPU上某些算子有精度损失。二是训练和推理的数据预处理不一致,比如训练时归一化到0到1,推理时忘了归一化。解决:先检查预处理代码是否完全对齐。对齐后再看算子精度问题,尝试把模型转成ONNX格式,用ONNX Runtime跑CPU推理,它做了大量算子融合优化,CPU上精度和速度通常比PyTorch原生CPU模式更好。

5.4 踩坑记录:Django开发服务器跑推理,请求超时

现象:开发模式下runserver跑推理接口,大图片处理时间超过30秒,前端直接报超时。原因:runserver是单进程串行模型,推理阻塞期间其他请求全部排队,图片越大越明显。解决:生产环境换Gunicorn加多worker部署,每个worker独立加载一份模型,并发能力线性提升。显存不够就限制worker数量,或者把模型放进独立推理服务,Django通过HTTP或gRPC调用。标题项目的体量放Gunicorn+2个worker足够,显存占用约6GB。

6. 进阶技巧:把推理结果转成车道线方程并接入实时视频流

前面所有步骤讲的是单张图的完整链路。落地到实际场景时,还需要把分割mask进一步拟合回车道线方程,以及处理连续帧视频。这个阶段我一般会把mask按列扫描,找每行像素的横向峰值位置,把这些峰值点聚合成若干条线,再用RANSAC拟合二次多项式曲线。每帧的拟合参数不直接用,而是平滑后再输出。

视频流处理和单张图处理最大的差别在状态管理。Django的请求是无状态的,但视频流检测天然需要跨帧信息。每次只处理当前帧、不做跨帧关联,车道线位置会抖动,拟合方程在直道转弯道时有一帧明显的滞后。常见的做法是用一个全局字典保存最近几帧的拟合曲线参数,下一帧到来时先按参数预测位置,再和当前帧检测结果加权融合。插值权重一般0.6对0.4,历史帧权重太大转弯时反应迟钝,太小滤波效果等于零。

更进一步的工程优化是把检测模型导出成ONNX:

import torch # 导出ONNX格式,CPU和GPU都能跑,部署不强制装PyTorch torch.onnx.export(model, dummy_input, "lane_seg.onnx", input_names=["input"], output_names=["output"], dynamic_axes={"input": {0: "batch"}, "output": {0: "batch"}})

导出时dynamic_axes保证batch维度可变,部署端可以用onnxruntime-gpu跑,推理速度比PyTorch原版快20%左右。这个过程要注意的是:导出前模型一定要切到eval模式,否则batch norm的统计量不对,导出的ONNX在部署端效果和训练时差一截,这个坑我吃过亏,后来每次导出都先检查model.eval()。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询