从零复现:YOLO缺陷检测模型 TensorRT 全量化部署到 Jetson Orin Nano Super(FP32/FP16/INT8 三路对比)

文章定位:零基础可跟做的完整部署教程,手把手讲清楚每一个"坑"和"为什么"。
关键词:TensorRT量化 / Jetson部署 / YOLO / INT8校准 / FP16推理 / MJPEG推流


一、背景与目标

做模型部署最头疼的不是代码,而是"跑不起来的时候不知道为什么"。这篇文章记录了我把一个工业表面缺陷检测YOLO模型,从Windows上训练好的 .pt 文件,一步步部署到 Jetson Orin Nano Super 上,实现 USB 摄像头实时推理 + 浏览器 MJPEG 直播的完整过程。

目标任务:检测电子元件表面的四类缺陷——

类别 含义
ZF-scratch 锌层划伤
scratch 普通划痕
broken 断裂缺陷
pinbreak 引脚折断

最终效果:FP16引擎在 Jetson Orin Nano Super 上实现实时推理,通过浏览器远程查看带标注框的视频流。


二、硬件与软件环境

2.1 我的设备配置

设备 规格
边缘计算板 NVIDIA Jetson Orin Nano Super (67 TOPS)
JetPack 6.2.1
CUDA 12.6
TensorRT 10.3.0
开发主机 Windows 10
USB摄像头 普通 UVC 免驱摄像头

2.2 Windows 开发机需要安装

pip install ultralytics      # YOLO模型加载和导出
pip install onnx onnxsim     # ONNX模型处理
pip install onnxruntime      # ONNX验证推理
pip install paramiko scp     # SSH远程操控Jetson

2.3 Jetson 上的 conda 环境

# 创建专用环境
conda create -n yolo_trt python=3.10 -y
conda activate yolo_trt

# 安装核心依赖
pip install pycuda

# ⚠️ 关键修复:解决 GLIBCXX 版本冲突(后面会讲为什么)
conda install libstdcxx-ng -c conda-forge -y

三、项目结构

整个项目按流水线方式组织,每个步骤独立一个文件夹,互不干扰,方便调试:

Quantification/
├── best.pt             # 原始PyTorch模型(38.7MB,20.06M参数)
├── best.onnx           # 导出的ONNX模型(76.66MB,opset17)
└── steps_v2/
    ├── step01_model_inspect/     # 第一步:解剖模型
    ├── step02_onnx_export/       # 第二步:导出ONNX
    ├── step03_onnx_inspect/      # 第三步:验证ONNX
    ├── step04_jetson_env_check/  # 第四步:检查Jetson环境
    ├── step05_transfer/          # 第五步:传输文件
    ├── step06_trt_fp32/          # 第六步:构建FP32引擎
    ├── step07_trt_fp16/          # 第七步:构建FP16引擎
    ├── step_int8_calib/          # 第八步:INT8校准
    │   └── calib_images/         # 50张校准图片
    ├── step08_accuracy_compare/  # 第九步:三路精度对比
    └── step09_inference_demo/    # 第十步:实时推理演示
        ├── infer_v2.py           # 在Jetson上运行的推理脚本
        └── run.py                # 在Windows上运行的上传脚本

统一规则:每个 run.py 都在 Windows 上运行,自动 SSH 进 Jetson 完成操作,实时打印日志。


四、模型信息确认(Step 01)

在动手之前,先搞清楚模型的"底细",避免后面出现莫名其妙的维度错误。

from ultralytics import YOLO
model = YOLO("best.pt")
print(model.task)         # detect(检测任务,非分割/分类)
print(model.names)        # {0:'ZF-scratch', 1:'scratch', 2:'broken', 3:'pinbreak'}
print(model.info())       # 模型参数量

关键结果

任务类型  : detect
类别数量  : 4
输入尺寸  : 640 × 640
输出形状  : (1, 8, 8400)
参数量    : 20.06 M
模型大小  : 38.7 MB

输出 (1, 8, 8400) 怎么理解?

1    = batch_size
8    = 4 (cx, cy, w, h) + 4 (每个类别的置信度)
8400 = 8400个候选框(特征图上的锚点数)

后处理时,先按置信度过滤,再做NMS(非极大值抑制),才能得到最终检测框。


五、导出 ONNX(Step 02)

