记一次 i.MX6 SL YOLO 模型编译和性能调优

记一次 i.MX6 SL YOLO 模型编译和性能调优

目标平台:i.MX6 SL(Cortex-A9,ARMv7-a NEON,低性能嵌入式设备)

应用场景:皮肤图像毛发识别,核心关注识别数量准确度 ,锚框精度次要

技术栈:AlexeyAB darknet(训练)→ ncnn(推理)→ Qt5.7(GUI)

开发环境:Windows + WSL2(GPU 训练 + WSLg 桌面调试)


一、背景与目标

1.1 硬件与场景约束

说明
处理器 i.MX6 SL(Cortex-A9 单核,ARMv7-a NEON 硬浮点)
内存 1GB
目标平台 Qt Qt5.7(低性能 ARM Linux)
检测目标 皮肤表面的毛发,小尺寸、低特征
核心需求 毛发识别数量准确度(锚框精度次要,但偏移太多的误判不算)
推理预算 允许当前状态最多翻倍推理时间(2.0x)

1.2 技术选型

  • 训练框架:AlexeyAB darknet(YOLOv4-tiny 系列,嵌入式友好)
  • 推理框架:ncnn(轻量级,支持 ARM NEON 优化)
  • GUI:Qt Widgets(兼容目标平台 Qt5.7)
  • 开发环境:WSL2(GPU 训练用 RTX 3050,GUI 调试用 WSLg)

二、环境搭建与训练(darknet + GPU)

2.1 编译 AlexeyAB darknet(WSL + GPU)

修改 Makefile 开启 GPU/CUDNN/OPENCV:

makefile 复制代码
GPU=1
CUDNN=1
CUDNN_HALF=0
OPENCV=1
AVX=0
OPENMP=1
LIBSO=1
bash 复制代码
cd darknet && make -j$(nproc)

2.2 踩坑一:WSL 下 cublas 初始化失败

症状:训练启动即崩溃,报 CUDA/cublas 相关错误:

复制代码
cudaGetDeviceCount failed
CUDA error: initialization error

原因:WSL2 的 GPU 加速依赖 WSLg 的显卡转发,CUDA 运行时库需要显式指定动态库路径。

解决 :训练命令前设置 LD_LIBRARY_PATH

bash 复制代码
export LD_LIBRARY_PATH=/usr/lib/wsl/lib

2.3 数据准备:LabelMe 标注 → YOLO 格式

用 LabelMe 标注多边形,转 YOLO 检测格式(外接框):

bash 复制代码
python3 verify/labelme2yolo_det.py \
    dataset/annotations dataset/ori_image dataset/yolo_det

输出(示例):

复制代码
类别: ['hair']
完成:train=38,val=10
输出目录: dataset/yolo_det

2.4 训练参数适配小数据集

踩坑二:burn_inmax_batches 还大

初始训练配置:

ini 复制代码
learning_rate=0.00261
burn_in=1000      # 问题:burn_in 大于 max_batches,学习率永远没进入正常阶段
max_batches = 500

修正(小数据集 48 张,降低学习率 + 开启 mosaic 增强):

ini 复制代码
learning_rate=0.0005
burn_in=100
mosaic=1
max_batches = 2000
steps=1600,1800

2.5 启动训练(GPU)

bash 复制代码
cd darknet
LD_LIBRARY_PATH=/usr/lib/wsl/lib ./darknet detector train \
    /home/ncnn/dataset/yolo_det/obj.data \
    /home/ncnn/dataset/yolo_det/yolov4-tiny-train.cfg \
    /home/ncnn/dataset/yolo_det/yolov4-tiny.conv.29 \
    -dont_show

训练日志:

复制代码
layer     filters    size              input                output
    0 conv     32  3 x 3 / 2   320 x 320 x   3   ->   160 x 160 x  32  0.044 BFLOPs
    1 conv     64  3 x 3 / 2   160 x 160 x  32   ->    80 x  80 x  64  0.236 BFLOPs
    2 conv     64  3 x 3 / 1    80 x  80 x  64   ->    80 x  80 x  64  0.472 BFLOPs
    3 route  2
Unused field: 'groups = 2'
Unused field: 'group_id = 1'
    4 conv     32  3 x 3 / 1    80 x  80 x  64   ->    80 x  80 x  32  0.236 BFLOPs
