
简介基于TensorRT在Jetson Nano上实现闭眼检测的算法部署项目面向算法工程师、嵌入式开发者以及需要在资源受限设备上运行深度学习模型的团队。资源聚焦闭眼检测算法的模型转换与推理优化内容覆盖TensorRT的序列化与图层融合、精度校准、模型加载以及如何结合实时视频流进行人脸检测与人眼关键点识别可应用于疲劳驾驶预警、注意力监测等场景。压缩包共5个文件包含2个trt格式模型文件分别用于人脸检测与关键点提取、2个python脚本负责模型加载与检测逻辑调用以及1个md说明文档文档中提供环境配置与运行说明总大小14.85MB体量紧凑、重点集中。项目中的人脸检测与关键点模型可直接用于视频帧的人眼区域提取再由分类模型判断闭眼状态逻辑清晰。项目目录简单适合快速复现代码。已有157人学习。通过该资源可完整理解深度学习模型在Jetson这类低成本边缘设备上的部署流程掌握TensorRT优化技巧同时获得可直接运行的实验代码借助其中的模型和脚本可在实际项目中对比不同优化选项、进一步扩展功能适合作为课程设计或算法部署实战的参考。README还对模型和依赖环境作了说明能帮助初学者减少搭建环境的时间。1. 用 TensorRT 在 Jetson Nano 上部署闭眼检测先别急着优化模型驾驶疲劳检测、驾驶员监控系统DMS这类场景里闭眼检测通常是第一步。很多人把精力全花在训练精度上却在 Jetson Nano 这样的嵌入式设备上栽了跟头模型在 PC 上跑 30 FPS到了板子上只剩 5 FPSCPU 占用却飙到 80% 以上。问题的根源不是模型太小而是推理引擎用错了。Jetson Nano 的 Maxwell GPU 架构跟 PC 上的 Ampere 卡完全不同PyTorch 的 CPU 推理根本没吃到 GPU 的红利更没触发 TensorRT 的层融合和精度校准能力。TensorRT 之所以成为这类算法嵌入式部署的默认选择是因为它把训练框架里的算子图重新做了一遍规划和内核自动调优。同样的 ResNet 结构TensorRT 在 Jetson 上可能比 PyTorch 直接转出快 3 到 8 倍。这篇文章沿着“闭眼检测模型 → ONNX → TensorRT engine → 板端 C 推理 → 性能调优”这条线讲透不会去碰那些虚的优化理论每一步都给你能直接抄的代码和参数。适合手里有一块 Jetson Nano、想把一个图像分类模型做成实时推理服务的人。2. 闭眼检测模型的选型与 ONNX 导出的硬性要求2.1 为什么闭眼检测用分类模型而不是目标检测闭眼检测在工程上通常拆成两步先人脸检测再对眼睛区域做睁眼/闭眼二分类。人脸检测框已经定位到眼睛大概位置接下来只需要判断这一小块区域是睁开还是闭合所以一个轻量分类网络就够用YOLO 在这里反而浪费算力。常见做法是选用 MobileNetV2 或一个自定义的小型 CNN。MobileNetV2 的深度可分离卷积在 Jetson Nano 上的 CUDA 内核支持度很好TensorRT 对它做层融合的空间也大。自定义 CNN 的好处是参数量可以压到 500 KB 以下但需要自己写好 BN 层折叠逻辑否则导出 ONNX 时容易带出多余的算子。从部署角度看分类网络转 TensorRT 的坑比目标检测少很多。检测网络输出层有锚框解码、NMS 这类动态逻辑TensorRT 不同版本对 NMS 的支持差异很大分类网络输出就是一个 Softmax 或裸 Logits结构干净适合作为第一个 TensorRT 实战项目。2.2 PyTorch 导出 ONNX 的关键参数无论你用什么框架训练最终都要转成 ONNX再交给 TensorRT。PyTorch 导出时需要固定输入尺寸和 batch。闭眼检测的输入图像通常裁剪成 48×48 或 64×64 的灰度图这个尺寸在 Jetson Nano 上非常友好。import torch from models import EyeNet # 假设是你训练的模型 model EyeNet(num_classes2) checkpoint torch.load(eyenet_v1.pth, map_locationcpu) model.load_state_dict(checkpoint[state_dict]) model.eval() dummy_input torch.randn(1, 1, 48, 48) torch.onnx.export( model, dummy_input, eyenet.onnx, input_names[input], output_names[logits], dynamic_axes{input: {0: batch}, logits: {0: batch}}, opset_version11, do_constant_foldingTrue, )逻辑说明input_names和output_names必须在导出时定义清楚后续用trtexec或 C 加载 engine 时按名字绑定 tensor。dynamic_axes里把 batch 维度设成动态这样同一个 engine 可以跑 batch1 也能跑 batch8。TensorRT 对动态 batch 的支持比较成熟但会牺牲一点固定 shape 的优化空间。opset_version11是 Jetson 上 TensorRT 8.x 最稳妥的选择。opset 太高会导致部分算子不兼容太低又缺少一些新算子支持。导出后务必检查 ONNX 模型里有没有Cast、ConstantOfShape这类算子。它们跑在 CPU 上会打断 GPU 推理流水线。用netron打开模型如果发现这两个算子回到 PyTorch 里把这些显式操作换成torch.clamp或直接去掉。2.3 INT8 校准的取舍先跑通 FP16 再谈量化Jetson Nano 的 GPU 对 FP16 有硬件加速而 INT8 需要额外的校准数据集和样本采样。首版部署不要碰 INT8。原因很简单FP16 在 TensorRT 下通常只比 INT8 慢 10%20%但精度几乎无损而 INT8 的校准过程一旦选错样本分布输出 logits 会偏移到所有类别都不置信。如果精度确实不够再上 INT8校准数据要覆盖真实使用场景。闭眼检测的话校准集至少包含 1000 张不同人种、不同光线、不同角度的人眼图像睁眼闭眼比例接近 1:1。把校准数据存成二进制文件然后跑 TensorRT 的校准器生成calibration.cache文件。这个 cache 可以复用不用每次构建 engine 时都重新跑一遍校准。3. Jetson Nano 开发环境配置与 TensorRT engine 构建3.1 开发环境的内存与控制台模式Jetson Nano 有 2GB 和 4GB 两个版本部署前首先确认内存型号。4GB 版本跑完整推理流水线还算从容2GB 版本需要关掉桌面环境以纯命令行模式运行否则 GPU 内存会被桌面合成器抢占。环境配置上Jetson Nano 需要刷 JetPack 4.6 或对应版本的系统镜像。刷完系统后确认 TensorRT 版本dpkg -l | grep TensorRT输出里会显示TensorRT 8.2.x这类版本号。Jetson 平台上的 TensorRT 是 NVIDIA 定制版不能直接从 PC 端拷贝.so库否则会立刻段错误。接着把开发模式的功耗上限放开。Jetson Nano 默认 5W 模式GPU 频率被锁在很低的范围。切换到最大性能模式sudo nvpmodel -m 0 sudo jetson_clocks逻辑说明nvpmodel -m 0把系统切到 10W MAXN 模式GPU 和 CPU 的最高频率放开。jetson_clocks强制所有核心工作在最高频率避免温控策略降频。注意这会让开发板温度升高长时间运行建议加风扇或散热片。实际部署时可以做折中只开nvpmodel -m 0不锁频率让温控策略自动调节。确认 CUDA 环境可用后用内置的trtexec工具快速验证 ONNX 模型可以构建 engine/usr/src/tensorrt/bin/trtexec \ --onnxeyenet.onnx \ --saveEngineeyenet_fp16.engine \ --fp16 \ --minShapesinput:1x1x48x48 \ --optShapesinput:1x1x48x48 \ --maxShapesinput:16x1x48x48参数说明--fp16启动 FP16 精度模式。不加这个参数默认 FP32Jetson Nano 上损失了一半的 GPU 吞吐。--minShapes/--optShapes/--maxShapes配合动态 batch 使用。这里输入是 NCHW 格式batch 从 1 到 16 动态变化最优化 shape 取 1 表示部署时优先保证单帧延迟。--saveEngine把构建好的序列化 engine 保存到文件。构建过程需要跑一遍图优化和内核选择耗时可能长达数分钟生产环境不要每次启动都构建。如果trtexec一次性跑通说明 ONNX 模型里的算子全部映射到了 TensorRT 的 native 层没有触发 fallback。如果出现Node (Unnamed Layer) is not supported这样的报错把模型层数往浅了调或者换成 TensorRT 支持更完整的算子版本。3.2 用 C 加载 engine 并执行推理项目实战推荐 C 而非 Python因为 Jetson Nano 内存紧张Python 解释器和 PyTorch/NumPy 的开销会让整体内存占用多出 300 MB。TensorRT 的 C API 稍微繁琐一点但它对 buffer 管理更精确性能调优空间更大。下面这段完整代码演示了 engine 文件加载、输入输出 buffer 分配、执行推理的核心流程#include fstream #include vector #include cuda_runtime_api.h #include NvInfer.h using namespace nvinfer1; static const int INPUT_H 48; static const int INPUT_W 48; static const int INPUT_C 1; static const int OUTPUT_SIZE 2; class EyeEngine { public: EyeEngine(const std::string engine_path) { std::ifstream file(engine_path, std::ios::binary); std::vectorchar data( (std::istreambuf_iteratorchar(file)), std::istreambuf_iteratorchar()); runtime_ createInferRuntime(gLogger); engine_ runtime_-deserializeCudaEngine(data.data(), data.size()); context_ engine_-createExecutionContext(); } float* forward(const float* input_cpu) { // 分配 GPU 显存 buffer只做一次 if (!input_gpu_) { cudaMalloc(input_gpu_, INPUT_C * INPUT_H * INPUT_W * sizeof(float)); cudaMalloc(output_gpu_, OUTPUT_SIZE * sizeof(float)); } // 固定 shape 下可以用 bindings 直接传 cudaMemcpy(input_gpu_, input_cpu, INPUT_C * INPUT_H * INPUT_W * sizeof(float), cudaMemcpyHostToDevice); void* bindings[] {input_gpu_, output_gpu_}; context_-executeV2(bindings); cudaMemcpy(output_cpu_, output_gpu_, OUTPUT_SIZE * sizeof(float), cudaMemcpyDeviceToHost); return output_cpu_; } private: IRuntime* runtime_; ICudaEngine* engine_; IExecutionContext* context_; float* input_gpu_ nullptr; float* output_gpu_ nullptr; float output_cpu_[OUTPUT_SIZE]; };逻辑说明和参数说明createInferRuntime之后要立刻deserializeCudaEngine。engine 文件在 Jetson 平台上有序列化版本要求TensorRT 8.2 构建的 engine 文件没法在 8.4 上直接加载需要重新构建。executeV2是异步接口但这里没用 stream相当于同步执行简单但不高效。推理线程只有一个时直接用没问题。bindings 数组里的顺序必须跟构建 engine 时的输入输出顺序一致通常输入是第一个 tensor。可以在构建 engine 时用getTensorName(0)确认。这段代码跑通之后剩下的工程问题是图像预处理。闭眼检测的输入是 48×48 灰度图从摄像头采集的 1080p BGR 图直接缩放再灰度化用 OpenCV 的cuda::resize和cuda::cvtColor可以把预处理时间从 8 ms 压到 1 ms 以内。Jetson Nano 的 CPU 跑 1080p OpenCV 预处理相当吃力务必把这部分也丢给 GPU。4. 算法嵌入式部署中的性能瓶颈定位与参数调优4.1 跑起来之后先量化瓶颈CPU、GPU、还是拷贝在 Jetson Nano 上部署完成后第一个测试是算一下端到端延迟看瓶颈在哪里。输入图像经过摄像头采集、预处理、推理、后处理这四个环节用std::chrono分别计时auto t1 std::chrono::high_resolution_clock::now(); // GPU 预处理resize cvtColor auto t2 std::chrono::high_resolution_clock::now(); // 推理 forward auto t3 std::chrono::high_resolution_clock::now(); // 后处理softmax 闭眼逻辑 auto t4 std::chrono::high_resolution_clock::now();观察各组时间差。常见的几种瓶颈特征如下现象一预处理耗时 5 ms。说明cuda::resize和cuda::cvtColor没有用上或者 OpenCV 没有启用 CUDA 模块。JetPack 自带的 OpenCV 支持 CUDA确认代码里调用的是cv::cuda命名空间下的函数而不是普通 CPU 函数。检查链接的库是libopencv_core.so.4.1而不是 CPU-only 版本。现象二推理耗时 15 ms。说明 TensorRT engine 用的是 FP32或者没有把 batch 利用好。回到trtexec构建阶段确认加了--fp16。Jetson Nano 的 FP16 推理比 FP32 快接近一倍这个优化是免费的。现象三CPU 占用率持续 100%。意味着推理流程里有 H2D 和 D2H 的拷贝没有走 pinned memory。用cudaHostAlloc代替malloc分配输入输出内存可以减少拷贝延迟。这是 jetson 这类嵌入式设备上性能调优的典型手段PC 上几乎感觉不到但在 Nano 上差距可能到 3 ms。4.2 线程模型与 CUDA stream 的选择Jetson Nano 有 4 核 CPU合理分配线程模型对流畅度提升明显。第一版通常是单线程循环——拿一帧、预处理、推理、返回结果。帧率一上去就卡壳瓶颈在同步等待。进阶做法是生产-消费模型// 采集线程读取摄像头帧放入环形缓冲区 void capture_thread(cv::cuda::GpuMat input_gpu) { while (running) { cv::Mat frame cap.read(); cv::cuda::GpuMat frame_gpu; frame_gpu.upload(frame); queue.push(frame_gpu); } } // 推理线程从队列取帧执行预处理 - TensorRT 推理 - 回调 void infer_thread() { input_context-executeV2(bindings); cudaStreamSynchronize(stream_); // 触发闭眼检测回调 }逻辑说明采集线程负责从 USB 摄像头读帧cudaMemcpyAsync可以在这个阶段提前把图像上传到 GPU与采集线程的 CPU 部分重叠。推理线程只做 GPU 上的工作resize、cvtColor、TensorRT 推理。不要在推理线程里做任何磁盘写入或网络发送。两个线程之间用队列解耦队列里存 GPU frame 的索引而不是图片本身避免大块内存拷贝。这里涉及一个重要的 CUDA 概念pinelined memory 和 stream。上面代码里cudaMemcpyAsync需要在非默认 stream 上执行才能和 CUDA kernel 重叠。给推理线程单独建立一个cudaStream_tcudaStream_t infer_stream; cudaStreamCreate(infer_stream); context_-enqueueV2(bindings, infer_stream, nullptr); cudaStreamSynchronize(infer_stream);4.3 系统裁剪优化与低延迟验证嵌入式的性能调优不止 GPU 一件事。系统裁剪会直接影响推理延迟的稳定性尤其是像 Jetson 这种 CPU 和 GPU 共享内存带宽的 SoC。开完nvpmodel -m 0后建议关闭图形界面服务sudo systemctl set-default multi-user.target systemctl stop gdm3 # 或 lightdm这样能释放出 800 MB 左右的内存。另外把不必要的后台服务关掉比如蓝牙、WiFi 热点。这些服务会周期性唤醒 CPU打断推理线程。验证调优成果用nsys工具记录 CUDA kernel 的时间线nsys profile -o eyenet_profile -t cuda,nvtx --force-overwrite true ./eyenet_demo分析导出后的.nsys-rep文件重点看几个关键指标单个 CUDA kernel 的耗时分布如果耗时最大的 kernel 不是 TensorRT 的卷积层而是 cuDNN 的预处理说明预处理没有完全优化。GPU 利用率曲线是否在推理过程大部分时间有活跃 kernel。如果中间有大段空白说明 CPU 生产者没有及时推送数据。H2D 和 D2H 的时间占比总耗时 20ms 里如果拷贝占了 6ms问题比推理本身更值得处理。5. 闭眼检测误报抑制与持续优化路径模型部署上线后闭眼检测的挑战从“能不能跑”变成了“跑得准不准稳不稳”。单纯按帧输出 softmax 分数会产生大量抖动眨眼这种瞬间动作、眼镜反光、低头看方向盘都会被误判成闭眼疲劳。5.1 用时序状态机替代单帧阈值单帧分类概率阈值设为 0.5 是最粗的策略。驾驶员不会瞬间睡过去从睁眼状态到闭眼疲劳状态需要连续数秒的闭眼事件。一个三状态机睁眼态、疑似闭眼态、确认疲劳态可以大幅降低误报睁眼态连续 N 帧 softmax(闭眼) 0.7进入疑似态。疑似态继续观察后续帧如果闭眼概率持续 0.7 达到 3 秒进入确认疲劳态并触发提醒。任何一帧睁眼概率 0.5立即回到睁眼态重置计数。状态机的参数用真实场景数据调优不同驾驶员眼睛大小、戴不戴眼镜差异很大固定阈值很难统一。5.2 验证闭环与数据回灌部署阶段给程序加一个旁路模式真实推理结果写入日志但同时也保存这一帧的输入图像。把连续误报的样本整理成难例集加入训练数据做在线增强。目标是把闭眼检测的准确率指标拆成两个数来考核单帧分类 F1 和疲劳事件检出率。前者看模型性能后者看流程逻辑。TensorRT 在 Jetson Nano 上做闭眼检测的链路不难真正拉开差距的是对数据流、线程模型和 GPU 内存管理的把控。从 trtexec 构建 engine 到 C 推理再到状态机抑制误报这四层全都稳了整套部署才算真正结束。本文还有配套的精品资源点击获取