from ultralytics import YOLO
model = YOLO("best.pt")
model.export(
    format="onnx",
    imgsz=640,
    opset=17,       # TensorRT 10.x 兼容,推荐 ≥ 13
    simplify=True,  # 用 onnxsim 精简计算图,加速TRT构建
    dynamic=False,  # 固定batch=1,TRT优化效果更好
)

导出后文件为 best.onnx(76.66MB),同时记录 MD5 哈希值用于后续传输校验。

为什么opset选17?
TensorRT 10 支持 opset 9-20,opset 17 包含了 LayerNormGroupNorm 等新算子,对 YOLO 系列模型覆盖最全。


六、验证 ONNX(Step 03)

传到Jetson之前,先在Windows本地验证ONNX是否正确,避免"传了个坏文件"这种低级失误。

import onnxruntime as ort
import numpy as np

sess = ort.InferenceSession("best.onnx")
dummy = np.random.randn(1, 3, 640, 640).astype(np.float32)
output = sess.run(None, {"images": dummy})

print(f"输出形状: {output[0].shape}")  # (1, 8, 8400)
print(f"NaN数量: {np.isnan(output[0]).sum()}")   # 应为 0
print(f"Inf数量: {np.isinf(output[0]).sum()}")   # 应为 0

全部通过后再进行下一步。


七、检查 Jetson 环境(Step 04)

通过 SSH 远程检查 Jetson 是否满足部署条件:

import paramiko

ssh = paramiko.SSHClient()
ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
ssh.connect("192.168.0.166", username="nvidia", password="nvidia")

# 检查TensorRT版本
_, out, _ = ssh.exec_command("python -c \"import tensorrt; print(tensorrt.__version__)\"")
print(out.read().decode())  # 应输出 10.3.x

检查结果汇总

组件 状态 备注
TensorRT ✓ OK 10.3.0
pycuda ✓ OK 修复GLIBCXX后
torch+CUDA ✓ OK
cv2 ✓ OK
onnxruntime ✗ FAIL 不影响TRT流程,可忽略

同时自动创建部署目录结构:

/home/nvidia/yolo_deploy/
├── models/      # 存放引擎文件
├── scripts/     # 存放推理脚本
├── logs/        # 存放日志
└── calib_data/  # 存放校准图片

八、传输文件到 Jetson(Step 05)

使用 SCP 传输 ONNX 文件并验证完整性:

from scp import SCPClient

def progress(filename, size, sent):
    pct = int(sent / size * 100)
    print(f"\r  上传进度: {pct}%", end="", flush=True)

with SCPClient(ssh.get_transport(), progress=progress) as scp:
    scp.put("best.onnx", "/home/nvidia/yolo_deploy/models/best.onnx")

# 双端MD5校验
local_md5  = hashlib.md5(open("best.onnx","rb").read()).hexdigest()
remote_md5 = run_ssh(ssh, "md5sum /home/nvidia/yolo_deploy/models/best.onnx")[0].split()[0]
assert local_md5 == remote_md5, "文件传输损坏!"
print(f"MD5校验通过: {local_md5} ✓")

九、构建 TRT FP32 基准引擎(Step 06)

踩坑预警1:trtexec 不在 PATH 里!

JetPack 6 上 trtexec 的实际路径是 /usr/src/tensorrt/bin/trtexec,直接输入 trtexec 会报命令找不到。

# ❌ 错误写法
trtexec --onnx=best.onnx ...

# ✅ 正确写法(必须用完整路径并设置 LD_LIBRARY_PATH)
export LD_LIBRARY_PATH=/usr/src/tensorrt/lib:/usr/local/cuda-12.6/lib64:$LD_LIBRARY_PATH
/usr/src/tensorrt/bin/trtexec \
    --onnx=/home/nvidia/yolo_deploy/models/best.onnx \
    --saveEngine=/home/nvidia/yolo_deploy/models/best_fp32.engine \
    --memPoolSize=workspace:6G \
    --skipInference

构建耗时约 7 分钟,生成的 best_fp32.engine 约 100MB。


十、构建 TRT FP16 引擎(Step 07)

FP16 量化只需在 FP32 命令基础上加一个参数 --fp16

/usr/src/tensorrt/bin/trtexec \
    --onnx=/home/nvidia/yolo_deploy/models/best.onnx \
    --saveEngine=/home/nvidia/yolo_deploy/models/best_fp16.engine \
    --fp16 \          # ← 就这一个参数的区别
    --memPoolSize=workspace:6G \
    --skipInference