...
Loading weights from .../yolov4-tiny.conv.29...Done!
1: 228.561127, 228.561127 avg, 0.000000 rate, 0.907388 seconds, 32 images
2: 228.390640, 228.544083 avg, 0.000000 rate, 0.408662 seconds, 64 images
3: 222.587692, 227.948441 avg, 0.000000 rate, 0.413337 seconds, 96 images
...
300: 3.094278, 3.646923 avg, 0.000500 rate, 0.398237 seconds, 24096 images

训练完成后产物:

复制代码
backup/yolov4-tiny-train_final.weights

三、模型转换:darknet → ncnn

3.1 标准转换流程

bash 复制代码
# 1. darknet 权重转 ncnn param/bin
./build-darknet2ncnn yolov4-tiny-train.cfg \
    yolov4-tiny-train_final.weights \
    yolov4-tiny-hair.param yolov4-tiny-hair.bin 1

# 2. ncnnoptimize 优化
./build-ncnnoptimize yolov4-tiny-hair.param yolov4-tiny-hair.bin \
    yolov4-tiny-hair-opt.param yolov4-tiny-hair-opt.bin 0

转换输出:

复制代码
Loading weights...
Converting model...
80 layers, 88 blobs generated.
NOTE: The input of darknet uses: mean_vals=0 and norm_vals=1/255.f.
NOTE: Remember to use ncnnoptimize for better performance.

input = data
extract = output
estimated memory footprint = 15289.52 KB = 14.93 MB
mac = 2007347200 = 2007.35 M

3.2 深坑:[route] groups=2 权重错位 bug

症状 :ncnn 推理输出 inf/NaN,所有框 prob=1.0、坐标 (-inf, inf)

复制代码
Detected 37 object(s):
  [0] hair             prob=1.000  box=(-inf, 48.0) infx0.0
  [1] hair             prob=1.000  box=(-inf, 48.0) infx0.0

定位过程:编写逐层数值诊断程序,提取每个卷积层输出:

复制代码
0_34: 160x160x32 min=-5.083814 max=4.617772 mean=-0.099014   <- 正常
1_42: 80x80x64  min=-71.867104 max=60.714005 mean=-1.193188  <- 正常
4_63: 80x80x32  min=-21.759676 max=18.798439 mean=-0.946641  <- 正常
*** 5_71: 80x80x32 min=-373329.531250 max=1133812.125000     <- 从这里爆炸!

根因 :darknet2ncnn 对 [route] groups=2 生成 Crop 层(通道减半),

但 AlexeyAB darknet 训练时 route 的 groups 是 Unused field(输出完整通道)。

导致卷积输入通道数与权重不匹配,权重读取错位。

对比确认 :同一转换工具转 COCO 官方权重正常,转训练权重异常,

唯一差异是 route-groups 的处理。

修复 :修改 darknet2ncnn.cpp,route 单输入时始终按 Noop(full concat)处理:

cpp 复制代码
if (s->input_blobs.size() == 1)
{
    // AlexeyAB darknet 的 parse_route 忽略 groups/group_id(Unused field),
    // 始终按 Noop(full pass-through)处理,与 darknet 训练一致。
    s->layer_type = "Noop";
}

修复后验证:中间层数值恢复正常:

复制代码
0_35: 160x160x32 min=-5.047588 max=4.527293 mean=-0.072304   <- 正常
4_64: 80x80x32  min=-151.893982 max=462.142670 mean=2.809290  <- 正常
5_72: 80x80x32  min=-325.645844 max=3887.849609              <- 不再爆炸

四、交叉编译与 ARM A9 部署

4.1 工具链

bash 复制代码
# gcc-linaro 4.9.4 ARM hardfloat
export PATH=/opt/gcc-linaro-4.9.4-2017.01-x86_64_arm-linux-gnueabihf/bin:$PATH

4.2 ncnn 交叉编译

bash 复制代码
mkdir build-arm-a9 && cd build-arm-a9
cmake -DCMAKE_BUILD_TYPE=Release \
    -DCMAKE_TOOLCHAIN_FILE=/home/ncnn/toolchains/arm-linux-gnueabihf-linaro.toolchain.cmake \
    -DNCNN_BUILD_TOOLS=OFF -DNCNN_BUILD_EXAMPLES=OFF \
    -DNCNN_VULKAN=OFF -DNCNN_NEON=ON ..
make -j$(nproc) && make install

4.3 验证程序交叉编译(Makefile)

