ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

基于YOLO与SpringBoot的安全锥智能巡检系统实战:从数据标注到大模型分析

基于YOLO与SpringBoot的安全锥智能巡检系统实战:从数据标注到大模型分析 做安全锥检测这个项目其实是去年下半年接的一个公路养护单位的智能化巡检需求。他们每天派巡检员开车跑高速靠肉眼数施工区摆放了多少安全锥、有没有倒伏、间距是否合规一天下来上百公里既累又容易漏。当时我就在想这事完全可以让计算机视觉来干车前装个摄像头图像实时传入系统用YOLO把安全锥框出来再结合大模型生成一份巡检结论最后在网页上调出来看。这个思路落地之后效果比预期好很多今天就把整套系统的技术方案和实操过程完整写出来包含数据集处理、YOLO模型训练、SpringBoot后端、千问和DeepSeek接入、前后端分离的Web交互界面给正在做类似巡检类项目的朋友一个可以直接参考的路径。1. 项目全貌安全锥检测到底要做什么1.1 这个系统的真实应用场景安全锥的学名叫交通锥北方也叫路锥民间叫雪糕筒它算得上道路施工、事故处理、临时管制场景里出现频率最高的设备。传统巡检靠人工不仅效率低而且很难做量化管理——比如某个施工路段应该摆20个安全锥实际只摆了17个哪个桩号位置缺了、哪个倒在地上这些问题人工肉眼扫过去很难在第一时间发现。我把这个问题拆解成三块识别、统计、分析。识别靠目标检测模型把图像或视频流里的安全锥逐帧找出来统计是在识别基础上做计数、定位、条数对比分析则是把检测结果结构化之后交给大模型生成自然语言的巡检结论比如“当前路段共有安全锥23个其中1个疑似倒伏建议立即处理”。这三层做完再套一个Web界面整个系统就立体了现场人员不用懂AI打开网页上传图片或者看实时视频流就能用。1.2 技术栈组合背后的思路项目核心的技术选型是“YOLO SpringBoot 大模型API”。为什么不选TensorFlow或者更传统的Faster R-CNN因为巡检场景对实时性和部署复杂度很敏感YOLO系列本身就是工业部署的主流选择YOLOv8之后更是把训练、导出、推理这一套流程工具链做得很完善数据标注好之后几乎可以一键开训。SpringBoot则是后端服务的稳妥选项Java生态在做接口、权限、数据库这块太成熟了尤其是这种偏企业级的养护系统甲方往往对SpringBoot有明确要求。大模型部分用了通义千问和DeepSeek主要原因有两点一是接入成本低官方API按量付费不需要自己部署计算资源二是中文理解能力强对公路巡检这类中文报告场景千问和DeepSeek生成的分析文本质量实测比某些通用模型更靠谱。再单独说一下为什么采用前后端分离架构。这个项目从第一天起就预期会有多个使用端——Web管理后台、现场大屏、以后可能还有移动端。如果做单体模板渲染每一次加端都得动后端代码非常痛苦。前后端分离之后后端只负责出接口前端独立部署未来不管加什么客户端后端逻辑基本不用改。这个决策在后期联调阶段给我省了大量时间。2. 数据优先YOLO训练数据的准备与格式转换2.1 数据来源与标注工具做目标检测项目数据永远是最耗时的环节。安全锥这个类别并不冷门先从公开渠道找到了一批包含安全锥的交通场景图片真实场景中还包括行人和各类车辆正好顺带把模型做成多类别检测。但这部分数据集直接拿来用会有一个问题标注框的风格差异大有些标注得比较宽松有些又框得很紧统一性不足。我的建议是把公开数据当作预训练基础必须标注一批自己业务现场的真实数据来兜底。我当时找合作单位要了几段养护作业的监控视频抽帧之后筛出不同光照、不同角度、有遮挡的图片补标了约800张数量和公开数据合并后大概有3000多张这个规模对单类别检测已经够用了。标注工具推荐用LabelImg或者X-AnyLabeling。LabelImg是老牌工具操作简单X-AnyLabeling支持半自动标注预加载一个YOLO模型你只需要微调效率能提高不少。标注时特别注意一点凡是露出一半以上锥体的都要标倒伏的、部分遮挡的、远处模糊的宁标勿漏否则模型推理时会把没见过的形态当作背景直接忽略。2.2 KITTI标注转YOLO格式实操很多开源自动驾驶数据集用的是KITTI标注格式比如Pedestrian 0.00 0 -0.20 712.40 143.00 810.73 307.92 ...前面是类别名称后面是目标框坐标。但YOLO训练需要的是归一化后的txt文件一行代表一个目标格式是class_id x_center y_center width height而且全部归一化到0到1之间。这俩格式不互通直接拿KITTI标注喂给YOLO训练出来的模型必然混乱。所以第一步就是写一个转换脚本。import os import shutil def kitti_to_yolo(kitti_txt_path, img_width, img_height, class_mapNone): 将KITTI格式的标注转为YOLO格式 class_map: {Car: 0, Pedestrian: 1, Cone: 2} yolo_lines [] with open(kitti_txt_path, r, encodingutf-8) as f: for line in f: parts line.strip().split() if len(parts) 15: # 某些KITTI标注行可能缺后面的一串属性跳过即可 continue cls_name parts[0] left float(parts[4]) top float(parts[5]) right float(parts[6]) bottom float(parts[7]) # 过滤掉无效框 if right left or bottom top: continue x_center (left right) / 2.0 / img_width y_center (top bottom) / 2.0 / img_height w (right - left) / img_width h (bottom - top) / img_height # 归一化后做一次边界裁剪防止出现超出0~1的数值 x_center max(0.0, min(1.0, x_center)) y_center max(0.0, min(1.0, y_center)) w max(0.0, min(1.0, w)) h max(0.0, min(1.0, h)) cls_id class_map.get(cls_name, 0) yolo_lines.append(f{cls_id} {x_center:.6f} {y_center:.6f} {w:.6f} {h:.6f}) return yolo_lines # 使用示例 lines kitti_to_yolo(000001.txt, img_width1280, img_height720, class_map{Cone: 0}) with open(000001.txt, w, encodingutf-8) as f: f.write(\n.join(lines))拿到YOLO格式标注之后还要把数据集整理成目录结构。我的习惯是直接按YOLO官方风格建images和labels两个根目录下面再分train和valdatasets/cone/ ├── images/ │ ├── train/ │ │ ├── 000001.jpg │ │ └── ... │ └── val/ │ ├── 001200.jpg │ └── ... ├── labels/ │ ├── train/ │ │ ├── 000001.txt │ │ └── ... │ └── val/ │ └── ... └── data.yaml这里有个很多人会忽略的问题图片和标注文件必须同名同前缀。YOLO训练时是按文件名找对应标注的一旦名字对不上那张图就会被当作无目标图片处理轻则影响训练重则导致模型学不到内容。我第一次整理数据时就吃过这个亏后来写了一个校验脚本把没有对应标注的图全部筛出来清理掉了。2.3 数据增强与小目标漏检的应对安全锥在画面里往往是很小的目标尤其是几百米外的安全锥可能只有十几个像素这属于典型的小目标检测难题。模型容易漏检本质上是因为COCO预训练模型对8x8像素以下的物体几乎无感。当时我试过几种解决办法第一种是提升输入分辨率把imgsz从默认的640提到960小目标特征保留明显变多但训练和推理时间上涨明显第二种是给训练加Mosaic和MixUp数据增强让模型见过更多遮挡、拼接的情况第三种是在数据集里有意多放一些安全锥占比小的全景图强迫模型去学习小目标特征。我的实际结论是项目优先保证中等距离目标的检测精度近处目标因为尺寸大什么模型都能检测到真正影响用户体验的是远距离漏检。所以最终采用的方案是imgsz960关闭了部分过于激进的增强策略如90度随机旋转因为安全锥有显著的上下方向特征旋转过度会导致语义错乱。3. 模型训练与版本选型3.1 YOLOv8/v10/v11/v12怎么选标题里列了四个版本的YOLO很多朋友会纠结到底用哪个其实它们各自的应用场景差异比较大。我实际测试下来的体验是YOLOv8最稳的版本文档最全、社区最大、导出的ONNX和TensorRT方案非常成熟。如果你的项目处于快速验证阶段优先用v8。YOLOv10主打无NMS推理去掉了Non-Maximum Suppression环节推理延迟理论更低部署时少一个后处理步骤。但训练得到的模型在密集目标场景里偶尔会出现重复框需要额外留意。YOLOv11在v8基础上优化了Backbone和C3K2模块精度小幅提升推理速度基本持平属于小步快跑的升级可以无缝迁移v8的训练经验。YOLOv12引入了注意力机制对复杂背景的抗干扰能力更强不过训练速度偏慢需要仔细调学习率。我的建议很直接如果你的推理设备算力有限用v8n或者v8s就够了如果服务器算力充足且对精度要求高直接上v11m或v12m。对于安全锥检测这种目标类别少、形态相对固定的任务没必要追求最大的模型。小模型推理速度快实时性更好日常巡检完全够用。我最终在服务端部署的是YOLOv11s精度和速度的平衡最理想。3.2 训练配置与损失函数理解确定数据集和模型之后训练配置是下一步重点。我用的YOLOv11配置文件大致如下# cone.yaml path: ./datasets/cone train: images/train val: images/val nc: 1 names: 0: cone训练命令yolo detect train \ datacone.yaml \ modelyolo11s.pt \ epochs200 \ imgsz960 \ batch16 \ device0 \ patience30 \ projectruns/cone \ nameexp1 \ pretrainedTrue \ cacheTrue有几个参数值得重点解释。patience30是早停策略连续30个epoch在验证集上mAP没有提升就停止训练避免过拟合。cacheTrue会把数据集缓存到内存显著减少训练时磁盘IO的等待时间如果你的机器内存够大16G以上建议打开。pretrainedTrue表示加载COCO预训练权重对收敛速度帮助非常大尤其适合数据集规模只有几千张的场景。说到损失函数很多人容易把它当成一个黑盒。YOLO的损失由三部分组成box_loss边框回归损失、cls_loss分类损失、dfl_loss分布焦点损失。box_loss衡量预测框和真实框的差异用的是CIoU或者WIoU这类损失让模型不只是框得“差不多”而是框得“准”。cls_loss是二值交叉熵负责让类别预测更准确。dfl_loss是YOLOv8之后新加的它把边框的坐标预测当成一个分布问题来优化对小目标定位的提升比较明显。训练时如果发现最终模型边界框偏大或者偏小可以考虑调整box_loss的权重系数不过对安全锥这种框体一致性较强的目标来说默认权重就够用。3.3 评估指标与训练结果解读训练完之后别急着部署先用验证集做一轮评估。YOLO训练过程中会在验证集上持续输出mAP50、mAP50-95、precision、recall这几个指标。安全锥检测项目我最关注的是mAP50和recall。前者衡量的是实例检测的“框得准不准”后者则更聚焦“有没有漏掉目标”。当时我的模型最终停在epoch 160左右早停触发mAP50到了0.93recall 0.90这个成绩对单类别检测来说已经是可用状态了。还有一件事千万别省把验证集的预测结果图翻出来一张一张看。训练日志里的数值只能说明模型“总体上还行”真正暴露问题的是具体图片。比如我当时发现远距离小目标还有明显的漏检后来回去核对了一下数据分布发现训练集里小目标图片占比偏低于是补了一批远景图重新训练mAP50勉强到了0.94但实际的远距离漏检率下降了大概三分之一。所以我的结论是训练完多花一个小时翻图比盲目调参一整天更有效。4. 模型部署与推理服务4.1 部署形态选择为什么单独拆一个Python服务模型训练好后部署时面临一个技术选型问题是直接把模型集成进SpringBoot通过ONNX Runtime Java API或者Deep Java Library还是把模型单独部署成一个Python推理服务再让SpringBoot通过HTTP调用我的实践结论是单独拆一个Python推理服务。原因有几个层面。一是生态问题YOLO官方对Python的支持最完善很多新特性比如TensorRT导出、SAHI切片推理Python侧的支持永远是最及时的Java侧的兼容性不确定性太高。二是资源隔离目标检测推理是典型的CPU/GPU密集型任务单独部署可以独立扩缩容不会和业务接口互相抢占资源。三是维护成本模型更新非常频繁今天调个权重、明天换个版本如果集成在Java进程里每次更新都要重新编译打包而独立服务只需要替换模型文件再重启省事太多。4.2 模型导出与加速YOLO训练出来的原生权重是PyTorch格式.pt生产环境直接用这个文件推理速度太慢。我当时把它导出成两种格式CPU服务器上用ONNX有GPU卡的环境用TensorRT引擎。# 导出ONNX yolo export modelruns/cone/exp1/weights/best.pt formatonnx imgsz960 # 导出TensorRT需要NVIDIA GPU yolo export modelruns/cone/exp1/weights/best.pt formatengine device0 imgsz960ONNX导出后推理框架我用的是ONNX Runtime比直接用PyTorch快不少。TensorRT引擎要针对具体显卡型号和CUDA版本生成换机器之后得重新导出这也是它最大的不便之处。但换来的是推理速度几乎翻倍如果你手头有可用的GPU卡值得花这个时间。CPU下推理性能也要关注。我当时在一台2核4G的云服务器上测试ONNX模型跑960分辨率输入单帧推理耗时大概220毫秒换TensorRT之后同一台机器如果没GPU的话只能靠CPU推理也就到180毫秒左右。如果后续要做实时视频流检测我建议把输入尺寸降到640或者用SAHI切片推理——把大图切成若干小块分别检测再合并结果虽然总耗时上升但小目标的召回率会好很多。4.3 推理接口封装推理服务我用FastAPI写的接口设计得很简单一个健康检查接口一个图片上传检测接口一个Base64直传接口。关键代码如下from fastapi import FastAPI, UploadFile, File from PIL import Image import numpy as np import cv2 from ultralytics import YOLO app FastAPI() model YOLO(cone_best.onnx) # 加载ONNX格式模型 app.post(/detect) async def detect(file: UploadFile File(...)): # 读图 image_bytes await file.read() img cv2.imdecode(np.frombuffer(image_bytes, np.uint8), cv2.IMREAD_COLOR) # YOLO推理 results model.predict(img, conf0.4, iou0.45, imgsz960, verboseFalse) # 整理成JSON结果 detections [] for r in results: boxes r.boxes.xyxy.cpu().numpy().tolist() scores r.boxes.conf.cpu().numpy().tolist() classes r.boxes.cls.cpu().numpy().astype(int).tolist() for box, score, cls in zip(boxes, scores, classes): detections.append({ bbox: [round(v, 2) for v in box], score: round(score, 4), class_id: cls, class_name: model.names[cls] }) return { success: True, count: len(detections), detections: detections }这里的conf0.4阈值是调出来的。阈值太高容易漏检太低又会出现大量误报。安全锥颜色醒目、形态特征强0.4到0.5之间是一个比较可靠的范围。处理视频流时还可以加一个轻量的目标追踪逻辑把相邻帧的同一个锥连接起来避免重复计数。5. SpringBoot后端开发5.1 后端模块与数据库设计推理服务就位后SpringBoot后端要承担起业务编排的角色。我按功能拆分成这几个模块用户模块登录鉴权、巡检任务模块创建、查询、状态流转、检测记录模块存储每次检测的图片、结果JSON、报告、大模型分析模块调用千问和DeepSeek的封装、系统配置模块API Key管理、阈值设置。数据库方面MySQL是主力存储。第一次建表时我直接用项目的SQL脚本初始化后来发现开发环境来回换表结构经常变干脆引入了自动建表机制。Spring Boot整合MyBatis-Plus后配置mybatis-plus.global-config.db-config.table-auto-create或者用Flyway做版本化迁移都可以实测Flyway的迁移能力更优雅每次脚本变更都有版本记录团队协作时不会被本地库差异坑到。存储检测结果时有个细节要提一下目标检测返回的bbox、类别、置信度列表如果做多字段表存储会很痛苦而且后期调整检测逻辑时还要改表结构。我的做法是直接存一个JSON类型的字段把Python推理服务返回的detections数组原样塞进去。查询时按任务ID过滤即可报表统计时用JSON_EXTRACTMySQL 5.7也能很方便地取出数量。5.2 核心接口实现接口设计遵循RESTful风格主要端点大概是这样POST /api/task 创建巡检任务 POST /api/task/{id}/upload 上传巡检图片/视频 POST /api/task/{id}/detect 触发检测并保存结果 GET /api/task/{id}/result 获取检测结果 GET /api/task/{id}/report 获取大模型分析报告 GET /api/task/history 历史巡检记录列表 GET /api/stats/overview 统计面板数据上传检测这个链路有一个设计决策值得说说我把上传和检测拆成两步。原因是巡检场景经常面临弱网环境巡检员拍完照片之后可能要先上传之后网络稳定了再批量触发检测。如果上传接口内部直接同步调用推理服务碰到大图或者多图请求时前端等一个HTTP响应的体验会比较差。所以上传接口只做文件存储存OSS或本地磁盘返回文件ID检测接口则把待检测文件ID列表传进来后端同步调用Python推理服务完成检测。Java侧调用推理服务时要注意超时设置。当时我踩过一次坑视频抽帧检测的任务耗时比较长Python服务要处理几十帧图像结果超过默认的5秒超时时间SpringBoot直接抛了Read Timeout异常。后来我用RestTemplate或者WebClient设置连接超时10秒、读取超时60秒瓶颈问题就解决了。更复杂的场景建议上消息队列RabbitMQ或Kafka异步处理长任务。5.3 SSE流式输出与配置安全大模型生成分析报告是有“思考时间”的可能持续几秒到几十秒。如果前端一直都在转圈等待一个完整的JSON返回体验很差。我采用的方案是SSEServer-Sent Events服务器推送事件后端把大模型返回的文本按token拆包推送给前端前端逐字渲染用户看起来就像AI在做实时打字体验一下就好很多。Spring Boot实现SSE非常简单用SseEmitterRestController RequestMapping(/api/task) public class ReportController { GetMapping(/{id}/report/stream) public SseEmitter streamReport(PathVariable Long id) { SseEmitter emitter new SseEmitter(60_000L); // 异步调用大模型服务每拿到一段文本就send一次 executorService.execute(() - { try { String fullReport aiAnalysisService.analyze(id, chunk - { try { emitter.send(SseEmitter.event().data(chunk)); } catch (IOException e) { emitter.completeWithError(e); } }); emitter.send(SseEmitter.event().data([DONE])); emitter.complete(); } catch (Exception e) { emitter.completeWithError(e); } }); return emitter; } }配置安全这块API Key不能明文写在application.yml里尤其是项目要推给甲方或者放到Git仓库的时候。我当时用Jasypt做配置文件加密把spring.datasource.password、dashscope.api-key、deepseek.api-key全部加密存储启动时通过环境变量注入解密密码。在application.yml里只能看到一串密文即使仓库泄露数据库和大模型的密钥也不会被人直接拿走。6. 千问DeepSeek接入实现智能分析6.1 大模型API接入千问和DeepSeek都提供了兼容OpenAI格式的HTTP接口这点很关键意味着你可以用同一个SDK或者同一套调用代码只需要换base_url和api_key。千问走的是DashScope服务DeepSeek走的是其官方开放平台。它们都支持gpt-3.5-turbo风格的chat/completions接口从代码层面看换个配置就能切换底座。我当时的封装思路是定义一个LlmProvider接口包含chat(messages, callback)方法然后分别实现QwenProvider和DeepSeekProvider业务层通过Value注入当前启用的provider。这样不仅支持双模型切换还能做简单的负载分发——比如分析报告用千问因为它的中文Summarization能力在同级别里较好日常问答和短文本处理用DeepSeek性价比更高。实际调用代码大致如下// 以千问DashScope为例 public String callQwen(String prompt, ConsumerString onChunk) { // 使用OpenAI SDK设置baseUrl为DashScope的兼容地址 OpenAIClient client OpenAIClient.builder() .baseUrl(https://dashscope.aliyuncs.com/compatible-mode/v1) .apiKey(qwenApiKey) .build(); ChatCompletionRequest request ChatCompletionRequest.builder() .model(qwen-max) .messages(List.of( Message.system(你是一名道路养护巡检专家...), Message.user(prompt) )) .stream(true) .build(); StringBuilder sb new StringBuilder(); client.chatCompletionStream(request) .doOnNext(resp - { String delta resp.getChoices().get(0).getMessage().getContent(); if (delta ! null) { onChunk.accept(delta); sb.append(delta); } }) .blockingSubscribe(); return sb.toString(); }不过有个细节必须提醒流式API的HTTP连接不能被代理或者网关切断。我们当时在排查一个问题时发现通过Nginx反代SSE接口时如果Nginx没开proxy_buffering off大模型返回的内容会一直被缓冲前端迟迟收不到增量数据表现就是页面一直没文字输出。这个问题困扰了我们大半天最后定位到是Nginx配置缓存造成的把缓冲关掉后立刻正常了。6.2 提示词设计与结构化输出同样是调用大模型不同提示词拿到的结果天差地别。为了让大模型的分析能真正投入巡检业务我把提示词设计成了“结构化输入约束输出”的模式。调接口时传入的检测数据长这样{ taskId: T20240612001, roadSection: G2高速K128500至K129000, detectedCount: 23, expectedCount: 25, detections: [ {bbox: [523, 410, 572, 478], score: 0.91, class_id: 0, class_name: cone}, {bbox: [1020, 386, 1158, 442], score: 0.82, class_id: 0, class_name: cone} ], abnormalFlags: [count_mismatch, low_confidence_near_km128_800] }提示词的模板我写了很长一段核心是下面几层指令角色设定你是具有十年高速公路养护经验的安全巡检工程师。任务指令根据检测JSON生成一份巡检简报必须包含总体判断、问题清单、处置建议三部分。约束条件只基于给定的检测数据进行判断不要编造未检测到的信息使用简明中文如无异常直接说明输出格式用固定的Markdown序号结构。边界处理如果检测出的数量与预期不符优先给出人工复核建议而不是直接下结论说现场违规。实测下来这个提示词设计让千问和DeepSeek返回的分析报告质量非常稳定。之前有人用很简单的“帮我分析一下这些数据”之类的提示词大模型容易发散甚至生成一堆无关痛痒的废话。提示词里带上精确的数字和明确的三段式结构输出质量立刻上了一个台阶。这里顺带说一下Postman调试技巧。很多人在调大模型API时直接在代码里试错效率很低。其实这类兼容OpenAI的接口完全可以用Postman先跑通新建一个POST请求填上https://dashscope.aliyuncs.com/compatible-mode/v1/chat/completionsHeaders里加Authorization: Bearer 你的KeyBody里按JSON格式把model、messages、stream填好点Send就能看到完整响应。先把接口在Postman里调通再回Java代码写封装出问题时的排查范围会小很多。7. Web前端与前后端联调实战7.1 前端页面结构前端我选的是Vue3 Vite Element Plus ECharts这套组合做中后台管理系统非常顺手。页面拆成四个核心视图巡检大屏展示当前巡检任务的实时推流画面如有或最近检测的图片左侧安全锥计数卡片右侧检测框叠加层底部事故等级预警。检测记录按时间倒序展示历史检测记录点开一张图能看到原始图像、YOLO标注框、检测结果表格、大模型分析报告。统计分析用ECharts画安全锥数量趋势、倒伏锥比例、各路段检出量对比柱状图甲方汇报时直接截这个页面就行。系统配置模型置信度阈值、大模型API Key、任务预期数量等参数的动态配置。前端调用接口时统一封装了request.js基于Axios拦截器里塞Token、统一处理错误码。大模型流式部分用EventSource或者fetch的ReadableStream实现。需要注意一件事EventSource只能发GET请求如果要带比较复杂的请求体建议改用fetchReadableStream自由度更高。7.2 联调中的常见问题前后端联调阶段我遇到最多的问题是CORS跨域。SpringBoot默认不允许跨域前端在开发模式下访问http://localhost:8080而后端跑在http://localhost:9999直接被浏览器拦截。解决办法两种一种是在后端写一个CorsFilter配置类统一放行另一种是用Vite的代理开发时把/api前缀代理到后端地址生产环境用Nginx做同源转发。我推荐第二种因为生产环境本来就需要Nginx挂前端静态资源和反向代理后端开代理之后连CORS都不用管了。开发代理配置很简单// vite.config.js export default defineConfig({ server: { proxy: { /api: { target: http://localhost:9999, changeOrigin: true, rewrite: (path) path.replace(/^\/api/, /api) } } } })联调时另一个常见问题是前端展示的图片路径不对。SpringBoot把图片存到本地磁盘的/data/upload/目录后前端拿到的是/files/xxx.jpg需要后端做一个静态资源映射把http://localhost:9999/files/**映射到磁盘目录。用WebMvcConfigurer的addResourceHandlers方法即可。如果图片存的是OSS那只需要返回完整URL注意URL签名过期时间的问题即可。还有一个很隐蔽的坑请求体大小限制。SpringBoot默认spring.servlet.multipart.max-file-size是1MB而巡检现场拍的高清图动辄5MB以上。第一次联调上传大图时后端直接抛FileSizeLimitExceededException前端却显示请求成功但数据为空排查了好久。后来在application.yml里调大spring: servlet: multipart: max-file-size: 50MB max-request-size: 100MB8. 部署与常见问题排查8.1 服务器部署与硬件兼容整个系统我最终部署在一台云服务器上前端用Nginx托管打包后的静态文件后端SpringBoot打成jar包用systemd守护进程管理Python推理服务单独跑在另一个端口上。数据库一开始就用云数据库不需要自己运维MySQL。如果你要把项目部署到阿里云这类云平台流程基本是前端npm run build生成dist把dist传到服务器并配置Nginx指向Java进程用nohup java -jar或者systemd跑起来Python推理服务用uvicorn启动并配置systemd。需要注意云服务器的安全组规则9999、8000这类非标准端口默认不开需要在控制台手动添加规则放行否则外部访问不到。这里要重点说一下硬件兼容性问题。很多同学私信问我AMD的RX 580显卡能不能跑YOLO训练我的回答是可以装环境但很折腾。RX 580对CUDA生态不友好YOLO官方在Linux下主要是靠NVIDIA CUDA加速AMD显卡需要转向ROCm方案但RX 580对ROCm的支持非常有限实测性能也远不如同价位的NVIDIA显卡而且经常遇到库版本兼容问题。如果你手里只有AMD显卡建议训练阶段先租一块云GPU比如阿里云的A10或T4实例本地用CPU跑些小数据的debug流程通了再上云训练。如果坚持本机训练可以试试DirectML这个方向Windows下能调用AMD显卡做推理加速但训练速度依然不乐观。总结一句话目标检测训练这件事N卡仍然是当前最省心的选择。8.2 高频问题速查表最后整理一份我在整个项目周期里踩过的高频问题清单按出现频率排序#问题现象根因分析解决方案1SpringBoot启动报错依赖冲突SpringBoot 3.x版本过高部分第三方starter还未适配生产环境先固定在2.7.x稳定版本或用3.2并逐项检查依赖2上传大图后接口超时multipart限制或Python服务推理耗时过长调大文件限制设置RestTemplate合理超时时间3YOLO预测结果在远处频繁漏检小目标特征不足可视化阈值过高提升imgsz到960合理设置conf阈值考虑SAHI切片4SSE前端一直不显示内容Nginx缓冲未关闭在Nginx location配置proxy_buffering off、X-Accel-Buffering: no5训练时显存不足batch或imgsz过大调小batch或开启cache并将数据放进内存降低IO压力6KITTI标注转YOLO后训练报错坐标归一化后出现负值或大于1写转换脚本时对归一化结果做裁剪过滤掉无效标注框7MyBatis说表不存在项目换了新库但没初始化表引入Flyway迁移或用MyBatis-Plus的自动建表配置8CORS跨域报错前后端端口不一致且未配置CORS开发环境用Vite代理生产环境用Nginx转发表格里最让我印象深刻的是第一个SpringBoot版本问题。当时图新直接用了SpringBoot 3.3.0结果接工作流引擎的时候发现兼容性很差一些老牌库只适配到Spring Boot 2.7。后来我把项目降级到2.7.18才彻底消停。这个教训让我后来做项目选型时不再盲目追求最新版本尤其是企业项目和甲方系统对接时稳定优先、版本尽量跟主流才是正道。最后再分享一点个人的经验整套系统从数据标注到上线前后花了不到三周。回头看最核心的经验可以浓缩成三句话。第一数据永远比模型重要安全锥检测这个任务你花三天精标数据比花三天调任何模型参数的收益都大。第二系统集成比单个模型更费时间YOLO模型训练其实是这个项目里最顺利的一环真正卡壳的地方全在SpringBoot和前端联调、大模型流式输出、服务器部署这些环节所以做项目时一定要给集成阶段预留足够的时间。第三大模型接入看似简单提示词和流式体验才是关键你给模型的数据结构和约束指令写得越明确生成报告的质量就越稳定。这个项目后续我还打算加上骨密度检测之类的新品类理论上只要换一个训练好的检测模型再改改提示词模板整套系统架构完全不用动。技术选型一旦做对后面扩展就是水到渠成的事。
返回列表