TRT 内部自动完成的工作

  1. 权重量化:将 float32 权重转换为 float16,内存减半
  2. Kernel 搜索:为每一层寻找支持 Tensor Core 的最优 CUDA kernel(这是构建慢的原因)
  3. 自动回退:对数值敏感的层(如 Softmax),自动保留 FP32 精度

构建耗时约 16 分钟,生成的 best_fp16.engine 约 50MB(比FP32小了一半)。


十一、INT8 量化校准(Step INT8)

11.1 什么是 INT8 量化

INT8 量化将神经网络的权重和激活值从 32-bit 浮点数压缩为 8-bit 整数。

FP32: 每个数值占 4 字节,范围 ±3.4×10³⁸
INT8: 每个数值占 1 字节,范围 -128 ~ 127
压缩比: 4×

关键问题:如何把浮点范围映射到整数范围?这就需要校准(Calibration)

11.2 为什么需要校准图片

校准的本质是:用真实数据统计每一层激活值的分布范围,再确定最优的缩放因子(scale factor),使量化误差最小。

  • 校准图片越多、越有代表性 → INT8精度越高
  • 本项目用了 50 张真实缺陷图,是较低限(生产环境建议 500 张以上)

11.3 核心代码:自定义校准器

import tensorrt as trt
import pycuda.driver as cuda
import numpy as np

class YOLOCalibrator(trt.IInt8EntropyCalibrator2):
    def __init__(self, calib_images, cache_file):
        super().__init__()
        self.imgs = calib_images
        self.idx  = 0
        self.cache_file = cache_file
        # 在GPU上预分配输入内存
        self.d_input = cuda.mem_alloc(1 * 3 * 640 * 640 * 4)

    def get_batch_size(self):
        return 1

    def get_batch(self, names):
        if self.idx >= len(self.imgs):
            return None  # 校准结束

        # 图像预处理:letterbox缩放 → RGB → /255 → NCHW float32
        img  = cv2.imread(str(self.imgs[self.idx]))
        blob = preprocess(img)  # shape: (1, 3, 640, 640)
        cuda.memcpy_htod(self.d_input, np.ascontiguousarray(blob))
        self.idx += 1
        print(f"校准进度: {self.idx}/{len(self.imgs)}")
        return [int(self.d_input)]

    def read_calibration_cache(self):
        if Path(self.cache_file).exists():
            return Path(self.cache_file).read_bytes()

    def write_calibration_cache(self, cache):
        Path(self.cache_file).write_bytes(cache)
        print(f"校准缓存已保存: {self.cache_file}")

构建INT8引擎:

builder = trt.Builder(logger)
config  = builder.create_builder_config()
config.set_memory_pool_limit(trt.MemoryPoolType.WORKSPACE, 6 * 1024**3)

# 开启INT8模式
config.set_flag(trt.BuilderFlag.INT8)
calibrator = YOLOCalibrator(calib_images, "calib_cache.bin")
config.int8_calibrator = calibrator

# 构建引擎(约16分钟)
engine = builder.build_serialized_network(network, config)

11.4 INT8 的局限性

由于本项目校准图片只有50张,INT8引擎在实际测试中检测框很少甚至没有,原因分析:

  1. 校准数据不足:50张远低于TRT推荐的500+张
  2. 数据分布偏差:校准图和实际推理图的缺陷分布可能不完全一致
  3. 量化误差累积:4.07% 的相对误差对置信度较低的小目标影响更大

结论:本项目最终选用 FP16 引擎进行部署,精度损失仅 0.202%,速度提升约 2×。


十二、三路精度对比(Step 08)

用相同的随机输入让三个引擎分别推理,以 FP32 输出为基准计算误差:

# 三个引擎分别推理同一个 (1,3,640,640) 随机张量
fp32_out = run_engine(fp32_engine, test_input)
fp16_out = run_engine(fp16_engine, test_input)
int8_out = run_engine(int8_engine, test_input)

# 计算误差
def report_error(ref, pred, name):
    diff = np.abs(ref - pred)
    print(f"{name}:")
    print(f"  最大绝对误差: {diff.max():.4f}")
    print(f"  平均绝对误差: {diff.mean():.4f}")
    print(f"  平均相对误差: {diff.mean() / (np.abs(ref).mean() + 1e-6) * 100:.3f}%")

