2026.05.06征图日记7(测通app,总结&&内化)
继4.30mentor把代码写出来后,就让我回去自己调试。回去其实当天晚上就调好了,后续都是在把玩mac。今天早上一来leader就过来问我做得怎么样了?我:“现在前端已经ok了,后端代码也写好了,现在下一步就是部署到服务器上了。。。。。。巴拉巴拉”
leader打断我现在这个展示界面是怎么搞的?因为我当时只管把这个跑起来就行,其他的都没管,后面下来读代码才知道,app要往下滑选择缺陷字段,之前没有选,所以一直推理失败。
之后就是熟悉项目前后端代码,之后mentor过来问我跑起来了吗,之后他就自己去部署后端服务了。我再问他就说没我什么事了,叫我自己去熟悉代码。之后就是LinuxDo启动了
看了一会Linuxdo之后,就在梳理项目代码了,主要是后端代码,程序是怎么启动的,数据是怎么流的,服务端返回的字段,接口测试。。。。。。
下午和mentor一起测试打包,打包成功在ios上下载之后就剩下网络了,因为服务器部署在公司内网,但手机不能连接内网。所以需要内网传统或者买公网ip,域名进行映射
之后就在熟悉代码,我要把这个项目说成是我自己做的,所以需要彻底熟悉
今日工作内容:
1、梳理app项目代码,本地测试接口返回
2、完成本地测试回环
3、研究如何打包分发
下阶段计划:
1、联调前后端、测试
2、优化前端界面
整理项目后端服务如下:
征图视检 — 后端完整技术手册
一、项目速览
| 项 | 说明 |
|---|---|
| 项目名 | 征图视检 (Zhengtu Vision) |
| 功能 | 多模态工业缺陷检测 |
| 后端 | Python FastAPI,双模式:mock(开发)/ TRT(生产) |
| 模型 | GroundingDINO(开放词汇目标检测 + 分割) |
| 推理加速 | NVIDIA TensorRT |
| 部署 | Docker + NVIDIA Container Toolkit |
| 客户端 | React Native iOS(竖屏,iOS 16+) |
一分钟跑起来
1 | cd backend |
二、架构全貌
2.1 代码分层
1 | backend/app/ |
2.2 启动初始化流程
1 | uvicorn app.main:app |
2.3 双模式设计(策略模式)
1 | InferenceEngine (ABC 抽象接口) |
切换方式:改 .env 中 INFER_BACKEND=mock 或 trt,零代码改动。
三、一次请求的完整数据旅程
1 | 手机 App |
关键语法解释
1 | async with self._lock: # 异步锁 = await acquire(); try; finally release() |
| 语法 | 含义 |
|---|---|
out: InferOutput |
类型注解(给人/IDE 看,运行时不管) |
await |
暂停当前协程,等异步操作完成再继续 |
asyncio.to_thread(func, *args, **kwargs) |
在线程池中调用同步函数,不阻塞事件循环 |
async with self._lock |
同一时刻只允许一个请求进入 GPU |
四、响应 JSON 结构
1 | { |
两个开关的四组组合
use_segmentation |
return_rendered_image |
segmentation |
rendered_image |
|---|---|---|---|
false |
false(默认) |
null |
null |
true |
false |
base64 掩膜 | null |
false |
true |
null |
base64(只有紫框) |
true |
true |
base64 掩膜 | base64(紫框 + 绿轮廓线) |
use_segmentation:控制是否计算像素级分割掩膜,不影响 detections 矩形框return_rendered_image:后端直接用 visualizer.py 在原图上画框/轮廓后返回,测试时最直观
五、模型架构:GroundingDINO
5.1 基本信息
| 项 | 详情 |
|---|---|
| 模型 | GroundingDINO(IDEA Research,2023) |
| 类型 | 开放词汇目标检测 (Open-Vocabulary Object Detection) |
| 文本编码器 | BERT-base-uncased(12 层 Transformer) |
| 视觉编码器 | Swin Transformer(或 ViT,取决于训练配置) |
| 跨模态融合 | Feature-based early fusion(文本特征融入视觉 backbone) |
| 输出头 | Box 回归 + 逐 token 分类 logits + Mask 预测 |
| 本项目版本 | fsnet_gd_v1.0,在工业缺陷数据上微调 |
| 引擎文件 | engines/fsnet_gd_model.engine(TensorRT 编译产物) |
5.2 GroundingDINO 工作原理
1 | 输入: |
核心优势:不像传统检测模型只认训练时见过的类别,GroundingDINO 通过文本编码器理解任意英文描述,可以检测”训练时没见过但能描述出来”的缺陷类型。
5.3 本项目微调
fsnet_gd 可能是 “Few-Shot Network + GroundingDINO” 的缩写,在工业缺陷数据集上 fine-tune:
- 输入:800×800 裁剪窗口
- 输出:边界框(xyxy 格式)+ 置信度 + 分割掩膜
- Prompt 体系:英文缺陷描述(scratch、crack、bubble 等),用
.分隔
六、TRT 推理管线(GPU 生产模式)
6.1 完整流程
1 | 原始图片 ndarray (H×W×3) |
6.2 为什么要切图
GroundingDINO 的 TensorRT engine 编译时把输入尺寸固定为 800×800。这个约束来自 TRT——动态 shape 的 engine 性能会下降,所以编译时选择固定尺寸以获得最优 kernel 调度。
工业检测的真实图片通常是 4000×3000 甚至更大。如果直接把整张图缩放:
1 | 4000×3000 → resize → 800×600 |
微小缺陷在缩放中会丢失像素,模型根本检测不到。必须用原分辨率切块。
6.3 切图逻辑逐行拆解
切图入口在 pipeline.py:26-50 的 sliding_window_crop。
① padding 补齐(pipeline.py:39-44)
1 | pad_h = (win_h - h % stride) % stride # 例如 h=2500, stride=720 |
如果不补:y=2160+800=2960 > 2500 → 最后一个窗口越界。补 460px 后 h=2960,所有 y=0,720,1440,2160 的窗口都恰好落入边界内。
② 滑动循环(pipeline.py:46-49)
1 | for y in range(0, h - win_h + 1, stride): # y: 0, 720, 1440, 2160 |
stride = 800 × stride_ratio = 720,重叠 = 800 - 720 = 80px。
③ 切图全景视图
1 | w=4000 (pad 后) |
6.4 重叠区设计(为什么不能 stride=800 无重叠)
1 | 没有重叠 (stride=800): |
80px 重叠意味着:任何横跨边界的缺陷,只要最大宽度 ≤ 80px,就一定会被至少一个窗口完整包含。工业缺陷(划痕通常 2-20px 宽)绰绰有余。
6.5 坐标还原:窗口坐标 → 全图坐标
这段逻辑在 pipeline.py:135-152。
模型输出的格式:归一化坐标 [cx, cy, w, h],范围 0~1。
三步还原:
1 | # 第一步:记录窗口尺寸 |
窗口偏移(postprocess.py:85-95):在 merge_detections 中加上窗口在全局的位置偏移:
1 | for detections, (x1, y1, _, _) in zip(all_detections, positions): |
边界 clamp(postprocess.py:103-108):padding 区域可能产生越界框,钳制到 [0, w]/[0, h]。
6.6 双层 NMS 去重
为什么需要两层:
- 第一层 nms_small(单窗口内):模型对一个缺陷可能预测出 3-5 个高度重叠的 anchor → 只保留置信度最高的
- 第二层 nms_large(跨窗口后):重叠区内同一缺陷被 A/B 两个窗口各检测一次 → 两个框几乎重叠,去重保留更完整的
IoU 计算(postprocess.py:8-49):两个版本 _iou_small 和 _iou_large 逻辑相同(代码完全一样),但各自 NMS 排序策略不同。
1 | # 标准 IoU = 交集面积 / 并集面积 |
为什么取 max? 考虑大小框嵌套:大框 200×200 内套小框 20×20 → 标准 IoU=0.01(不会被抑制),但 min_area IoU=1.0(抑制!)。这样保证大框优先。
两种排序策略的差异:
| nms_small | nms_large | |
|---|---|---|
| 排序依据 | 置信度降序(x[4]) |
面积降序 (x[3]-x[1])*(x[2]-x[0]) |
| 代码位置 | postprocess.py:53 | postprocess.py:65 |
| 为什么 | 单窗口内框来自同一模型输出,置信度直接可比 | 跨窗口后重叠框来自不同推理,置信度不是绝对可比;面积大的框通常框住完整缺陷,更可信 |
NMS 算法(贪心):按排序键降序排列 → 取第一个加入 keep → 计算与剩余框 IoU → 丢弃 IoU > 0.5 的 → 重复直到空。
6.4 Prompt 拼接规则(trt_infer.py:91-100)
1 | def _join_prompts(prompts): |
补到 60 个是为了固定 token span 长度,避免 TRT engine 的输入 shape 抖动触发 re-build。
七、GPU 显存管理(trt_runtime.py)
7.1 初始化时的预分配 (trt_runtime.py:52-91)
1 | 读取 .engine 文件 → deserialize_cuda_engine |
所有显存在 __init__ 时一次性分配,推理时直接 memcpy → execute → memcpy,不产生任何 malloc/free 开销。这也是为什么只能单 worker —— 多进程各自分配一份显存,很快就会 OOM。
7.2 推理时的数据搬运 (trt_runtime.py:145-150)
1 | def infer(self, img_data): |
7.3 释放 (trt_runtime.py:191-194)
1 | def cleanup(self): |
cleanup() 在 FastAPI lifespan 的 finally 块里调用(main.py:60),确保进程退出时释放 GPU 资源。
八、优化细节清单
以下是代码中已经做的和可以进一步做的优化:
8.1 已实现的优化
| # | 优化点 | 代码位置 | 效果 |
|---|---|---|---|
| 1 | GPU 显存预分配 | trt_runtime.py:71-81 | 推理热路径零 malloc,单次推理节省 ~5-10ms |
| 2 | 文本特征复用 | pipeline.py:116(token_spans 变窗不变) | BERT 分词只跑一次,N 个窗口复用 |
| 3 | 图片自动降采样 | inference_service.py:62-64 | 超限图片先缩小再入管线,窗口数指数级减少 |
| 4 | LANCZOS 高质量插值 | image_io.py:37 | 降采样时保留边缘细节,对缺陷检测至关重要 |
| 5 | 双层 NMS | postprocess.py | 窗口级 NMS 削减 90% 候选框,全局 NMS 只处理合并后的少量框 |
| 6 | EXIF 自动旋转 | image_io.py:14 | 手机拍照旋转后方向正确,否则模型会误判 |
| 7 | PNG optimize=True | image_io.py:22 | base64 响应体积缩小约 20% |
| 8 | asyncio.to_thread | inference_service.py:76 | GPU 操作丢线程池,事件循环不被卡住 |
| 9 | Prompt 补齐到 60 | trt_infer.py:96-100 | 固定输入 shape,避免 TRT re-optimization |
| 10 | 层层回退机制 | main.py:31-33, prompt_service.py:38-40 | 单个组件失败不影响整体启动 |
8.2 可进一步优化的方向
① caption_tokenize 移出循环(最小改动,立竿见影)
当前 _get_grounding_output 每次都调用 engine.caption_tokenize(pipeline.py:69),但 caption 在所有窗口不变:
1 | # 当前:每窗口都过 BERT encode(N 次) |
收益:省 N-1 次 BERT 编码(单次约 50-100ms),24 窗口图省 1-2 秒。只改 3 行代码。
② 窗口批处理(投入产出比最高)
当前逐窗口串行推理(pipeline.py:128):
1 | for crop in tqdm(crops, desc="Processing crops"): # 一个接一个 |
改法:将多个窗口拼成 batch,一次 execute_v2 处理 4-8 个窗口:
1 | batch_size = 4 |
前提:重新编译 engine 时打开 batch dimension(profile_shape 支持 batch > 1)。
收益:24 窗口从 24 次 kernel launch → 6 次,GPU 利用率 40% → 85%,推理时间约减半。
③ stride_ratio 动态调整
当前 stride_ratio 全局固定 0.9(trt_infer.py:63)。优化思路:边缘加密,中心稀疏。
实际可落地方案:保持全局 0.9 不变,仅在图像四周边界额外多切一行/列窗口(用 stride=760,重叠 40px)。窗口数增加约 10%,但彻底消除边界漏检风险。
不要做动态 stride——逻辑复杂收益不大。
④ 其他优化方向
| # | 优化方向 | 思路 | 预期收益 |
|---|---|---|---|
| 1 | FP16 精度 engine | 重新编译 engine 时选 FP16,精度损失 <0.5% | 速度翻倍 |
| 2 | CUDA Stream 异步 | 用 CUDA stream 重叠 memcpy 和 kernel 执行 | 吞吐 +15-20% |
| 3 | 多 GPU 实例 | 前置 nginx 负载均衡,每个 GPU 跑一个 worker(一个进程) | 并发 N 倍 |
| 4 | 分级响应 | use_segmentation=false 跳过 mask 后处理 |
省 ~30% 后处理时间 |
| 5 | 响应体积优化 | rendered_image 用 JPEG 替代 PNG | base64 体积减 80% |
8.3 优化效果估计
对于 4000×3000 图片(约 24 个窗口,当前 TRT 耗时约 3-5 秒):
| 优化 | 改动量 | 预期耗时 | 累计提升 |
|---|---|---|---|
| 基线(当前) | - | 4.0s | - |
| caption 移出循环 | 3 行代码 | 2.5s | 37% ↓ |
| + window batch=4 | 重编译 engine + ~20 行 | 1.3s | 67% ↓ |
| + FP16 engine | 重新编译 | 0.8s | 80% ↓ |
| + 关 segmentation | 参数控制(已有) | 0.5s | 87% ↓ |
投入产出比排序:关 segmentation(已有,零成本)→ caption 移出循环(3 行)→ batch 推理(重编译 engine)→ FP16(重编译 engine)
九、Docker 部署细节
9.1 Dockerfile (backend/Dockerfile)
1 | FROM nvcr.io/nvidia/tensorrt:24.04-py3 # NVIDIA 官方 TensorRT 镜像 |
9.2 启动脚本 (scripts/start.sh)
1 | WORKERS=${WORKERS:-1} # GPU 推理强制 1 worker |
9.3 运行命令
1 | docker build -t zhengtu-backend . |
模型和配置通过 volume 挂载,更新不重建镜像。回滚只需替换旧版 engine 文件并重启容器。
9.4 为什么模型文件不进 Docker 镜像?
.engine文件通常 500MB-2GB,进镜像会让每次构建极慢- 更新模型不需要重新构建和推送镜像
- 同一镜像可以挂载不同版本的 engine 给不同环境(测试/生产)
- 回滚只需替换文件重启容器
十、面试要点速查
10.1 一句话概括项目
“这是一个多模态工业缺陷检测系统,我负责 FastAPI 后端开发。核心是基于 TensorRT 加速的 GroundingDINO 模型,通过滑窗分块处理工业大图,配合双层 NMS 合并跨窗口检测结果。设计了 Mock/TRT 双引擎架构,开发生产零代码切换。”
10.2 高频问答
Q: 为什么只能 1 个 worker?
TRT
ExecutionContext非线程安全。uvicorn 多 worker 是多进程,每个进程独立创建 TRT context 争抢 GPU 显存。所以单 worker +asyncio.Lock串行化,保证安全。
Q: 为什么滑窗而不是直接缩放?
工业缺陷(如 0.1mm 划痕)在全图缩放后会变成 1-2 个像素,模型无法识别。滑窗在原分辨率下推理每个局部区域,stride_ratio 保留重叠区防止边界漏检。
Q: TensorRT 为什么快?
层融合(Conv+BN+ReLU 合并为一个 kernel)、FP16/INT8 精度量化、显存预分配消除运行时 malloc、根据目标 GPU 型号自动选择最优 CUDA kernel。
Q: 怎么处理大图?
三步:① LANCZOS 降采样(长边 >4096 先缩);② 滑窗切 800×800 块;③ 推理结果按窗口偏移量还原坐标,双层 NMS 去重。
Q: 模型怎么更新?
.engine通过 Docker volume 挂载,替换文件 → 重启容器 → 生效。不需要重建镜像。
Q: 双模式的好处?
开发和部署用同一套代码。Mac/Windows 无 GPU 也能跑 mock 模式调试接口、联调前端。生产切
INFER_BACKEND=trt即走真实推理。切换只需要一个环境变量。
10.3 最适合展开讲的技术点
如果面试官让你挑一个点深入讲,选 滑窗管线,因为它涉及:
- 为什么要切图(高分辨率 vs 模型输入限制)
- 重叠区设计(边界缺陷问题)
- 坐标还原(窗口坐标 → 全图坐标)
- NMS 去重(IoU 计算,为什么两层)
- 可优化方向(batch 并行、stride_ratio 动态调整)
这个点能体现你对计算机视觉工程落地的完整理解。
十一、Python 常用语法速查
| 语法 | 作用 | 代码位置举例 |
|---|---|---|
@router.post("/detect") |
装饰器,把函数注册到 HTTP 路由 | inference.py:15 |
async def / await |
异步函数,等 IO 时让出 CPU | inference.py:16 |
Depends(func) |
FastAPI 依赖注入,框架自动调用并传参 | inference.py:27 |
@dataclass |
简化版结构体,自动生成 __init__ |
interface.py:11-22 |
ABC + @abstractmethod |
抽象基类定义接口,子类必须实现 | interface.py:25-51 |
BaseModel (Pydantic) |
数据模型,自动校验 + JSON 序列化 | schemas/inference.py:8 |
asyncio.Lock() |
异步互斥锁 | inference_service.py:29 |
asyncio.to_thread(func, *args) |
同步函数丢线程池 | inference_service.py:76 |
from __future__ import annotations |
允许前向引用未定义的类型注解 | 几乎所有文件 |
Field(default=...) |
Pydantic 字段默认值 + 校验 | config.py:20-33 |
@asynccontextmanager |
异步上下文管理器(startup/shutdown) | main.py:38 |