首页 / 资讯中心 / 文章详情

YOLOv11多任务视觉实战:检测、OBB、分割、姿态与Streamlit部署

YOLOv11多任务视觉实战:检测、OBB、分割、姿态与Streamlit部署 ★ FEATURED ARTICLE
这两年做视觉应用我最大的感受是算法再强落不了地就是一堆权重文件。最近我把基于深度学习应用项目的常见需求打了个包YOLOv11做统一主干同时吃下目标检测、OBB 旋转框识别、图像分割和人体姿态检测四类任务最后全部接到Streamlit上做可视化操作。整个项目跑通之后基本告别了“换一个任务就换一套工程”的状态。这篇文章我会从项目拆分、每个任务的设计逻辑、后端推理代码、前端可视化交互到踩坑记录都写清楚适合想把 CV 模型快速做成工具界面的开发者也适合刚接触多任务视觉系统的人照着搭一套。1. 项目盘点YOLOv11 到底在做一件什么事1.1 一个工具链把四类视觉任务装进同一个推理框架先把这个项目要解决的问题说透。过去的典型做法是要做检测开一个检测项目要做分割再去写一套分割接口姿态估计又是另一个仓库。每个任务的数据格式不一样前后处理不一样界面还得单独做。对于做算法验证、产品原型、课程设计或者内部工具的人来说这种“任务隔离”非常浪费。这个项目的核心思路是选一个能够同时输出多类结果的主干网络然后在头部接不同的任务分支。YOLOv11 在整体设计上继承了早期 YOLO 系列的思路同时把不同任务的输出头整合到一起检测给边界框OBB 给旋转框分割给掩码姿态给关键点。这样一套推理链路下来输入一张图返回的不再是单一结果而是一整个结构化的解析结果。我这边用 Streamlit 做外壳用户上传图片或者视频点一下按钮四类结果直接出来。这么做最大的收益在于“一套基础设施复用”图像读取、预处理、模型加载、GPU 调度、结果后处理、可视化叠加这些逻辑九成以上是公共的。新增一个任务只是多一个输出解析函数而不是再造一个系统。1.2 为什么选用 YOLO 而不是“多模型拼装”其实“多模型拼装”也能实现同样效果检测用一个模型分割用一个模型姿态再单独用一个模型最终各自输出到同一个前端。但实际做过的都知道这条路有几处硬伤。第一工程成本高。多模型需要管理多个权重文件、多个预处理规格、多个推理上下文单独加载的显存占用会线性叠加。第二推理速度慢。模型之间没有共享任何特征。虽然现在显卡性能不差但对视频流或者批量图片来说每一毫秒的差距都很明显。第三集成风险高。不同模型输出的坐标系、类别编号、置信度阈值都是各算各的一旦数据对应不上做一个简单的“检测到人并估计姿态”都会因为坐标不一致而出各种诡异问题。单模型多任务的优势就在于特征共享。主干网络把图像编码成多尺度特征图不同分支从这些特征图里各取所需。就像同一个中央厨房把食材处理好了不同窗口分别出菜不必每个窗口单独再建一个厨房。实际跑起来的效率模型推理占用能省下三到四成代码量也少一大截。1.3 这套项目适合什么人直接抄作业如果你符合下面任一情况这套东西基本可以直接借鉴。想快速给算法团队做个结果展示平台而不是每次都在命令行里看保存下来的图片做毕业设计或者个人项目需要同时体现检测、分割、姿态等多个技术点做工业视觉方案预研想验证 OBB 在倾斜目标场景下的可用性在写内部工具希望非技术同事也能上传图片看模型效果。它不适合谁如果你的目标是训练强业务精度的模型或者要处理千万级数据量的生产系统那还是要单独做数据标定和推理加速。Streamlit 这一层更多是“低门槛可视化 轻量交互”不等同于高并发的生产服务。2. 四个核心能力逐个拆解检测、OBB、分割、姿态2.1 目标检测从边框到置信度YOLO 家族的关键演变目标检测是这个项目的地基。给定一张图模型输出若干个边界框每个框带类别和置信度。YOLO 系列的核心思路是把检测当成“一次前向推理”不需要两阶段方法里先提候选区域再逐块分类的流程。整个家族在主干上经历了比较明显的演变。早期版本用类似全卷积的方式把图像分成网格每个网格预测候选框中间几代引入了 anchor预设框为了让不同尺度的目标都能有一个比较好的初始回归位置后来的版本逐步走向 anchor-free直接把目标中心点和宽高作为回归目标简化了解码过程。YOLOv11 这一代在结构上延续了这些改进并且进一步强化了对多尺度特征的利用小目标检测的稳定性比我最初接触的版本好了不少。这里的“为什么”值得展开anchor 机制本质上是要给网络一个“框大概长什么样”的先验。但这个先验需要根据数据集统计得到换一套数据场景往往得重新聚类。anchor-free 等于把这个负担拿掉让网络直接从特征图位置回归物体尺寸省事且泛化能力更好。实际验证下来对于通用目标、行人、车辆、工业零件这些场景anchor-free 的鲁棒性确实更稳。2.2 OBB 旋转框它不是花活而是航空影像的刚需OBB 全称 Oriented Bounding Box也就是带角度信息的旋转框。普通目标检测输出 axis-aligned 的矩形框不管目标实际角度怎样框的边永远平行于图像坐标轴。这在多数场景够用但一旦目标本身是倾斜的问题就出现了。拿遥感影像里的船只举例。一艘船停在岸边码头船身是斜着的。水平框要把整艘船包住面积会比真实船身大很多旁边如果还有密集停靠的船两个水平框会大面积重叠后处理的非极大值抑制很容易把其中一艘船误删掉。旋转框则能贴着船身角度随着目标方向走框体紧凑得多目标之间天然分得开。实现上OBB 比普通检测多了一个角度回归目标。网络会预测七个值中心点坐标、宽高、旋转角度外加类别和置信度。这里最常见的坑是角度的定义范围。有的模型定义是[-90, 90)有的是[0, 180)如果不统一可视化时会出现“框突然翻转 90 度”的怪异现象。后处理也需要用旋转框版本的 NMS计算的是带角度的 IoU而不是普通矩形交并比。YOLOv11 里面的 OBB 分支在架构上并不复杂关键在于模型训练时的角度损失设计。角度这个量是一个循环量0 度和 180 度其实是同一个方向如果损失函数直接算差值模型在边界处会学得很不稳定。所以实际推理时我要求自己先明确模型内部的 angle 约定再去做可视化否则看到的多边形就是乱飞的。2.3 实例分割mask 分支为什么能共享主干图像分割从任务层面看可以分成语义分割和实例分割。语义分割只管像素属于哪个类别不管同一类内部有多少个独立物体实例分割要同时区分“这是人”“这是第一个人”“这是第二个人”。YOLOv11 做的是实例分割掩码和单个检测框绑定而不是输出一整张逐像素标签图。原理上分割分支会在每个检测框内生成一个固定尺寸的 mask 系数再与原型掩码做组合解码出边界框内对应目标的像素掩码。这种思路不需要像早期分割网络那样对每个像素做高分辨率分类计算量小很多精度也够用。共享主干的优点在这里体现得最明显。分割需要的语义信息和检测需要的语义信息有大量重叠低层特征负责边缘、纹理高层特征负责语义类别。如果单独做分割模型这些特征要重新算一遍而多任务共享主干之后分割分支只需要在共享特征之上多处理一层细节。内存和时间的收益都很直接。实际可视化时掩码常常要和原图做半透明叠加。用 OpenCV 的话cv2.addWeighted可以处理但如果想做得精细一点可以把掩码作为 alpha 通道与 BGR 图像做逐像素混合效果比整体权重叠加更自然。2.4 姿态估计17 个关键点背后是回归问题人体姿态估计要解决的是“人的主要关节点在哪里”。YOLO 系模型采用的自顶向下还是自底向上的思路需要看版本。实际看到的是直接在检测框内回归关键点坐标先检测人框再预测框内关节点。这就把姿态任务拆解成了两段先找到人再定骨骼点。模型默认的关键点数量通常是 17 个对应 COCO 数据集的标注格式鼻子、双眼、双耳、双肩、双肘、双腕、双髋、双膝、双踝。每个关键点不仅有坐标还带一个置信度表示这个点在当前框内是否可见、预测是否可靠。这里的“为什么”在于姿态估计本质上是一个回归任务不是分类任务。模型不是回答“这个点是什么”而是输出(x, y, confidence)。所以损失函数一般用均方误差或者平滑 L1加一个关键点置信度的辅助监督。推理拿到的是归一化坐标需要乘以原图尺寸才还原到真实像素位置。可视化时别直接把点打上去就完事。骨架连接关系是有固定索引表决定的左肘连着左肩膝盖连着髋。索引关系错一位画出来的骨骼就成了“神经错乱”。所以工程里我会把连接表单独做成常量代码里写清楚不靠记忆。3. 工具选型解析为什么尾端一定要接 Streamlit3.1 可视化层的作用不只是展示很多做算法的开发者对可视化界面并不重视觉得模型效果好就行。实际上一套模型做完给同事演示、给客户验收、给导师汇报你不可能让人家去终端里翻图片。交互式界面本身就是“结果交付”的一环。Streamlit 在这么多 Python 可视化工具里比较特殊。它不要求你会写前端整个界面用 Python 脚本描述写变量、写函数Streamlit 自动把交互控件和数据更新关系串起来。改动代码保存后页面自动刷新不需要重启服务更不需要打包成前端资源。对一个人完成整个项目的情况来说省下的时间相当可观。选它而不是直接用 Flask 静态页面的原因也简单Flask 可以做到但你要维护路由、请求处理、前端模板、数据传输协议那一套Streamlit 全部把这些隐藏掉了。写代码的方式跟写普通 Python 脚本几乎一样把模型推理结果放到变量里页面自然就更新了。3.2 缓存与状态管理别让模型重复加载Streamlit 脚本有一个很重要的特性每次页面交互都会重新执行整个脚本。这要是处理不好模型权重就会被反复加载页面卡到没法用。幸好它有缓存机制。加载模型这个动作必须放进缓存函数里。模型权重文件通常几百 MB加载一次需要数秒到十几秒。如果每次点击按钮都加载一次用户早崩溃了。用st.cache_resource装饰加载函数Streamlit 会保证同一个函数逻辑只执行一次结果在后续重运行中直接复用。这个和st.cache_data的区别在于前者用于全局资源比如模型、数据库连接后者用于数据计算比如读取文件、做预处理。状态管理同样重要。假设用户先上传一张图拿到检测结果然后调整阈值状态如果没保留页面刷新后之前的结果会丢失又得重新推理。实际中我会把“模型结果”存在st.session_state里只有用户主动点击“开始识别”或者上传新图时才重新计算参数调整时只改变后处理不会重跑模型前向推理。3.3 交互设计的取舍单文件脚本 vs 多页面应用Streamlit 做小项目一个app.py足够。但项目扩展成四类任务之后把所有功能堆在一个脚本里会让代码越来越乱也不利于维护。这时候可以拆成多页面用pages/目录划分功能模块每个页面独立一个.py文件。我的实际做法是首页做总览上传图片后显示四类任务的结果摘要然后根据具体需要再开独立页面做单一任务深度分析比如 OBB 角度分布、姿态骨架对比、掩码面积统计。这样用户不会一进来就被“四个任务同时展示”的信息量吓到演示时也更有层次。页面拆分的代价是状态共享变麻烦。不同页面之间如果要共用同一份推理结果可以考虑把结果写入磁盘缓存或者 session也可以用数据库。但多数演示场景文档说清楚“每个页面独立处理上传文件”就够了。4. 实操过程从环境搭建到端到端跑通4.1 环境准备与依赖安装这一步不复杂但版本问题很容易卡人。我建议在虚拟环境里安装不要直接装到系统 Python。推荐 Python 3.10 或 3.11多数深度学习库对这两个版本支持比较稳定。python -m venv yolo-app source yolo-app/bin/activate # Windows 下执行 yolo-app\Scripts\activate pip install --upgrade pip pip install ultralytics pip install streamlit pip install opencv-python pip install torch --index-url https://download.pytorch.org/whl/cu121关于 PyTorch 的安装建议先到官网确认当前环境下对应的 CUDA 版本。如果你本机 CUDA 是 12.x就可以用上面的 index-url 安装对应 wheel如果只用 CPU直接pip install torch就行但推理速度会明显慢。YOLOv11 的核心能力都包含在ultralytics这个包里不需要自己改网络结构。包内部会处理好模型定义、权重下载和任务分发的逻辑。实际项目里我把依赖锁在一个requirements.txt里避免环境漂移。ultralytics8.0.0 streamlit1.28.0 opencv-python4.6.0 numpy1.23.0 pillow9.0.0安装完成后可以先跑一个最小验证脚本确认能成功加载模型from ultralytics import YOLO model YOLO(yolo11n.pt) results model(bus.jpg) print(results[0].boxes)这一步能通环境基本就没问题了。4.2 目标检测 OBB 推理代码实现普通检测的推理代码非常简单。但 OBB 有一些细节需要注意尤其是结果解析。下面我写一版可以直接改的推理函数。import cv2 import numpy as np from ultralytics import YOLO def run_detect(model, image_bgr, conf0.25): results model.predict( sourceimage_bgr, confconf, verboseFalse, ) result results[0] boxes result.boxes.xyxy.cpu().numpy() # (N,4) classes result.boxes.cls.cpu().numpy().astype(int) confs result.boxes.conf.cpu().numpy() names model.names return boxes, classes, confs, names def draw_detections(image_bgr, boxes, classes, confs, names): for box, cls, conf in zip(boxes, classes, confs): x1, y1, x2, y2 map(int, box) label f{names[cls]} {conf:.2f} cv2.rectangle(image_bgr, (x1, y1), (x2, y2), (0, 255, 0), 2) cv2.putText(image_bgr, label, (x1, y1 - 8), cv2.FONT_HERSHEY_SIMPLEX, 0.6, (0, 255, 0), 2) return image_bgrOBB 的区别在于result.obb。每个旋转框由 4 个角点组成也就是 8 个数字(x1,y1,x2,y2,x3,y3,x4,y4)。def run_obb(model, image_bgr, conf0.25): results model.predict( sourceimage_bgr, confconf, taskobb, verboseFalse, ) result results[0] if result.obb is None: return [], [], [] xyxyxyxy result.obb.xyxyxyxy.cpu().numpy() # (N,4,2) confs result.obb.conf.cpu().numpy() classes result.obb.cls.cpu().numpy().astype(int) return xyxyxyxy, confs, classes, model.names def draw_obb(image_bgr, obb_polygons, confs, classes, names): for poly, conf, cls in zip(obb_polygons, confs, classes): pts poly.reshape(-1, 1, 2).astype(int) cv2.polylines(image_bgr, [pts], True, (0, 255, 255), 2) x_min int(poly[:, 0].min()) y_min int(poly[:, 1].min()) label f{names[cls]} {conf:.2f} cv2.putText(image_bgr, label, (x_min, y_min - 6), cv2.FONT_HERSHEY_SIMPLEX, 0.6, (0, 255, 255), 2) return image_bgr注意cv2.polylines第三个参数必须是True表示连线闭合否则旋转框会缺一条边。4.3 分割与姿态推理代码实现分割推理拿到的核心对象是result.masks。它有原始数据和坐标数据两种形态。masks.data是掩码张量可以通过mask.data[idx]取出单个目标的掩码masks.xy是每个目标的外轮廓点坐标格式是点到点的坐标数组。def run_segment(model, image_bgr, conf0.25): results model.predict( sourceimage_bgr, confconf, verboseFalse, ) result results[0] masks result.masks.data.cpu().numpy() if result.masks is not None else None boxes result.boxes.xyxy.cpu().numpy() if result.boxes is not None else None classes result.boxes.cls.cpu().numpy().astype(int) if result.boxes is not None else None return masks, boxes, classes, model.names def visualize_mask(original_bgr, mask_bool, color(0, 200, 0), alpha0.45): overlay original_bgr.copy() colored np.zeros_like(original_bgr) colored[mask_bool] color overlay cv2.addWeighted( cv2.bitwise_and(original_bgr, original_bgr, maskmask_bool.astype(np.uint8)), 0.6, cv2.bitwise_and(colored, colored, maskmask_bool.astype(np.uint8)), 0.4, 0, ) return overlay姿态推理结果主要在result.keypoints.data里维度是(N, 17, 3)最后一个维度是x, y, confidence。绘制时先过滤掉置信度低的关键点再按骨骼连接表画线。SKELETON [ (0, 1), (0, 2), (1, 3), (2, 4), # 鼻子到眼睛到耳朵 (5, 6), (5, 7), (7, 9), (6, 8), (8, 10), # 肩膀-手肘-手腕 (5, 11), (6, 12), # 肩膀到髋 (11, 13), (13, 15), (12, 14), (14, 16), # 髋到膝盖到脚踝 ] def run_pose(model, image_bgr, conf0.25): results model.predict( sourceimage_bgr, confconf, verboseFalse, ) result results[0] keypoints result.keypoints.data.cpu().numpy() if result.keypoints is not None else None return keypoints, model.names def draw_pose(image_bgr, keypoints, thr0.3): h, w image_bgr.shape[:2] for person in keypoints: pts person[:, :2] visible person[:, 2] thr kp [] for i, (x, y) in enumerate(pts): if visible[i]: kp.append((int(x * w), int(y * h))) cv2.circle(image_bgr, kp[-1], 4, (0, 0, 255), -1) for a, b in SKELETON: if visible[a] and visible[b]: pa (int(pts[a][0] * w), int(pts[a][1] * h)) pb (int(pts[b][0] * w), int(pts[b][1] * h)) cv2.line(image_bgr, pa, pb, (255, 0, 0), 2) return image_bgr这里有个高频错误模型输出的关键点坐标在部分版本中是 0~1 归一化的部分版本直接用像素坐标。我建议先打印keypoints[0]检查如果数值范围在 [0,1] 之间就乘以原图宽高否则不要乘。这个错误最隐蔽因为如果目标恰好出现在图像中心附近不乘也不会偏得离谱容易让人忽略。4.4 Streamlit 应用完整实现Streamlit 端我保持“结构清楚优先”。页面上方放上传区左侧放模型参数主区域放原图和结果图下面再放一个结果表格。关键点是把模型加载做成缓存把推理函数做成独立函数。import io import cv2 import numpy as np import streamlit as st from PIL import Image from ultralytics import YOLO st.cache_resource def get_model(): return YOLO(yolo11n.pt) def load_image_from_upload(upload): raw io.BytesIO(upload.read()) pil_img Image.open(raw).convert(RGB) return cv2.cvtColor(np.array(pil_img), cv2.COLOR_RGB2BGR) st.set_page_config(page_titleYOLOv11 多任务视觉演示, layoutwide) st.title(YOLOv11 多任务识别) model get_model() with st.sidebar: st.header(推理设置) conf st.slider(置信度阈值, 0.05, 0.95, 0.25, 0.05) task st.selectbox(任务类型, [检测, OBB, 分割, 姿态]) upload st.file_uploader(上传图片, type[jpg, jpeg, png, bmp]) if upload is not None: image load_image_from_upload(upload) if st.button(开始识别): with st.spinner(模型推理中...): if task 检测: boxes, classes, confs, names run_detect(model, image, conf) output draw_detections(image.copy(), boxes, classes, confs, names) elif task OBB: polygons, confs, classes, names run_obb(model, image, conf) output draw_obb(image.copy(), polygons, confs, classes, names) elif task 分割: masks, boxes, classes, names run_segment(model, image, conf) output image.copy() for i, m in enumerate(masks): mask_bool m 0.5 output visualize_mask(output, mask_bool) elif task 姿态: keypoints, _ run_pose(model, image, conf) output draw_pose(image.copy(), keypoints) st.image(cv2.cvtColor(output, cv2.COLOR_BGR2RGB), caption识别结果, use_container_widthTrue)注意st.image在后续版本中use_column_width已不推荐改用use_container_width。这个属于 Streamlit 每个版本都会动的 API写代码前先看下当前版本。4.5 性能与参数调优如果只是单张图片速度基本无所谓。一旦要做视频流或者批量处理有几点值得调。第一尽量用 GPU。模型默认会自动选择设备但你可以显式指定devicecuda:0。第二关掉不必要的预处理日志和可视化输出verboseFalse就够。第三推理时调整imgsz默认是 640如果不是小目标特别多用 480 或 320 会快不少但精度会下降需要权衡。第四把输入图像做适当缩放控制在长边 1280 以内既不会丢太多细节也不会把内存爆掉。Streamlit 端如果要做视频建议不要直接st.image逐帧刷新那样会非常卡。更好的是把处理结果先写到缓存列表用户拖动进度条选择帧查看。对视频流做实时渲染这件事Streamlit 并不是最合适的工具它的交互模型是为“事件驱动”设计的不是低延迟流媒体。5. 常见问题与排查技巧实录5.1 模型加载慢 / 显存溢出模型加载慢有两个常见原因。一是第一次加载要下载权重文件网络慢会比较明显可以把权重文件提前下到本地再用本地路径。二是缓存没做好的情况下每点一次按钮就重新加载这要在 Streamlit 层处理相关对策上面已经说过。显存溢出大多是任务分支同时加载导致的。如果做了多任务并行一个页面里同时挂着检测、分割、姿态三个模型实例显存占用自然会高。解决办法是只加载一次模型跑不同任务时在同一个模型实例上调用不同推理入口不要分别实例化。实际上ultralytics的模型对 OBB、分割、姿态都支持只是训练头不同同一个权重里可能同时存在多套输出头推理时选对应任务即可。如果还是溢出可以把imgsz调小或者把batch降到 1。另外注意推理完及时释放 CPU/GPU 中间变量必要时调用torch.cuda.empty_cache()但不要频繁调会有性能损耗。5.2 OBB 结果可视化时点多边形错乱这个坑我踩得最深。OBB 输出 4 个角点的顺序不都是顺时针也不都是逆时针有的模型按“左上、右上、右下、左下”的顺序给点有的模型顺序会变化。直接cv2.polylines画还好因为闭合线会自动连起来。但如果后续要做轮廓转 Mask或者要计算旋转角度就必须统一顺序。处理办法是写一个函数把 4 个点按顺时针重排序。常见做法是先求中心点然后按极角排序。极角排序要从中心点出发每个点计算相对角度再按角度排顺序。def order_points_clockwise(pts): center pts.mean(axis0) angles np.arctan2(pts[:, 1] - center[1], pts[:, 0] - center[0]) sorted_idx np.argsort(angles) ordered pts[sorted_idx] # 检查是否为顺时针若需要可反向 return ordered还要注意 OBB 与普通检测的类别编号可能不完全一致。如果同一个权重文件同时支持 OBB 和普通检测建议打印出model.names和result.obb.cls对应一下避免出现“画出来框的位置对但标签是猫还是狗都对不上”的奇怪状况。5.3 Streamlit 长时间运行变慢长时间运行变慢通常跟 session 里堆积了太多历史结果有关。我把所有图片和推理结果都存在st.session_state里默认每个文件能放挺多历史记录时间久了内存占用越来越大页面响应越来越慢。解决思路很直接限制 session 里的记录数量。只保留最近三到五条结果对不需要的历史结果调用删除操作或者在写入前判断数量超过阈值就清空。另外如果上传的是多张图片不要一次性全部读进内存按需读取处理完就释放。Streamlit 本身的st.cache_data也有自己的缓存上限默认可能保留较多数据。如果 main 进程内存涨得很厉害可以考虑减少缓存数据量或者在关键函数上设置ttl让它定期过期。5.4 关键点画线、掩码叠加的显示问题姿态画线最常出问题的有两个点。第一个是置信度过滤范围设置不对。关键点置信度一般在 0 到 1 之间如果设置成 0.5很多轻遮挡场景下的点会被过滤掉画的骨架断成一截一截设到 0.2 又会出现一堆错位的小点。建议保留一个可调滑杆现场观察调整。第二个是坐标归一化问题前面提到过不再多说。掩码叠加的问题多数出在掩码数据类型上。masks.data里是概率值不是二值 mask。直接用if mask 0会拿到很大一片“幽灵掩码”。建议以 0.3 到 0.5 作为阈值做二值化。如果掩码边缘锯齿严重可以先用cv2.morphologyEx做开闭运算或者用高斯模糊边缘过渡视觉上会更平滑。mask_bool mask 0.5 mask_uint8 (mask_bool * 255).astype(np.uint8) kernel cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (3, 3)) mask_clean cv2.morphologyEx(mask_uint8, cv2.MORPH_CLOSE, kernel)叠加的时候尽量用单张掩码单独生成 overlay再合并到结果图不要把所有掩码一次性画到原图上否则无法区分实例边界。6. 经验与教训做完这个项目之后我强烈建议你注意的几件事6.1 别把 Demo 当作生产系统Streamlit 做出来的东西演示和内部验证非常好用但它不是高并发服务。每个用户打开页面会启动一个 session模型推理是同步阻塞的如果三个人同时点“开始识别”会有排队等待。要做生产级的 API 或者多用户并发建议后端换成异步推理服务Streamlit 只做数据展示端。另一个容易被忽视的点是输入校验。用户上传的图可能非常大比如几十 MB 的航拍图直接丢给模型推理会把内存占满。一定要在入口处限制图片尺寸比如先做长边压缩再做推理。这不是偷懒是工程上必须做的保护。6.2 标注数据与评测指标容易被忽略但最影响效果多任务系统里“效果好不好”不是一个模型决定的。检测能不能识别出小而密集的目标OBB 角度准不准分割掩码边界是否贴合姿态点是否稳定每一项都需要单独评测。跟项目组成员讨论时建议把各类任务的指标分开清单列出来比如检测测 mAP50 和 mAP50-95OBB 测旋转框版本的平均精度分割测 mask mAP姿态测 OKS 相关指标。千万不要觉得模型框架能出多任务结果就默认每个任务精度都一样。实际上任务的难度和训练数据数量完全不同后处理差异也很大。好的做法是先用预训练权重跑通流程再针对短板任务专门做数据微调。否则整套系统看起来什么都能干细看每项都差一点。6.3 后续扩展从单图到视频再到多模型对照这个项目往下走有几个方向。一是支持视频推理视频文件上传后按帧处理输出结果视频保存到本地。二是支持多模型对照同一个界面上放两个模型跑同一张图直观对比误检和漏检。三是把结果导出结构化比如将检测框、OBB 角点、掩码面积、关键点坐标统一写成 JSON 或 CSV方便下游分析。还有一个很低成本但很实用的扩展把当前识别结果做成可下载的报告页。Streamlit 能直接渲染 Markdown 和 HTML把图中各目标的数量统计、每类目标的置信度分布、姿态平均角度这些信息生成一个可保存的文档。给非技术角色看效果时这份报告比界面截图更有说服力。做完整个项目我的一个强烈体会是视觉模型的“模型训练”只占整个项目的一部分真正花时间的是输入输出适配、结果后处理、界面交互和坑点排查。单模型多任务的设计能省下大量重复工作但没有一个模型是万能钥匙。把一个流程稳定跑通、把可视化做到能让外行看明白远比在网上挑一个“最强模型”更值得投入精力。最后分享一个实用小技巧给所有推理函数加上统一的输入输出约定。无论检测、OBB、分割还是姿态统一接收 BGR 的numpy.ndarray统一返回可叠加的可视化图和结构化的结果对象。一旦约定好了后面换模型、加界面、做视频流都只是替换内部调用不用动外层代码。这个习惯帮我省下的调试时间比任何网络结构调整都多。
阅读完成 · 觉得有帮助?
咨询建站