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
2
3
4
cd backend
source .venv/bin/activate
uvicorn app.main:app --reload --host 0.0.0.0 --port 8282
# 浏览器打开 http://localhost:8282/docs 即可测试

二、架构全貌

2.1 代码分层

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
backend/app/
├── main.py # FastAPI 应用入口 + lifespan 启动钩子
├── config.py # pydantic-settings 读取 .env
├── deps.py # FastAPI Depends 依赖注入
├── api/v1/
│ ├── router.py # 路由拼装 /api/v1 + 子路由
│ ├── inference.py # POST /inference/detect 参数校验
│ ├── prompts.py # GET /prompts 类别列表
│ └── health.py # GET /health 健康检查
├── services/
│ ├── inference_service.py # 业务编排:解码 → 推理 → 格式化
│ └── prompt_service.py # Prompt 字典(CSV → key/prompt/label 映射)
├── core/
│ ├── interface.py # InferenceEngine 抽象基类
│ ├── mock_infer.py # MockEngine:随机假数据,开发用
│ ├── trt_infer.py # TRTEngine:真实 GPU 推理
│ ├── trt_runtime.py # TensorRT 运行时:显存管理 + 前向传播
│ ├── pipeline.py # 滑窗调度 + 结果合并
│ ├── postprocess.py # NMS + 跨窗口合并
│ ├── tokens.py # Prompt token span 解析
│ └── visualizer.py # 检测结果可视化
├── schemas/ # Pydantic 请求/响应模型
└── utils/
├── image_io.py # bytes ↔ ndarray / base64 编解码
├── color.py # 标签 → 颜色映射
└── logging.py # loguru 配置

2.2 启动初始化流程

1
2
3
4
5
6
7
8
9
uvicorn app.main:app

lifespan() 函数(main.py:38-62)
├── PromptCatalog.load() → 从 CSV 加载缺陷类别字典
├── _build_engine() → 根据 INFER_BACKEND 选择引擎
│ ├── "mock" → MockEngine()
│ └── "trt" → TRTEngine(),失败回退 MockEngine
├── engine.warmup() → 预热(TRT 模式下预分配 GPU 显存)
└── 存入 app.state → 后续请求通过 Depends 注入

2.3 双模式设计(策略模式)

1
2
3
4
5
6
7
8
InferenceEngine (ABC 抽象接口)
├── warmup()
├── infer(image, prompts, thresholds...) → InferOutput
└── cleanup()

实现类:
├── MockEngine → 随机生成 0-5 个假框 + 椭圆 mask(开发期)
└── TRTEngine → TensorRT GPU 推理(生产环境)

切换方式:改 .envINFER_BACKEND=mocktrt,零代码改动。


三、一次请求的完整数据旅程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
手机 App
│ POST /api/v1/inference/detect(multipart 表单:图片 + 阈值 + prompt)

┌─ 路由层 inference.py ──────────────────────────────────┐
│ • FastAPI 自动解析 multipart → image bytes + 参数 │
│ • Depends(get_inference_service) → 从 app.state 注入 │
│ • json.loads(selected_prompts) 解析 prompt key 列表 │
│ • 校验:content-type、非空图片、prompt 格式 │
│ • await svc.detect(...) 交给 Service 层 │
└───────────────────────────────────────────────────────┘

┌─ 业务层 inference_service.py ───────────────────────────┐
│ • bytes_to_ndarray() → 解码图片,EXIF 自动旋转 │
│ • downscale_if_needed() → 长边 >4096 自动缩小 │
│ • catalog.selected_prompts(keys) → key 翻译为英文 prompt│
│ • async with self._lock: ← 异步锁,GPU 串行化 │
│ await asyncio.to_thread(self.engine.infer, ...) │
│ • 耗时统计 elapsed_ms │
└───────────────────────────────────────────────────────┘

┌─ 引擎层(mock 模式)────────────────────────────────────┐
│ time.sleep(0.4~1s) 模拟延迟 │
│ 随机生成 0-5 个检测框(位置、大小、置信度均为随机) │
│ 可选:画椭圆 mask 模拟分割 │
│ 返回 InferOutput │
└───────────────────────────────────────────────────────┘

┌─ 业务层 打包响应 ───────────────────────────────────────┐
│ • 每个框补中文标签(label_cn) + 颜色(color) │
│ • mask → pil_to_base64_png() → segmentation.data │
│ • 可选:visualize_results() → rendered_image base64 │
│ • summary = "检测到 N 处缺陷" │
│ • 返回 DetectResponse (Pydantic → FastAPI 自动 JSON) │
└───────────────────────────────────────────────────────┘

关键语法解释