makefile 复制代码
CROSS_PREFIX  = /opt/gcc-linaro-4.9.4-2017.01-x86_64_arm-linux-gnueabihf/bin/arm-linux-gnueabihf
CXX           = $(CROSS_PREFIX)-g++
NCNN_INC      = ../install-arm-a9/include/ncnn
NCNN_LIB      = ../install-arm-a9/lib/libncnn.a
CXXFLAGS      = -O2 -std=c++11 -march=armv7-a -mfloat-abi=hard -mfpu=neon -I$(NCNN_INC)
LDFLAGS       = -pthread -static-libstdc++ -static-libgcc
bash 复制代码
cd verify && make

产物:

复制代码
yolov4_tiny_verify: ELF 32-bit LSB executable, ARM, EABI5 version 1 (SYSV),
    dynamically linked, interpreter /lib/ld-linux-armhf.so.3

4.4 host 端验证运行

bash 复制代码
./yolov4_tiny_verify_host yolov4-tiny-hair-opt.param \
    yolov4-tiny-hair-opt.bin image.bmp 1

输出(模拟):

复制代码
image: hair_val_test.bmp (640x480)
[timing] model load: 121.08 ms
[timing] preprocess: 0.57 ms
[timing] inference: 54.60 ms
[timing] total: 176.29 ms

Detected 8 object(s):
  [0] hair             prob=0.936  box=(118.9, 178.6) 231.8x65.7
  [1] hair             prob=0.849  box=(571.8, 113.7) 52.0x45.9

4.5 动态库(.so)编译探索

ncnn 原生支持动态库:

bash 复制代码
mkdir build-shared-arm-a9 && cd build-shared-arm-a9
cmake -DCMAKE_BUILD_TYPE=Release \
    -DCMAKE_TOOLCHAIN_FILE=.../arm-linux-gnueabihf-linaro.toolchain.cmake \
    -DNCNN_SHARED_LIB=ON -DNCNN_NEON=ON ..
make -j$(nproc)

产物:

复制代码
src/libncnn.so -> libncnn.so.1 -> libncnn.so.1.0.20260804
ELF 32-bit LSB shared object, ARM, EABI5 version 1 (SYSV)

静态 vs 动态体积对比(A9)

方式 程序 总计
静态 5367 KB - 5367 KB
动态 352 KB 2862 KB 3214 KB

五、数据扩充:从 48 张到 311 张

5.1 新增 labelImg 矩形标注

用 labelImg 直接画矩形(输出 Pascal VOC XML),转 YOLO 格式:

bash 复制代码
python3 verify/voc2yolo_det.py \
    "/mnt/d/lfjinying/Downloads/Pictures" \
    dataset/yolo_det/all/images dataset/yolo_det/all/labels

输出(示例):

复制代码
完成:263/263 张,共 1610 个目标
图片目录: .../all/images
标注目录: .../all/labels

5.2 合并新旧数据

bash 复制代码
python3 verify/merge_datasets.py

输出(示例):

复制代码
收集到 311 张标注样本
划分: train=248, val=63
train.txt: 248 张
val.txt: 63 张
统一图片目录: .../images (311 张)
统一标注目录: .../labels (311 个)

5.3 重新训练

数据量翻倍,适当增加迭代:

ini 复制代码
max_batches = 3000
steps=2400,2700

训练日志(loss 收敛更好):

复制代码
649: 2.469519, 2.430475 avg, 0.000500 rate, 0.425889 seconds
1745: 1.301837, 1.443995 avg, 0.000500 rate, 0.464960 seconds
2833: 0.822025, 0.983016 avg, 0.000005 rate, 0.444818 seconds
3000: 0.598312, 1.052939 avg, 0.000005 rate, 0.444648 seconds

六、性能调优:针对小目标的模型增强

6.1 问题分析

毛发是小尺寸、低特征目标,在 320 输入、最大 32x 下采样后严重丢失:

  • tiny 模型只有 2 个检测头(stride 16/32),最小检测尺度 20x20
  • 小目标在低分辨率下像素稀少,特征微弱

6.2 方案对比(推理时间预算 2.0x 约束)

方案 计算量 推理耗时(估) 预算内 小目标改善
tiny @320 (现状) 2007 MMAC 36.9ms - 基线
3l @320 2548 MMAC 50.6ms ✓ 1.37x 高(3检测头)
3l @416 4306 MMAC 68.7ms ✓ 1.86x 最高(3头+高分辨率)
3l @448 4994 MMAC 99.2ms ✓ 1.96x 接近上限,风险大

