ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

不连云端,纯本地方案实现交通路牌目标检测全流程

不连云端,纯本地方案实现交通路牌目标检测全流程 最近在做交通路牌识别项目拿到了一批道路视频素材需要完成从数据标注、模型训练到结果输出的整套流程。刚开始我也和大多数人一样准备开一台云GPU、SSH连上去干活结果光是在环境配置和数据传输上就耗掉了大半天。后来我干脆把整套工作流全部放在本地用labelme做标注、VSCode管理代码与脚本、本地Python环境跑目标侦测训练最后用FinalFTP把成果文件传走交付。整套流程不SSH、不连云端全程在一台自用电脑上跑通效率反而高了不少。这篇文章就把这套本地工作流完完整整拆开讲清楚包括labelme标注环境的搭建与避坑、VSCode在标注与训练环节里的实际用法、FinalFTP作为交付工具的定位以及一条从视频抽帧到标注、训练再到视频检测的完整落地流程。无论你是刚开始接触目标侦测项目的新手还是想摆脱频繁连服务器、传环境这类麻烦事的从业者这套方案都值得直接照搬。1. 为什么我要坚持不SSH、不连云端的纯本地方案1.1 交通路牌识别这个量级本地完全跑得动很多人做目标侦测一上来就开云GPU其实是出于惯性。先算一笔账假设你有40段道路视频素材每段5分钟按每秒1帧抽帧大概能得到12000张图像。人工筛选后剔除模糊、重复、无路牌的帧最终标注500到1000张这个数据量撑死也就1到2GB。用YOLOv8n模型、640x640输入尺寸在这些数据上训练100个epoch一张RTX 306012GB显存大概只需要1到2小时。换成RTX 4070或者4060Ti时间还能再压缩。这个成本放在本地就是电费而放云上则是按小时计费还要加上数据打包上传、下载权重的时间。两者一比本地的性价比几乎碾压。还有一个更实际的问题云端SSH流程里的环境坑实在太多。CUDA版本、PyTorch版本、依赖库冲突每一个都能卡住你一个下午。更别提每次修改代码后都要重新同步文件经常出现本地改了三行代码服务器上跑的还是旧版本的尴尬情况。把这些时间全部省下来本地工作流香得很。1.2 数据不出本机项目可控性更高交通路牌这类数据有个特点它往往带有地理位置信息和路网特征。很多机构对这类数据有明确要求不允许上传到外部服务器。我当时手里拿到的视频素材也明确标注了内部使用禁止外传这种情况下连云端本身就是违规操作。纯本地运行意味着整个过程中一张原图都不离开本机标注文件、训练权重、预测结果全部生成在本地文件夹里。哪怕是最后用FinalFTP传输成果传出去的也是加工后的结果文件而不是原始道路视频素材。这一点对于企业项目和课程作业来说都非常关键。另外不连云端也意味着不依赖外网。断网状态下标注照标、训练照跑、代码照写整个项目进度不会因为网络波动而停滞。这对移动办公或者网络条件不稳定的场景特别友好。1.3 一图看懂本地工作流的四个组成部分这套方案总结下来就是标注、编码、训练、交付四件事labelme负责对道路视频抽帧得到的图像做多边形标注生成JSON标注文件VSCode负责写转换脚本、训练脚本、查看标注结果、盯着训练曲线本地Python环境负责JSON转YOLO格式、数据集划分、模型训练与视频推理FinalFTP负责把训练好的权重、检测结果视频、代码工程上传到指定服务器完成交付整个链路里面没有一次远程命令行操作。FinalFTP只是传文件不执行命令这也正是它替代SSH的位置所在。2. labelme环境搭建与交通路牌标注实操2.1 labelme安装与pyqt5-sip那个经典坑labelme安装本身不难核心命令就一条pip install labelme但如果你直接装最新版很可能会碰到热搜里反复出现的两个报错labelme 无法安装 pyqt5和labelme error pyqt5-sip。这两个问题其实是同一个根源labelme底层依赖pyqt5而新版pyqt5-sip和旧版labelme存在兼容性冲突。我实测下来最稳定的组合是Python 3.9或3.10环境安装labelme 5.4.1。如果你已经装了新版导致报错可以这样降级处理pip install labelme5.4.1 pip install pyqt5-sip12.12.1顺便建议用虚拟环境隔离不要直接装到系统全局Python里。我习惯用conda创建独立环境conda create -n road_sign python3.9 conda activate road_sign pip install labelme5.4.1装好之后命令行输入labelme即可启动图形界面。如果你希望在标注的同时预览图像建议把VSCode窗口放在一侧、labelme窗口放在另一侧操作起来非常顺手。2.2 从道路视频到标注图像OpenCV抽帧脚本拿到道路视频素材之后第一件事就是抽帧。这里的抽帧策略直接决定后续标注工作量和数据集质量。如果每帧都抽会出现大量相邻帧几乎相同的冗余数据标注起来浪费时间训练时还会因为重复样本导致过拟合。我的做法是每秒抽1帧同时根据车速和场景适当跳帧。简单场景高速、无岔路可以每2到3秒抽1帧复杂场景市区、路口每0.5到1秒抽1帧。下面是一个可直接用的OpenCV抽帧脚本import cv2 import os video_path D:/road_sign/videos/road_01.mp4 save_dir D:/road_sign/frames/road_01 os.makedirs(save_dir, exist_okTrue) cap cv2.VideoCapture(video_path) fps cap.get(cv2.CAP_PROP_FPS) # 实际帧率 frame_interval int(fps) # 每1秒抽1帧 frame_count 0 saved_count 0 while True: ret, frame cap.read() if not ret: break if frame_count % frame_interval 0: # 可以在这里加一个简单筛选过滤过暗或过曝的帧 gray cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY) brightness gray.mean() if brightness 40 and brightness 220: # 粗略质量过滤 out_path os.path.join(save_dir, froad_01_{saved_count:05d}.jpg) cv2.imwrite(out_path, frame) saved_count 1 frame_count 1 cap.release() print(f共提取 {saved_count} 帧)抽完帧后建议快速过一遍所有图片删掉没有路牌出现或者路牌占比过小的帧。这个筛选过程用Windows照片查看器或VSCode的图像预览插件都能做我习惯直接在VSCode里用Image Preview插件快速浏览。2.3 标注规范多边形贴边、类别名统一labelme的标注核心操作是左侧工具栏的Create Polygons创建多边形。针对交通路牌我的标注原则是多边形要尽量贴着路牌外轮廓边界和路牌边缘之间留2到3个像素的余量。留太大训练出来的预测框会偏大影响mAP计算完全贴死又容易因为标注误差导致模型学到的特征边界过紧不好泛化一个路牌只标一个多边形不要因为路牌上有多个文字就拆成几个标注遮挡严重的路牌如果超过30%区域被遮挡直接跳过不标类别命名必须完全统一建议提前定好类别清单再动手交通路牌常见的类别划分方式是按功能分禁令标志禁行、限速、警告标志急弯、施工、指示标志直行、环岛、指路标志地名、方向。我这次项目里用的类别如下类别 ID名称说明0speed_limit限速标志1warning警告类标志2prohibition禁令类标志3guidance指示/指路标志4other_sign其他交通标志这里有一个血泪教训类别名一定要在标注开始前锁定中途不要改名。labelme生成的JSON文件里记录的是字符串类别名如果你标到一半把speed_limit改成speed_lim要么重标要么写脚本批量替换两种方案都浪费大量时间。2.4 标注数据质量的快速检查方法标完一批图之后不要直接进入训练环节先做质量检查。我常用的检查手段有两种第一种是在labelme里勾选Edit菜单下的Edit Labels重新打开一个JSON文件逐张复核标注框是否贴合路牌第二种是直接在VSCode里打开图像和同名JSON文件检查shapes字段里的points坐标是否落在路牌的真实位置附近。如果标注人员不止一个额外注意类别名的统一性。多人标注最常出问题的地方就是同一种路牌被不同人起了不同的名字。检查方法很简单用Python脚本统计所有JSON文件中的标签集合看看实际出现了多少种类别名。出现预期之外的名字基本都是标注疏漏。import json from glob import glob labels set() for json_file in glob(D:/road_sign/labels_json/*.json): with open(json_file, r, encodingutf-8) as f: data json.load(f) for shape in data[shapes]: labels.add(shape[label]) print(sorted(labels))这一招能快速发现标着标着类别名写错的问题非常实用。3. VSCode本地开发从标注检查到训练脚本一肩挑3.1 我为什么用一个编辑器统管整个项目很多人觉得VSCode就是个写代码的编辑器其实在整个本地工作流里它的角色比这重要得多。我把整个项目的目录直接丢进VSCode工作区左侧文件树就是项目的完整结构右侧编辑区既可以看Python脚本也可以看JSON标注文件内置终端可以直接跑pip命令、训练脚本、推理脚本完全不需要额外找终端窗口。针对这个项目我装的插件就三个Python微软官方提供语法高亮、代码补全也可以直接选择你创建好的conda环境Pylance更智能的代码分析和类型提示写脚本时能提前暴露很多低级错误Image Preview在编辑器里直接预览图片标注质量检查时非常方便这三个插件配合使用VSCode实际上就成了这套本地工作流的控制台。整个标注、转换、训练、推理环节我都没有离开过这个窗口。3.2 自己写JSON转YOLO脚本比工具更可控labelme标注生成的默认是JSON格式而YOLOv8训练需要的是TXT格式的标签文件每一行代表一个目标框格式为类别ID 中心x坐标 中心y坐标 宽度 高度所有数值都归一化到0到1之间。网上常用labelme2yolo等第三方工具转换但我更推荐自己写脚本原因有两个一是转换过程中你可以顺手做数据清洗和数据划分不用来回切工具二是当出现数据格式问题时自己写的脚本一眼就能看出问题在哪。核心转换逻辑如下import json import os def convert_one_json(json_path, out_txt_path, class_names): with open(json_path, r, encodingutf-8) as f: data json.load(f) img_w data[imageWidth] img_h data[imageHeight] lines [] for shape in data[shapes]: label shape[label] if label not in class_names: print(f警告: 未知类别 {label} 文件 {json_path}) continue cls_id class_names.index(label) # labelme的points是Polygon点集坐标为[[x1,y1],[x2,y2],...] pts shape[points] xs [p[0] for p in pts] ys [p[1] for p in pts] x_min, x_max min(xs), max(xs) y_min, y_max min(ys), max(ys) # 转换为YOLO格式的归一化中心点宽高 cx ((x_min x_max) / 2) / img_w cy ((y_min y_max) / 2) / img_h w (x_max - x_min) / img_w h (y_max - y_min) / img_h # 剪裁到[0,1]区间防止浮点误差导致越界 cx max(0, min(1, cx)) cy max(0, min(1, cy)) w max(0, min(1, w)) h max(0, min(1, h)) lines.append(f{cls_id} {cx:.6f} {cy:.6f} {w:.6f} {h:.6f}) with open(out_txt_path, w) as f: f.write(\n.join(lines))注意到没有labelme的坐标是左上角为原点、x向右、y向下转换逻辑中完全不需要调整方向直接归一化即可。唯一要小心的是labelme里多边形点的顺序和JSON字段解析不要误把points当成单个坐标值。批量化处理时遍历所有JSON文件生成同名的TXT文件放到labels目录图像放到images目录。做完之后再顺手把图像和标签一一配对确认避免漏转或误转。3.3 数据划分训练集验证集不能有同源图像数据处理里最容易被忽视的问题就是训练集和验证集的数据同源性。如果从同一段视频相邻帧抽帧后一帧进训练集、下一帧进验证集那么验证集里基本是一眼就能认出来的几乎相同的路牌最终验证结果虚高一上真实道路立即翻车。所以我建议两类划分策略配合使用先按视频素材分桶同一条道路视频抽出来的帧必须分配到同一个集合要么全进训练集要么全进验证集再对每个桶内的帧做随机打乱保证训练时不出现连续重复帧成批涌入这是很多人会踩的隐形坑。我当时第一版训练mAP50跑到了0.91觉得效果很棒结果换了一条没见过的道路视频马上掉到0.6左右。后来仔细排查才发现就是验证集泄漏导致的虚高。我用下面这个脚本按视频片段做分组划分import os import random from glob import glob # 假设文件名格式为 road_XX_帧号.jpg all_images glob(D:/road_sign/dataset/images/*.jpg) video_buckets {} for img_path in all_images: fname os.path.basename(img_path) video_id fname.rsplit(_, 1)[0] # 提取 road_01 这样的前缀 video_buckets.setdefault(video_id, []).append(img_path) random.seed(42) val_ratio 0.2 train_files [] val_files [] for video_id, img_list in video_buckets.items(): random.shuffle(img_list) split_idx max(1, int(len(img_list) * val_ratio)) val_files.extend(img_list[:split_idx]) train_files.extend(img_list[split_idx:]) # 写好train.txt和val.txt with open(D:/road_sign/dataset/train.txt, w) as f: f.write(\n.join(train_files)) with open(D:/road_sign/dataset/val.txt, w) as f: f.write(\n.join(val_files))3.4 用VSCode内置终端跑训练实时盯损失曲线数据准备好之后训练环境建议在本机装YOLOv8的官方包pip install ultralytics训练命令直接在VSCode的内置终端里执行yolo train dataD:/road_sign/dataset/data.yaml modelyolov8n.pt epochs100 imgsz640 batch16 device0其中data.yaml是数据配置文件内容大致如下path: D:/road_sign/dataset train: train.txt val: val.txt nc: 5 names: [speed_limit, warning, prohibition, guidance, other_sign]训练过程中VSCode左侧的文件树里会实时生成runs/detect/train目录里面有results.png、混淆矩阵、PR曲线等。我习惯每隔一段时间点开results.png看训练loss曲线。如果训练loss持续下降但验证loss在某个epoch后开始反弹说明过拟合了可以提前停掉不需要傻等100个epoch跑完。4. FinalFTP在整条链路中的角色成果交付不用SSH4.1 本地跑完不等于项目结束成果还得交付有些人会疑惑既然全程本地为什么还要用FinalFTP传文件实际上不SSH代表的是不做远程命令操作、不在远程机器上跑程序但项目成果还是要交出去的。无论是课程作业提交、实验报告存档还是把权重文件交给团队后续应用都需要一个可靠的文件传输环节。SSH模式下大家习惯用scp或者rsync命令传文件这确实可行但也有几个问题一是要记命令参数二是断线后要重新开始虽然rsync支持断点续传但很多人不会用三是对方服务器不一定开着SSH端口。FinalFTP这类图形化FTP客户端就是为了解决这些问题而存在的。4.2 FinalFTP站点配置与上传实操FinalFTP的界面逻辑很直观左侧是本地文件夹右侧是远程目录。第一步先把远程服务器信息配置好操作路径是站点菜单里新建站点协议如果对方提供的是标准FTP服务就选FTP如果是带加密的SFTP服务就选SFTP注意这里SFTP虽然底层也用了SSH协议但它只是用于文件传输不代表你要去做远程命令行操作主机填写对方服务器的IP地址或域名端口FTP默认21SFTP默认22确认对方实际端口后填写用户密码根据对方分配的账号填写连接成功后在左侧选中本地成果文件夹右键选择上传或者直接鼠标拖拽到右侧目录。FinalFTP支持多线程传输几十个GB的文件夹也不怕慢。传输过程中如果网络中断可以右键点未完成的文件选择续传不会前功尽弃。4.3 我要上传的成果是什么、目录怎么组织上传之前建议先把本地的结果目录整理干净不要把中间产物乱七八糟地一起打包传过去。我这次项目的交付目录结构如下road_sign_project/ ├── README.md # 项目说明 ├── src/ # 全部源代码抽帧、转换、训练、推理 ├── weights/ │ └── best.pt # 训练好的最佳权重 ├── outputs/ │ ├── detect_video_01.mp4 # 检测结果视频 │ └── stats_report.txt # 结果统计信息 └── dataset/ ├── images/ # 筛选并标注后的图像 └── labels/ # 对应的YOLO格式标签注意不要把原始视频或者抽取但未筛选的帧也塞进去。交付文件体积尽量精简只保留可复现、可检查的文件。FinalFTP上传前可以先在右侧远程目录里建好同名文件夹再整体拖拽上传层级清晰对方也方便下载。5. 从抽帧到检测视频一条完整流程的实测记录5.1 项目目录规划整套流程跑通之前先把目录规划好避免后面返工。我的目录结构如下D:/road_sign/ ├── videos/ # 原始道路视频素材 ├── frames/ # 抽帧后的图像未筛选 ├── dataset/ │ ├── images/ # 筛选并完成标注的最终图像 │ ├── labels/ # 转换后的YOLO格式标签 │ ├── train.txt │ └── val.txt ├── scripts/ # 所有Python脚本 └── outputs/ # 检测结果视频与日志这个结构的好处是每个环节都有独立目录互相不污染。frames里的原始抽帧永远不会动如果标注标坏了还能重新来一遍标注后的图像单独复制到dataset/images确保训练数据是干净的。5.2 流程中各阶段的耗时记录我这次项目实际数据规模是6条道路视频、每条3到8分钟不等抽帧得到约1800张图像人工筛选后保留1200张最终标注有效图像860张。以下是每个环节的实测耗时环节操作内容实际耗时抽帧OpenCV脚本批量处理6段视频约15分钟图像筛选VSCode里人工过目删除无效帧约40分钟标注labelme逐张画多边形约4小时格式转换JSON转YOLO格式脚本约3分钟数据划分按视频分桶生成train/val约2分钟模型训练YOLOv8n 100个epochRTX 3060约1小时20分钟视频推理新道路视频检测约5分钟整个流程从动手到出结果不到一天时间。如果走云端SSH路线光上传1200张图像和下载权重就需要不少时间还可能因为环境问题额外花半天调试综合效率本地方案明显占优。5.3 训练与推理过程中的关键实测参数训练环节我用的超参数如下直接照抄也能跑模型YOLOv8n预训练权重yolov8n.pt因为交通路牌是相对简单的目标不需要上yolov8s以上输入尺寸640x640batch size1612GB显存足够epoch100但实际跑到第72轮早停因为验证集loss不再下降优化器AdamW初始学习率0.0001最终验证集上mAP50约为0.85mAP50-95约为0.61。不同路牌类别的表现有差异限速标志这类样式标准且变化少的类别准确率最高警告标志里的临时施工标志因为外观变化大单独看mAP稍低这也是正常现象。推理环节我用训练好的best.pt对一段从未参与训练的道路视频做目标侦测yolo predict modelD:/road_sign/weights/best.pt sourceD:/road_sign/videos/test_road.mp4 saveTrue imgsz640 conf0.4输出视频保存在runs/detect/predict目录下。整段3分钟的视频推理RTX 3060大约耗时15秒关键帧上能稳定框出限速牌和警告牌。5.4 本地推理的后处理把检测结果提炼成交付材料视频推理完成后我不会直接把原视频交出去一般还会加一道后处理抽取关键帧把检测到的路牌数量和类型做一个统计报告。用Python脚本读取predict目录下的标签文件统计每个类别出现了多少次输出成stats_report.txt。这个报告可以直接作为项目交付的说明材料帮对方快速了解模型的实际表现。这一步不属于必须操作但做出来后项目的专业度会提升很多强烈建议花5分钟处理一下。6. 这套流程里最值得记住的踩坑清单6.1 labelme安装时的pyqt5-sip版本冲突这是出现频率最高的坑几乎每次帮别人复现这套流程都会碰到。症状是安装pyqt5-sip时报编译错误或者启动labelme时直接报sip相关异常。解决办法在前面已经提到用旧版labelme配上适配的pyqt5-sip版本最稳定。不要碍于强迫症追求全部依赖最新版这套组合在多个机器上验证过稳定性优先。6.2 标注类别名不一致导致的训练崩溃YOLOv8加载自定义数据集时如果标签中出现data.yaml里不存在的类别ID会直接报标签格式错误训练崩溃。更隐蔽的情况是类别名正确但标注顺序混乱模型训练时把限速样本教成警告。这种情况标签值正常但语义错乱损失函数不会报警只有看到分类混淆矩阵时才能发现。所以强烈建议在标注完成后、转换前用前面那段统计标签名称的脚本全量扫描一遍发现问题提前处理。6.3 JSON转YOLO时最容易算错的坐标归一化labelme的JSON里图像宽高是imageWidth和imageHeight字段像素坐标的分母必须正确取到这两个值。如果误拿标注时窗口显示的分辨率当分母或者把JSON里points的坐标理解反了转换出来的框轻则偏移、重则严重变形。实测中最常见的错误是小数点精度问题归一化坐标保留6位小数基本够用但如果图像尺寸很大或目标框很小建议保留8到10位小数避免精度损失导致目标框漂移。6.4 视频推理时的显存占用虽然推理显存比训练小得多但如果你打开多个视频同时预测或者推理视频分辨率非常高比如4K道路视频显存还是可能爆掉。我的策略是推理前先确认imgsz参数不要盲目用大尺寸。对交通路牌这类目标imgsz640和imgsz1280的精度差异很小但推理速度和显存占用差好几倍。如果视频特别长建议先用OpenCV切段逐段推理再拼接结果视频。6.5 FinalFTP传输中断后的续传处理传输几个GB的成果文件夹时中途断网或者对方服务器重启会导致传输中断。很多人的第一反应是把文件删掉重新传这其实没必要。在FinalFTP的传输队列里右键中断的任务选择续传它会自动对比本地和远程文件大小从断点处继续传。前提是中途不要手动删除远程的临时文件否则续传会从头再来。另外注意FinalFTP传输过程中如果遇到磁盘满或目录权限问题它会把任务标记为失败而不是自动重试。传完之后记得核对远程侧的文件数量和大小避免交付材料缺文件。最后几件小事整个流程跑下来我最大的体会就是很多项目用不着那么大动干戈。数据量在千张级别、模型在YOLOv8n这个量级本地一台游戏本完全扛得住。不SSH、不连云端不是技术上的妥协反而帮你把精力从环境配置里解放出来专注于标注质量和数据分布这些真正影响模型效果的事。这套方案里最关键的几个决定分别是把标注类别提前锁死、抽帧时按视频分桶做数据划分、交付前用FinalFTP把成果整理成清晰的目录结构。建议你也刻意把这些节点当成流程的一部分而不是临时想起来的后处理。后面再遇到类似项目直接把这套目录模板和脚本拷过来改改就能用。
返回列表