边缘计算 + 机器视觉:在 Jetson/瑞芯微上部署检测模型
一、为什么要把模型塞进巴掌大的板子里?
想象一条流水线:传送带上的零件以每分钟 120 件的速度飞驰而过,每个零件需要做外观缺陷检测。如果每张图片都上传到云端推理——哪怕只增加 200ms 的网络延迟,整条线就得降速 30%。更糟的是,工厂内网可能根本没有公网出口。
这不是虚构的场景。去年我在一个 3C 电子代工厂做项目时,客户的原方案就是把图片传到阿里云 GPU 实例上跑 YOLO,结果产线一断网就全线停工。后来我们把检测模型部署到了产线旁的 Jetson Orin Nano 上,推理延迟从 500ms 降到 18ms,而且还省掉了每月上万块的 GPU 云服务器费用。
边缘计算 + 机器视觉的核心价值就三点:低延迟(毫秒级响应)、离线可用(不依赖网络)、成本可控(硬件是一次性投入)。但好处背后是严峻的工程挑战——算力只有服务器的零头,内存以 GB 计,功耗被严格限制。本文就从实战角度,带你探索如何在 Jetson 和瑞芯微平台上高效部署工业视觉检测模型。
工业场景的典型需求拆解
在深入技术细节之前,先把工业检测的真实需求说清楚,因为很多做算法出身的同学容易忽略工程约束:
硬件层面:工业相机不是 USB 摄像头。你需要考虑 GigE Vision 或 USB3 Vision 接口、全局快门(避免运动拖影)、镜头畸变校正。光源才是真正的灵魂——环形光、同轴光、背光、偏振光,每种光源和打光角度直接影响缺陷的对比度。一个划痕在环形光下可能完全看不见,换同轴光后清晰无比。我们曾在项目中因为光源角度差了 5 度,导致检测率从 96% 跌到 72%。这意味着你的推理 pipeline 输入端要做光源同步(strobe control):相机触发信号→ 光源点亮→ 曝光→ 图像采集→ 推理,整个时序必须精确到微秒级。
软件层面:工业软件不是跑一次就完事的 Jupyter Notebook。你需要处理相机掉线自动重连、镜头遮挡检测、照明衰减补偿、日/夜班光照变化自适应。推理结果要对接 PLC(可编程逻辑控制器),用 Modbus TCP 或数字 IO 信号触发剔除气缸。这意味着你的推理服务必须具备确定的延迟上限(比如 50ms 内必须返回结果),超时就触发安全停机——不能因为模型卡住导致缺陷品流入下一道工序。
数据层面:工业缺陷数据的最大特点是极度不平衡。良品占 99.9%,缺陷品千分之一都不到。而且缺陷种类繁多且层出不穷——今天缺胶,明天多胶,后天可能是新的模具磨损纹路。这就要求模型具备开集识别能力,而不是闭集分类。同时,采集环境恶劣(震动、粉尘、油污、温度变化),对模型鲁棒性提出极高要求。
这些约束意味着:边缘推理不是「把云端的模型搬过来跑」那么简单,而是一场从硬件选型、光学设计、数据策略到推理引擎优化的全栈工程实践。
二、边缘推理引擎大比拼:TensorRT vs RKNN vs ONNX Runtime
边缘设备上的推理引擎,本质上是把训练好的模型「翻译」成芯片能高效执行的指令。不同芯片有不同的指令集和加速器,这决定了你需要不同的工具链。
2.1 NVIDIA Jetson 生态:TensorRT
Jetson 平台(Orin、Xavier、Nano)的核心优势是CUDA + TensorRT组合。TensorRT 不是简单的模型格式转换器,它是一个完整的推理优化编译器,会做几件关键的事:
- 层融合(Layer Fusion):把 Conv + BN + ReLU 三个算子合并成一个 CBR kernel,减少显存读写。以 YOLOv8n 为例,融合后计算 kernel 从 168 个降到 72 个。
- 精度校准(INT8 Calibration):用少量真实数据跑一遍推理,统计每一层的激活值分布,然后确定量化 scale。TensorRT 的 INT8 量化通常只损失 0.5-1.5 个 mAP 点,但推理速度提升 2-3 倍。
- 显存优化:自动分析计算图,复用中间 tensor 的显存空间,YOLOv8s 在 TensorRT 下显存占用能从 1.2GB 降到 400MB。
# TensorRT 模型构建核心流程(Python API)importtensorrtastrtdefbuild_engine(onnx_path,engine_path,fp16=True):logger=trt.Logger(trt.Logger.WARNING)builder=trt.Builder(logger)network=builder.create_network(1<<int(trt.NetworkDefinitionCreationFlag.EXPLICIT_BATCH))# 解析 ONNXparser=trt.OnnxParser(network,logger)withopen(onnx_path,'rb')asf:parser.parse(f.read())config=builder.create_builder_config()config.set_memory_pool_limit(trt.MemoryPoolType.WORKSPACE,2<<30)# 2GBiffp16:config.set_flag(trt.BuilderFlag.FP16)# 序列化引擎engine=builder.build_serialized_network(network,config)withopen(engine_path,'wb')asf:f.write(engine)print(f"Engine built:{engine_path}")2.2 瑞芯微生态:RKNN
瑞芯微(Rockchip)的 RK3588/RK3568 是国产边缘 AI 芯片的主力,NPU 算力 6 TOPS(INT8)。它的工具链是RKNN-Toolkit2,核心思路和 TensorRT 类似但细节差异很大:
- 算子支持比 TensorRT 少很多,YOLOv8 的某些激活函数可能需要手动替换为 ReLU
- 量化是强制 INT8(NPU 不支持浮点推理),但 CPU 上可以跑 FP16 作为对比
- 模型转换需要混合量化:检测头部分用 INT8 对精度影响大,可以 retrain 或做 QAT
ONNX → RKNN-Toolkit2 → .rknn 模型 ├─ 混合量化配置 ├─ 自定义算子注册(必要时) └─ 精度对比(与 ONNX FP32 baseline 对比余弦相似度)2.3 关键决策:怎么选?
| 维度 | Jetson Orin Nano | RK3588 |
|---|---|---|
| INT8 算力 | ~40 TOPS | 6 TOPS |
| 推理精度 | FP16/INT8 双模式 | 仅 INT8(NPU) |
| 工具链成熟度 | ★★★★★ | ★★★☆☆ |
| 模型生态 | YOLO/MMDetection 直接支持 | 需手动适配 |
| 成本(模组) | $199 | $80-120 |
| 功耗 | 7-15W | 3-8W |
选 Jetson 的场景:模型复杂(如 YOLOv8 + 分割头)、需要 FP16 精度、快速原型验证。
选瑞芯微的场景:成本敏感、大批量部署、模型相对简单(单检测头)、对国产化有要求。
三、实战:YOLOv8 缺陷检测的端到端部署
接下来我们走一遍完整流程:从训练好的 PyTorch 模型,到在 Jetson 上跑 TensorRT 推理。
3.1 导出 ONNX(带动态 batch)
# export_yolo.py — 导出 ONNX,保留动态 batch 维度fromultralyticsimportYOLO model=YOLO("yolov8n.pt")# 或用你自己的训练权重model.export(format="onnx",dynamic=True,# 支持动态 batch sizesimplify=True,# onnx-simplifier 简化图opset=12,# TensorRT 8.x 兼容imgsz=640,)# 输出: yolov8n.onnx3.2 TensorRT 引擎构建
# build_trt.py — 构建 TensorRT 引擎importtensorrtastrtimportpycuda.driverascudaimportpycuda.autoinit TRT_LOGGER=trt.Logger(trt.Logger.WARNING)defbuild_engine(onnx_path,engine_path,fp16=True):builder=trt.Builder(TRT_LOGGER)network=builder.create_network(1<<int(trt.NetworkDefinitionCreationFlag.EXPLICIT_BATCH))parser=trt.OnnxParser(network,TRT_LOGGER)withopen(onnx_path,'rb')asf:ifnotparser.parse(f.read()):foriinrange(parser.num_errors):print(f"Parse error:{parser.get_error(i)}")raiseRuntimeError("ONNX parsing failed")config=builder.create_builder_config()config.set_memory_pool_limit(trt.MemoryPoolType.WORKSPACE,2<<30)iffp16:config.set_flag(trt.BuilderFlag.FP16)print("FP16 mode enabled")# INT8 校准(可选)# config.set_flag(trt.BuilderFlag.INT8)# config.int8_calibrator = MyCalibrator(calib_dataset)serialized_engine=builder.build_serialized_network(network,config)withopen(engine_path,'wb')asf:f.write(serialized_engine)print(f"Engine saved to{engine_path}")if__name__=="__main__":build_engine("yolov8n.onnx",