6.3 训练 3l 模型

3l = 3 个检测头(stride 32/16/8),其中 stride=8 细粒度头专攻小目标。

anchors 按输入分辨率等比缩放。

bash 复制代码
# 320 输入 anchors
anchors = 6,8, 10,19, 21,15, 19,39, 40,29, 38,77, 75,58, 101,128, 242,211

# 416 输入 anchors(×1.3)
anchors = 8,10, 13,25, 27,20, 25,51, 52,38, 49,100, 98,75, 131,166, 315,274

训练(模拟日志):

复制代码
83: 7.423612, 12.449652 avg, 0.000237 rate, 0.487618 seconds
1548: 2.867455, 2.615760 avg, 0.000500 rate, 0.493778 seconds
3000: 2.022038, 2.034217 avg, 0.000005 rate, 0.484384 seconds
Region 30 Avg IOU: 0.687542, Class: 0.993559, Obj: 0.668988, .5R: 1.000000
Region 44 Avg IOU: 0.668461, Class: 0.999427, Obj: 0.645655, .5R: 0.909091

6.4 数量准确度 benchmark

评分原则(按用户定义):

  • 核心指标 CountScore = 1 - 平均单图 |预测数 - 真实数| / max(真实数, 1)
  • IoU 匹配阈值放宽到 0.15(锚框精度次要)
  • 未匹配任何真实框的预测 = FP(误判);未匹配到的真实框 = FN(漏检)
  • F2 侧重召回(数量场景漏检代价更大)
bash 复制代码
# 1. 图片转 BMP(benchmark 避免 JPG 解码依赖)
python3 verify/preprocess_bmp.py dataset/yolo_det/images /tmp/bench_bmp

# 2. 编译 benchmark
g++ -O2 -std=c++11 -I install-host/include/ncnn -o hair_benchmark \
    verify/hair_benchmark.cpp install-host/lib/libncnn.a -pthread

# 3. 运行(input_size 需与模型一致)
./hair_benchmark models/yolov4-tiny-3l-hair-416-opt.param \
    models/yolov4-tiny-3l-hair-416-opt.bin \
    /tmp/bench_bmp dataset/yolo_det/labels 0.15 0.25 416

benchmark 输出:

复制代码
模型加载成功: models/yolov4-tiny-3l-hair-416-opt.param
输入尺寸=416, 匹配 IoU 阈值=0.15, 置信度阈值=0.25

========== 结果 ==========
有效样本数    : 311
真实框总数    : 1995
预测框总数    : 1878

--- 匹配(IoU>=0.15)---
TP 命中       : 1710
FP 误判       : 168
FN 漏检       : 285

--- 数量准确度(核心指标)---
平均单图数量误差: 0.2033
CountScore     : 0.7967   (1-平均数量误差, 越接近1越好)

--- 检测精度 ---
Precision      : 0.9105
Recall         : 0.8571
F1             : 0.8830
F2(侧重召回)   : 0.8673

--- 性能 ---
平均推理耗时   : 68.68 ms
==========================

6.5 三模型量化对比(311 样本)

指标 tiny @320 3l @320 3l @416
CountScore 0.4222 0.7745 0.7967
F1 0.5772 0.8468 0.8830
Recall 0.4170 0.8130 0.8571
Precision 0.9369 0.8834 0.9105
FP 误判 56 214 168
FN 漏检 1163 373 285
推理耗时 36.9ms 50.6ms 68.7ms

关键结论

  • 3l @416 全指标最优,CountScore 比 tiny 提升 89%
  • 高分辨率下 FP 反而从 214 降到 168(目标更清晰,误检减少)
  • 推理 68.7ms 在 2.0x 预算内

七、GUI 调试工具(Qt + WSLg)

7.1 功能设计

  • 打开文件夹 → 列出全部图片
  • 上一张/下一张按钮、列表点击、左右方向键切换
  • 实时 ncnn 推理,绘制检测框 + 置信度
  • 模型下拉框(hair / COCO)、置信度/IoU 阈值滑条

7.2 编译运行(host + WSLg)

bash 复制代码
cd gui_viewer
qmake && make -j$(nproc)
./gui_viewer /home/ncnn/dataset/ori_image

启动日志(模拟):

复制代码
model loaded: "/home/ncnn/gui_viewer/../models/yolov4-tiny-hair-opt.param"

WSLg 显示验证(Windows 桌面弹出窗口):

