从Swin Transformer到PyQt5:目标识别桌面工具的落地实践
简介这是一份面向人工智能与计算机视觉开发者、对目标识别落地感兴趣的进阶学习者的完整资源包。项目将SwinTransformer视觉模型与PyQt5界面封装为桌面应用用户可上传图像并查看识别结果直观理解模型推理与界面集成的全流程。资源共1047个文件包含266个Python脚本、778个编译后的pyc文件另有UI定义文件、演示图片与预训练权重如epoch_100.pth压缩包约494.79MB其中py与pyc覆盖模型调用、图像预处理及界面控制逻辑pth可直接用于推理。目前已有413人学习下载。除核心源码外还含模型配置与调试痕迹便于逐步追踪SwinTransformer在真实任务中的调用方式适合做检测项目改造、PyQt5工具开发或深度学习模型部署练习。配套模型配置与可视化界面结合学习者可据此理解从图像输入到检测输出的完整链路并可替换自有数据集进行迁移训练。1. 从 Swin Transformer 到 PyQt5一条被低估的交付链路把“人工智能 目标识别 Swin Transformer PyQt5 界面”放进同一个项目是把一个模型变成桌面工具的最后一百米。Swin Transformer 在 COCO 上精度不错但很多人在训练完权重后就停在show_result这一步等项目方要一个可交互界面时才发现模型推理与 Qt 事件循环之间还隔着线程、坐标映射和显存开销三道坎。这篇文章按照常见落地顺序走一遍backbone 和检测头怎么选MMDetection 里的 Swin 配置参数是什么含义推理结果怎么结构化PyQt5 里如何用 QThread 把推理丢到子线程并用 QPainter 画框最后用 COCO 评估和推理延迟验证整个链路。适合算法工程师接手“做个界面”的需求也适合负责模型部署的全栈工程师快速判断这个组合到底能不能跑。2. 目标识别里的 Swin Transformerbackbone 怎么选、检测头怎么配2.1 Swin Transformer 为什么比 CNN backbone 更划算Swin Transformer 真正引入目标识别时靠的不是全局 self-attention而是把注意力限制在窗口里让计算复杂度从图像尺寸的平方降为线性。它的 4 个 stage 逐层下采样最后把不同分辨率特征输送给 FPN这一点和 ResNet 的输出形态几乎一致所以目标识别框架替换骨干时并不需要重写 neck。换句话说Swin-T 的易用性来自它保留了stride 4/8/16/32的金字塔特征和 RPN、ROI 头的接口天然兼容。对桌面识别工具来说我一般不会直接上 Swin-L。本地推理对显存和延迟很敏感Swin-T 的参数量约 28M和 ResNet-50 接近但同样的检测头下精度要高出一截代价是 CPU 上慢得多。如果目标是实时视频流而不是单张图片Swin-T 也只适合 GPU 环境CPU 上想流畅往往要把输入分辨率降到 960 甚至 800或者放弃 Swin 换用轻量 CNN。在真实项目里“换了 backbone 反而跑不起来”大多不是因为精度而是因为使用者低估了浮点运算量和内存占用。2.2 检测头选择Cascade R-CNN 与 RetinaNet 的取舍Swin 官方仓库的典型配置是Swin Cascade R-CNN但工程上不一定要照搬。检测头决定了最终预测的边界框质量也决定了能否在低配置机器上运行。下面是我常用的选型对照按“桌面工具优先”排序检测头适合场景与 Swin 搭配的感知本地推理压力Cascade R-CNN对框质量要求高有小目标、遮挡重叠Swin 输出特征质量高级联回归能进一步压低边界框误差较高推荐 GPUFaster R-CNN数据集本身干净、类别少比 Cascade 少一两个 head训练更稳中等RetinaNet需要更快推理或训练资源有限用 FPN 直接分类回归注意正负样本均衡相对最低CPU 可挣扎一下从 COCO 预训练权重迁移到自定义数据集时我会把num_classes改掉再训练例如 COCO 的 80 类改成自己的num_classes5。此时检测头的bbox_head或roi_head的类别数必须同步改否则会出现权重形状不匹配。Swin backbone 的预训练权重可以保留因为它是分类任务学到的通用特征不依赖目标类别。真正要留意的是每一次修改类别后推理脚本里的class_names列表也必须对应更新否则识别结果和标签名会错位。2.3 Swin 配置里的必调参数window_size、out_indices 与 pretrained_img_size打开 MMDetection 的 Swin 配置文件backbone 一段通常长这样backbonedict( typeSwinTransformer, embed_dims96, patch_size4, depths[2, 2, 6, 2], num_heads[3, 6, 12, 24], window_size7, mlp_ratio4, qkv_biasTrue, qk_scaleNone, drop_rate0.0, attn_drop_rate0.0, drop_path_rate0.2, out_indices(1, 2, 3), pretrained_img_size224, pretrainedNone, )window_size7是窗口内参与自注意力的像素边长窗口越大感受野越大但计算量和显存也会增大。out_indices(1,2,3)表示只取后三个 stage 的特征给 FPN对应 stride 8、16、32如果你的 FPN 配置里还用了 stride 4 的特征就要把out_indices改为(0,1,2,3)否则会报通道不匹配。pretrained_img_size224用来初始化相对位置偏置表如果你的目标识别输入习惯是 1333×800 这种长图模型加载时会自动插值但插值会略微改变原始位置先验微调时最好固定test_pipeline中的resize尺度避免每次推理输入尺寸不同导致位置偏置表反复插值。这里最容易掉坑的是mmcv和mmdet的版本匹配。Swin 实现依赖 mmcv 里的窗口注意力算子有的算子需要编译扩展如果mmcv不是从源码装会有SwinTransformer.forward相关报错。我一般在项目初始就固定到某个 mmdet 3.x 版本和匹配的mmcv2.0不要用 pip 单独装一个最新版容易撞上 CUDA 算子默认版本问题。另外常见认知“transformer 一定比 CNN 慢”在 Swin 上不是铁律。窗口注意力把复杂度锁在了局部实际耗时取决于窗口数量和特征图大小。这个特性对界面应用很关键推理不管怎么优化前端都得有一条稳定的响应路径否则模型再准用户在 PyQt5 里点一下按钮就“假死”几秒也算失败。3. 可复现的 Swin Transformer 推理脚本从权重到结构化结果3.1 用 MMDetection 初始化 Swin 模型推荐用 mmdet 自带推理 API 起步不要自己拼 forward。下面是以Swin-T Cascade R-CNN FPN为例的最小目标识别推理脚本from mmdet.apis import init_detector, inference_detector config_file configs/swin/cascade_rcnn_swin-t-p4-w7_fpn_1x_coco.py checkpoint_file epoch_12.pth model init_detector( config_file, checkpoint_file, devicecuda:0, cfg_optionsdict(modeldict(test_cfgdict(rcnndict(score_thr0.3)))) ) result inference_detector(model, demo/street.jpg)这段代码的含义init_detector先读取 MMDetection 配置文件并构建整个模型图再加载权重cfg_options可以临时覆盖配置比如这里把测试时的得分阈值score_thr调成 0.3返回结果里就只保留置信度 0.3 以上的检测框。inference_detector会按配置文件里的test_pipeline做读图、归一化和 resize再走一次前向返回一个包含pred_instances的结果对象。CPU 推理时把device改为cpu但这只适合单张图片验证。Swin-T 在 CPU 上跑 1333×800 输入通常要数秒界面里必须配合异步处理否则用户视觉上就是卡死。如果只有 CPU更实际的做法是用 ONNX Runtime 而不是 PyTorch 做推理后面第 5 章再展开。3.2 从 result.pred_instances 拿到坐标、标签和置信度如果直接调用model.show_result虽然能画图却拿不到结构化数据。在 PyQt5 界面里你需要自己整理检测结果。常见做法是读pred_instancesimport torch import numpy as np pred result.pred_instances boxes pred.bboxes # Tensor, shape (N, 4), 对应 [x1, y1, x2, y2] labels pred.labels # Tensor, shape (N,) scores pred.scores # Tensor, shape (N,) keep scores 0.45 boxes boxes[keep].cpu().numpy().astype(np.int32) labels labels[keep].cpu().numpy() scores scores[keep].cpu().numpy() class_names [person, car, dog, cat, bicycle] for box, label, score in zip(boxes, labels, scores): print(class_names[label], score, box)这里的bboxes坐标系是原始图像坐标不是归一化坐标所以后续在 Qt 里绘制时只需要计算缩放比例不需要额外反归一化。labels默认是 COCO 类别索引如果你换了自定义数据集记得在界面代码里更新class_names否则会出现“狗识别成人”的误会。如果检测输出里同时出现多个重叠框可以用torchvision.ops.nms再做一次后处理虽然配置里的test_cfg.nms已经做了一轮但自定义后处理经常还要过滤重复框。NMS 的iou_threshold一般设 0.5小目标密集场景可以调到 0.3但不要低于 0.2否则容易把同一辆车的两个部件当成两个目标。配置点常见默认界面落地时建议test_pipeline[1].scale(1333, 800)资源紧张可调成(960, 640)model.test_cfg.rcnn.score_thr0.05界面过滤用 slider 关联 0.3~0.6devicecuda:0无 GPU 时才改cpu并转 ONNX3.3 显存不足时的梯度关闭和输入裁剪界面推理不像训练不需要保存梯度。部分人直接把训练代码拉进 Qt 项目导致一个按钮点下去显存里同时存了计算图。推理时务必开启torch.inference_mode()并尽可能固定 batch 为 1model.eval() with torch.inference_mode(): result inference_detector(model, img_path)显存仍然不够的话优先降低输入尺寸而不是裁剪背景。裁剪会破坏目标框的相对位置后处理时边界框坐标还要还原容易出现坐标偏移。更优雅的做法是修改配置中的test_pipeline里的resize把scale从默认(1333, 800)改为(960, 640)或(800, 600)。精度会下降几个点但界面响应会明显变快。推理结果与其在主线程里转成字典不如直接打印一条 JSON 流方便前面 Qt 层的信号槽接收payload [ { label: class_names[int(label)], score: float(score), box: [int(x) for x in box], } for box, label, score in zip(boxes, labels, scores) ]这样把检测器和界面解耦PyQt5 只负责展示payload不关心torch.Tensor从哪个设备来。后面如果要接的是 Web 前端或者上位机这段代码也能直接复用。4. PyQt5 界面接入 Swin Transformer线程、坐标映射与界面卡顿根治4.1 为什么不能把推理直接放在按钮槽函数里最直观的错误写法是在on_click_open_file里同步调用inference_detector。这张图如果花 2 秒Qt 的事件循环就会被阻塞窗口显示为“无响应”操作系统的窗口管理器甚至会弹出“是否强制关闭”的对话框。PyQt5 的按钮信号槽跑在 UI 线程里耗时任务必须搬出主线程。另外一个反直觉的地方Python 的多线程受 GIL 限制为什么还能承担推理任务因为 PyTorch 的底层 C 扩展在执行张量运算时会释放 GIL真正被 GIL 卡住的主要是图像编码、列表转换这类 Python 代码。所以QThread里跑模型推理是可行的只要别在子线程里直接改界面控件。4.2 QThread 推理 worker把模型放到子线程用信号把结果带回界面下面是一个可复用的InferWorker骨架import cv2 import numpy as np from PyQt5.QtCore import QThread, pyqtSignal from mmdet.apis import inference_detector class InferWorker(QThread): # 第一个参数传绘制用图片第二个参数传结构化结果 finish pyqtSignal(object, list) def __init__(self, model, image_path, score_thr0.4, parentNone): super().__init__(parent) self.model model self.image_path image_path self.score_thr score_thr def run(self): result inference_detector(self.model, self.image_path) pred result.pred_instances keep pred.scores self.score_thr boxes pred.bboxes[keep].cpu().numpy().astype(int) labels pred.labels[keep].cpu().numpy() scores pred.scores[keep].cpu().numpy() payload [ {box: box.tolist(), label: int(label), score: float(score)} for box, label, score in zip(boxes, labels, scores) ] frame cv2.imread(self.image_path) frame cv2.cvtColor(frame, cv2.COLOR_BGR2RGB) self.finish.emit(frame, payload)信号finish(object, list)第一个参数是转成 RGB 的numpy.ndarray第二个是过滤后的检测框列表。这里不能在run里调用任何QLabel.setText否则线程不安全。界面主类里连接信号后在槽函数里统一刷新画面和状态栏。这个 worker 每次点“选择图片”就新建实际场景也可以做成常驻线程不断从queue里取图片路径输出发信号这样避免反复创建线程开销。PyQt5 的信号槽会按队列方式跨线程投递所以finish信号发送的frame和payload在槽函数里安全可用。4.3 QPainter 画检测框处理缩放、字体与高 DPI拿到原始图像后应该让QLabel显示缩放后的图像同时保存一个original_size。绘制边界框用 QPainter 写在 QPixmap 上关键是坐标映射def paint_boxes(self, frame, payload): h, w, _ frame.shape label_w self.image_label.width() label_h self.image_label.height() sx label_w / w sy label_h / h from PyQt5.QtGui import QImage, QPixmap, QPainter, QPen, QColor, QFont img QImage(frame.data, w, h, frame.strides[0], QImage.Format_RGB888) pix QPixmap.fromImage(img).scaled(label_w, label_h) painter QPainter(pix) painter.setRenderHint(QPainter.Antialiasing) painter.setPen(QPen(QColor(0, 200, 255), 2)) class_names self.class_names for det in payload: x1, y1, x2, y2 det[box] rect QRect(int(x1 * sx), int(y1 * sy), int((x2 - x1) * sx), int((y2 - y1) * sy)) painter.drawRect(rect) text f{class_names[det[label]]} {det[score]:.2f} painter.drawText(rect.left(), max(0, rect.top() - 6), text) painter.end() self.image_label.setPixmap(pix)这里最容易踩坑的是两点。第一QImage(frame.data, ...)需要frame是 C 连续内存块建议统一先执行np.ascontiguousarray(frame)。第二坐标缩放系数要分开算不能直接用单个缩放因子否则QLabel设置了缩放内容模式后画框会整体偏移。drawText默认不带背景容易被复杂背景干扰可以用QRect计算文字宽度再填充一个半透明底色。PyQt5 类/方法用途容易出错的地方QThread把推理放到子线程子线程里不能直接 setTextpyqtSignal(object, list)在 UI 线程接收 ndarray 和结果信号参数不要写np.ndarray要用 objectQPainter.drawRect绘制检测框坐标需要按 label 缩放比换算QPainter.drawText显示类别和置信度文字没有背景复杂背景下难辨认4.4 调节阈值时如何不触发重复推理界面上放一个QSlider调节置信度阈值是常规需求。如果每次 slider 变化都重新跑模型用户拖一下就会卡住。常见做法是 slider 只缓存self._score_thr等用户点击“重新筛选”或松开鼠标时才把新阈值送入推理线程。更轻量的做法是保留上一帧的原始未过滤结果在界面侧重新做scores thr因为过滤逻辑纯粹在 numpy 上速度可以忽略完全没必要回到模型前向。如果你用QComboBox切换检测模型比如在 Swin-T 和 ResNet-50 之间切换需要把旧 model 先删除再通过init_detector重建。不要在主线程里同步切模型否则界面又会卡。可以把模型列表做成model_name - config_file映射在子线程里完成加载后发model_ready信号切换期间界面显示“模型加载中”。5. 用 COCO 评估和延迟测量验证 Swin Transformer 应用界面能跑之后真正交付前还要反过来验证模型有没有退化。这里给出两个常用的验证技巧。第一个技巧是用 mmdet 的test.py在验证集上跑评测而不是在界面上截图肉眼数框。以 Swin-T Cascade R-CNN 为例命令行通常是这样python tools/test.py \ configs/swin/cascade_rcnn_swin-t-p4-w7_fpn_1x_coco.py \ epoch_12.pth \ --out results.pkl \ --eval bbox命令里的--eval bbox让脚本在测试集上计算 COCO 的 AP 指标。固定权重后如果想要更稳定的评估结果最好把test_dataloader的shuffle设为 False并保持验证集的原始尺寸。如果只关心某几个类别可以看per-class AP那一行不要只看mAP小目标的AP_s如果明显低于AP_m说明 Swin 输入分辨率不够或窗口注意力对小目标不太友好。第二个技巧是真正测量 PyQt5 界面里用户能感知的延迟。不要直接用 Python 的time.time()包住整个inference_detector因为 PyTorch 的 CUDA 核是异步提交的GPU 可能还没算完就返回了 Python。测量延迟的正确姿势是import time import torch model.eval() with torch.inference_mode(): torch.cuda.synchronize() start time.perf_counter() result inference_detector(model, test.jpg) torch.cuda.synchronize() cost_ms (time.perf_counter() - start) * 1000用torch.cuda.synchronize()强行等 GPU 完成得到的cost_ms才接近真实推理延迟。把耗时打印在 PyQt5 状态栏里你会看到一次完整前向往往在几十毫秒量级但加上图片解码、后处理和绘制整个按钮流程会到几百毫秒这也再次说明为什么必须用异步线程。如果还想进一步提速可以在 PyQt5 外面包一层 ONNX Runtime把 Swin 的config和权重导出为 ONNX再用onnxruntime-gpu推理。常见导出做法是使用torch.onnx.export固定输入尺寸比如 640×640得到静态 ONNX 图配合 CUDA 提供程序常驻显存帧率通常比 PyTorch eager 模式高一截。导出时要注意 Pytorch 版本、opset 版本不一致时第一轮运行报出来的错误多半是某个自定义算子不在 ONNX 注册表里因此实现时要保留 PyTorch 回归路径。跑完这个流程后界面状态栏里打印的端到端耗时就是这次交付最值得记录、也最能说服使用者验收的数字。本文还有配套的精品资源点击获取