ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

基于YOLOv4与TensorRT在Jetson Nano上实现实时口罩检测

基于YOLOv4与TensorRT在Jetson Nano上实现实时口罩检测 1. 项目概述在边缘端实现口罩检测的实战价值最近几年在边缘计算设备上部署轻量级AI模型成了一个热门方向尤其是在一些需要实时、离线响应的场景里。我手头正好有一个Nvidia Jetson Nano开发板一直想找个实际项目练练手。看到“口罩检测”这个需求感觉它特别适合作为边缘AI的入门和验证项目一来需求明确二来对实时性有要求三来可以完整走一遍从模型选择、训练、优化到部署的全流程。这个项目我们就是要用YOLOv4这个经典的目标检测模型在Jetson Nano这块小小的板子上实现一个实时、准确的口罩佩戴检测器。Jetson Nano作为Nvidia推出的边缘AI计算平台其核心优势在于集成了GPU虽然算力有限可以运行经过优化的深度学习模型实现端侧推理。而YOLOv4以其在精度和速度上的良好平衡著称虽然已经不是最新但其生态成熟、资料丰富非常适合在资源受限的边缘设备上进行迁移学习和部署。这个项目的目标不仅仅是让模型“跑起来”更是要探索如何在Jetson Nano的算力约束下通过一系列工程化手段如模型剪枝、量化、TensorRT加速等让检测流程达到可用的帧率例如15-30 FPS并保证较高的检测准确率。这对于安防巡检、公共空间管理、智能门禁等实际应用场景具有直接的参考价值。2. 核心思路与技术选型解析2.1 为什么选择YOLOv4而非更新版本在目标检测领域YOLO系列迭代很快v5、v6、v7乃至v8、v9都已出现。选择YOLOv4主要基于以下几点考量生态与稳定性YOLOv4拥有非常庞大和稳定的社区支持。其原始的Darknet框架实现以及后续大量的PyTorch、TensorFlow移植版本都经过了充分测试。在Jetson平台这种特定环境下一个稳定、已知的代码库意味着更少的兼容性问题和更丰富的排错资料。许多针对Jetson的优化教程和案例都是以YOLOv4为蓝本的。精度与速度的经典平衡YOLOv4引入了Mosaic数据增强、CmBN、SAT自对抗训练等技巧在COCO数据集上达到了当时很好的精度AP 43.5%。对于“口罩检测”这类目标相对单一、场景固定的任务YOLOv4的精度完全足够。更重要的是其网络结构CSPDarknet53 SPP PANet虽然比v3复杂但经过优化后在Jetson Nano的GPU上依然有希望达到实时性要求。部署路径清晰从Darknet或PyTorch训练出的YOLOv4模型到转换为ONNX格式再到使用Nvidia的TensorRT进行推理加速这条技术路径非常成熟。TensorRT对YOLOv4的支持很好可以方便地进行FP16甚至INT8量化这对提升Jetson Nano上的推理速度至关重要。相比之下一些新版本YOLO的官方实现可能更偏向PyTorch生态与TensorRT的集成可能需要更多自定义工作。注意如果你追求极致的轻量化和速度并且可以接受一定程度的精度妥协那么YOLOv5n或YOLOv8n这类Nano版本也是不错的选择。但本项目旨在展示一个相对完整的、经典的边缘AI部署流程因此选择了更具代表性的YOLOv4。2.2 Jetson Nano作为部署平台的优势与挑战Jetson Nano是一块信用卡大小的AI计算机搭载了128核Maxwell架构的GPU和4核ARM Cortex-A57 CPU。它的优势在于提供了一个完整的、GPU加速的AI计算环境。优势完整的AI软件栈预装了JetPack SDK包含了CUDA、cuDNN、TensorRT等核心库省去了在ARM架构上手动编译这些复杂库的麻烦。功耗与成本典型功耗5-10W非常适合嵌入式、移动或对功耗敏感的应用场景。丰富的接口拥有CSI摄像头接口、GPIO、USB等方便连接各种传感器和外设构建完整的应用系统。挑战有限算力其GPU算力约为472 GFLOPsFP16内存共享系统4GB。这意味着我们必须对模型进行深度优化不能直接将庞大的原始模型丢上去运行。ARM架构生态一些x86架构上常见的软件包可能没有ARM版本或者需要自己编译可能会遇到依赖问题。散热持续高负载运行时散热是关键。不加散热片或风扇很容易因过热导致降频性能骤降。我们的技术方案就是围绕“克服挑战发挥优势”来设计的。核心思路是在性能更强的服务器或PC上完成模型的训练和初步优化然后将优化后的模型部署到Jetson Nano上利用TensorRT进行最终加速并编写高效的预处理和后处理代码充分榨干板子的每一分算力。3. 开发环境搭建与依赖部署在Jetson Nano上开展工作第一步就是准备好系统环境和所有必要的软件库。这个过程虽然有些繁琐但搭建一个干净、稳定的环境是后续所有工作的基础。3.1 JetPack系统刷机与基础配置我使用的是Jetson Nano 4GB开发者套件。首先需要从Nvidia官网下载最新的JetPack SDK镜像。写这篇文章时JetPack 5.1.2基于Ubuntu 20.04是一个稳定且广泛支持的选择。使用Etcher或balenaEtcher工具将镜像烧录到SD卡建议至少32GBUHS-I速度以上。首次启动后按照向导完成系统设置包括语言、时区、用户名密码等。紧接着有几项关键操作更换软件源默认的国外源速度可能很慢。建议更换为国内镜像源如清华源或中科大源可以极大提升软件包下载速度。修改/etc/apt/sources.list和/etc/apt/sources.list.d/nvidia-l4t-apt-source.list文件中的网址即可。系统更新与基础工具安装sudo apt update sudo apt full-upgrade -y sudo apt install -y curl wget git vim python3-pip python3-dev python3-venvfull-upgrade会升级内核和所有软件包过程可能较长但能确保系统处于最新状态。配置交换空间4GB内存对于编译一些大型库如OpenCV可能不够。增加交换空间Swap可以防止内存耗尽导致编译失败。我通常增加4-6GB的交换文件。sudo fallocate -l 6G /swapfile sudo chmod 600 /swapfile sudo mkswap /swapfile sudo swapon /swapfile # 为了永久生效将下面一行添加到 /etc/fstab # /swapfile swap swap defaults 0 03.2 深度学习框架与关键库的安装JetPack已经包含了CUDA、cuDNN和TensorRT。我们需要确认版本并安装Python层面的接口。检查预装环境# 查看CUDA版本JetPack 5.1.2 通常对应 CUDA 11.4 nvcc --version # 查看TensorRT版本 dpkg -l | grep tensorrt安装PyTorch这是训练和转换模型所必需的。必须安装与JetPack CUDA版本匹配的预编译PyTorch wheel。Nvidia为Jetson提供了专门的下载页面。例如对于JetPack 5.1.2 (CUDA 11.4)和Python 3.8wget https://nvidia.box.com/shared/static/p57jwntv436lfrd78inwl7iml6p13fzh.whl -O torch-1.12.0a02c916ef.nv22.3-cp38-cp38-linux_aarch64.whl pip3 install numpy torch-1.12.0a02c916ef.nv22.3-cp38-cp38-linux_aarch64.whl安装后在Python中执行import torch; print(torch.__version__); print(torch.cuda.is_available())验证是否安装成功且CUDA可用。安装TorchVision同样需要安装匹配版本。通常需要从源码编译因为预编译的wheel可能不兼容。编译过程需要一些时间。sudo apt install -y libjpeg-dev zlib1g-dev libpython3-dev libavcodec-dev libavformat-dev libswscale-dev git clone --branch v0.13.0 https://github.com/pytorch/vision torchvision cd torchvision export BUILD_VERSION0.13.0 python3 setup.py install --user安装其他Python依赖pip3 install matplotlib tqdm scipy pyyaml tensorboard seaborn pandas pip3 install opencv-python # 如果安装失败可以尝试 apt install python3-opencv pip3 install onnx onnx-simplifier # 用于模型转换 pip3 install pycocotools # 用于评估如果需要实操心得在Jetson Nano上从源码编译大型库如OpenCV、TorchVision非常耗时可能长达数小时。务必确保供电稳定最好使用5V4A的桶形电源而非Micro USB并做好散热加装风扇。编译时使用make -j4可以利用所有四个CPU核心但要注意内存和交换空间的使用如果卡住可以尝试make -j2。4. 口罩数据集准备与YOLOv4模型训练模型部署的上游是模型本身。一个针对场景优化过的模型是高效部署的前提。我们虽然可以在Jetson Nano上训练但受限于算力训练会非常慢。因此更合理的流程是在一台拥有更强GPU的机器训练机上完成数据准备和模型训练。4.1 数据收集、标注与格式转换公开的口罩检测数据集很多例如MAFA、Face Mask Detection等。我选择了一个结合了多个来源的数据集包含“戴口罩”with_mask和“未戴口罩”without_mask两类总共约8000张图片。数据的质量至关重要需要确保人脸大小、角度、光照、遮挡情况多样并且标注框精准。标注工具使用LabelImg或更现代的CVAT进行标注导出为PASCAL VOC格式XML文件或YOLO格式每个图片对应一个.txt文件内容为class_id x_center y_center width height坐标是归一化后的值。数据集组织按照YOLO训练的要求组织目录结构。mask_dataset/ ├── images/ │ ├── train/ │ └── val/ └── labels/ ├── train/ └── val/需要确保images/train里的每个xxx.jpg在labels/train里都有对应的xxx.txt标注文件。创建数据集配置文件创建一个mask_data.yaml文件内容如下# 类别数量和名称 nc: 2 names: [with_mask, without_mask] # 训练和验证图像的路径可以是绝对路径或相对路径 train: /path/to/mask_dataset/images/train val: /path/to/mask_dataset/images/val4.2 基于Darknet框架的训练与调优我选择使用AlexeyAB版本的Darknet进行训练因为它对YOLOv4的支持最原汁原味且社区活跃。1. 克隆与编译Darknet 在训练机上通常是x86 Linux/Windows with CUDA操作。git clone https://github.com/AlexeyAB/darknet cd darknet # 修改Makefile设置GPU1, CUDNN1, OPENCV1等然后编译 make -j82. 下载预训练权重并修改网络配置 下载YOLOv4的预训练权重yolov4.conv.137。然后修改cfg/yolov4-custom.cfg复制自yolov4.cfg修改batch和subdivisions根据你的GPU显存调整。例如显存8GB可以设batch64, subdivisions16。修改width和height输入图像尺寸。为了速度我选择了608x608。更小的尺寸如416x416会更快但精度可能下降。修改max_batches总迭代次数对于2个类可以设为classes*20004000但通常需要更多我设了8000。修改steps学习率衰减步长设为max_batches的80%和90%如6400,7200。修改classes在三个YOLO输出层[yolo]层之前的那一层[convolutional]中将classes80改为classes2。修改filters在三个[convolutional]层每个[yolo]层之前的那个将filters255改为filters(classes 5) * 3 21。计算公式是(类别数 坐标置信度)*每个网格预测的框数。3. 开始训练./darknet detector train mask_data.data cfg/yolov4-custom.cfg yolov4.conv.137 -dont_show -mapmask_data.data是另一个配置文件指向mask_data.yaml和权重保存路径等。-map选项会定期计算mAP方便评估。训练过程可以通过-mjpeg_port选项在网页上查看实时损失曲线和预览检测效果。训练调优经验学习率如果训练初期损失值nan说明学习率太大需要调小cfg文件中的learning_rate。数据增强Darknet的YOLOv4默认开启了Mosaic等增强。如果数据集较小可以尝试修改cfg文件中的angle, saturation, exposure, hue等参数来增强数据防止过拟合。早停观察验证集mAP。当mAP在连续多个epoch内不再显著上升时就可以提前停止训练避免过拟合。训练完成后会在backup目录下得到最终的权重文件如yolov4-custom_best.weights和yolov4-custom_last.weights。我们使用_best.weights作为最终模型。5. 模型优化与转换通向TensorRT之路拿到Darknet训练好的.weights文件后我们不能直接将其用于Jetson Nano上的高效推理。需要经过一系列转换和优化生成TensorRT引擎才能充分发挥Jetson的硬件加速能力。5.1 从Darknet到ONNX标准化模型转换ONNXOpen Neural Network Exchange是一个开放的模型格式作为不同框架之间转换的桥梁。我们的路径是Darknet (.weights) - PyTorch - ONNX。1. 将Darknet权重转换为PyTorch模型 有很多开源脚本可以实现这个转换。我使用了一个广泛使用的版本。核心是加载Darknet的cfg和weights文件然后按照网络结构一层层地读取权重并加载到对应的PyTorch模块中。转换后会得到一个PyTorch的.pth文件和一个对应的模型定义文件。2. 导出为ONNX格式 使用PyTorch的torch.onnx.export函数进行导出。这里有几个关键点动态轴为了支持推理时不同批大小和图像尺寸需要设置动态维度。通常对输入图像的批处理维度batch和尺寸维度height, width设置为动态。import torch # 假设 model 是你的PyTorch模型dummy_input是一个示例输入张量 dummy_input torch.randn(1, 3, 608, 608, devicecuda) # 批大小13通道608x608 dynamic_axes {input: {0: batch, 2: height, 3: width}, output: {0: batch}} torch.onnx.export(model, dummy_input, yolov4_mask.onnx, export_paramsTrue, opset_version12, do_constant_foldingTrue, input_names[input], output_names[output], dynamic_axesdynamic_axes)opset_version设置为12或更高以确保支持后续的TensorRT转换。简化ONNX模型导出的ONNX模型可能包含一些冗余算子。使用onnx-simplifier可以优化网络结构有时能解决后续转换中的问题。python3 -m onnxsim yolov4_mask.onnx yolov4_mask_sim.onnx5.2 使用TensorRT进行优化与引擎生成这是最核心的加速步骤。TensorRT是Nvidia的高性能深度学习推理SDK它会对模型进行图层融合、精度校准、内核自动调优等优化并生成一个在特定GPU上高度优化的“引擎”engine文件。1. 在Jetson Nano上安装必要的工具 确保TensorRT已安装JetPack自带。我们还需要pycuda和TensorRT的Python绑定。pip3 install pycuda # TensorRT的Python包通常位于 /usr/lib/python3.8/dist-packages/已在系统路径中。2. 将ONNX模型转换为TensorRT引擎 我们可以使用TensorRT的Python API编写转换脚本。这里涉及到选择精度FP32, FP16, INT8。FP32最高精度速度最慢。FP16半精度在Jetson Nano上能带来显著的速度提升而精度损失通常很小强烈推荐。INT8整型8位精度速度最快但需要校准数据集来量化过程更复杂且精度损失可能较大。以下是一个FP16精度的转换脚本核心部分import tensorrt as trt TRT_LOGGER trt.Logger(trt.Logger.WARNING) EXPLICIT_BATCH 1 (int)(trt.NetworkDefinitionCreationFlag.EXPLICIT_BATCH) def build_engine(onnx_file_path, engine_file_path): builder trt.Builder(TRT_LOGGER) network builder.create_network(EXPLICIT_BATCH) parser trt.OnnxParser(network, TRT_LOGGER) with open(onnx_file_path, rb) as model: if not parser.parse(model.read()): for error in range(parser.num_errors): print(parser.get_error(error)) return None config builder.create_builder_config() config.max_workspace_size 1 30 # 1GB config.set_flag(trt.BuilderFlag.FP16) # 启用FP16 # 对于INT8还需要设置校准器config.set_flag(trt.BuilderFlag.INT8) 并提供一个校准器 # 设置动态形状profile因为我们导出时设置了动态轴 profile builder.create_optimization_profile() # 设置最小、最优、最大输入尺寸 profile.set_shape(input, min(1, 3, 416, 416), opt(1, 3, 608, 608), max(1, 3, 832, 832)) config.add_optimization_profile(profile) engine builder.build_engine(network, config) with open(engine_file_path, wb) as f: f.write(engine.serialize()) return engine运行这个脚本输入yolov4_mask_sim.onnx输出yolov4_mask_fp16.engine。关键技巧max_workspace_size不能设太小否则可能无法创建引擎。1GB对于YOLOv4是足够的。profile的设置很重要它定义了引擎能处理的输入尺寸范围。推理时输入的尺寸必须在这个范围内。6. Jetson Nano上的推理程序实现有了TensorRT引擎文件接下来就是在Jetson Nano上编写Python推理脚本处理摄像头或视频流并执行检测。6.1 TensorRT引擎的加载与推理流程推理脚本的主要步骤包括加载引擎、创建执行上下文、准备输入输出缓冲区、预处理图像、执行推理、后处理结果。1. 加载引擎并分配缓冲区import tensorrt as trt import pycuda.driver as cuda import pycuda.autoinit import numpy as np import cv2 def load_engine(engine_file_path): TRT_LOGGER trt.Logger(trt.Logger.WARNING) with open(engine_file_path, rb) as f, trt.Runtime(TRT_LOGGER) as runtime: engine runtime.deserialize_cuda_engine(f.read()) return engine engine load_engine(yolov4_mask_fp16.engine) context engine.create_execution_context() # 获取输入输出绑定binding的索引和形状 # 假设我们的模型只有一个输入和一个输出 input_binding_idx engine.get_binding_index(input) # 根据导出时的名字 output_binding_idx engine.get_binding_index(output) # 获取动态输入的最大形状并分配固定大小的内存按最大形状分配 input_shape engine.get_binding_shape(input_binding_idx) # e.g., (-1, 3, -1, -1) # 我们设定一个推理时使用的固定尺寸例如608 inference_h, inference_w 608, 608 actual_input_shape (1, 3, inference_h, inference_w) # 为输入输出在GPU上分配内存 d_input cuda.mem_alloc(1 * np.prod(actual_input_shape) * np.dtype(np.float32).itemsize) output_shape context.get_binding_shape(output_binding_idx) # 输出形状可能也是动态的需要根据输入形状推断 # 对于YOLO输出通常是1x(bbox_info)xN。一个简单的方法是先分配一个较大的空间。 max_output_size 1 * 10000 * np.dtype(np.float32).itemsize # 假设最多10000个检测项 d_output cuda.mem_alloc(max_output_size) # 创建CUDA流 stream cuda.Stream()2. 图像预处理 预处理必须与训练时保持一致归一化、BGR转RGB等。YOLOv4通常使用/255进行归一化。def preprocess_image(image, target_size(608, 608)): # 调整大小并保持长宽比填充灰边 h, w image.shape[:2] scale min(target_size[0] / h, target_size[1] / w) new_h, new_w int(h * scale), int(w * scale) resized cv2.resize(image, (new_w, new_h), interpolationcv2.INTER_LINEAR) # 创建画布并填充 canvas np.full((target_size[0], target_size[1], 3), 128, dtypenp.uint8) top (target_size[0] - new_h) // 2 left (target_size[1] - new_w) // 2 canvas[top:topnew_h, left:leftnew_w, :] resized # 转换通道顺序 BGR - RGB归一化转置为CHW格式 canvas canvas[:, :, ::-1].transpose(2, 0, 1).astype(np.float32) / 255.0 # 添加批次维度 canvas np.ascontiguousarray(canvas[np.newaxis, ...]) return canvas, scale, (left, top)3. 执行推理def inference(image_np): # 预处理 input_blob, scale, (pad_left, pad_top) preprocess_image(image_np) # 将数据从Host拷贝到Device cuda.memcpy_htod_async(d_input, input_blob, stream) # 设置动态输入形状 context.set_binding_shape(input_binding_idx, input_blob.shape) # 执行推理 context.execute_async_v2(bindings[int(d_input), int(d_output)], stream_handlestream.handle) # 将输出从Device拷贝回Host output_data np.empty(output_shape, dtypenp.float32) # 需要根据实际输出形状调整这里简化了 cuda.memcpy_dtoh_async(output_data, d_output, stream) stream.synchronize() return output_data, scale, (pad_left, pad_top)6.2 YOLOv4输出的后处理与可视化TensorRT引擎的输出通常是扁平化的张量我们需要将其解析成边界框、置信度和类别。1. 解析输出 YOLOv4的输出格式比较复杂通常是多个尺度的特征图拼接。一个常见的方法是使用reshape和transpose操作将其恢复为(batch, num_anchors, grid_h, grid_w, box_attrs)的形状其中box_attrs包含(tx, ty, tw, th, obj_score, class_score1, class_score2, ...)。def parse_yolo_output(trt_outputs, input_shape, anchors, num_classes, confidence_thresh0.5, nms_thresh0.4): # trt_outputs: 推理得到的原始数据可能是一个或多个输出多尺度 # 这里需要根据你导出模型时的具体输出结构来编写解析代码。 # 以下是一个高度简化的伪代码逻辑 boxes [] confidences [] class_ids [] # 假设我们只有一个输出且已经reshape好了 # output shape: (1, 3*(5num_classes), grid_h, grid_w) - (1, 3, grid_h, grid_w, 5num_classes) # 遍历每个网格、每个锚框 for i in range(grid_h): for j in range(grid_w): for a in range(num_anchors): # 取出该锚框的预测数据 box_data output[0, a, i, j] # 解码中心坐标和宽高基于YOLO公式 tx, ty, tw, th, obj_score box_data[:5] class_scores box_data[5:] # 计算最终置信度 目标置信度 * 最大类别置信度 class_id np.argmax(class_scores) confidence obj_score * class_scores[class_id] if confidence confidence_thresh: # 解码为实际像素坐标相对于输入网络图像608x608 cx (j tx) / grid_w * input_shape[1] cy (i ty) / grid_h * input_shape[0] w np.exp(tw) * anchors[a][0] / input_shape[1] h np.exp(th) * anchors[a][1] / input_shape[0] # 转换为左上角坐标 x1 int((cx - w/2)) y1 int((cy - h/2)) x2 int((cx w/2)) y2 int((cy h/2)) boxes.append([x1, y1, x2, y2]) confidences.append(float(confidence)) class_ids.append(class_id) # 应用非极大值抑制(NMS) indices cv2.dnn.NMSBoxes(boxes, confidences, confidence_thresh, nms_thresh) final_boxes [] final_confidences [] final_class_ids [] if len(indices) 0: for i in indices.flatten(): final_boxes.append(boxes[i]) final_confidences.append(confidences[i]) final_class_ids.append(class_ids[i]) return final_boxes, final_confidences, final_class_ids注意实际的解析代码需要严格对应你训练时使用的YOLOv4 cfg文件中的锚点anchors和输出层结构。上述代码仅为示意。2. 映射回原始图像并绘制 解析出的坐标是基于网络输入如608x608的需要根据预处理时的缩放和填充信息映射回原始图像的坐标。def draw_detections(image, boxes, confidences, class_ids, scale, pad, class_names): h, w image.shape[:2] for i, box in enumerate(boxes): x1, y1, x2, y2 box # 将坐标从网络输入空间转换回原始图像空间 # 首先去除填充 x1 (x1 - pad[0]) / scale y1 (y1 - pad[1]) / scale x2 (x2 - pad[0]) / scale y2 (y2 - pad[1]) / scale # 确保坐标在图像范围内 x1, y1, x2, y2 int(max(0, x1)), int(max(0, y1)), int(min(w, x2)), int(min(h, y2)) # 绘制矩形和标签 color (0, 255, 0) if class_ids[i] 0 else (0, 0, 255) # 假设0是戴口罩1是不戴 label f{class_names[class_ids[i]]}: {confidences[i]:.2f} cv2.rectangle(image, (x1, y1), (x2, y2), color, 2) cv2.putText(image, label, (x1, y1-10), cv2.FONT_HERSHEY_SIMPLEX, 0.5, color, 2) return image7. 性能优化与系统集成实战让模型跑起来只是第一步要达到实用的帧率例如15-30 FPS和稳定性还需要进行一系列优化并将整个流程集成成一个完整的应用。7.1 多线程与流水线优化推理过程特别是预处理和后处理可能成为瓶颈。我们可以使用多线程或异步流水线来重叠I/O图像读取、预处理、推理和后处理的时间。生产者-消费者模式一个线程专门负责从摄像头抓取帧生产者放入一个队列。另一个或多个线程消费者从队列中取帧进行预处理、推理、后处理和显示。Python的queue.Queue和threading模块可以实现。使用CUDA流进行异步操作在预处理和后处理中尽量使用CUDA函数例如使用PyCUDA或OpenCV的GPU函数cv2.cuda并将操作放入同一个CUDA流中与TensorRT推理异步执行减少CPU和GPU之间的等待。一个简化的流水线伪代码结构import threading import queue import time frame_queue queue.Queue(maxsize2) # 限制队列大小防止内存堆积 result_queue queue.Queue(maxsize2) def capture_thread(cap): while True: ret, frame cap.read() if not ret: break if not frame_queue.full(): frame_queue.put(frame) else: time.sleep(0.001) # 队列满时稍等 def inference_thread(engine, context): while True: if not frame_queue.empty(): frame frame_queue.get() # 预处理、推理、后处理 input_blob, scale, pad preprocess_image(frame) # ... 执行TensorRT推理 ... boxes, scores, ids parse_yolo_output(...) result_queue.put((frame, boxes, scores, ids, scale, pad)) def main(): cap cv2.VideoCapture(0) # 或视频文件路径 # ... 加载引擎等 ... # 启动线程 threading.Thread(targetcapture_thread, args(cap,), daemonTrue).start() threading.Thread(targetinference_thread, args(engine, context), daemonTrue).start() # 主线程负责显示结果 while True: if not result_queue.empty(): orig_frame, boxes, scores, ids, scale, pad result_queue.get() result_frame draw_detections(orig_frame.copy(), boxes, scores, ids, scale, pad, [with_mask, without_mask]) cv2.imshow(Mask Detection, result_frame) if cv2.waitKey(1) 0xFF ord(q): break7.2 实际性能测试与瓶颈分析在Jetson Nano上运行完整的检测流程后需要使用工具测量各阶段的耗时。可以使用Python的time模块或更专业的nvprofNvidia性能分析器。测量方法import time start time.perf_counter() # 执行预处理 preprocess_time time.perf_counter() - start start time.perf_counter() # 执行推理 inference_time time.perf_counter() - start start time.perf_counter() # 执行后处理 postprocess_time time.perf_counter() - start print(fPreprocess: {preprocess_time*1000:.2f}ms, Inference: {inference_time*1000:.2f}ms, Postprocess: {postprocess_time*1000:.2f}ms)典型瓶颈与优化策略预处理瓶颈如果使用CPU进行cv2.resize和颜色转换可能较慢。优化尝试使用cv2.cuda模块的GPU函数进行预处理如cv2.cuda.resize。但需要注意数据在CPU和GPU之间的传输开销。简化如果允许降低推理分辨率如从608降到416能直接减少预处理和推理的计算量。推理瓶颈这是最核心的部分。精度确保使用了FP16模式生成引擎。与FP32相比通常能有1.5-2倍的速度提升。批处理虽然实时检测通常批大小为1但如果你处理的是视频流可以考虑累积几帧进行一次批处理推理能提高GPU利用率。但这会增加延迟。引擎优化检查TensorRT引擎构建时是否设置了合适的max_workspace_size和优化profile。后处理瓶颈Python循环实现的NMS和解析可能较慢。优化尽可能使用向量化NumPy操作替代循环。对于NMS使用OpenCV内置的cv2.dnn.NMSBoxes函数它由C实现速度较快。移植将后处理的核心部分如解码边界框用C编写并编译成Python扩展可以大幅提升速度。但这增加了复杂度。在我的实测中使用Jetson Nano 4GB输入分辨率608x608FP16精度在单线程模式下整个流程包括预处理、推理、后处理的帧率大约在8-12 FPS。通过上述的流水线优化并可能将分辨率降至416x416帧率可以提升到15-20 FPS对于很多实时性要求不极端的场景已经可用。8. 常见问题排查与经验总结在Jetson Nano上部署深度学习模型总会遇到各种各样的问题。这里记录一些我踩过的坑和解决方案。8.1 模型转换与推理过程中的典型错误问题1ONNX转TensorRT时出现[TensorRT] ERROR: ... INVALID_ARGUMENT: ... getPluginCreator could not find plugin ...错误。原因YOLOv4模型中可能包含了一些TensorRT默认不支持的算子如早期的YOLO层、Upsample层使用Resize算子等。解决使用支持这些算子的TensorRT版本确保JetPack和TensorRT版本较新。在导出ONNX时尝试使用不同的opset版本如11, 12, 13。使用onnx-simplifier简化模型有时能自动将不支持的算子模式替换掉。最根本的方法是使用一个已知与TensorRT兼容的YOLOv4实现版本进行训练和导出。例如一些PyTorch实现的YOLOv4会使用标准的nn.Conv2d和nn.Upsample兼容性更好。问题2推理时输出结果全是乱码或NaN。原因预处理不一致推理时的图像预处理归一化、通道顺序、尺寸必须与训练时完全一致。动态形状未正确设置如果导出ONNX时设置了动态轴但在TensorRT推理时没有通过context.set_binding_shape设置正确的输入形状会导致输出错误。精度问题使用FP16时如果模型权重范围很大可能会溢出。可以尝试使用FP32引擎看看问题是否消失。解决仔细检查并统一预处理代码。打印预处理后输入张量的均值和范围进行验证。确保在context.execute_async_v2之前调用了context.set_binding_shape。在训练时可以考虑进行一些权重归一化或者尝试使用INT8量化需要校准。问题3帧率远低于预期。原因Jetson Nano运行在低功耗模式默认可能是5W模式。可以切换到10W模式以获得全部性能。sudo nvpmodel -m 0 # 0代表MAX-N10W模式 sudo jetson_clocks # 设置时钟为最大值CPU频率被限制检查sudo jetson_clocks --show查看时钟状态。过热降频触摸芯片是否烫手。必须安装主动散热风扇。内存/交换空间不足使用htop或tegrastats命令监控内存使用。如果频繁使用交换空间速度会急剧下降。没有使用TensorRT FP16确认引擎是以FP16模式构建的。8.2 Jetson Nano系统层面的优化技巧电源管理务必使用5V4A的桶形电源适配器Micro USB供电无法提供持续稳定的大电流会导致性能不稳定甚至关机。散热是生命线一块好的散热片和一个小风扇是必需品。没有散热Jetson Nano会在几分钟内因过热而强制降频性能下降超过50%。我推荐使用带风扇的散热器套装。关闭图形桌面如果应用是纯后台服务可以考虑关闭图形界面使用sudo systemctl set-default multi-user.target并重启可以节省出可观的内存和CPU资源。使用jetson_stats工具监控安装jetson-stats工具包sudo pip3 install jetson-stats使用jtop命令可以非常直观地查看CPU/GPU频率、温度、功耗、内存和GPU使用率是性能调优的利器。精简系统服务禁用不必要的系统服务如蓝牙、apt自动更新等可以减少系统负载。经过这一整套从数据准备、模型训练、转换优化到最终部署和调优的流程我们成功在Jetson Nano上构建了一个能够实时检测口罩佩戴情况的系统。这个过程虽然涉及多个环节但每一步拆解开来都有成熟的工具和社区支持。最大的收获不在于实现了某个特定功能而在于掌握了将AI模型落地到真实、资源受限的硬件环境中的完整方法论。当你看到自己训练的模型在巴掌大的板子上流畅地分析视频流时那种成就感是单纯在服务器上跑通代码无法比拟的。对于想入门边缘AI的朋友我强烈建议从这样一个完整的项目开始它几乎涵盖了你会遇到的所有典型问题。
返回列表