还是老板子:rk3399,需要进行目标检测,类似人脸框检测吧,看了下还是用yolov8吧,速度精度都还可以,开始准备选择是naodet 但是可能是我的数据质量问题吧,精度一直达不到,所以最后综合考虑还是选择了yolo家族的yolov8n,这里记录下训练过程方便后续记忆

对比项 YOLOv8n NanoDet
模型结构 更现代(Anchor-free + PAFPN) 轻量但较旧(基于 ShuffleNet/GhostNet)
精度上限 高(COCO mAP ~37.3) 中(COCO mAP ~20~25)
社区支持 官方维护(Ultralytics),生态完善 社区维护,更新慢
部署友好性 支持 ONNX / TensorRT / NCNN / OpenVINO 主要依赖 NCNN,RK3399 适配需手动
数据敏感度 对标注质量要求高,但容错更强 小模型对噪声更敏感,易欠拟合

地址:https://github.com/ultralytics/ultralytics

代码准备:git clone https://github.com/ultralytics/ultralytics.git

一、数据准备

首先准备数据,检测对象的数据,网络爬取或者现场采集,大约2000张各种不同种类的照片,比如最简单采集的人脸照片,这里不暴露项目内容,采用人脸替代吧。

1.1 数据标注

LabelImg  下载地址:https://github.com/HumanSignal/labelImg/releases

通过标注矩形框进行数据标准,可以先标注一部分,然后训练出模型,然后通过模型标注剩余的图片,然后再手工微调,这样数据标注就可以提速了,也可以在少数数据量的情况下用数据增强(原图旋转、翻转、加噪点、马赛克等)

数据标注之后会 一个xml文件

具体如下,就是把图片地址、尺寸及目标类型和矩形框的坐标进行标注了

<!-- 整个标注文档的根元素,表示这是一个目标检测的标注 -->
<annotation>

  <!-- 图像所属的文件夹名称(通常用于组织数据集结构) -->
  <folder>OXIIIT</folder>

  <!-- 图像文件名(不含路径) -->
  <filename>4000.jpg</filename>

  <!-- 图像来源信息 -->
  <source>
    <!-- 数据集名称 -->
    <database>Dataset</database>
    <!-- 标注类型或来源(此处为缩写 OXIIIT,可能指 Oxford-IIIT) -->
    <annotation>OXIIIT</annotation>
    <!-- 图像原始来源 -->
    <image>none</image>
  </source>

  <!-- 图像尺寸信息 -->
  <size>
    <!-- 图像宽度(像素) -->
    <width>500</width>
    <!-- 图像高度(像素) -->
    <height>667</height>
    <!-- 图像通道数(3 表示 RGB 彩色图) -->
    <depth>3</depth>
  </size>

  <!-- 是否包含分割掩码(1 表示有,0 表示无;目标检测任务通常为 0) -->
  <segmented>0</segmented>

  <!-- 一个目标对象的标注信息(可有多个 <object>) -->
  <object>
    <!-- 目标类别名称(此处为 "face",但注意:原 Oxford-IIIT Pet 数据集通常标注宠物品种,这里可能是自定义修改) -->
    <name>face</name>
    
    <!-- 目标姿态(如 Frontal、Left、Right 等,常用于人脸或物体朝向) -->
    <pose>Frontal</pose>
    
    <!-- 是否被截断(1 表示目标在图像边界被裁切,0 表示完整) -->
    <truncated>0</truncated>
    
    <!-- 是否被遮挡(1 表示部分不可见,0 表示完全可见) -->
    <occluded>0</occluded>
    
    <!-- 边界框坐标(以像素为单位) -->
    <bndbox>
      <!-- 左上角 x 坐标 -->
      <xmin>90</xmin>
      <!-- 左上角 y 坐标 -->
      <ymin>147</ymin>
      <!-- 右下角 x 坐标 -->
      <xmax>333</xmax>
      <!-- 右下角 y 坐标 -->
      <ymax>374</ymax>
    </bndbox>
    
    <!-- 是否为“难例”(1 表示模糊、极小或难以识别,通常训练时可忽略;0 表示正常样本) -->
    <difficult>0</difficult>
  </object>

</annotation>

1.2 数据格式转换

标注完成转换成yolov8的数据格式,yolov8默认的图片和标注位置对应

数据构造格式:
data
|__ images
    ├─ 001.jpg
    ├─ 002.jpg
    ├─ ..
    └─ NNN.jpg
|__ labels
    ├─ 001.txt
    ├─ 002.txt
    ├─ ..
    └─ NNN.txt

把xml格式转换为txt格式,txt格式如下