对比结果

量化方式 最大绝对误差 平均绝对误差 平均相对误差 推荐场景
FP32(基准) 0 0 0% 精度要求极高
FP16 1.086 0.048 0.202% 绝大多数场景首选
INT8 74.08 2.45 4.07% 需要最高速度 + 足量校准数据

十三、实时推理部署(Step 09)

13.1 部署架构

整体数据流:

USB摄像头
    │  cv2.VideoCapture(0)
    ▼
图像预处理
    │  letterbox(640×640) → BGR转RGB → /255 → NCHW float32
    ▼
TRT FP16推理
    │  CUDA memcpy H→D → execute_async_v3 → CUDA memcpy D→H
    ▼
后处理
    │  置信度过滤(0.25) → NMS(IoU=0.45) → 坐标反缩放
    ▼
MJPEG HTTP服务器(:8080)
    │  multipart/x-mixed-replace
    ▼
Windows浏览器  http://192.168.0.166:8080/

13.2 TRT 10.x 推理代码

注意:TensorRT 10.x 的推理 API 与 8.x 有重大变化,网上大量教程用的是旧版本写法。

# ❌ TRT 8.x 旧写法(不适用于 TRT 10.x)
context.execute_async_v2(bindings=[int(d_input), int(d_output)], stream_handle=stream.handle)

# ✅ TRT 10.x 正确写法
# 第一步:获取所有张量名称
names = [engine.get_tensor_name(i) for i in range(engine.num_io_tensors)]

# 第二步:按名称绑定内存地址
bufs = {n: cuda.mem_alloc(int(np.prod(engine.get_tensor_shape(n))) * 4) for n in names}
for n, b in bufs.items():
    ctx.set_tensor_address(n, int(b))

# 第三步:异步执行(v3 版本)
cuda.memcpy_htod_async(bufs["images"], input_np, stream)
ctx.execute_async_v3(stream.handle)
cuda.memcpy_dtoh_async(output_np, bufs["output0"], stream)
stream.synchronize()

13.3 MJPEG 推流服务器(纯 Python,不需要 Flask)

from http.server import BaseHTTPRequestHandler, HTTPServer
import threading

_stream_frame = None  # 全局共享帧(线程间通信)
_stream_lock  = threading.Lock()

class MJPEGHandler(BaseHTTPRequestHandler):
    def do_GET(self):
        if self.path == "/":
            # 返回包含图像刷新的HTML页面
            html = b"""<html><body style='background:#000'>
                <img src='/stream' style='width:100%;max-width:800px'>
                </body></html>"""
            self.send_response(200)
            self.send_header("Content-Type", "text/html")
            self.end_headers()
            self.wfile.write(html)

        elif self.path == "/stream":
            # 返回MJPEG流
            self.send_response(200)
            self.send_header("Content-Type", "multipart/x-mixed-replace; boundary=frame")
            self.end_headers()
            while True:
                with _stream_lock:
                    frame = _stream_frame
                if frame is not None:
                    _, jpg = cv2.imencode(".jpg", frame, [cv2.IMWRITE_JPEG_QUALITY, 85])
                    self.wfile.write(b"--frame\r\n")
                    self.wfile.write(b"Content-Type: image/jpeg\r\n\r\n")
                    self.wfile.write(jpg.tobytes())
                    self.wfile.write(b"\r\n")
                time.sleep(0.04)  # 控制推流帧率 ≈ 25fps

13.4 启动实时推理

第一步:在 Windows 上运行上传脚本:

cd steps_v2/step09_inference_demo
python run.py

第二步:SSH 到 Jetson 启动推理:

ssh nvidia@192.168.0.166

source ~/miniforge3/etc/profile.d/conda.sh
conda activate yolo_trt
export LD_LIBRARY_PATH=~/miniforge3/envs/yolo_trt/lib:$LD_LIBRARY_PATH
cd /home/nvidia/yolo_deploy
python scripts/infer_v2.py --source 0 --stream --conf 0.25

第三步:Windows 浏览器打开:

http://192.168.0.166:8080/

就能看到实时带检测框的画面了!


十四、踩坑全记录

坑1:paramiko 没有安装

现象ModuleNotFoundError: No module named 'paramiko'

