
简介面向边缘端AI部署工程师与嵌入式开发者这份项目实战资源基于NVIDIA Jetson-Nano与TensorRT完整演示闭眼检测算法从模型转换到实时视频流推理的落地过程适用于疲劳驾驶预警、人机交互等需要轻量级实时检测的场景。压缩包共5个文件包含2个TensorRT优化模型、2个Python运行脚本和1份README说明文档整体大小14.85MB结构清晰、可直接对照代码学习。已有157人学习下载。资源涵盖TensorRT模型序列化、图层融合与精度校准等关键处理思路以及Python调用模型、处理视频帧并输出检测结果的实现细节对模型转换和部署过程中的常见问题也给出了说明。通过README可复现从模型加载到摄像头实时检测的完整流程还能了解Jetson-Nano上的性能调优与光照、角度变化等鲁棒性处理要点适合希望从训练走向真实硬件部署的开发者参考。1. 在 Jetson Nano 上用 TensorRT 做闭眼检测部署卡点通常不在模型而在推理路径闭眼检测在算法层面只是一个二分类问题给一只眼睛的 ROI 输出“睁眼 / 闭眼”两类置信度。但放到 Jetson Nano 上摄像头采集、人脸检测、眼睛对齐、时序平滑会同时抢占 CPU 和 GPU留给单次分类的时延预算通常只有 10ms。直接拿 PyTorch 在设备上逐帧推理Python 解释器开销和动态图调度就能把延迟推到 30ms 以上。TensorRT 把网络编译成针对 NVIDIA GPU 的显式引擎算子融合、显存复用全部前移到构建期推理时只剩一次 execute 调用这也是 Jetson 生态里算法部署的标准路径。下面按环境锁定、ONNX 转换、推理实现、性能调优的顺序把整条链路走一遍。2. Jetson Nano 上闭眼检测部署的环境版本选型与检查嵌入式部署和服务器推理的显著区别是环境不能随便升级。Jetson Nano 的 TensorRT 依赖同一套 CUDA、cuDNN 和内核驱动三者由刷机镜像统一打包。在 x86 服务器上可以随意更换 Python 库但在 Nano 上动任何一个底层组件都可能让已经构建好的 engine 失效。闭眼检测这类安全相关任务不追求框架版本新追求的是稳定可复现。2.1 JetPack 4.6.4 的软件栈绑定关系与版本对照在 Jetson 算法部署项目中最省事的选择是 JetPack 4.6.4 镜像它内含一组被大量推理项目共同验证过的组件组件版本部署中的角色L4T / 内核R32.7.x管理 GPU 与摄像头驱动CUDA10.2.89GPU 运行时环境TensorRT8.2.3.0模型推理引擎cuDNN8.2.1卷积等底层算子加速OpenCV4.5.4图像读取、缩放、颜色转换这套组合里TensorRT 8.2 的 ONNX 解析器对 opset 11 支持非常完整。闭眼检测网络最常用的 Conv、BatchNorm、ReLU、MaxPool、Gemm、Softmax 都能直接解析不需要额外写 plugin。如果训练代码里用了torch.einsum或torch.topk这类高级算子导出后会被拆成多节点组合TensorRT 虽能解析但性能会打折这种问题应回到模型层面换等价写法而不是在部署阶段做算子替换。2.2 验证 tensorrt 与 pycuda 运行时是否就绪刷完系统先不要急着跑推理脚本。打开终端做两个快速检查dpkg -l | grep nvinfer python3 -c import tensorrt as trt; print(trt.__version__)第一行确认系统装的是 TensorRT 8.2.x第二行确认 Python 绑定能加载。如果 import 失败常见原因是系统 Python 环境缺少 apt 的 libnvinfer 包sudo apt-get install python3-libnvinfer后面推理时要用 PyCUDA 完成主机内存与设备内存的拷贝安装并验证pip3 install pycuda python3 -c import pycuda.driver as cuda; cuda.init(); print(cuda.Device(0).name())看到NVIDIA Tegra X1说明 CUDA 上下文可以正常创建。这里有一个部署中常见的认知误区不要尝试在 x86 服务器上先把 ONNX 转成 engine 再拷到 Nano。TensorRT engine 与 GPU 架构和 TensorRT 版本强绑定服务器上构建的引擎在 Nano 上无法加载必须在目标设备上重新构建。2.3 固定功耗模式并配置 swap避免频率与内存成为隐性问题Jetson Nano 默认工作在 5W 功耗模式GPU 频率被压低闭眼检测跑在这种模式下TensorRT 优化得再好也会被频率卡住。切换到 Max-N 10W 模式sudo nvpmodel -m 0 nvpmodel -qnvpmodel -q返回 0 表示当前是 Max-N。调试阶段还可以执行sudo jetson_clocks把 CPU/GPU 频率锁到最高用于排除 DVFS 切换带来的延迟抖动。但jetson_clocks不适合常开长时间运行会让温度快速抬升触发 Tegra 热降频所以生产环境只保留 nvpmodel 配置即可。2GB 内存版本的 Nano 经常出现进程被 OOM 杀掉的问题。推理进程本身占用不大但人脸检测模型的权重、输入图像缓存、CUDA context 叠加后容易触顶。常见做法是追加 4GB swapfilesudo fallocate -l 4G /var/swapfile sudo chmod 600 /var/swapfile sudo mkswap /var/swapfile sudo swapon /var/swapfileswap 不是治本方案但它能避免调试阶段进程直接消失为观察内存峰值和调整前处理逻辑留出时间。2.4 用 jtop 记录部署前的性能基线安装 jetson-stats 后执行sudo jtop可以看到 GPU 利用率、CPU 占用、内存压力、温度和电源模式的实时数据。部署前建议记录三个指标开机一段时间后的空载温度、CPU 空闲占用、可用内存大小。后面所有 TensorRT 调优效果都要和这份基线对比否则无法判断帧率提升来自引擎优化还是运行环境变化。3. 闭眼检测模型从 ONNX 到 TensorRT 引擎的转换实现环境就绪后核心流程是把训练好的 PyTorch 权重转换为 TensorRT 可用的 engine。全过程分三段导出 ONNX、简化模型、构建引擎。每一步有一个需要重点注意的技术细节。3.1 导出前先锁定输入 shape别把动态维度带进部署闭眼检测模型通常输入是 48×48 或 64×64 的单通道灰度或三通道 RGB 图像。训练时为了数据并行会保留可变 batch 维度但部署到 Nano 上基本都是 batch1。导出时建议把输入固定为(1,3,48,48)不打开dynamic_axes。动态 shape 会让 TensorRT 在建图时保留多套 profile在 Nano 上性能和显存占用都不占优静态 shape 对嵌入式部署是明确的收益。3.2 用 PyTorch 导出固定形状 ONNX导出脚本的核心代码import torch from eye_net import EyeNet model EyeNet() model.load_state_dict(torch.load(eye_close.pth, map_locationcpu)) model.eval() dummy_input torch.randn(1, 3, 48, 48) torch.onnx.export( model, dummy_input, eye_close.onnx, input_names[input], output_names[logits], opset_version11, dynamic_axesNone, )dynamic_axesNone把 batch 固定为 1opset_version11与 TensorRT 8.2 解析器覆盖范围匹配如果训练框架默认导出更新版本的 opset建议统一改为 11避免 OnnxParser 报不识别。map_locationcpu是嵌入式部署的常见要求避免把 GPU 训练的权重加载过程与当前 CUDA 上下文耦合。导出后可以用 Netron 打开 ONNX 文件确认输入维度。如果输入边写着unk__或?说明动态维度还在需要回到导出步骤重新固定。3.3 用 onnx-simplifier 清掉推理时多余的算子PyTorch 导出的 ONNX 会包含大量常量 Reshape、冗余 Cast、Unsqueeze 节点。TensorRT 能解析这些节点但每多一个节点就多一份融合不充分的隐患。简化操作pip3 install onnx onnx-simplifier python3 -m onnxsim eye_close.onnx eye_close_sim.onnx运行后文件体积通常会变小不少。如果简化器报 shape 推断失败多半是模型里包含动态控制流需要先回到模型层面修复。3.4 在 Nano 本地用 Python API 构建 engine 并序列化构建引擎的最小代码import tensorrt as trt logger trt.Logger(trt.Logger.WARNING) builder trt.Builder(logger) network builder.create_network( 1 int(trt.NetworkDefinitionCreationFlag.EXPLICIT_BATCH) ) parser trt.OnnxParser(network, logger) with open(eye_close_sim.onnx, rb) as f: if not parser.parse(f.read()): for i in range(parser.num_errors): print(parser.get_error(i)) raise RuntimeError(ONNX parse failed) config builder.create_builder_config() config.max_workspace_size 1 30 config.set_flag(trt.BuilderFlag.FP16) engine builder.build_engine(network, config) with open(eye_close_fp16.engine, wb) as f: f.write(engine.serialize())EXPLICIT_BATCH标志让 network 使用显式 batch 模式配合固定输入形状建图。max_workspace_size设为 1GB 表示允许构建器临时使用最多 1GB 设备内存来尝试不同内核实现这个值不是推理时的常驻占用但设太低会降低内核选择质量。FP16 标志是提前打开的一项低成本优化下一章会专门讲。构建过程在 Nano 上通常需要 10~30 秒因此序列化保存 engine 是必须的。否则每启动一次服务就要重新构建一遍摄像头预览一直黑屏卡在初始化。3.5 用 trtexec 快速定位 ONNX 解析失败问题如果解析报错最快的定位工具是系统自带的 trtexec/usr/src/tensorrt/bin/trtexec --onnxeye_close_sim.onnx --saveEnginetest.enginetrtexec 输出的第一屏信息包含网络输入输出张量名和维度报错时通常带着失败层对应的 ONNX 节点名。拿到层名后回 Netron 比对结构先看输入输出 shape 是否出现动态维度如果错误信息是算子 not supported优先调整 opset 或做算子替换不要直接写 TensorRT plugin样机阶段插件调试成本远高于模型改动。4. 基于 TensorRT 的闭眼检测推理循环实现engine 构建完成后的工作分为三块加载引擎、管理输入输出显存、编写逐帧推理循环。4.1 反序列化 engine 并创建执行上下文import tensorrt as trt import numpy as np import pycuda.driver as cuda def load_engine(path): with open(path, rb) as f: runtime trt.Runtime(trt.Logger(trt.Logger.WARNING)) return runtime.deserialize_cuda_engine(f.read()) engine load_engine(eye_close_fp16.engine) context engine.create_execution_context()反序列化走trt.Runtime不经过 Builder消耗的资源比重新构建少一个数量级。create_execution_context()为推理创建独立的执行状态。多个线程同时推理时每个线程要持有一个独立的 context不能共用同一个但同一时刻只有一个线程跑推理的场景保持单 context 能显著降低设备内存占用。4.2 固定分配输入输出显存避免反复 mallocTensorRT 的execute_v2需要传入输入输出张量的设备内存地址。静态 batch 下地址在初始化阶段分配一次即可input_shape (1, 3, 48, 48) output_shape (1, 2) input_size int(np.prod(input_shape)) * np.float32(1).nbytes output_size int(np.prod(output_shape)) * np.float32(1).nbytes d_input cuda.mem_alloc(input_size) d_output cuda.mem_alloc(output_size)输出长度 2 对应睁眼/闭眼两个类别。这里把显存一次性分配好并复用避免每帧推理时反复 malloc 造成内存碎片。Jetson 的显存和系统内存共用物理 DRAM显存碎片多了之后人脸检测部分的临时分配可能失败表现为运行时段时间后突然掉帧。4.3 单帧推理代码从 ROI 到二分类置信度以 48×48 RGB 输入为例完整推理函数import cv2 def infer_eye_state(roi_bgr): # 输入已经是裁剪到 48x48 的眼睛区域BGR 顺序 img cv2.cvtColor(roi_bgr, cv2.COLOR_BGR2RGB) img img.astype(np.float32) / 255.0 img img.transpose(2, 0, 1)[np.newaxis, ...].copy() cuda.memcpy_htod(d_input, img) context.execute_v2(bindings[int(d_input), int(d_output)]) out np.empty(2, dtypenp.float32) cuda.memcpy_dtoh(out, d_output) prob np.exp(out - out.max()) prob prob / prob.sum() return prob[1] # 闭眼概率几个容易出错的细节transpose(2,0,1)把 HWC 转成 CHW随后必须.copy()保证内存连续否则memcpy_htod会报类型错误execute_v2的 bindings 列表顺序必须与构建 engine 时的输入输出索引一致静态网络不会自动重排输出数组是未归一化的 logitssoftmax 在 CPU 端计算对 2 分类任务额外在 GPU 跑一个 Softmax 层收益有限连续多帧状态判断比如“连续 5 帧闭眼才触发报警”应放在这个函数之外不要污染推理路径。4.4 execute_v2 与 execute_async_v2 的适用边界TensorRT 8.2 提供同步和异步两个执行方法。同步的execute_v2阻塞当前线程直到推理完成异步的execute_async_v2需要配合 CUDA stream 使用方法阻塞方式适用场景execute_v2阻塞到推理结束单路视频流、报警逻辑简单execute_async_v2提交到 stream 后立即返回多路摄像头、队列调度复杂闭眼检测单模型推理只有 2ms 量级同步版本占用的 CPU 等待时间很短。如果同一台 Nano 还要跑人脸检测两个模型建议各建一个 context串行调用而不是共用 context 交叉执行。交叉执行会引入额外的同步开销在 Nano 上实测反而比串行更不稳定。5. Jetson Nano 上闭眼检测的 TensorRT 性能调优与系统裁剪推理循环跑通后进入性能调优阶段。这一章按 FP16、INT8、系统裁剪三个层次推进每步都在前一版本基础上做增量改动。5.1 先拿 FP32 引擎建立基线再开启 FP16第一次构建 engine 时不要加任何精度 flag默认 FP32 版本用来验证正确性最合适同一张 ROI 输入PyTorch 和 TensorRT 输出的闭眼概率应非常接近。确认逻辑无误后再开启 FP16config.set_flag(trt.BuilderFlag.FP16)FP16 对闭眼检测这种二分类任务影响很小logits 数值通常落在 -2 到 2 之间FP16 的表示精度足够。在 48×48 轻量网络上FP16 相比 FP32 通常能减少 40%~50% 延迟而预测标签几乎不变化。5.2 INT8 量化校准数据多样性比数量更重要当 FP16 之后帧率仍不够再考虑 INT8。TensorRT INT8 需要用户提供校准数据来统计每层激活值分布确定量化范围。实现方式是实现IInt8EntropyCalibrator2接口import tensorrt as trt class EyeCalibrator(trt.IInt8EntropyCalibrator2): def __init__(self, images, batch_size1): super().__init__() self.images images self.idx 0 self.buf cuda.mem_alloc(1 * 3 * 48 * 48) def get_batch_size(self): return 1 def get_batch(self, names): if self.idx len(self.images): return None img self.images[self.idx].astype(np.float32) / 255.0 img img.transpose(2, 0, 1)[np.newaxis, ...] cuda.memcpy_htod(self.buf, np.ascontiguousarray(img)) self.idx 1 return [int(self.buf)] config.set_flag(trt.BuilderFlag.INT8) config.int8_calibrator EyeCalibrator(calib_images)校准数据建议收集 200~500 张不同人眼、不同光照、不同遮挡程度的 ROI。分布足够接近真实推理场景即可不要直接拿训练集图片做校准。训练集和实际摄像头画面的亮度分布差异较大可能导致量化后闭眼类别错检率上升。INT8 的收益取决于模型。以典型闭眼检测网络实测INT8 比 FP16 进一步降低 30%~50% 延迟但精度可能下降 0.5~1 个百分点。闭眼检测用于疲劳驾驶预警时涉及安全判断业务侧没有精度余量就不要上 INT8FP16 是更稳妥的默认选项。5.3 构建参数与输入分辨率的权衡max_workspace_size越大构建器越有空间尝试更多候选算法。有人为了省内存设成 256MB代价是构建器可能放弃部分高性能 kernel。Nano 平台建议给 1GB原因是在构建阶段临时分配不参与推理时显存占用只要构建时设备内存没耗尽即可。输入分辨率调整必须在训练阶段完成不要直接在部署时把 96×96 的模型输入改成 48×48。没有重新训练就改输入尺寸BN 层的统计量和卷积感受野都会失配精度崩坏会超出 INT8 量化的误差范围。5.4 不同精度档位的性能参考为了量化每一步的收益在同样设备、同样功耗模式下记录三类引擎的实际延迟。以下是一组典型参考区间引擎版本输入分辨率推理延迟相对 FP32FP32 engine48×484.2ms1.0xFP16 engine48×482.1ms2.0xINT8 engine48×481.3ms3.2x测量时用 warm-up 之后的平均值。TensorRT 引擎第一次调用会包含 CUDA context 初始化和内核加载耗时显著高于后续调用拿第一次的数值做对比毫无意义。5.5 系统裁剪与设备树配置对部署的实际影响性能调优不只在 engine 内部。Jetson Nano 默认带完整桌面环境桌面 UI 会持续占用 CPU 与内存挤压人脸检测和视频读取的资源。作为常驻推理盒子建议关闭图形桌面sudo systemctl set-default multi-user.target sudo reboot启动后不再加载图形桌面通常能释放 300~500MB 内存并减少 CPU 频率抖动。这个操作对部署收益非常直接比任何引擎参数调整都更明显。如果盒子还外接 GPIO 报警灯、串口状态输出等设备才需要接触设备树配置。常见做法是在 L4T 设备树源文件中确认 GPIO 节点声明重新编译 dtb并在/boot/extlinux/extlinux.conf中指定新的FDT路径。设备树只解决引脚功能映射运行时的业务逻辑应放在用户态服务里不要在设备树层做判断逻辑。6. 闭眼检测部署后的稳定性验证与现场问题定位部署收尾阶段的重点从“跑得快”转向“跑得久”。闭眼检测盒子在驾驶室或机柜内连续运行最常遇到的不是模型推理错误而是温度升频、内存碎片和服务静默退出。6.1 用长时间运行验证温度与降频边界在正式验收前让系统连续运行至少 10 小时同时记录设备状态tegrastats --interval 1000 /tmp/tegrastats.log运行结束后关注 GPU 频率曲线。如果同一引擎推理延迟从 2ms 逐渐爬升到 3.5ms同时 GPU 频率从 900MHz 掉到 600MHz说明已经接近散热边界。此时先从系统层减负压缩日志、降低后台任务频率给 GPU 留出更多调频空间仍不满足才更换散热方案。6.2 用 trtexec --dumpProfile 定位层级瓶颈整条流水线帧率不达标时需要区分瓶颈在哪一层。trtexec 提供层级耗时分析/usr/src/tensorrt/bin/trtexec --loadEngineeye_close_fp16.engine --dumpProfile输出会列出每个节点的平均耗时和占比。如果发现某个 Transpose 或 Resize 层占比异常高说明输入数据排布与模型预期不一致应回模型源码调整而不是在推理侧反复搬运数据。排在前三的算子占总耗时通常超过 60%这个输出是判断 engine 是否还有优化空间的直接依据。6.3 用 systemd 托管推理服务实现异常自动拉起不推荐自己写 shell 守护脚本直接用 systemd 更可靠。假设入口脚本是/home/nano/eye_deploy/run_server.pyunit 文件[Unit] Descriptioneye close detection inference service Afternetwork.target [Service] Usernano WorkingDirectory/home/nano/eye_deploy ExecStart/usr/bin/python3 run_server.py Restartalways RestartSec5 EnvironmentLD_LIBRARY_PATH/usr/lib/aarch64-linux-gnu [Install] WantedBymulti-user.target保存到/etc/systemd/system/eye-server.service后执行sudo systemctl daemon-reload sudo systemctl enable --now eye-serverRestartalways让进程异常退出后 5 秒自动拉起Environment这一行解决 TensorRT 动态库路径找不到的问题对应第 2 章检查LD_LIBRARY_PATH的逻辑。服务启动后用journalctl -u eye-server -f查看日志能确认每次重启的时间和原因。把第 2 章记录的性能基线、第 5 章生成的精度对照表和本次部署的 engine 文件版本号放在同一个目录。下一次升级模型时直接对比历史 p50/p99 和引擎改动记录帧率掉了多少、换了哪个版本一眼就能定位省下的时间足够多跑两轮回归测试。本文还有配套的精品资源点击获取