<class_id> <x_center> <y_center> <width> <height>

0 0.631667 0.287500 0.153333 0.215000
字段 含义
class_id 0 类别索引,表示第 0 类(例如:faceperson 等,具体看你的 data.yaml 中的 names 定义)
x_center 0.631667 目标边界框中心点的 x 坐标(归一化)
y_center 0.287500 目标边界框中心点的 y 坐标(归一化)
width 0.153333 边界框宽度(归一化)
height 0.215000 边界框高度(归一化)

提供一段转换代码

import io
import os
import shutil
import xml.etree.ElementTree as ET
from pathlib import Path


def convert_voc_to_yolo(xml_file, output_dir,class_mapping=None):
    """
    将单个 Pascal VOC XML 文件转换为 YOLO 格式的 .txt 标签文件

    Args:
        xml_file: 输入 XML 文件路径
        output_dir: 输出 .txt 文件目录
        class_mapping: 类别名到 ID 的映射,例如 {"cat": 0}
    """
    if class_mapping is None:
        class_mapping = {"cat": 0, "dog": 1}  # 默认只处理 cat -> 0

    tree = ET.parse(xml_file)
    root = tree.getroot()

    # 获取图像尺寸
    size = root.find('size')
    if size is None:
        raise ValueError(f"XML {xml_file} 缺少 <size> 标签")
    img_w = int(size.find('width').text)
    img_h = int(size.find('height').text)

    lines = []
    for obj in root.findall('object'):
        name = obj.find('name').text
        if name not in class_mapping:
            continue  # 跳过非目标类别(如 dog)
        bndbox = obj.find('bndbox')
        xmin = float(bndbox.find('xmin').text)
        ymin = float(bndbox.find('ymin').text)
        xmax = float(bndbox.find('xmax').text)
        ymax = float(bndbox.find('ymax').text)

        # 转换为 YOLO 格式
        x_center = (xmin + xmax) / 2.0 / img_w
        y_center = (ymin + ymax) / 2.0 / img_h
        width = (xmax - xmin) / img_w
        height = (ymax - ymin) / img_h

        class_id = class_mapping[name]
        lines.append(f"{class_id} {x_center:.6f} {y_center:.6f} {width:.6f} {height:.6f}")

    # 写入 .txt 文件(与 XML 同名)
    output_path = output_dir + '\\' + filename + ".txt"
    with open(output_path, 'w') as f:
        f.write("\n".join(lines))
    return filename


#获取目录下的所有文件
def get_all_files_os_walk(root_dir):
    all_files = []
    for dirpath, dirnames, filenames in os.walk(root_dir):
        for filename in filenames:
            full_path = os.path.join(dirpath, filename)
            all_files.append(full_path)
    return all_files

#获取文件名称
def get_all_filenames_os_walk(root_dir):
    filenames = []
    for dirpath, dirnames, files in os.walk(root_dir):
        for file in files:
            filenames.append(file)  # 只取文件名,不含路径
    return filenames


#拷贝原始文件到目的
def copy(src, path):
    pass

cur_path = r'F:\work\code\python\yolov8\ultralytics-train\ultralytics\data'

cur_path_img_images = cur_path +r'\labels\test2017\img'
cur_path_label_xmls = cur_path +r'\labels\test2017\xml'

cur_path_img_train = cur_path +r'\images\train'
cur_path_label_train = cur_path +r'\labels\train'
cur_path_label_val = cur_path +r'\labels\val'
filenames = get_all_filenames_os_walk(cur_path_img_images)

#遍历
total_index = 0
for filename in filenames:
    itmes = filename.split('.')
    xmlFileName = cur_path_label_xmls+'\\'+itmes[0]+'.xml'
    print('filename:'+str(total_index))
    if (itmes[1] not in {"jpg","png","JPEG","PNG"} ):
        continue
    if os.path.exists(xmlFileName):
        index = 0
        hasType = False
        with open(xmlFileName, "r", encoding="utf-8") as f:
            content = f.read() # 返回 list,每行末尾保留 \n
        content = str(content)
        if content is None:
            shutil.copy2(cur_path_img_images + '\\' + filename,
                         cur_path_img_train + "\\no_" + str(total_index) + '.' + itmes[1])
            total_index += 1
            continue
        if content.find('face') >= 0:
            print('face')
            continue
        else:
            shutil.copy2(cur_path_img_images + '\\' + filename,
                         cur_path_img_train + "\\no_" + str(total_index) + '.' + itmes[1])
            total_index += 1
            continue
        # 拷贝文件(保留元数据,如修改时间)
        objname = convert_voc_to_yolo(xmlFileName, cur_path_label_train)
        shutil.copy2(cur_path_img_images+'\\'+filename, cur_path_img_train+"\\"+objname+'.'+itmes[1])
        print(cur_path_img_images+'\\'+filename)
        total_index += 1
    else:
        shutil.copy2(cur_path_img_images+'\\'+filename, cur_path_img_train+"\\no_"+str(total_index) +'.'+itmes[1])
        total_index += 1

