☰
YOLOv11多任务视觉实战:检测、OBB、分割、姿态与Streamlit部署
2026/10/17 7:37:03 网站建设 项目流程

这两年做视觉应用,我最大的感受是:算法再强,落不了地就是一堆权重文件。最近我把基于深度学习应用项目的常见需求打了个包: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. 工具选型解析:为什么尾端一定要接 Streamlit

3.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里,避免环境漂移。

ultralytics>=8.0.0 streamlit>=1.28.0 opencv-python>=4.6.0 numpy>=1.23.0 pillow>=9.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, conf=0.25): results = model.predict( source=image_bgr, conf=conf, verbose=False, ) 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_bgr

OBB 的区别在于result.obb。每个旋转框由 4 个角点组成,也就是 8 个数字:(x1,y1,x2,y2,x3,y3,x4,y4)。

def run_obb(model, image_bgr, conf=0.25): results = model.predict( source=image_bgr, conf=conf, task="obb", verbose=False, ) 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, conf=0.25): results = model.predict( source=image_bgr, conf=conf, verbose=False, ) 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), alpha=0.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, mask=mask_bool.astype(np.uint8)), 0.6, cv2.bitwise_and(colored, colored, mask=mask_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, conf=0.25): results = model.predict( source=image_bgr, conf=conf, verbose=False, ) 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, thr=0.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_title="YOLOv11 多任务视觉演示", layout="wide") 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_width=True)

注意st.image在后续版本中use_column_width已不推荐,改用use_container_width。这个属于 Streamlit 每个版本都会动的 API,写代码前先看下当前版本。

4.5 性能与参数调优

如果只是单张图片,速度基本无所谓。一旦要做视频流或者批量处理,有几点值得调。

第一,尽量用 GPU。模型默认会自动选择设备,但你可以显式指定device="cuda:0"。第二,关掉不必要的预处理日志和可视化输出,verbose=False就够。第三,推理时调整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(axis=0) 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-95,OBB 测旋转框版本的平均精度,分割测 mask mAP,姿态测 OKS 相关指标。

千万不要觉得模型框架能出多任务结果,就默认每个任务精度都一样。实际上任务的难度和训练数据数量完全不同,后处理差异也很大。好的做法是先用预训练权重跑通流程,再针对短板任务专门做数据微调。否则整套系统看起来什么都能干,细看每项都差一点。

6.3 后续扩展:从单图到视频再到多模型对照

这个项目往下走有几个方向。一是支持视频推理,视频文件上传后按帧处理,输出结果视频保存到本地。二是支持多模型对照,同一个界面上放两个模型跑同一张图,直观对比误检和漏检。三是把结果导出结构化,比如将检测框、OBB 角点、掩码面积、关键点坐标统一写成 JSON 或 CSV,方便下游分析。

还有一个很低成本但很实用的扩展:把当前识别结果做成可下载的报告页。Streamlit 能直接渲染 Markdown 和 HTML,把图中各目标的数量统计、每类目标的置信度分布、姿态平均角度这些信息生成一个可保存的文档。给非技术角色看效果时,这份报告比界面截图更有说服力。

做完整个项目,我的一个强烈体会是:视觉模型的“模型训练”只占整个项目的一部分,真正花时间的是输入输出适配、结果后处理、界面交互和坑点排查。单模型多任务的设计能省下大量重复工作,但没有一个模型是万能钥匙。把一个流程稳定跑通、把可视化做到能让外行看明白,远比在网上挑一个“最强模型”更值得投入精力。

最后分享一个实用小技巧:给所有推理函数加上统一的输入输出约定。无论检测、OBB、分割还是姿态,统一接收 BGR 的numpy.ndarray,统一返回可叠加的可视化图和结构化的结果对象。一旦约定好了,后面换模型、加界面、做视频流都只是替换内部调用,不用动外层代码。这个习惯帮我省下的调试时间,比任何网络结构调整都多。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询