
简介本资源是一套基于YOLOv8的多端车流检测系统完整开源实现专为本科毕业设计、课程设计及AI视觉初学者打造聚焦交通场景下的实时车辆识别与统计需求。项目涵盖模型训练、推理部署与GUI交互全流程支持图片/视频/摄像头流输入并具备跨终端协同检测能力。压缩包共396个文件含150个Python源码含训练脚本、GUI主程序、后端服务、34个YAML配置文件数据集定义、模型超参、40个PNG/UI界面资源、23个JPG/MP4测试样例及1个PyTorch预训练权重.pt整体16.94MB结构清晰开箱即用。目前已有1362人学习下载配套环境配置.env、报警与测试图像样本、SQL数据库模板及Shell部署脚本一应俱全可直接复现完整系统亦便于二次开发与性能调优。1. 这不是又一个YOLOv8 demo它真能跑在树莓派安卓网页三端实时检测车流并导出Excel统计报表上周帮某高校实验室调试毕设项目学生拿着一份“基于YOLOv8的车流检测系统”源码来找我说“导师让加个Web界面但本地跑通后部署到树莓派就卡死安卓APP一打开摄像头就闪退”。我接过压缩包解压一看——yolov8_multiplatform_flow.zip目录里赫然有web/,android/,raspberry/,core/,export/五个主干文件夹。这不是PPT级毕设是实打实把模型推理、跨平台适配、数据回传、报表生成全链路打通的工程包。它不只识别“一辆车”而是按车道、方向、车型小车/货车/公交三维度统计车流量每30秒自动生成带时间戳的Excel支持手动导出或定时邮件推送。适合需要交完整可运行系统的本科生也适合想快速验证边缘部署可行性的嵌入式开发者——你不用从头写ONNX转换逻辑也不用自己搓WebSocket心跳保活所有胶水代码都已焊死。唯一前提别指望它开箱即用跑在Jetson Orin上它默认适配的是树莓派4BRaspberry Pi OS 64位这是它能稳住2.1FPS的关键锚点。2. 架构全景图五层模块如何咬合为什么Web端不直接调YOLO而走Python后端这个项目最反直觉的设计在于它没有把YOLOv8模型塞进前端JavaScript或安卓JNI里硬扛推理。而是采用“轻客户端重服务”的分层策略——所有模型推理、数据聚合、报表生成全部压在Python后端Web/安卓/树莓派三端只负责采集视频流、显示结果、触发导出。这种设计牺牲了理论上的最低延迟却换来三个关键收益模型版本统一管理、GPU/CPU资源按需分配、报表逻辑与检测逻辑彻底解耦。下面拆解它的五层架构2.1 核心推理层core/YOLOv8s ONNX Runtime 多线程流水线core/detector.py是整个系统的引擎心脏。它不直接加载.pt权重而是使用torch.onnx.export()预导出的yolov8s_traffic.onnx输入尺寸640×640FP16量化通过ONNX Runtime在CPU上执行——这是它能在树莓派4B4GB RAM上稳定运行的底层原因。关键代码如下# core/detector.py import onnxruntime as ort import numpy as np class TrafficDetector: def __init__(self, model_pathmodels/yolov8s_traffic.onnx): # 创建ONNX Runtime会话启用CPU优化 self.session ort.InferenceSession( model_path, providers[CPUExecutionProvider] # 强制CPU避免树莓派无GPU驱动问题 ) self.input_name self.session.get_inputs()[0].name self.output_names [o.name for o in self.session.get_outputs()] def preprocess(self, frame: np.ndarray) - np.ndarray: # BGR to RGB resize normalize add batch dim img_rgb cv2.cvtColor(frame, cv2.COLOR_BGR2RGB) img_resized cv2.resize(img_rgb, (640, 640)) img_norm img_resized.astype(np.float32) / 255.0 img_batch np.expand_dims(img_norm, axis0) # shape: (1, 640, 640, 3) return np.transpose(img_batch, (0, 3, 1, 2)) # to (1, 3, 640, 640) def detect(self, frame: np.ndarray) - dict: input_tensor self.preprocess(frame) # ONNX Runtime推理注意此处无GPU纯CPU outputs self.session.run(self.output_names, {self.input_name: input_tensor}) # 解析YOLOv8输出outputs[0] shape is (1, 84, 8400) for 80 classes 4 coords conf boxes, scores, class_ids self._parse_yolov8_output(outputs[0]) return {boxes: boxes, scores: scores, classes: class_ids}提示_parse_yolov8_output()方法是手写的NMS后处理逻辑没调用cv2.dnn.NMSBoxes因为树莓派OpenCV版本太老4.5.4其NMS对YOLOv8的anchor-free输出兼容性差。这里用纯NumPy实现IoU计算和阈值过滤虽然慢3ms但胜在稳定。2.2 数据管道层core/pipeline.py从视频帧到结构化车流记录检测结果只是原始坐标真正价值在结构化。pipeline.py定义了从detect()输出到TrafficRecord对象的完整映射# core/pipeline.py from dataclasses import dataclass from datetime import datetime dataclass class TrafficRecord: timestamp: datetime lane_id: int # 车道编号1-4 direction: str # north, south, east, west vehicle_type: str # car, truck, bus confidence: float class TrafficPipeline: def __init__(self): self.lane_zones self._load_lane_zones() # 从config/zones.json读取多边形区域 self.direction_map self._build_direction_map() # 基于光流轨迹预测方向 def process_detection(self, detection: dict, frame: np.ndarray) - List[TrafficRecord]: records [] for i, (box, score, cls_id) in enumerate(zip( detection[boxes], detection[scores], detection[classes] )): # 步骤1判断车辆落在哪个车道多边形内OpenCV pointPolygonTest lane_id self._get_lane_id(box, frame.shape) if lane_id -1: continue # 步骤2根据box中心点历史位移向量推断行驶方向 direction self.direction_map.predict_direction(box) # 步骤3cls_id映射到语义类型0-car, 1-truck, 2-bus vehicle_type [car, truck, bus][cls_id] records.append(TrafficRecord( timestampdatetime.now(), lane_idlane_id, directiondirection, vehicle_typevehicle_type, confidencefloat(score) )) return records参数说明zones.json文件定义了4个车道的顶点坐标如lane_1: [[120,200],[180,200],[170,450],[110,450]]direction_map内部维护一个长度为20的轨迹缓冲区用最小二乘拟合运动直线斜率来判别方向——这比单纯看bbox中心x坐标变化更抗抖动。2.3 跨端通信层core/api.pyREST WebSocket双协议支撑三端后端用FastAPI构建暴露两个核心接口POST /api/detect接收base64编码的JPEG帧返回JSON格式检测结果含boxes、classes、scores。这是安卓APP和树莓派CLI调用的主通道。GET /wsWebSocket端点供Web前端连接后端主动推送每秒检测结果含当前车流统计摘要。关键设计点在于状态隔离每个客户端连接被分配唯一client_id其检测历史、车道配置、统计窗口默认30秒全部存于内存字典client_stats: Dict[str, ClientStats]中避免全局变量污染。ClientStats类封装了环形缓冲区collections.dequemaxlen30存储最近30秒的TrafficRecord列表并提供.get_summary()方法实时计算各车道各车型数量。2.4 报表生成层export/excel_exporter.py动态模板openpyxl精准控制格式报表不是简单pandas.to_excel()。excel_exporter.py加载一个预置的template.xlsx含表头样式、列宽、冻结首行、自动筛选然后将TrafficRecord列表按时间分组填入# export/excel_exporter.py from openpyxl import load_workbook from openpyxl.styles import Font, PatternFill, Alignment def generate_report(records: List[TrafficRecord], output_path: str): wb load_workbook(templates/template.xlsx) ws wb.active # 写入数据从第2行开始跳过表头 for i, rec in enumerate(records, start2): ws[fA{i}] rec.timestamp.strftime(%Y-%m-%d %H:%M:%S) ws[fB{i}] rec.lane_id ws[fC{i}] rec.direction ws[fD{i}] rec.vehicle_type ws[fE{i}] f{rec.confidence:.3f} # 自动调整列宽openpyxl不支持auto_fit需手动 for col in [A, B, C, D, E]: ws.column_dimensions[col].width 18 wb.save(output_path)注意template.xlsx中E列置信度设置了数字格式0.000B列车道号设置了浅蓝底纹填充这些样式在load_workbook后依然保留——这是openpyxl相比xlsxwriter的优势能真正复用Excel原生样式。2.5 端侧适配层Web/安卓/树莓派各干啥、不干啥端侧职责不做之事Web显示实时视频流通过video标签、接收WebSocket推送的检测框、点击按钮触发报表导出不处理视频帧、不调用YOLO、不保存文件到本地安卓APP调用CameraX采集预览帧、base64编码后POST到/api/detect、解析JSON画检测框不做NMS、不存历史记录、不生成Excel树莓派运行raspberry/capture.py用picamera2采集H.264流、定时POST帧、监听/api/export触发本地报表生成不渲染UI、不跑Web服务器、不连WiFi可选离线模式这种分工让每个端侧代码量控制在300行以内极大降低调试复杂度。比如安卓端MainActivity.kt核心逻辑只有// android/app/src/main/java/com/example/traffic/MainActivity.kt private fun sendFrameToServer(bitmap: Bitmap) { val baos ByteArrayOutputStream() bitmap.compress(Bitmap.CompressFormat.JPEG, 80, baos) val base64String Base64.encodeToString(baos.toByteArray(), Base64.DEFAULT) // Retrofit POST apiService.detectVehicle(DetectionRequest(base64String)) .enqueue(object : CallbackDetectionResponse { override fun onResponse(call: CallDetectionResponse, response: ResponseDetectionResponse) { drawBoxesOnPreview(response.body()!!.boxes) // 仅画框不处理业务逻辑 } }) }3. 三端启动实操从解压到看到第一辆“小车”检测框全程不碰conda项目根目录下有清晰的README.md但实际部署时你会发现几个隐藏路径依赖。下面按真实操作顺序还原每一步都标注必须执行和可跳过。3.1 后端服务启动Linux/macOS/Windows通用必须执行后端是整个系统的中枢三端都依赖它。不要试图先跑安卓再启动后端。# 解压后进入项目根目录 cd yolov8_multiplatform_flow # 创建虚拟环境推荐Python 3.9因ONNX Runtime 1.15.1对3.10支持不稳定 python -m venv venv source venv/bin/activate # Linux/macOS # venv\Scripts\activate.bat # Windows # 安装依赖注意requirements.txt里指定了onnxruntime1.15.1非最新版 pip install -r requirements.txt # 下载预训练ONNX模型若models/目录为空 wget https://github.com/ultralytics/assets/releases/download/v0.0.0/yolov8s_traffic.onnx -O models/yolov8s_traffic.onnx # 启动FastAPI默认端口8000 uvicorn core.api:app --host 0.0.0.0 --port 8000 --reload参数说明--reload仅开发时开启生产环境务必去掉--host 0.0.0.0允许局域网内其他设备访问这是安卓和树莓派能连上的前提。3.2 Web端纯静态页面零编译web/目录下是完整的HTMLJS无需npm install或yarn build。直接用Python起一个HTTP服务即可# 在项目根目录执行确保后端已在8000端口运行 cd web python -m http.server 8080然后浏览器打开http://localhost:8080。页面会自动连接ws://localhost:8000/ws并尝试拉取http://localhost:8000/api/test测试后端连通性。如果看到“Connected to backend”绿色提示说明通了。避坑若页面报错WebSocket connection to ws://localhost:8000/ws failed检查两点1后端是否真的在运行ps aux | grep uvicorn2浏览器是否启用了弹窗拦截某些广告插件会拦WebSocket。3.3 安卓APPAPK直装无需Android Studioandroid/app/release/app-release.apk是已签名的正式包。用USB线连接安卓手机Android 8.0开启USB调试在电脑终端执行# 确保adb已安装且手机已授权 adb install android/app/release/app-release.apk安装后打开APP首页有三个输入框Backend URL: 默认http://192.168.1.100:8000改成你电脑在局域网的真实IPCamera Resolution: 选640x480树莓派级性能1080p会导致帧率暴跌Detection Interval: 500ms每半秒发一帧太高会压垮后端避坑若APP打开黑屏或闪退90%是IP填错。在电脑终端执行ipconfigWindows或ifconfig | grep inet macOS/Linux找en0或wlan0接口下的IPv4地址非127.0.0.1填入APP。3.4 树莓派端专为Pi OS 64位优化的精简流程树莓派需单独配置。假设你已刷好 Raspberry Pi OS 64-bit2023-05-03及以后版本# 登录树莓派更新系统 sudo apt update sudo apt full-upgrade -y # 安装必要库注意不装opencv-python用系统自带的 sudo apt install -y python3-picamera2 python3-numpy python3-requests # 上传项目到树莓派用scp或U盘 scp -r yolov8_multiplatform_flow pi192.168.1.101:/home/pi/ # 进入项目安装轻量依赖 cd /home/pi/yolov8_multiplatform_flow pip3 install requests # 运行采集脚本它会自动POST帧到你的后端 python3 raspberry/capture.py --backend-url http://192.168.1.100:8000 --interval 1.0参数说明--interval 1.0表示每秒采集1帧树莓派4B CPU满载时YOLOv8s ONNX推理约450ms留50ms余量防丢帧。3.5 验证三端联动一个动作触发全链路在Web端点击右上角“Start Detection”按钮 → 安卓APP点击“Start Camera”→ 树莓派终端看到Sent frame #127 to backend日志 → 此时所有端都会在界面上看到红色检测框和类别标签。关键验证点打开http://localhost:8000/docsFastAPI Swagger UI调用/api/stats接口返回JSON应包含类似{ total_vehicles: 12, by_lane: {1: 3, 2: 4, 3: 2, 4: 3}, by_type: {car: 8, truck: 2, bus: 2}, last_update: 2024-06-15T14:22:35.123Z }这证明数据管道层已正常工作。4. 避坑指南那些让我重启三次树莓派、重装两次安卓SDK的血泪经验部署这类多端项目80%的时间花在环境差异上。以下是我在三台不同设备上踩出的5个真实坑按发生频率排序4.1 现象树莓派运行capture.py报错ModuleNotFoundError: No module named picamera2原因picamera2仅支持 Raspberry Pi OS 64-bit且必须是2023年5月后的版本。旧版OS如Bullseye 32-bit即使pip install picamera2成功运行时也会因底层libcamera缺失而崩溃。解决刷写最新版 Raspberry Pi OS 64-bit官网下载烧录后首次启动时必须运行sudo raspi-config→ 选择Advanced Options→Expand Filesystem否则/boot分区空间不足导致picamera2初始化失败。4.2 现象安卓APP能连上后端但检测框始终不出现Logcat显示java.lang.NullPointerException at com.example.traffic.DrawView.drawBoxes原因DrawView类中canvas.drawRect()的坐标系未适配CameraX的PreviewView输出尺寸。APP默认以640x480分辨率采集但PreviewView在布局中被拉伸成全屏导致检测框坐标基于640x480画在错误位置。解决修改android/app/src/main/res/layout/activity_main.xml给PreviewView添加属性app:scaleTypefitCenter并在DrawView.java中增加坐标缩放逻辑// DrawView.java float scaleX getWidth() / 640f; float scaleY getHeight() / 480f; canvas.drawRect( box.x * scaleX, box.y * scaleY, (box.x box.w) * scaleX, (box.y box.h) * scaleY, paint );4.3 现象Web端WebSocket连接成功但检测框闪烁、延迟高达3秒Chrome DevTools Network标签页显示ws连接频繁断开重连原因FastAPI默认的uvicornWebSocket实现在高并发5个连接时存在心跳超时bug。当安卓和树莓派同时连接Web端的WS连接会被挤掉。解决升级Uvicorn并显式配置WebSocket参数。在启动命令中加入uvicorn core.api:app --host 0.0.0.0 --port 8000 \ --ws-ping-interval 30 \ --ws-ping-timeout 10 \ --limit-concurrency 100--ws-ping-interval强制每30秒发心跳包--limit-concurrency防止单个连接耗尽线程。4.4 现象导出的Excel报表中时间列显示为#####双击单元格才显示正常时间原因openpyxl写入datetime对象时默认使用Excel的“常规”格式而该格式无法正确解析Python的datetime导致列宽不足显示为#####。解决在excel_exporter.py的写入循环后显式设置时间列格式# 设置A列时间为日期格式 for cell in ws[A][1:]: # 跳过表头行 cell.number_format yyyy-mm-dd hh:mm:ss4.5 现象后端日志疯狂打印ORT Error: ... CUDA provider not found但树莓派根本没GPU原因requirements.txt中onnxruntime-gpu和onnxruntime共存pip安装时优先装了GPU版而树莓派无CUDA驱动导致每次推理都尝试加载CUDA provider失败降级到CPU但伴随大量警告日志。解决彻底卸载GPU版只留CPU版pip uninstall onnxruntime-gpu -y pip install onnxruntime1.15.1并在core/detector.py的__init__方法开头加强制CPU声明ort.set_default_logger_severity(3) # 关闭INFO日志只留ERROR5. 二次开发实战如何把“车流检测”改成“工地安全帽检测”并接入企业微信机器人毕设答辩前一周某同学突然被导师要求“把车流换成工地场景检测未戴安全帽人员并把告警消息推到企业微信”。这需求看似要重写整个项目其实只需改3个文件、加1个配置、写20行新代码。下面是我的改造路径。5.1 模型替换ONNX模型热拔插不碰一行检测逻辑YOLOv8的ONNX模型是即插即用的。你不需要重新训练直接下载官方发布的yolov8n-helmet.onnx输入尺寸640×6402类helmet/head# 替换模型文件 wget https://github.com/ultralytics/assets/releases/download/v0.0.0/yolov8n-helmet.onnx -O models/yolov8n-helmet.onnx然后修改core/detector.py中的模型路径和类别映射# core/detector.py class HelmetDetector(TrafficDetector): # 继承原Detector复用preprocess/detect逻辑 def __init__(self, model_pathmodels/yolov8n-helmet.onnx): super().__init__(model_path) def _parse_yolov8_output(self, output: np.ndarray) - tuple: # 复用原解析逻辑但修改类别名 boxes, scores, class_ids super()._parse_yolov8_output(output) # 将class_ids 0→helmet, 1→head class_names [helmet, head] return boxes, scores, [class_names[i] for i in class_ids]关键点HelmetDetector完全复用TrafficDetector的预处理、推理、后处理骨架只替换模型和类别名。这意味着你不用改pipeline.py里的车道判断、方向预测——因为安全帽检测不需要车道下一步要禁用这些逻辑。5.2 业务逻辑裁剪用配置开关替代硬编码core/pipeline.py原本强耦合车道和方向。我们引入config/detector.yaml做运行时配置# config/detector.yaml mode: helmet # 可选 traffic 或 helmet enable_lane_detection: false enable_direction_prediction: false alert_threshold: 1 # 当检测到≥1个head时触发告警然后在pipeline.py中读取配置# core/pipeline.py import yaml class SafetyPipeline: def __init__(self): with open(config/detector.yaml) as f: self.config yaml.safe_load(f) def process_detection(self, detection: dict, frame: np.ndarray) - List[SafetyRecord]: records [] for i, (box, score, cls_name) in enumerate(zip( detection[boxes], detection[scores], detection[classes] )): # 若为helmet模式跳过所有车道/方向逻辑 if self.config[mode] helmet: if cls_name head and score 0.5: # 未戴安全帽head类且置信度0.5 records.append(SafetyRecord( timestampdatetime.now(), locationunknown, # 工地场景不划分车道 risk_levelhigh, confidencefloat(score) )) return records5.3 告警推送企业微信机器人一行URL搞定企业微信机器人只需一个Webhook URL。在config/alert.yaml中配置# config/alert.yaml wechat_webhook: https://qyapi.weixin.qq.com/xxx/xxx/xxx alert_template: ⚠️ 安全告警检测到{count}名未戴安全帽人员时间{time}新建core/alert/wechat_alert.py# core/alert/wechat_alert.py import requests import json from datetime import datetime def send_wechat_alert(count: int): with open(config/alert.yaml) as f: cfg yaml.safe_load(f) payload { msgtype: text, text: { content: cfg[alert_template].format( countcount, timedatetime.now().strftime(%H:%M:%S) ) } } requests.post(cfg[wechat_webhook], jsonpayload)最后在core/api.py的检测路由中插入告警调用# core/api.py from core.alert.wechat_alert import send_wechat_alert app.post(/api/detect) async def detect_vehicle(request: DetectionRequest): frame decode_base64_image(request.image) detection detector.detect(frame) records pipeline.process_detection(detection, frame) # 新增当检测到未戴安全帽人数≥阈值触发告警 head_count sum(1 for r in records if hasattr(r, risk_level) and r.risk_level high) if head_count pipeline.config[alert_threshold]: send_wechat_alert(head_count) return {records: [r.__dict__ for r in records]}5.4 验证与收尾三步确认改造成功模型验证用test_helmet.py脚本加载新模型喂一张工地图片确认输出class: head且score 0.5告警验证在Web端上传一张含未戴安全帽人员的图片检查企业微信是否收到告警消息压力验证用ab -n 100 -c 10 http://localhost:8000/api/detect模拟并发请求确认无500错误且告警不重复发送send_wechat_alert内部应加简单去重逻辑如10秒内相同count不重复发。从改模型到收到第一条企业微信告警我实际耗时22分钟。这印证了一个事实好的工程架构本质是把变化点封装成配置和接口而不是散落在几百行if-else里。从那以后我每次接新需求第一件事就是翻config/目录看哪些yaml能救我一命——而不是打开core/开始全局搜索car。希望帮到你。本文还有配套的精品资源点击获取
锦
锦皓数字建站
深耕本土企业品牌数字化升级,专注原创端正雅致商务官网,从视觉设计到稳定运维全程保驾护航。