print('it is ok')

修改对应的目录,即可

  1. 遍历 test2017/xml 目录下的所有 Pascal VOC 格式(.xml)标注文件
  2. 将每个 .xml 文件中的目标框信息转换为 YOLO 格式的归一化坐标(.txt
  3. 将对应的图像文件(位于 test2017/img)复制到输出目录 images/train/
  4. 将生成的 YOLO 标注文件保存到 labels/train/ 目录下,文件名与图像一致
  5. 自动创建目标目录(若不存在),并确保图像与标签一一对应

原始目录

test2017/
├── img/
│   ├── 0001.jpg
│   ├── 0002.jpg
│   └── ...
└── xml/
    ├── 0001.xml
    ├── 0002.xml
    └── ...

输出目录

images/
└── train/
    ├── 0001.jpg
    ├── 0002.jpg
    └── ...
labels/
└── train/
    ├── 0001.txt
    ├── 0002.txt
    └── ...

到这里数据格式就转换好了。

二、训练

2.1 修改配置文件

我的数据放到 F:/work/code/python/yolov8/ultralytics-train/data下面

我在这里创建一个yaml的配置文件:face_yolov8n.yaml

# Ultralytics 🚀 AGPL-3.0 License - https://ultralytics.com/license

# COCO8 dataset (first 8 images from COCO train2017) by Ultralytics
# Documentation: https://docs.ultralytics.com/datasets/detect/coco8/
# Example usage: yolo train data=coco8.yaml
# parent
# ├── ultralytics
# └── datasets
#     └── coco8 ← downloads here (1 MB)

# Train/val/test sets as 1) dir: path/to/imgs, 2) file: path/to/imgs.txt, or 3) list: [path/to/imgs1, path/to/imgs2, ..]
# 数据集根目录路径(所有图像和标签路径都相对于此路径)
path: F:/work/code/python/yolov8/ultralytics-train/data  # dataset root dir
# 训练集图像所在目录(相对 path 的路径)
train: images/train_q  # train images (relative to 'path') 4 images
# 验证集图像所在目录(相对 path 的路径)
val: images/val        # val images (relative to 'path') 4 images
# 测试集图像所在目录(可选,若不使用测试集可删除或注释掉)
test: images/test      # test images (optional)
# 类别名称列表,按类别索引(class_id)顺序定义
# 注意:YOLOv8 要求此处使用字典格式 {id: name},且 id 必须从 0 开始连续
names:
  0: face              # 类别 ID 0 对应的类别名称为 "face"
  • 实际训练图像路径为:F:/work/code/python/yolov8/ultralytics-train/data/images/train/xxx.jpg

  • 标签文件位置:YOLOv8 会自动在与 images 同级的 labels 目录下查找对应 .txt 文件。
    即:若图像在 images/train/001.jpg,则标签应位于 labels/train/001.txt

2.2 环境

python:3.10
安装
pip install ultralytics
pip install polars

2.3 训练代码

这边选择的是yolov8n.pt,图片大小选择320x320

from ultralytics import YOLO

# 加载预训练模型
model = YOLO('yolov8n.pt')


path = r"F:\work\code\python\yolov8\ultralytics-train\ultralytics\data"
datayaml = path +  "/data/face_yolov8n.yaml"

# Train the model on the COCO8 dataset for 100 epochs
train_results = model.train(
    data=datayaml,  # Path to dataset configuration file
    epochs=100,  # Number of training epochs
    imgsz=320,  # Image size for training
    lr0=0.001,  # 降低学习率,避免破坏预训练特征
    batch=32,
    device=0,  # Device to run on (e.g., 'cpu', 0, [0,1,2,3])
    name='face_detection'
)

# Evaluate the model's performance on the validation set
metrics = model.val()

# Perform object detection on an image
# results = model(path+"data/images/test/face_300.jpg")  # Predict on an image
# results[0].show()  # Display results

# Export the model to ONNX format for deployment
path = model.export(format="onnx")  # Returns the path to the exported model

执行完成后,会在runs-yolov8n_320_320目录下包含训练后的模型

F:\work\code\python\yolov8\ultralytics-train\ultralytics\runs-yolov8n_320_320\detect\face_detection4\weights 下面包含有

beast.pt 最优的模型

last.pt 最后生成的模型

一般都是选择beast.pt ,训练选择last.pt

2.4 推理验证

把模型拷贝到跟目录F:\work\code\python\yolov8\ultralytics-train\ultralytics,然后模型名称修改为

yolov8n_beast.pt

推理一张图片,是否正常显示画脸框

import cv2
import torch
from matplotlib import pyplot as plt

from ultralytics import YOLO

# 加载预训练模型(自动下载 if not exists)
model = YOLO("yolov8n_beast.pt")


# 推理(支持单张图、多图、视频、URL 等)
print(model .model.nc)  # 应该输出 2
# 方法1:通过 Ultralytics API 查看
print("Detect layer output channels (cv3):", [m[-1].out_channels for m in model .model.model[-1].cv3])

# 方法2:直接读取 state_dict
ckpt = torch.load("yolov8n_beast.pt", map_location="cpu")
print("Checkpoint nc:", ckpt['model'].nc if 'model' in ckpt else "Not found")

# 检查检测头最后卷积层的权重形状(应为 [2, ...])
if 'model' in ckpt:
    state_dict = ckpt['model'].state_dict()
    for k, v in state_dict.items():
        if 'cv3' in k and 'weight' in k:
            print(f"{k}: {v.shape}")  # 应该看到 torch.Size([2, ..., ..., ...])

results = model.predict(
    source=r"F:\work\code\python\yolov8\ultralytics-train\ultralytics\data\images\test\1.jpg",   # 输入源
    conf=0.25,            # 置信度阈值
    iou=0.45,             # NMS 的 IoU 阈值
    save=True,            # 是否保存结果
    show=False,           # 是否实时显示(需 GUI 环境)
    device="cpu"          # 或 "cuda:0"
)

# 遍历结果(每张图一个 result 对象)
for result in results:
    boxes = result.boxes      # 边界框
    masks = result.masks      # 分割掩码(如果模型支持)
    probs = result.probs      # 分类概率(分类任务)
    orig_img = result.orig_img  # 原始图像 (HWC, BGR)
    annotated_frame = result.plot()
    # 转 BGR -> RGB(因为 OpenCV 是 BGR,matplotlib 是 RGB)
    rgb_img = cv2.cvtColor(annotated_frame, cv2.COLOR_BGR2RGB)
    plt.figure(figsize=(12, 8))
    plt.imshow(rgb_img)
    plt.axis("off")
    plt.show()

    # 打印检测到的类别和坐标
    for box in boxes:
        cls_id = int(box.cls.item())
        conf = float(box.conf.item())
        xyxy = box.xyxy[0].tolist()  # [x1, y1, x2, y2]
        print(f"Class: {cls_id}, Conf: {conf:.2f}, Box: {xyxy}")

三、部署

如果部署到rk板子,有几种选择,有npu的部署为rknn的模型,如果没有如rk3399 没有npu,那么部署onnx 或者ncnn模型,这里推荐ncnn,腾讯开源多年比较成熟。

3.1 环境

安装环境: 
pip3 install -U ultralytics pnnx ncnn 
创建ncnn目录 mkdir ncnn && cd ncnn

3.2 导出torchscript

把模型修改为yolov8n.pt 拷贝到ncnn下面

执行命令

yolo export model=yolov8n.pt format=torchscript

导出了模型:yolov8n.torchscript

pnnx yolov8n.torchscript

3.3 修改pnnx文件

编辑 yolov8n_pnnx.py

修改如下

修改为

v_168后面直接return掉,然后后面的删除掉,这里去掉后,那么nms后面执行部分就需要再代码里面处理了,这样可以优化速度,不去除那么直接返回分类结果

3.4 重新导出yolov8 ncnn

执行如下命令重新导出,ncnn

这里我的文件是yolov8n_pnnx.py,所以import 这个名称,如果是其他的请导入其他名称

python -c "import yolov8n_pnnx; yolov8n_pnnx.export_torchscript()"
python -c "import yolov8n_pnnx; yolov8n_pnnx.export_ncnn()"

然后获取最新的ncnn文件

后面部署到板子或者android请参考:https://zhuanlan.zhihu.com/p/16030630352

这里最新已经不需要先转onnx再转ncnn,挺方便一步到位ncnn

后续:在rk3399的android下面发现,执行需要大概300ms左右才能执行一帧,速度有点慢,后面再试试剪枝、蒸馏,量化好像之前试过不加速,反量化还可能耗费时间。

Logo

腾讯云面向开发者汇聚海量精品云计算使用和开发经验,营造开放的云计算技术生态圈。

更多推荐