1. 项目概述与思路拆解
做目标检测平台的项目,网上其实一抓一大把,但大多数要么是只跑通了一个YOLOv5的Python脚本,要么是拿个React框架套了一个看起来漂亮的壳子,前后端各自为政,面试官一问就露馅。我这次要拆解的项目,是真正按照大厂工程化标准来做的:YOLOv5负责模型推理,Flask提供后端API服务,Vue构建前端可视化界面,三者通过标准的HTTP接口和WebSocket通信串联起来。整套系统的核心是解决一个很实际的问题——让模型不仅能在后台跑,还能被普通用户通过网页使用,并且输出结果可视化、可下载、可追溯。
这个平台适合谁来看?如果你是正在准备算法岗或全栈开发岗面试的求职者,这篇内容可以帮你建立一个完整的项目叙事体系;如果你是在校学生,想做一个有分量的毕设或竞赛项目,这套架构也能直接复用;如果你已经工作了,但对工程化落地不太熟悉,这里讲的前后端联调、模型封装、性能优化思路同样适用。毕竟,目标检测不只是调包跑通,更关键的是你怎么把它做成一个别人能用的产品。
整个项目的模块划分很清晰:前端负责交互展示、图片/视频上传、检测结果渲染;后端负责图像预处理、模型推理、结果解析,以及和文件存储、数据库的对接;YOLOv5模型本身则是核心计算引擎。三者之间的数据流向是:用户在前端上传图片或视频,后端接收后调用YOLOv5推理,再把标注框、类别、置信度等信息返回给前端,前端用Canvas或者SVG把检测框画出来。
选型上有几个关键考量。YOLOv5(在严格遵循原仓库协议约定的前提下)是目前工业界部署最轻量、社区生态最完善的检测模型之一,权重文件小、推理速度快、Python接口简洁,特别适合作为中小型项目的检测引擎。Flask比Django更轻,适合快速搭建API服务,而且和YOLOv5的Python生态天然匹配。Vue则比React上手曲线更低,模板语法直观,配合Element UI这类组件库,两天就能做出一个像样的管理界面。这套组合在各大公司的基础架构里很常见,因此写在简历上几乎不会遇到“没听过”的情况。
难点其实不在单个技术点上,而是三个技术栈之间的衔接。YOLOv5的输入是NumPy数组或者OpenCV的BGR图像,输出是形状为(n, 6)的张量,其中每行包含x1, y1, x2, y2, conf, class_id,如何把这些数据进行JSON序列化传给前端?前端如何把检测框坐标映射到原始图片尺寸上?视频流检测时如何保证帧率?这类“缝合”问题,正是面试官更想听到的细节。
2. 后端服务搭建:Flask 与 YOLOv5 的工程化封装
2.1 模型加载与推理接口的封装思路
很多初学者喜欢在每次请求里都加载一次模型,那是性能灾难。正确做法是在Flask应用启动时把模型加载进内存,整个进程生命周期内复用。
# app.py 核心结构 import torch import yaml from flask import Flask, request, jsonify from models.experimental import attempt_load from utils.general import non_max_suppression, scale_coords from utils.torch_utils import select_device app = Flask(__name__) # 启动时加载模型,只加载一次 device = select_device('0' if torch.cuda.is_available() else 'cpu') model = attempt_load('weights/yolov5s.pt', map_location=device) model.eval() # 类别名称,可以从数据集的yaml里读取 with open('data/coco128.yaml', 'r', encoding='utf-8') as f: data = yaml.safe_load(f) class_names = data['names']注意这里用attempt_load而不是torch.load,是因为YOLOv5的权重中可能包含一些自定义结构,直接加载容易报错。attempt_load会先尝试正常加载,失败后自动处理兼容性问题,这一步是必要的容错。
封装推理函数时,需要考虑对输入图像的处理:调整尺寸、归一化、BGR转RGB、维度扩展。YOLOv5默认输入尺寸是640x640,但实际推理时最好保持原始宽高比做letterbox填充,否则目标物体会被拉伸变形,导致检测精度下降。
from utils.augmentations import letterbox import cv2 import numpy as np def detect_img(img_bytes): # 读取图片 img = cv2.imdecode(np.frombuffer(img_bytes, np.uint8), cv2.IMREAD_COLOR) # letterbox保持比例 img_letterbox, ratio, (dw, dh) = letterbox(img, (640, 640), auto=False) # 转为模型输入格式 img_tensor = img_letterbox[:, :, ::-1].transpose(2, 0, 1) img_tensor = np.ascontiguousarray(img_tensor) img_tensor = torch.from_numpy(img_tensor).to(device) img_tensor = img_tensor.float() / 255.0 if len(img_tensor.shape) == 3: img_tensor = img_tensor.unsqueeze(0) with torch.no_grad(): pred = model(img_tensor)[0] pred = non_max_suppression(pred, conf_thres=0.35, iou_thres=0.45) # 解析结果 results = [] for det in pred: if len(det): det[:, :4] = scale_coords(img_tensor.shape[2:], det[:, :4], img.shape[:2]).round() for *xyxy, conf, cls in reversed(det): x1, y1, x2, y2 = map(int, xyxy) results.append({ "bbox": [x1, y1, x2, y2], "confidence": round(float(conf), 4), "class_id": int(cls), "class_name": class_names[int(cls)] }) return results这段代码里我认为需要解释的是scale_coords。前端显示的坐标必须是原图像素坐标,而模型输出的是640x640尺寸下的坐标,因此要做一个等比映射。如果你去掉这一步,前端显示会出现检测框整体偏移,这在视频流里尤其明显。
2.2 API 路由设计与参数校验
API接口设计要遵循RESTful风格,同时做好参数校验和异常捕获。基础接口清单如下:
| 接口路径 | 方法 | 功能说明 | 参数 |
|---|---|---|---|
/api/detect/image | POST | 图片目标检测 | file(图片文件) |
/api/detect/video | POST | 视频目标检测 | file(视频文件),interval(抽帧间隔) |
/api/detect/stream | WebSocket | 实时视频流检测 | —— |
/api/history | GET | 查询检测历史记录 | page,page_size |
/api/model/info | GET | 获取当前模型信息 | —— |
图片检测接口的处理流程是:校验文件存在、判断扩展名合法性、限制文件大小(比如不超过10MB)、调用推理函数、返回JSON结果。视频检测接口稍复杂,因为不能一次性把整个视频读进内存,必须逐帧读取、逐帧检测,并实时返回进度。
@app.route('/api/detect/image', methods=['POST']) def detect_image(): if 'file' not in request.files: return jsonify({"code": 400, "message": "未收到文件"}), 400 file = request.files['file'] if file.filename == '': return jsonify({"code": 400, "message": "文件名为空"}), 400 # 检查扩展名 ext = file.filename.rsplit('.', 1)[-1].lower() if ext not in ['jpg', 'jpeg', 'png', 'bmp', 'webp']: return jsonify({"code": 400, "message": "不支持的图片格式"}), 400 # 限制大小 10MB if file.content_length and file.content_length > 10 * 1024 * 1024: return jsonify({"code": 400, "message": "图片过大"}), 400 try: img_bytes = file.read() results = detect_img(img_bytes) return jsonify({ "code": 200, "data": { "count": len(results), "detections": results } }) except Exception as e: return jsonify({"code": 500, "message": f"推理失败: {str(e)}"}), 500在并发处理上,Flask自带的开发服务器是单进程的,如果同时有多个用户上传图片做检测,后来的请求会堵塞。工程上建议用gunicorn + 多worker部署,每个worker是独立进程,模型会各自加载一份到显存/内存里。这里有个取舍:worker数开太多,显存会爆;开太少,并发能力不够。我的经验是,一张NVIDIA T4显卡,模型用YOLOv5s,开2到3个worker比较合适,再往上叠加就得用--preload配合共享内存机制,但配置复杂度会指数级上升。
2.3 视频检测与进度回传
视频检测是个典型的耗时操作,一个1分钟的视频抽帧检测可能需要几秒甚至几十秒。如果让客户端一直等,体验极差。我采用的方案是:任务异步执行 + 前端轮询进度。
具体实现思路:提交视频后,后端把这个检测任务丢给一个后台线程,返回一个任务ID。前端每隔500毫秒请求一次/api/task/status/{task_id}接口,获取当前进度百分比。任务完成后,进度接口返回最终结果文件的URL,前端跳转到结果页面渲染。
import threading import uuid import os tasks = {} @app.route('/api/detect/video', methods=['POST']) def detect_video(): file = request.files['file'] task_id = str(uuid.uuid4()) tasks[task_id] = { "status": "pending", "progress": 0, "result_url": None, "message": "" } # 保存文件 file_path = os.path.join(UPLOAD_FOLDER, f"{task_id}_{file.filename}") file.save(file_path) # 后台线程执行检测 thread = threading.Thread(target=process_video_task, args=(task_id, file_path)) thread.daemon = True thread.start() return jsonify({"code": 200, "data": {"task_id": task_id}})process_video_task内部循环读取视频帧,每隔N帧调用一次模型推理,每处理完一帧更新进度,最后保存结果到服务器磁盘。这里有两个细节要注意:一是有几个模型调用别忘记放torch.no_grad()上下文里,显存能被及时释放;二是结果视频用cv2.VideoWriter写入时,编码器参数要选mp4v,否则生成的视频在浏览器里可能无法播放。
3. 前端可视化平台:Vue 交互设计与渲染
3.1 项目初始化与组件划分
前端用Vue 3 + Vite + Element Plus这套组合。Vite在开发环境下的启动速度和热更新体验比Webpack好太多,创建项目时直接用npm create vue@latest脚手架,选择是否需要路由、状态管理、代码规范检查等选型。
页面结构分四块:侧边栏导航、顶部工具栏、主内容区、底部状态栏。主内容区根据路由切换展示图片检测、视频检测、历史记录和模型信息四个视图。
组件划分建议:
UploadPanel.vue:统一的上传组件,支持拖拽上传、粘贴图片ResultCanvas.vue:在Canvas上绘制检测框和标签ImageDetectView.vue:图片检测主视图VideoDetectView.vue:视频检测任务管理视图HistoryView.vue:历史记录表格ModelInfoView.vue:模型参数展示
3.2 图片上传与检测结果渲染
图片检测的交互流程设计为五步:选择图片、预览原图、点击检测按钮、等待结果、在图上叠加检测框。Canvas绘制检测框的核心逻辑是按比例缩放坐标。
// ResultCanvas.vue 核心逻辑 const drawDetections = (detections) => { ctx.clearRect(0, 0, canvas.width, canvas.height) ctx.drawImage(imageObj, 0, 0, canvas.width, canvas.height) detections.forEach(det => { const [x1, y1, x2, y2] = det.bbox // 原始图片坐标映射到Canvas坐标 const scaleX = canvas.width / imageObj.naturalWidth const scaleY = canvas.height / imageObj.naturalHeight ctx.strokeStyle = getColorByClass(det.class_id) ctx.lineWidth = 3 ctx.strokeRect(x1 * scaleX, y1 * scaleY, (x2 - x1) * scaleX, (y2 - y1) * scaleY) // 标签背景 const label = `${det.class_name} ${(det.confidence * 100).toFixed(1)}%` ctx.fillStyle = 'rgba(0, 0, 0, 0.7)' const labelWidth = ctx.measureText(label).width + 10 ctx.fillRect(x1 * scaleX, y1 * scaleY - 25, labelWidth, 25) // 标签文字 ctx.fillStyle = '#ffffff' ctx.font = '14px sans-serif' ctx.fillText(label, x1 * scaleX + 5, y1 * scaleY - 8) }) }这里高级一点的交互,我加了一个检测结果侧栏。点击左侧原图上的任意检测框,右侧会展示该目标的类别、置信度、坐标、面积的详细信息。这个功能需要前端在渲染检测框时,为Canvas的点击事件做命中测试,本质是判断点击坐标是否落在某个保存的bbox内。
前端接API时注意跨域问题。开发环境下Vite配置代理:
// vite.config.js export default defineConfig({ server: { proxy: { '/api': { target: 'http://localhost:5000', changeOrigin: true } } } })生产环境中,前端打包后的dist目录直接由后端Flask托管,或分开部署到Nginx。我推荐后者,图上是静态资源和API请求分离,便于做CDN加速和独立扩容。
3.3 视频检测任务管理与实时状态
视频检测页面,我用Element Plus的el-progress组件展示进度条,用el-timeline展示任务状态流转(排队中 → 检测中 → 已完成/失败)。用户在表单里输入抽帧间隔(默认每5帧检测一帧),点击提交后拿到task_id,开启一个定时器轮询进度接口。
const pollTask = (taskId) => { const timer = setInterval(async () => { const res = await axios.get(`/api/task/status/${taskId}`) task.value = res.data.data if (task.value.status === 'completed' || task.value.status === 'failed') { clearInterval(timer) if (task.value.status === 'completed') { resultVideoUrl.value = task.value.result_url } } }, 500) }这里有一个容易被忽略的问题:用户离开页面时定时器还在跑,会造成无效请求和内存泄漏。需要监听组件onUnmounted生命周期,清除定时器。还有一个细节是,如果任务失败,在轮询响应里要判断status是否为failed,否则前端会一直空转等待。
前端展示的视频检测结果,我采用和图片检测相同的Canvas方案,不过数据源变成了抽帧渲染。后端返回的是一个结果JSON文件,里面记录了每一帧的检测信息、原图URL和标注视频URL。前端用setInterval按帧索引播放检测结果,比直接播放标注好的视频更有灵活性,用户可以逐帧查看、跳转、统计每帧的检测物数量。
4. 全栈工程化联调与部署落地
4.1 接口协议定义与异常处理规范
前后端联调顺畅的前提是接口文档先行。我习惯用Swagger/OpenAPI规范写接口文档,Flask中可以用flask-restx自动生成,但项目量级不大就手写一个Markdown文档也行。关键是要约定清楚以下内容:请求和响应的字段命名风格用驼峰还是下划线、时间格式统一、错误码语义。我在这个项目里统一用驼峰命名,错误码设计为200/400/500三段式,简单粗暴但好用。
响应格式统一如下:
{ "code": 200, "message": "success", "data": { "detections": [], "count": 0 } }前端用axios拦截器统一处理异常:
// axios 拦截器 service.interceptors.response.use( response => { const res = response.data if (res.code !== 200) { ElMessage.error(res.message || '请求失败') return Promise.reject(new Error(res.message)) } return res }, error => { ElMessage.error('网络异常,请稍后重试') return Promise.reject(error) } )前后端联调中最常见的问题是接口字段对不上。比如后端返回class_id而前端用的是classId,这种情况在联调阶段一抓一大把,非常耗费时间。解决办法是:写一份跨端共享的TypeScript类型定义(如果后端是Python的话可以导出JSON Schema并转成TS类型),至少保证前端开发时能静态感知字段变化。不能全用any,否则联调时的低级错误完全无感知。
4.2 部署方式与 Docker 容器化
部署方案我推荐Docker Compose一键编排,这个项目到生产环境能够快速交付,同时面试过程能讲出亮点。
Dockerfile分为两个部分:前端用Node镜像构建静态文件,后端用Python镜像加GPU支持。前端构建完成之后把dist目录复制进Nginx镜像,后端则把Flask应用和模型权重一起打包。
后端Dockerfile关键片段:
FROM python:3.9-slim WORKDIR /app # 复制依赖文件并安装 COPY requirements.txt . RUN pip install --no-cache-dir -r requirements.txt -i https://pypi.tuna.tsinghua.edu.cn/simple # 复制项目代码和模型权重 COPY . . # 暴露端口 EXPOSE 5000 # 启动命令 CMD ["gunicorn", "--workers", "2", "--bind", "0.0.0.0:5000", "app:app"]前端Dockerfile:
FROM node:16-alpine AS build-stage WORKDIR /app COPY package*.json ./ RUN npm install COPY . . RUN npm run build FROM nginx:stable-alpine AS production-stage COPY --from=build-stage /app/dist /usr/share/nginx/html COPY nginx.conf /etc/nginx/conf.d/default.conf EXPOSE 80 CMD ["nginx", "-g", "daemon off;"]docker-compose.yml里定义两个服务:frontend映射宿主机的80端口,backend映射5000端口。后端如果有GPU需求,需要在compose文件里指定gpus: all,这也是面试中能拿出来讲的加分项——说明你了解GPU容器化部署的细节。
4.3 性能优化与资源控制
模型推理是性能瓶颈,工程优化思路主要围绕三个方向展开。
第一个方向是模型轻量化。YOLOv5s权重大约14MB,在CPU上单帧推理耗时200毫秒左右;换成YOLOv5n只有4MB,推理耗时降到80毫秒,但mAP会下降约5个百分点。如果在没有GPU的环境演示,建议默认加载YOLOv5n,用户通过模型选择下拉框切换v5s、v5n甚至v5m(如果显存够)。切换模型时后端要重新加载权重,这个接口设计成POST请求。
第二个方向是推理批处理。当多个视频检测任务同时提交时,单帧推理无法充分利用GPU。更优的方案是把不同任务的帧拼成batch输入模型一次推理4张图。YOLOv5的letterbox支持动态batch,只需要把多个张量torch.cat成四维张量,scale_coords时按batch索引解析即可。这种方式在GPU利用率上能带来显著提升。
第三个方向是缓存机制。对同一张图片的重复检测结果,用哈希值做GET请求缓存,能大幅提升测试效率。前端上传图片后前端计算MD5,附带在请求头里传给后端,后端检查Redis里是否已有结果,有就直接返回。
实测数据分享:在单张NVIDIA T4上,YOLOv5s处理1080P视频,抽帧间隔5帧,一个10秒的视频大约需要7秒完成检测。CPU环境下(8核16线程)同一视频需要40秒左右。性能差距非常直观,这直接决定了部署硬件的选型判断。
5. 面试考点拆解与项目深挖思路
5.1 核心原理问题准备
面试官围绕这个项目必然会问算法基础和技术细节,我把高频考点整理了出来:
目标检测中的anchor机制:YOLOv5通过K-means聚类在训练集上自动生成自适应anchor,比手动设定更贴合数据集分布。在
models/yolo.py里可以看到anchor是作为先验传入Detect层的,解码时通过网格偏移和sigmoid激活输出边界框。NMS非极大值抑制的作用:多个检测框可能重叠覆盖同一目标,NMS按置信度排序,保留最高分的框,凡是IoU超过阈值的相邻框全部抑制。代码里的
non_max_suppression函数有一个agnostic参数,控制是否跨类别做NMS,默认False,表示每个类别独立做。模型推理为何选用FP16:GPU上用
model.half()转换模型为半精度,显存占用减半,推理速度几乎翻倍。但要注意在CPU上推理必须转回float32,否则会报错,所以代码里要根据设备类型做判断,这是很容易踩的坑。Flask多worker共享模型问题:每个worker进程独立加载模型,显存开销成倍增长,这就是为什么我建议用
gunicorn --preload来让模型只加载一次,然后通过fork机制共享给子进程,配合copy-on-write机制节省内存。
5.2 项目创新点提炼
项目不能只有CRUD功能,要包装出几个让面试官眼前一亮的亮点:
第一是动态模型热加载。常规检测平台启动时锁死一个模型,我这里增加了一个模型管理接口,支持运行时切换YOLOv5s/n/m不同规格的权重,无需重启服务。实现思路是给模型加一把全局读写锁,切换时新模型加载完成后再替换指针,避免并发请求时出现模型加载到一半的中间态。
第二是检测结果持久化与统计。每次检测任务完成后,后端将结果写入数据库记录表,包括图片哈希、目标类别分布、平均置信度、耗时等指标。前端历史页面提供一个简单的柱状图展示不同类别的检测频率,这个功能虽然简单,但体现了你具备数据分析意识和后端建模能力。
第三是一键导出检测报告。后端用ReportLab生成PDF报告,包含检测图片、标注框、各类别统计表格。这个功能在企业内部项目里很实用,放在简历里也是区别于其他候选人的亮点。
这三个创新点难度并不高,但每次都能在面试中引出很多话题。面试官会感兴趣的是,你怎么保证热加载时新旧模型切换不会出错、统计报表的SQL查询怎么优化、导出PDF时大量图片内存如何释放。
5.3 排查问题与优化追问
面试官还可能会刁钻地追问一些异常情况,说几个我实际踩过的问题:
问题一:显存泄漏。连续处理几百张图后显存暴涨,原因是推理时创建的中间张量占用的显存没有释放。排查下来发现是每次请求都重新创建了输入张量且没有释放引用。解决办法:推理函数结束后显式调用del删除大张量变量,并及时torch.cuda.empty_cache(),不过也不能频繁调用,因为它开销较大,建议每处理10张图调一次。
问题二:并发请求时模型输出错乱。Flask多线程模式下,多个请求同时进入模型推理,Pytorch模型本身是线程安全的,但如果你在代码里用了模型之外的共享变量存储中间结果,就可能出现数据覆盖。排查后把共享变量改为函数局部变量或使用threading.local。这是多线程编程的典型陷阱,我在面试中经常被问到,正好顺势展示自己遇到问题时的分析思路。
问题三:前端绘制大量检测框时卡顿。一张图片上有上百个检测结果时,Canvas同步绘制耗时过长。优化的思路是把绘制拆成两个步骤:先离线绘制到离屏Canvas,再用requestAnimationFrame一次性渲染到主Canvas,避免阻塞主线程,这样用户交互始终保持流畅。
6. 常见问题与排查技巧实录
6.1 后端常见报错
| 报错信息 | 原因分析 | 解决方案 |
|---|---|---|
ModuleNotFoundError: No module named 'models' | 未在项目根目录运行或缺失依赖 | 确认YOLOv5的models文件夹已复制到后端目录,运行目录切换至项目根 |
CUDA out of memory | 显存不足或模型加载了多次 | 检查是否有多个worker同时加载大模型,减少worker数或换用更小模型 |
KeyError: 'names' | 数据集的yaml文件格式异常 | 确认data/coco128.yaml中的names字段存在且为list格式 |
TypeError: Expected Ptr<cv::UMat> for argument 'img' | OpenCV读取图像失败 | 检查上传文件的读取方式是否正确,文件是否损坏,能否用cv2.imdecode代替cv2.imread处理内存中的bytes |
6.2 前端常见问题
前端最烦人的一个问题是跨域请求被拦截。我遇到过一次,前端请求http://localhost:5000/api/detect/image时报Access-Control-Allow-Origin错误。排查方法是用浏览器的DevTools看网络请求的响应头,确认是否真的有CORS头返回。如果后端没配置,用Flask-CORS扩展是更标准更省事的方案:
from flask_cors import CORS CORS(app, resources={r"/api/*": {"origins": "*"}})另一个常见问题是上传大文件时浏览器连接中断。这种情况要检查后端代理服务器的client_max_body_size配置,Nginx默认才1MB,上传大视频直接413。只要在Nginx配置里加一行client_max_body_size 50m;就能解决。
图片加载缓存问题也遇到过,修改模型后重新检测同一张图,前端仍然显示旧结果。原因是浏览器URL未变化导致强缓存。解决方法是给请求URL加时间戳参数,或者在响应头设置Cache-Control: no-cache。
6.3 模型推理相关坑
模型训练和推理阶段最容易忽略的一个坑是图像色彩通道顺序。OpenCV读取图像是BGR顺序,而Pytorch模型训练时通常用RGB。如果不做[:, :, ::-1]转换就直接送进模型,你会发现检测效果变得很差,尤其是色彩鲜明的物体类别错误率显著上升。这个问题在项目演示现场出现会相当尴尬。
另一个坑是图像的EXIF旋转信息。手机拍摄的照片经常含有EXIF orientation信息,直接用OpenCV读取不会自动处理旋转,导致检测结果里的物体方向是横着的。解决办法是读取EXIF信息并做旋正处理,或者在前端用Canvas转正后再上传。
6.4 一个容易忽视的安全细节
项目上线后如果暴露在公网,还需要对上传功能做好安全防护。我在代码里做了三个约束:限制上传文件类型白名单、限制单文件大小、对文件内容做MIME嗅探。不让用户自定义上传路径也是一个原则,文件落盘时的文件名一定是用服务端生成的唯一ID拼接的。这些细节虽小,但在企业环境里的安全审查是必须的,面试时提到也能体现工程素养。
7. 一些实操心得和建议
项目本身做到这一步,功能上其实已经完善了。如果还想继续扩展,有几个方向可以选:一是接入Redis做任务队列,替代当前的threading.Thread方案,支持任务优先级和失败重试;二是前端加一个目标检测的小地图模式,用热力图方式展示密集目标区域,适合安防场景;三是把模型推理独立成一个微服务,通过gRPC与Flask通信,这样模型升级和扩容会更灵活。
我个人在这次搭建中的真实体会有几点。第一,全栈项目最大的成本在联调。如果时间紧张,务必先定义好接口文档,把字段写清楚再动手写前后端。第二,模型推理代码和业务代码要解耦。我把YOLOv5的推理逻辑封装成一个独立的Detector类,与Flask路由层完全分离,后续换模型或者迁移到FastAPI,只需改Detector内部实现,路由层不用动。第三,视频检测这类耗时任务从第一天就该做成异步架构,省得后期数据量大了再推倒重来。
另一个很实在的建议:多写日志。我在关键节点都加了结构化日志,包括推理耗时、检测数量、内存占用、错误堆栈。排查问题时,一条完整的日志链路能节省大量时间。这些日志信息只要输出成JSON格式,后续对接ELK或者Loki的日志平台时基本不用改代码。
如果你打算拿这个项目去面试,我给的建议是:不要只背结论,把项目从“我能跑通”提升到“我能讲清楚为什么这么做”。每个技术选型、每个模块边界、每个性能瓶颈,都要能展开聊5分钟以上。面试官真正想招的,不是会用YOLOv5的人,而是能独立解决工程问题的人。
最后提醒一句:除模型加载外别把权重文件提交到Git仓库,用.gitignore排除;前端构建时注意环境变量区分开发和生产;验收演示前一定要先跑一遍全链路流程。这些都是过来人的经验,看起来不起眼,但关键时刻能救场。