从零复现: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 开发机需要安装
bash
pip install ultralytics # YOLO模型加载和导出
pip install onnx onnxsim # ONNX模型处理
pip install onnxruntime # ONNX验证推理
pip install paramiko scp # SSH远程操控Jetson
2.3 Jetson 上的 conda 环境
bash
# 创建专用环境
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)
在动手之前,先搞清楚模型的"底细",避免后面出现莫名其妙的维度错误。
python
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)
python
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 包含了 LayerNorm、GroupNorm 等新算子,对 YOLO 系列模型覆盖最全。
六、验证 ONNX(Step 03)
传到Jetson之前,先在Windows本地验证ONNX是否正确,避免"传了个坏文件"这种低级失误。
python
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 是否满足部署条件:
python
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 文件并验证完整性:
python
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 会报命令找不到。
bash
# ❌ 错误写法
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:
bash
/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 内部自动完成的工作:
- 权重量化:将 float32 权重转换为 float16,内存减半
- Kernel 搜索:为每一层寻找支持 Tensor Core 的最优 CUDA kernel(这是构建慢的原因)
- 自动回退:对数值敏感的层(如 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 核心代码:自定义校准器
python
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引擎:
python
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引擎在实际测试中检测框很少甚至没有,原因分析:
- 校准数据不足:50张远低于TRT推荐的500+张
- 数据分布偏差:校准图和实际推理图的缺陷分布可能不完全一致
- 量化误差累积:4.07% 的相对误差对置信度较低的小目标影响更大
结论:本项目最终选用 FP16 引擎进行部署,精度损失仅 0.202%,速度提升约 2×。
十二、三路精度对比(Step 08)
用相同的随机输入让三个引擎分别推理,以 FP32 输出为基准计算误差:
python
# 三个引擎分别推理同一个 (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 有重大变化,网上大量教程用的是旧版本写法。
python
# ❌ 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)
python
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 上运行上传脚本:
bash
cd steps_v2/step09_inference_demo
python run.py
第二步:SSH 到 Jetson 启动推理:
bash
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
bash
# 错误:直接双击或用系统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
bash
# 查找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++ 版本高于系统自带版本
解决方案:
bash
# 安装高版本 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 几乎为零
原因分析:
- 校准图片太少(50张 vs 建议500张+)
- 校准图和测试图数据分布有差异
- INT8 量化误差(4.07%)对小目标检测影响较大
解决方案:改用 FP16(精度损失 0.202%,速度约提升 2×,完全可接受)
坑5:MJPEG 流显示黑屏
原因:摄像头设备号不对
bash
# 先查看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 上运行):
bash
# 第一步:解剖模型
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/ 查看效果。
十七、参考资料
- TensorRT 10.x Developer Guide
- JetPack 6 Release Notes
- Ultralytics YOLO ONNX Export Docs
- TensorRT Python API: IInt8EntropyCalibrator2
- pycuda Documentation
如果这篇文章对你有帮助,欢迎点赞收藏~有问题可以在评论区留言,我会尽快回复。
完整代码见 GitHub(马上开源)。