基于Django与YOLOv5的靶纸弹孔识别与自动判分系统
1. 项目概述与核心需求解析先把这个项目的骨架讲清楚。标题里其实藏着一条很清晰的技术链路前端用html、css、jquery做页面展示和交互后端用django orm做业务逻辑和数据持久化数据库选mysql存结构化数据最后用pytorch训练一个模型来识别靶机上的弹孔位置、自动判分。说白了这就是一个图像识别 web管理系统的典型组合只不过应用场景从常见的车牌识别、人脸打卡换成了射击训练场里的靶纸判读。这类系统在真实场景里的需求是很明确的。一个射击训练场每天可能产生几百上千张靶纸如果全部靠教练肉眼去数弹孔、量环数效率低不说还存在主观误差——弹孔擦边的算几环、重叠弹孔怎么数不同教练的判断标准可能都不一样。用深度学习模型来做靶面识别读出的弹孔坐标、环数判定都是完全客观的能够直接复现同一个结果这本质上解决的是人工判读不一致、效率低、数据难归档三个痛点。适合来读这篇内容的人我大致分三类第一类是刚入门django、想做一个完整前后端项目练手的开发者能从里面看到一个项目从数据到模型到web落地的全链路第二类是在做工业视觉、OCR、目标检测相关工作的朋友可以借鉴模型选型和后处理逻辑的取舍第三类是自己想搭一套靶场管理系统的技术负责人我踩过的坑、实测过的方案你能直接拿去复用。整个项目的架构其实不复杂核心就是把原本线下的射击-收靶-人工判读-手工记分流程改造成射击-拍照上传-模型自动识别-系统自动判分归档-成绩在线查询的线上闭环。后面我会从技术选型的原因讲起把数据采集、模型训练、后端接口、前端交互、部署踩坑这几个环节全部拆开来说。2. 技术选型背后的设计思路2.1 为什么用django而不是flask或fastapi后端框架的选择我是在flask、fastapi之间反复对比过才定的django。很多人觉得django太重但对于这种需要管理后台 API接口 用户权限 数据库模型一起上的项目django的重量反而是优势。django自带admin后台这是我看中的第一点。你不需要额外开发一套管理界面靶场工作人员可以登录admin去手动修正模型识别错误的数据、管理射手账号、维护射击场次信息这在实际使用中是非常高频的操作。如果用flask这些全部要从零写起开发周期至少多一个星期。ORM是第二点。django的ORM能把python对象直接映射到mysql表迁移命令一条python manage.py makemigrations加migrate表结构就同步好了不需要手写sql。对于这个项目来说我们需要射手表、场次表、靶纸记录表、识别结果表、成绩统计表表之间的外键关系用ORM的ForeignKey字段就能表达清楚查询成绩的时候还能用select_related做联表查询优化代码写起来很顺手。还有一点容易被忽视django的CSRF防护、XSS过滤、SQL注入防护是默认开启的不需要额外引入安全库。这种系统虽然不涉及支付级别的敏感数据但射手信息、训练成绩算个人隐私数据安全防护能少操心就少操心。2.2 前端用原生html、css、jquery的取舍前端这里我承认走了一条不算时髦但绝对够用的路。没有上vue或react纯粹就是怕引入前端工程化之后整个项目的复杂度失控——node环境、打包配置、跨域问题、前后端联调这些都是拖慢项目进度的大头。我的判断依据很简单这个系统的界面交互密度不高。核心页面就三个射手登录后提交靶纸照片的上传页、查询自己历史成绩的成绩页、管理员查看全量数据和修正结果的管理页。这些页面用jquery的ajax异步提交、动态渲染表格完全够用而且jquery的生态非常成熟遇到问题网上随便一搜就有答案。另外靶纸识别最大的交互需求其实是图片预览和判读结果可视化——用户上传照片之后系统要在前端把弹孔位置用红点标在靶纸上展示识别出的环数和总环数。这个用canvas配合jquery就能实现不需要引入额外的图表库代码量也不会膨胀。不过这个选择也有代价。前端代码的组织容易变得零散如果不去做模块化管理后期维护会有点痛苦。我的做法是把公共的ajax请求封装成一个util.js把靶纸绘制的逻辑单独抽出来放在canvasDraw.js里数据渲染部分放在各自页面的script中避免一坨代码堆在一个html里。2.3 pytorch在模型推理中的定位模型识别这层是整个系统的技术核心也是工作量最大的部分。pytorch在这里承担的是训练和推理两件事离线阶段用标注好的靶纸图片训练目标检测模型识别弹孔的位置训练完成后导出模型权重在django后端通过torch加载模型对上传的图片做推理推理结果经过后处理算法计算环数。这里我特别说一下为什么用pytorch而不是直接调api。市面上的通用图像识别服务说实话是识别不好弹孔的——环数区域的微细划痕、不同材质靶纸的反光差异、弹孔边缘的撕裂痕迹这些都需要专门用靶纸数据微调模型才能有好的效果。用pytorch自己训练意味着这个系统换一种靶纸类型、换一个比赛规则我们都可以重新标注数据再训练系统的泛化空间大得多。推理性能方面pytorch的cpu推理速度对单个图片来说其实够用一张1920x1080的靶纸照片在cpu上跑yolov5s或者faster rcnn大概200到500毫秒加上后处理整个识别流程控制在1秒内是完全可以做到的。如果未来并发量上来了可以改成调用GPU推理服务架构上只需要把模型推理封装成单独的服务不影响整体设计。3. 靶面识别模型的技术难点与方案落地3.1 数据采集和标注才是这项目最花时间的活先泼一盆冷水模型本身不是最难的部分最难的是数据。你需要标注的靶纸图片必须是和实际使用场景高度一致的——同样的拍摄角度、同样的光照条件、同样的靶纸型号。我从一开始就定了规矩训练数据全部来自靶场实际拍摄的靶纸照片而不是网上随便扒下来的图。数据集规模方面我最初用500张靶纸做了第一版模型mAP只有0.6左右错检漏检比较严重。后来把数据扩充到3000张让采集的人刻意覆盖不同距离拍摄、不同光线下的靶纸加上对图片做了随机旋转、亮度变化、加噪声这些数据增强操作模型mAP才稳定到了0.82以上。弹孔这个目标的标注和常规目标检测有一个很大的区别弹孔尺寸非常小在整张图片里往往只占几十个像素。比如一张靶纸照片分辨率是1920x1080一个弹孔的直径可能只有30到50个像素和常见的行人、车辆这类大目标完全不是一个量级。小目标检测的难点在于特征提取时容易丢失位置信息解决思路有几个一是用1024或更高分辨率的输入尺寸二是用yolo这类对网格划分敏感的模型配合多尺度检测层三是做切片推理把大图切成多块重叠的小图分别检测再合并结果。另一个隐性问题是弹孔重叠。10环区域被连续打了几枪之后弹孔之间会出现覆盖视觉上就是一个大洞或者椭圆形的边界模型容易只检出一个目标。我的解决方案是在标注数据时对于重叠弹孔依然标出每个弹孔的独立边界框即使它们视觉上已经连在一起。模型在学习时能学到边界特征的部分差异推理阶段再用后处理做轮廓分割补全。3.2 模型选型yolov5s是当前平衡点目标检测领域现在可选的范围很广yolo系列、faster rcnn、ssd、centernet都试过最终我选的是yolov5s版本。解释一下原因faster rcnn的精度在弹孔这类密集小目标上的确更高但推理速度只有yolov5s的三分之一左右在cpu上跑一张图接近两秒对用户体验是一个明显的压力。centernet这类anchor-free的方法在目标尺度差异大的场景下有优势但弹孔这种全图都是小目标的场景反而凸显不出它的优点。yolov5s在满足精度要求的前提下模型体积只有14MB左右在cpu上对单张图片的推理耗时能控制在300到400毫秒批处理能力也不错。部署时我直接把pt权重文件放进django项目目录torch加载之后通过model(img, size1024)[0]拿到检测结果非常方便。我个人建议不要盲目追求高版本。yolov8、yolov9这些新版的检测精度确实更好但是解析输出格式的复杂度也更高而且对部署环境的torch版本要求更严格。做这种中小型项目永远遵循够用就行的原则把精力留给后处理逻辑而不是在模型版本的边角料上较劲。3.3 环数判定坐标几何计算比想象中要复杂识别出弹孔位置之后接下来要做的是把弹孔坐标转换成环数成绩。这个环节考验的完全不是深度学习的知识而是基础坐标系换算和几何计算。靶纸上的环数区域是同心圆结构从10环到1环每个环有固定的半径比例。但问题在于照片里的靶纸不一定是正对着镜头的可能有一点透视变形照片里靶纸的实际像素尺寸也不固定远端手机拍的和近景拍的大小差好几倍。所以绝对不能直接把检测框的像素坐标拿去做环数计算必须先做一个空间校正。我采用的做法是在每一个靶纸上预先打印四个定位角标分布在靶面的左上、右上、左下、右下四个方位。检测阶段用另一个轻量模型或者图像处理的方法找到这四个角标的坐标如果靶纸背景相对干净用opencv的轮廓检测就能实现然后通过透视变换矩阵把整个图像映射到一个标准的靶面坐标系。经过这一步校正之后弹孔的坐标就相对于靶纸平面了再按照标准靶纸的环区半径比例去判断落点误差就能控制在可接受的范围。边界判定是最容不得马虎的。擦边的弹孔到底算哪一环我的处理方法是弹孔圆心落在环线外缘即算高环数。举个例子8环和9环的分界线半径是R如果弹孔中心到靶心的距离d满足 d R就判为9环否则8环。虽然这种判定标准在实战中可能存在争议但作为系统来说规则一旦明确并且统一执行反而是最公平的——因为所有人的判定标准都是一样的不会因人而异。3.4 后处理策略如何滤掉误检和无效数据模型不是万能的即使mAP到了0.82推理时依然会出现漏检或者误检的情况。漏检可以通过降低置信度阈值来缓解但这样做的副作用是误检变多——靶纸上的无关痕迹、光线反光点、甚至灰尘颗粒都可能被模型错认成弹孔。我的后处理流程分成三关。第一关是置信度过滤置信度低于0.35的检测框直接丢弃这个阈值是我在验证集上反复调出来的平衡点高于0.35会漏掉大量真实弹孔低于0.35会带来太多噪声框。第二关是位置过滤检测框中心点必须落在靶面判定区域内如果检测框的位置在校正后的靶面坐标系里超出了有效范围说明它大概率是靶纸外的杂讯同样丢弃。第三关是重复框合并多个重叠度很高的检测框用NMS非极大值抑制会把置信度最高的留下其余消除。这三关走完之后剩下的检测结果可以直接参与环数计算了。整个流程的伪代码思路如下def infer_score(image_path): detections model.detect(image_path, threshold0.35) valid_points [] for det in detections: if not in_valid_area(det.center): continue mapped_point warp_perspective(det.center, points_corners) valid_points.append(mapped_point) points nms_merge(valid_points) return calculate_total_score(points)这里我特别提醒一点后处理逻辑一定要在训练之前就想清楚。很多入门的项目把数据喂进模型、出了结果就直接当最终成绩展示结果模型错检的那几个框直接让成绩变得离谱。后处理和模型本身一样重要要当作独立的模块严谨设计和测试。4. Django后端与MySQL数据模型设计4.1 数据表结构规划数据模型设计是这个系统的地基地基没打好后面联调的时候会很痛苦。我实际的表结构是这样的射手表shooter存储射手基本信息id主键、姓名、工号、所属队伍、注册时间。工号设置成unique唯一索引这是日常查询成绩最常用的字段。场次表session代表一次射击训练或比赛id、场次名称、开始时间、结束时间、射手外键关联、状态进行中/已结束。这里用外键关联射手表示一个射手可以参加多个场次是一对多关系。靶纸记录表target_paper每射击一张靶纸生成一条记录id、所属场次外键、靶纸编号、原始图片路径、上传时间。这个表是整个识别流程的输入侧。识别结果表recognition_resultid、所属靶纸外键、弹孔x坐标、弹孔y坐标、环数、置信度。一张靶纸可能有多个识别结果所以和靶纸表是一对多关系。成绩汇总表score_summaryid、所属射手外键、所属场次外键、靶纸数量、总环数、平均每靶环数、识别状态自动识别完成/已人工修正。这个表也可以不单独建直接sql聚合查询但考虑到高频查询成绩排名的场景独立建一张汇总表在性能上更稳。在django的models.py里关键的外键关联写法大概是这样的from django.db import models class Shooter(models.Model): name models.CharField(max_length50) employee_no models.CharField(max_length20, uniqueTrue) team models.CharField(max_length50) class Session(models.Model): shooter models.ForeignKey(Shooter, on_deletemodels.CASCADE, related_namesessions) name models.CharField(max_length100) start_time models.DateTimeField(auto_now_addTrue) status models.CharField(max_length10, defaultactive) class TargetPaper(models.Model): session models.ForeignKey(Session, on_deletemodels.CASCADE, related_namepapers) paper_no models.CharField(max_length30) image models.ImageField(upload_totargets/) uploaded_at models.DateTimeField(auto_now_addTrue) class RecognitionResult(models.Model): paper models.ForeignKey(TargetPaper, on_deletemodels.CASCADE, related_nameresults) x_coord models.FloatField() y_coord models.FloatField() score models.IntegerField() confidence models.FloatField()这个设计在查询某个场次的总成绩时只需要一条django ORM指令就能搞定total_score RecognitionResult.objects.filter( paper__session_idsession_id ).aggregate(totalmodels.Sum(score))4.2 ORM操作中的性能陷阱使用ORM时最需要警惕的是N1查询问题。如果你在一个循环里逐个查询每个靶纸关联的识别结果数据库会被反复查询几十次接口响应时间直接翻几倍。正确做法是用select_related或者prefetch_related一次性把关联数据都取出来。我举个实际例子渲染某射手全部场次的成绩列表时最先写出来的代码是sessions Session.objects.filter(shootershooter) for s in sessions: papers s.papers.all() for p in papers: results p.results.all()三层循环嵌套表面上看逻辑没问题实际执行时会产生 1 N M 次数据库查询数据多了页面直接卡顿。优化之后改成这样sessions Session.objects.filter(shootershooter).prefetch_related(papers__results) for s in sessions: for p in s.papers.all(): results p.results.all()prefetch_related会一次性把当前查询涉及的所有场次、靶纸、识别结果都加载到内存后面的循环访问全部命中内存数据不再打数据库。这个优化对数据量大的场景效果非常明显接口响应时间能从几秒降到几百毫秒。4.3 图片上传与静态文件管理的坑django的ImageField处理上传文件很方便但有几个坑必须提前填好。第一是MEDIA_ROOT和MEDIA_URL的配置上传的靶纸图片要存到一个独立目录一般放在项目根目录下的media文件夹访问url是http://your-domain/media/targets/xxx.jpg。我建议把这个目录挂载到独立的磁盘或者对象存储上面避免系统盘被图片撑爆。第二是表单上传时的内存控制。默认情况下django会把上传的文件先读到内存小文件无所谓但靶纸照片一般好几MB如果并发上传多张内存会迅速耗尽。我的配置是在settings.py里做了文件大小限制并强制超过一定大小的文件先落到临时目录再持久化DATA_UPLOAD_MAX_MEMORY_SIZE 5242880 # 5MB FILE_UPLOAD_MAX_MEMORY_SIZE 5242880 # 5MB第三是图片压缩。原始照片动辄几MB直接存到服务器既浪费存储空间又拖慢上传和访问速度。我在上传视图里用PIL库做了统一压缩处理最长边压缩到2000像素以内质量参数设置到85jpg格式压缩完基本能控制在500KB以内肉眼几乎看不出画质损失但推理速度和存储占用都优化了很多。5. 前端交互与识别结果可视化5.1 上传页的核心交互逻辑前端页面里技术要求最高的是上传页和结果展示页。上传页的流程是用户选择图片前端立即用canvas压缩图片避免把十几MB的原图直接提交到服务器然后通过ajax异步上传服务器处理完成后返回识别结果前端拿到数据后在靶纸图的对应位置绘制弹孔标记。用jquery实现这个流程的代码框架大致如下$(#uploadBtn).on(click, function() { var file $(#fileInput)[0].files[0]; var canvas document.createElement(canvas); var ctx canvas.getContext(2d); var img new Image(); img.onload function() { var scale Math.min(1, 2000 / Math.max(img.width, img.height)); canvas.width img.width * scale; canvas.height img.height * scale; ctx.drawImage(img, 0, 0, canvas.width, canvas.height); canvas.toBlob(function(blob) { var formData new FormData(); formData.append(image, blob, target.jpg); $.ajax({ url: /api/recognize/, type: POST, data: formData, processData: false, contentType: false, success: function(res) { drawResult(res.data); } }); }, image/jpeg, 0.85); }; img.src URL.createObjectURL(file); });这个部分是纯前端体验的优化通过canvas.toBlob压缩把一张可能10MB的照片压缩成几百KB再上传上传速度会有明显的感知提升。如果你之前没做过类似处理我强烈建议把这段代码留下来在任何需要图片上传的项目里都能复用。5.2 canvas绘制弹孔标记识别完成之后前端接收到的数据是一组弹孔坐标。注意这里的坐标是校正后的靶面坐标系坐标而不是原始图片的像素坐标。前端展示时要把靶面坐标映射到canvas上的像素位置。我的做法是在后端返回结果时直接把坐标转成前端canvas需要的像素值前端不做坐标转换只做渲染。具体来说后端在返回数据之前已经根据透视变换的结果把弹孔的标准化坐标乘以一个固定的canvas缩放系数换算成canvas上的像素位置。这样前端拿到的就是直接的绘制坐标了。绘制弹孔的canvas代码function drawResult(points) { var canvas document.getElementById(resultCanvas); var ctx canvas.getContext(2d); // 绘制靶纸底图 ctx.drawImage(targetImage, 0, 0, canvas.width, canvas.height); // 绘制每个弹孔标记 points.forEach(function(p) { ctx.beginPath(); ctx.arc(p.x, p.y, 4, 0, Math.PI * 2); ctx.fillStyle #ff0000; ctx.fill(); ctx.strokeStyle #ffffff; ctx.lineWidth 1; ctx.stroke(); // 标注环数 ctx.font 12px Arial; ctx.fillStyle #ffff00; ctx.fillText(p.score 环, p.x 8, p.y - 8); }); }将弹孔和环数标注叠加在靶纸图上教练一眼就能看出识别结果是否准确需要人工修正时也非常有针对性。5.3 ajax请求中的超时和错误处理图片上传模型推理这个链路比较长一次请求可能持续1到2秒网络差的时候甚至更久。jquery ajax默认是没有超时限制的用户点了上传按钮之后如果一直没有响应体验会非常差。我在代码里显式设置了timeout参数为15000毫秒并做了超时和失败提示。另外一个细节是loading状态。上传识别期间按钮要禁用显示识别中的文案防止用户重复提交同一个文件。识别完成后按钮再恢复可用。这个小交互看着简单实际上对系统稳定性的帮助很大——避免用户连点三次后端同时推理三张同样的图片白费算力。6. 系统联调、部署与运行效果分析6.1 本地开发环境的搭建整个系统的本地开发环境我建议直接用docker把mysql和django跑起来避免因为环境不一致带来各种奇怪问题。项目的根目录放一个docker-compose.yml内容大致如下version: 3 services: db: image: mysql:8.0 environment: MYSQL_ROOT_PASSWORD: yourpassword MYSQL_DATABASE: targetdb ports: - 3306:3306 volumes: - ./mysql_data:/var/lib/mysql web: build: . ports: - 8000:8000 depends_on: - db volumes: - ./media:/app/media第一次启动时先在web容器里执行数据库迁移命令创建表结构再创建一个管理员账号。mysql的编码要特别注意默认的字符集可能不支持中文如果表建好之后发现中文乱码需要在mysql的配置文件里指定utf8mb4编码。管理员的创建命令python manage.py createsuperuser进入admin后台还需要通过一段简短的代码或者直接在admin后台添加入口把识别结果和靶纸记录注册进去。这样管理员登录后台之后就能直接看到所有上传的靶纸和识别结果列表也方便做人工修正。6.2 模型推理服务化封装模型在django里不应该是跟着web请求同步跑的。如果每次识别请求都同步调用模型推理一个推理卡住整个web服务都会被拖住。我做的方案是把模型加载和推理封装成一个单独的service层模块模型被加载一次进内存后续所有请求复用同一个模型实例。在django的app里新建一个services.py文件内容结构如下import torch from .models import RecognitionResult model None def load_model(): global model if model is None: model torch.hub.load(ultralytics/yolov5, custom, pathweights/best.pt) return model def recognize_image(image_path): model load_model() results model(image_path, size1024) # 转换坐标并计算环数 ... return results模型懒加载的目的是django的wsgi服务启动时不会立刻加载几百MB的模型内存直到第一个识别请求到来时才加载。这样能缓解开发阶段的启动速度问题。还要注意的一点是线程安全。django默认的多线程模式下多个请求可能同时访问同一个模型实例做推理。pytorch的模型推理在cpu上是线程安全的但在gpu上如果并发执行显存可能会出问题。稳妥做法是用一个线程锁来保护模型推理import threading infer_lock threading.Lock() def recognize_image(image_path): with infer_lock: results model(image_path, size1024)这个锁的实现很简单但确确实实能避免并发场景下模型推理的各种奇怪错误。6.3 mysql连接池与并发配置django连接mysql默认的并发能力偏弱每个请求都会新建一个数据库连接在高并发下连接数会暴涨。我在settings.py里配置了CONN_MAX_AGE参数让数据库连接保持长连接避免频繁创建和销毁。DATABASES { default: { ENGINE: django.db.backends.mysql, NAME: targetdb, USER: root, PASSWORD: yourpassword, HOST: 127.0.0.1, PORT: 3306, CONN_MAX_AGE: 60, } }对于这种系统来说用户的并发量主要集中在上传靶纸的时刻比如一场比赛同时有20个人上传瞬时并发20个请求。mysql默认配置完全可以扛住但如果未来数据量涨到几十万条记录建议给score和result表加上合适的索引查询性能会好很多。6.4 运行效果实测这个系统我在靶场实际跑了将近一个月从真实使用反馈来看几个核心指标都达到了预期。识别准确率上总分判定和人工复核的误差在3%以内误差基本都来自极小概率的弹孔漏检。单次识别耗时在cpu推理下平均0.8秒加上图片上传、压缩、数据库写入用户从点击上传到看到结果大概2秒左右没有明显的等待焦虑。成绩管理效率的提升是最直观的——以前一场训练赛教练组三个人需要花一个多小时手工判定和登记成绩现在系统自动生成排名表教练只需要负责审核异常记录时间压缩到了十分钟以内。在系统的稳定性方面连续运行期间出现过两次小问题一次是并发上传时内存暴涨导致服务重启后来通过限制上传大小和压缩逻辑解决了另一次是mysql在凌晨的定时备份任务执行时和业务请求发生锁竞争后来把备份时间调整到低峰时段就稳定了。7. 常见问题排查与避坑实录7.1 模型识别结果为空或环数异常偏高这个问题出现的概率不低。识别结果为空大概率是后处理中的置信度阈值设置太高或者透视变换的角标没有成功检测。排查思路是先跑一遍原始的model推理结果看检测框输出的数量和置信度分布如果检测框本身很多但被后处理过滤掉了就是阈值的问题如果模型输出本身就很少就是数据问题需要针对性补充遮挡严重或者光线异常的样本来重新训练。环数异常偏高的现象通常是透视变换的参数错了。角标检测错位一个像素反映到靶面上弹孔坐标可能偏出好几环。我的排查方法是把透视变换后的靶面坐标系叠加在原图上视觉确认角标位置和靶纸边缘是否完全贴合如果发现偏移就重新调整角标检测模块的形态学参数。7.2 上传图片后显示服务器内部错误django默认的调试模式关闭后页面只显示500错误具体原因被隐藏了。排查这个问题的第一步是打开日志系统或者临时设置DEBUGTrue把异常堆栈打出来。我遇到过的原因主要有三个一是上传的图片路径包含中文PIL读取时编码报错——解决办法是在保存图片时统一改成uuid命名二是media目录没有写入权限文件落盘失败——用chmod给media目录加上写权限三是模型推理时图片尺寸过大导致内存溢出——前端压缩逻辑已经做了兜底但如果是直接通过api传图后端也要做一次最大尺寸检查。7.3 系统在多人同时使用时卡顿明显并发上来之后的卡顿大概率是nginx的默认配置没有开启缓冲或者worker进程数不足。我的部署方案是nginx把上传请求转发给django的uwsgi在nginx配置里调大了client_max_body_size同时增加了uwsgi的进程数。另外如果发现卡顿主要发生在识别接口那瓶颈就在模型推理的排队上参考前面提到的service层加锁方案把推理并发控制在合理范围内。7.4 django admin后台无法显示图片这个问题基本是MEDIA_URL没有配置成绝对路径。django默认admin后台会把图片的url渲染成相对路径浏览器无法正确拼接。解决办法是settings.py里把MEDIA_URL设置成/media/nginx的location配置里把这个路径映射到实际的media目录location /media/ { alias /path/to/project/media/; }配置完成后admin后台的缩略图就能正常显示了。7.5 新建的数据库表名在查询时带前缀django的ORM默认会根据app名称自动给表加前缀比如app叫target表名就是target_recogniton_result。这个前缀本身不影响使用但如果你在navicat等mysql客户端里查询时忘了前缀会白费很多时间排查表不存在的报错。如果有人习惯用自定义表名可以在model的Meta类里显式指定db_table但我不建议这样做保持django默认命名规则后续做数据迁移时最省心。8. 这个项目后续还能怎么扩展项目上线稳定运行只是开始从使用中我也总结出几个后续优化方向按投入产出比排序你可以根据自己场景决定做到哪一步。第一是增加视频流实时识别。靶场目前是打完一轮收靶纸、拍照、识别整体是离线的。如果未来要支持实时显示每一枪的环数需要接摄像头视频流帧抽出来做实时检测。这个改造的核心在于推理延迟要压到150毫秒以内否则体验会断崖式下降。yolov5s在GPU推理下是可以做到的但整套系统的架构就要从上传识别改成视频流接入帧检测成绩实时推送复杂度会上一个台阶。第二是射手画像分析。当前系统只是存了成绩没有进一步挖掘数据价值。如果累积一段时间的数据之后可以对每个射手的平均环数、稳定性环数的波动标准差、不同距离的表现差异做统计分析生成趋势图。这个功能的技术难度不大把score_summary表的数据日常聚合成统计指标就行但对训练指导的价值很高教练能看到每个射手的短板在哪里。第三是批量导入导出和报表系统。比赛结束后裁判组需要导出成绩单这份成绩单可能还要按单位汇总、按枪种分类。目前我是用python脚本生成excel后面可以直接在django里集成openpyxl把各类报表做成在线下载的按钮。同样的把历史数据批量导入系统的功能也很实用能避免从旧系统手工搬数据。第四是模型的自学习闭环。当前系统识别错误之后只能人工在admin后台修正修正的数据并没有反馈到训练集。如果能把人工修正后的数据自动收集成新的标注数据每隔一个月增量训练一次模型系统的识别准确率会不断提升越来越贴合自己靶场的实际场景。这个闭环做完整个系统才真正具备自我进化的能力。以我个人的经验来看这个项目最值得沉淀的其实不是代码本身而是工程化思维——从数据采集、模型选择、后处理设计到数据库建模、接口联调、部署运维每一环都有真正的取舍和权衡。把这些环节里的决策逻辑都复盘清楚你就掌握了一套完整的算法工程落地方法换一个场景同样能打。