原因:用的是系统 Python 而不是 conda 环境里的 Python

# 错误:直接双击或用系统Python运行
C:\Users\xxx\Python38\python.exe run.py

# 正确:先激活conda环境
conda activate your_env
python run.py

坑2:trtexec 找不到

现象bash: line 1: trtexec: command not found

原因:JetPack 6 上 trtexec 没有加入 PATH

# 查找trtexec实际位置
find /usr -name trtexec 2>/dev/null
# 输出:/usr/src/tensorrt/bin/trtexec

# 使用时必须:
# 1. 用完整路径
# 2. 设置对应的 LD_LIBRARY_PATH
export LD_LIBRARY_PATH=/usr/src/tensorrt/lib:/usr/local/cuda-12.6/lib64:$LD_LIBRARY_PATH
/usr/src/tensorrt/bin/trtexec --onnx=... --saveEngine=...

坑3:GLIBCXX 版本缺失

现象

ImportError: /lib/aarch64-linux-gnu/libstdc++.so.6:
    version 'GLIBCXX_3.4.32' not found (required by pycuda)

原因:pycuda 编译时链接的 libstdc++ 版本高于系统自带版本

解决方案

# 安装高版本 libstdc++(在yolo_trt环境内)
conda activate yolo_trt
conda install libstdcxx-ng -c conda-forge -y

# 关键:每次运行前必须把conda的lib路径放在最前面
export LD_LIBRARY_PATH=~/miniforge3/envs/yolo_trt/lib:$LD_LIBRARY_PATH

⚠️ 注意:这个 export 必须在每个运行 pycuda 的命令之前执行,否则还会报同样的错误。

坑4:INT8 引擎无检测框

现象:FP32/FP16 都有检测结果,INT8 几乎为零

原因分析

  1. 校准图片太少(50张 vs 建议500张+)
  2. 校准图和测试图数据分布有差异
  3. INT8 量化误差(4.07%)对小目标检测影响较大

解决方案:改用 FP16(精度损失 0.202%,速度约提升 2×,完全可接受)

坑5:MJPEG 流显示黑屏

原因:摄像头设备号不对

# 先查看Jetson上的视频设备
ls /dev/video*
# 可能是 /dev/video0, /dev/video1 等

# 指定对应设备号
python infer_v2.py --source 1 --stream  # 改成实际的设备号

十五、量化效果总结

指标 FP32 FP16 INT8
引擎大小 ~100 MB ~50 MB ~30 MB
构建时间 ~7 min ~16 min ~16 min
典型延迟 ~15-20 ms ~8-12 ms ~5-8 ms
理论 FPS ~50-65 ~80-120 ~125-200
精度损失 基准 ~0.2% ~4%(依赖校准质量)
是否需要校准数据 是(建议500+张)

选型建议

  • 日常生产部署 → 选 FP16:精度几乎无损,速度翻倍,无需校准数据
  • 追求极限性能 → 选 INT8:前提是有足量高质量的校准图片
  • 精度基准验证 → 选 FP32:作为参考标准

十六、完整运行流程

按顺序执行以下命令(全部在 Windows 上运行):

# 第一步:解剖模型
cd steps_v2/step01_model_inspect && python run.py

# 第二步:导出ONNX
cd ../step02_onnx_export && python run.py

# 第三步:验证ONNX
cd ../step03_onnx_inspect && python run.py

# 第四步:检查Jetson环境
cd ../step04_jetson_env_check && python run.py

# 第五步:传输文件
cd ../step05_transfer && python run.py

# 第六步:构建FP32引擎(约7分钟)
cd ../step06_trt_fp32 && python run.py

# 第七步:构建FP16引擎(约16分钟)
cd ../step07_trt_fp16 && python run.py

# 第八步(可选):INT8校准(约16分钟,需校准图片)
cd ../step_int8_calib && python run.py

# 第九步:精度对比
cd ../step08_accuracy_compare && python run.py

# 第十步:上传推理脚本
cd ../step09_inference_demo && python run.py

然后 SSH 到 Jetson 启动实时推理,浏览器打开 http://192.168.0.166:8080/ 查看效果。


十七、参考资料


如果这篇文章对你有帮助,欢迎点赞收藏~有问题可以在评论区留言,我会尽快回复。
完整代码见 GitHub(马上开源)。

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