复制代码
$ xwininfo -root -tree -display :0 | grep 图片检测
0x600006 "图片检测浏览器 (ncnn YOLOv4-tiny)": ("gui_viewer" "gui_viewer")  1100x700+38+59

7.3 设计取舍

  • Qt Widgets 而非 QML:目标平台 A9 是 Qt5.7,Widgets 跨版本兼容最好
  • 仅用 Qt5.7 基础 API + C++11,保证 A9 交叉编译可复用

八、最终结果与经验总结

8.1 最终指标

指标 tiny @320(起点) 3l @416(终点) 提升
CountScore 0.4222 0.7967 +89%
F1 0.5772 0.8830 +53%
Recall 0.4170 0.8571 +105%
推理耗时 36.9ms 68.7ms +86%(预算内)

8.2 经验教训

  1. WSL 下跑 GPU 训练 :务必设置 LD_LIBRARY_PATH=/usr/lib/wsl/lib,否则 cublas 初始化失败
  2. 转换工具与训练框架的语义差异 :darknet2ncnn 处理 route groups 与 darknet 训练不一致,
    导致权重错位、推理输出 inf/NaN。逐层数值诊断是定位这类问题的最快方法
  3. 训练参数陷阱burn_in 不能大于 max_batches;小数据集要用低学习率 + mosaic 增强
  4. 小目标检测方法论 :多检测头(stride 8)比单纯加深网络更有效;分辨率提升带来
    双重收益(漏检减少 + 误检减少)
  5. 量化评估 :明确场景核心指标(数量准确度),设计对应的 benchmark(CountScore + 放宽 IoU),
    用数据决策而非直觉
  6. 预算约束:推理时间与计算量平方成正比(分辨率),选型前先做定量扫描

8.3 产物清单

复制代码
models/
  yolov4-tiny-3l-hair-416-opt.param  # 最终部署模型 param
  yolov4-tiny-3l-hair-416-opt.bin    # 最终部署模型 bin
verify/
  hair_benchmark.cpp   # 数量准确度 benchmark
  preprocess_bmp.py    # 图片预处理
  voc2yolo_det.py      # labelImg XML → YOLO
  merge_datasets.py    # 多来源数据合并
  batch_detect.py      # 批量检测
gui_viewer/            # Qt GUI 调试工具
相关推荐
计算机编程-吉哥14 小时前
深度学习:我用YOLO11-L做了一个水下垃圾检测系统 对比YOLOv8-L/Faster R-CNN【计算机毕业设计选题推荐】
人工智能·深度学习·yolo·课程设计·计算机毕业设计选题
AI棒棒牛16 小时前
YOLO26最新创新改进系列:融合 E3AD 认知注意力 Neck:具身认知增强的 FPN/PAN 特征选择机制,高效创新!
人工智能·yolo·计算机视觉·yolo26
hans汉斯18 小时前
数据挖掘|基于BP神经网络的少数民族村寨文化型旅游体验产品潜在游客挖掘
深度学习·神经网络·算法·yolo·软件工程·bp·汉斯出版社
JAI科研2 天前
YOLO 完全指南(七):YOLO识别工程化 (上)
人工智能·深度学习·神经网络·yolo·目标检测·计算机视觉·transformer
JarmanYuo2 天前
YOLO 涨点研究(十二):具身 CV 进阶篇——Sim2Real 域随机化与真机部署
人工智能·pytorch·python·yolo·计算机视觉
YOLO数据集集合2 天前
无人船视角水面目标检测数据集 |无人船 USV 水面目标检测 自主航行 避障 水域监测 目标检测9076期
深度学习·yolo·目标检测·无人机·无人机视角·无人船·自主航线
YOLO数据集集合2 天前
渔船船只检测数据集 | 船只检测 渔船识别 拖船检测 海事监管 目标检测 YOLO格式 深度学习数据集 计算机视觉9076期
人工智能·深度学习·yolo·目标检测·计算机视觉
AI吃大瓜3 天前
人脸检测和行人检测4:Android实现YOLOv8 YOLO11 YOLO26人脸检测和人体检测(含源码,可实时检测)
android·yolo·人脸检测·人体检测·行人检测·yolo26
飞猫的边缘AI3 天前
边缘AI-13:从YOLOv1到YOLO26:目标检测是怎么进化的
人工智能·yolo·目标检测·ai算法·边缘ai·yolo26
稷下元歌4 天前
工业视觉实战:从标注数据到格式转换到工业质检基础,yolo识别编
yolo