1
2
3
4
5
6
7
8
9
async with self._lock:              # 异步锁 = await acquire(); try; finally release()
t0 = time.time()
out: InferOutput = await asyncio.to_thread(
self.engine.infer, # ← 没有 ()!传的是函数对象,由 to_thread 在新线程里调用
arr, prompts, # 位置参数 1, 2
box_threshold=..., # 以下都是关键字参数
...
)
elapsed_ms = (time.time() - t0) * 1000
语法 含义
out: InferOutput 类型注解(给人/IDE 看,运行时不管)
await 暂停当前协程,等异步操作完成再继续
asyncio.to_thread(func, *args, **kwargs) 在线程池中调用同步函数,不阻塞事件循环
async with self._lock 同一时刻只允许一个请求进入 GPU

四、响应 JSON 结构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
{
"success": true,
"task_id": "uuid",
"image_size": {"width": 800, "height": 600},
"process_time_ms": 456.78,
"detections": [
{
"id": 0,
"bbox": [100.0, 200.0, 300.0, 400.0],
"confidence": 0.85,
"label": "scratch",
"label_cn": "划痕",
"color": "#A855F7"
}
],
"segmentation": {
"format": "png_base64",
"data": "iVBORw0KGgo...",
"threshold": 0.4
},
"rendered_image": "iVBORw0KGgo...",
"summary": "检测到 3 处缺陷",
"model_version": "fsnet_gd_v1.0",
"backend": "trt"
}

两个开关的四组组合

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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
输入:
• 图片(800×800×3)
• 文本 prompt(如 "scratch.black broken line.crack.")

┌──────────────┐ ┌──────────────┐
│ BERT Tokenizer│ │ Swin Backbone│
│ "scratch" │ │ 提取视觉特征 │
│ → token_ids │ │ → features │
└──────┬───────┘ └──────┬───────┘
│ │
└───────┬───────────┘

┌────────────────┐
│ Grounding 融合 │ 文本特征注入视觉特征图
│ (Cross-Attn) │ → 每个位置获得语义信息
└───────┬────────┘

┌───────────────────┐
│ Detection Head │
│ ├── Box regressor │ → N 个候选框
│ ├── Class logits │ → 每个框 × 每个 token 的分数
│ └── Mask head │ → 每个框的分割掩膜
└───────────────────┘

核心优势:不像传统检测模型只认训练时见过的类别,GroundingDINO 通过文本编码器理解任意英文描述,可以检测”训练时没见过但能描述出来”的缺陷类型。

5.3 本项目微调

fsnet_gd 可能是 “Few-Shot Network + GroundingDINO” 的缩写,在工业缺陷数据集上 fine-tune:

  • 输入:800×800 裁剪窗口
  • 输出:边界框(xyxy 格式)+ 置信度 + 分割掩膜
  • Prompt 体系:英文缺陷描述(scratch、crack、bubble 等),用 . 分隔

六、TRT 推理管线(GPU 生产模式)

6.1 完整流程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
原始图片 ndarray (H×W×3)

├── [预处理] pipeline.py:106-115
│ downscale 缩放 → LANCZOS 插值
│ sliding_window_crop → 切 N 个 800×800 方块
│ stride = 800 × stride_ratio(0.9) = 720px
│ 相邻窗口有 80px 重叠区,边缘不足补 0

├── [文本编码, 仅一次] trt_runtime.py:96-143
│ BERT tokenize → input_ids + attention_mask + token_type_ids
│ generate_masks_with_special_tokens → text self-attention masks
│ create_positive_map_from_span → 每个 prompt 对应哪些 token
│ ★ 所有 token 数据 memcpy_host_to_device → GPU 显存
│ ★ 只调一次,N 个窗口复用同一份文本特征

├── [逐窗口推理] pipeline.py:128-155
│ for each crop in crops:
│ a. GroundingDINO Transform: ToTensor + Normalize(均值, 方差)
│ b. _get_grounding_output:
│ • memcpy 图片 → GPU 显存
│ • context.execute_v2() ← ★ TRT 前向传播(GPU kernel 执行)
│ • memcpy 结果 → CPU 内存
│ • sigmoid 转概率 + box_threshold 过滤
│ c. nms_small(IoU=0.5) ← 窗口内去重

├── [跨窗口合并] postprocess.py:76-113
│ 每个框坐标 + 窗口偏移量 → 还原原图坐标
│ nms_large(IoU=0.5) ← 跨窗口去重(按面积排序)
│ clamp 坐标到 [0, w] / [0, h]

└── [可选: 分割合并] pipeline.py:53-64
merge_masks: 逐窗口 mask 拼合到原图尺寸

6.2 为什么要切图

GroundingDINO 的 TensorRT engine 编译时把输入尺寸固定为 800×800。这个约束来自 TRT——动态 shape 的 engine 性能会下降,所以编译时选择固定尺寸以获得最优 kernel 调度。

工业检测的真实图片通常是 4000×3000 甚至更大。如果直接把整张图缩放:

1
2
4000×3000 → resize → 800×600
一条 4px 宽的划痕 → 缩到 0.8px → 消失了

微小缺陷在缩放中会丢失像素,模型根本检测不到。必须用原分辨率切块。

6.3 切图逻辑逐行拆解

切图入口在 pipeline.py:26-50sliding_window_crop

① padding 补齐(pipeline.py:39-44

1
2
3
4
pad_h = (win_h - h % stride) % stride    # 例如 h=2500, stride=720
pad_w = (win_w - w % stride) % stride # 2500 % 720 = 340 → pad_h = 460
image = cv2.copyMakeBorder(image, 0, pad_h, 0, pad_w, ..., value=0)
# 底部 / 右侧补黑边,确保滑动可整除

如果不补:y=2160+800=2960 > 2500 → 最后一个窗口越界。补 460px 后 h=2960,所有 y=0,720,1440,2160 的窗口都恰好落入边界内。

② 滑动循环(pipeline.py:46-49

1
2
3
4
for y in range(0, h - win_h + 1, stride):      # y: 0, 720, 1440, 2160
for x in range(0, w - win_w + 1, stride):
crops.append(image[y:y+win_h, x:x+win_w]) # 800×800 像素块
positions.append((x, y, x+win_w, y+win_h)) # 在全图的位置

stride = 800 × stride_ratio = 720重叠 = 800 - 720 = 80px

③ 切图全景视图

1
2
3
4
5
6
7
8
9
10
11
12
13
14
             w=4000  (pad 后)
├──────────────────────────────────────────┤
│ ┌────┐┌────┐┌────┐┌────┐┌────┐ │ y=0
│ │0,0 ││ ││ ││ ││ │ │
│ └────┘└────┘└────┘└────┘└────┘ │ y=720
│ ┌────┐┌────┐┌────┐┌────┐┌────┐ │
h=2500 │ ││ ││ ││ ││ │ │ y=1440
+pad └────┘└────┘└────┘└────┘└────┘ │
│ ┌────┐┌────┐┌────┐┌────┐┌────┐ │ y=2160
│ │ ││ ││ ││ ││ │ │
│ └────┘└────┘└────┘└────┘└────┘ │
└──────────────────────────────────────────┘

纵向 4 行 × 横向约 5 列 ≈ 20 个窗口

6.4 重叠区设计(为什么不能 stride=800 无重叠)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
没有重叠 (stride=800):
┌────────┐┌────────┐
│ ││ │ 缺陷恰在边界线上 →
│ ▄ ││ │ 每个窗口只看到半个缺陷
│ ││ │ 两个窗口都无法识别 —— 漏检!
└────────┘└────────┘

有重叠 (stride=720, 80px 重叠):
┌────────┐
│ ▄ │──┐
└────────┘ │ 窗口 A 看到完整缺陷 ✓
┌──────┘ 窗口 B 也看到(被 NMS 去重)

└────────┘

80px 重叠意味着:任何横跨边界的缺陷,只要最大宽度 ≤ 80px,就一定会被至少一个窗口完整包含。工业缺陷(划痕通常 2-20px 宽)绰绰有余。

6.5 坐标还原:窗口坐标 → 全图坐标

这段逻辑在 pipeline.py:135-152

模型输出的格式:归一化坐标 [cx, cy, w, h],范围 0~1。

三步还原

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 第一步:记录窗口尺寸
row.append(window_size[1]) # row[6] = 窗口宽 800
row.append(window_size[0]) # row[7] = 窗口高 800

# 第二步:归一化 → 窗口内像素坐标
row[0] *= row[-1] # cx × 800 → 窗口内像素 cx
row[1] *= row[-2] # cy × 800
row[2] *= row[-1] # w × 800
row[3] *= row[-2] # h × 800

# 第三步:cx/cy/w/h 中心宽高 → x1/y1/x2/y2 对角坐标
row[0] -= row[2] / 2 # x1 = cx - w/2
row[1] -= row[3] / 2 # y1 = cy - h/2
row[2] += row[0] # x2 = x1 + w
row[3] += row[1] # y2 = y1 + h

窗口偏移postprocess.py:85-95):在 merge_detections 中加上窗口在全局的位置偏移:

1
2
3
4
5
6
7
8
for detections, (x1, y1, _, _) in zip(all_detections, positions):
for det in detections:
box = [
det[0] + x1, # 窗口内 x1 + 窗口的全局 x 偏移
det[1] + y1, # 窗口内 y1 + 窗口的全局 y 偏移
det[2] + x1, # 窗口内 x2 + 窗口的全局 x 偏移
det[3] + y1, # 窗口内 y2 + 窗口的全局 y 偏移
]

边界 clamppostprocess.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
2
3
4
5
6
7
# 标准 IoU = 交集面积 / 并集面积
iou1 = intersection / union

# 小框保底 IoU = 交集面积 / min(框1面积, 框2面积)
iou2 = intersection / min_area

return max(iou1, iou2) # 取两者最大值

为什么取 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
2
3
4
5
def _join_prompts(prompts):
# 例:["scratch", "crack", "bubble"]
# → "scratch.crack.bubble."
# 不足 60 个 prompt 时用 "?" 补齐:
# → "scratch.crack.bubble.?.?.?...?." (60 个)

补到 60 个是为了固定 token span 长度,避免 TRT engine 的输入 shape 抖动触发 re-build。


七、GPU 显存管理(trt_runtime.py)

7.1 初始化时的预分配 (trt_runtime.py:52-91)

1
2
3
4
5
读取 .engine 文件 → deserialize_cuda_engine

遍历所有 I/O tensor:
├── input tensors → cudaMalloc() 预分配显存
└── output tensors → cudaMalloc() + host_allocation (CPU 侧缓冲区)

所有显存在 __init__ 时一次性分配,推理时直接 memcpy → execute → memcpy不产生任何 malloc/free 开销。这也是为什么只能单 worker —— 多进程各自分配一份显存,很快就会 OOM。

7.2 推理时的数据搬运 (trt_runtime.py:145-150)

1
2
3
4
5
6
def infer(self, img_data):
memcpy_host_to_device(self.inputs[0]["allocation"], img_data) # CPU → GPU
self.context.execute_v2(self.allocations) # TRT 执行
for o in range(len(self.outputs)):
memcpy_device_to_host(self.outputs[o]["host_allocation"], ...) # GPU → CPU
return [o["host_allocation"] for o in self.outputs]

7.3 释放 (trt_runtime.py:191-194)

1
2
3
def cleanup(self):
for each input: cudaFree(allocation)
for each output: cudaFree(allocation)

cleanup() 在 FastAPI lifespanfinally 块里调用(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_tokenizepipeline.py:69),但 caption 在所有窗口不变:

1
2
3
4
5
6
7
8
9
# 当前:每窗口都过 BERT encode(N 次)
def _get_grounding_output(engine, image_tensor, caption, ...):
engine.caption_tokenize(caption) # ← N 次调用
...

# 优化后:移到循环外,只调一次
engine.caption_tokenize(text_prompt) # ← 一次
for crop in crops:
_get_grounding_output(engine, image_tensor, ...) # 去掉 tokenize

收益:省 N-1 次 BERT 编码(单次约 50-100ms),24 窗口图省 1-2 秒。只改 3 行代码。

② 窗口批处理(投入产出比最高)

当前逐窗口串行推理(pipeline.py:128):

1
2
for crop in tqdm(crops, desc="Processing crops"):  # 一个接一个
boxes_filt, ... = _get_grounding_output(...)

改法:将多个窗口拼成 batch,一次 execute_v2 处理 4-8 个窗口:

1
2
3
4
batch_size = 4
for i in range(0, len(crops), batch_size):
batch = torch.stack(crops[i:i+batch_size]) # [4, 3, 800, 800]
outputs = engine.infer(batch) # 一次 GPU 前向

前提:重新编译 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
2
3
4
5
6
7
8
9
10
11
FROM nvcr.io/nvidia/tensorrt:24.04-py3    # NVIDIA 官方 TensorRT 镜像

ENV INFER_BACKEND=trt PORT=8282

COPY requirements.txt requirements-gpu.txt ./
RUN pip install --no-cache-dir -r requirements.txt \
&& pip install --no-cache-dir -r requirements-gpu.txt

COPY . /app
EXPOSE 8282
CMD ["bash", "scripts/start.sh"]

9.2 启动脚本 (scripts/start.sh)

1
2
3
WORKERS=${WORKERS:-1}   # GPU 推理强制 1 worker
# TRT context 非线程安全,多 worker = 多进程各自创建 context = GPU OOM
exec uvicorn app.main:app --host 0.0.0.0 --port 8282 --workers 1

9.3 运行命令

1
2
3
4
5
6
7
8
docker build -t zhengtu-backend .
docker run -d \
--gpus all \
-v /data/models/engines:/app/engines \ # engine 文件挂载
-v /data/models/bert:/wangjun/model_test \ # BERT 分词器挂载
-v /data/csv:/app/data \ # prompt CSV 挂载
-p 8282:8282 \
zhengtu-backend

模型和配置通过 volume 挂载,更新不重建镜像。回滚只需替换旧版 engine 文件并重启容器。

9.4 为什么模型文件不进 Docker 镜像?

  1. .engine 文件通常 500MB-2GB,进镜像会让每次构建极慢
  2. 更新模型不需要重新构建和推送镜像
  3. 同一镜像可以挂载不同版本的 engine 给不同环境(测试/生产)
  4. 回滚只需替换文件重启容器

十、面试要点速查

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