YOLOv8 姿态估计模型的 OpenVINO 部署——从 IR 格式转换、精度评估到 NNCF 量化部署及性能对比

部署运行你感兴趣的模型镜像

摘要

本文详细阐述了如何将 YOLOv8 姿态估计模型转换为 OpenVINO IR 格式,并利用 NNCF 工具进行量化8位量化优化,以提升推理推理性能。文中系统介绍了模型转换、推理验证、精度评估及量化优化的完整流程,并通过实验对比了原始模型与量化模型的性能差异,为计算机视觉领域的姿态部署提供了可行的技术方案。

前提条件

在开始之前,需安装必要的依赖包以确保后续实验的顺利进行:

%pip install -q "openvino>=2023.1.0" "nncf>=2.5.0" "protobuf==3.20.*" "torch>=2.1" "torchvision>=0.16" "ultralytics==8.0.159" "onnx" --extra-index-url https://download.pytorch.org/whl/cpu

导入所需的工具函数,包括从 GitHub 仓库获取辅助脚本:

from pathlib import Path

# 从 openvino_notebooks 仓库获取 notebook utils 脚本
import urllib.request
urllib.request.urlretrieve(
    url='https://raw.githubusercontent.com/openvinotoolkit/openvino_notebooks/main/notebooks/utils/notebook_utils.py',
    filename='notebook_utils.py'
)

from notebook_utils import download_file, VideoPlayer

结果可视化工具函数

为了直观展示模型推理结果,定义以下用于绘制边界框和关键点的工具函数:

from typing import Tuple, Dict
import cv2
import numpy as np
import random
from PIL import Image
from ultralytics.utils.plotting import colors

def plot_one_box(box:np.ndarray, img:np.ndarray, color:Tuple[int, int, int] = None, keypoints:np.ndarray = None, label:str = None, line_thickness:int = 5):
    """
    辅助函数,用于在图像上绘制单个边界框及关键点
    
    参数:
        box (np.ndarray):边界框坐标,格式为 [x1, y1, x2, y2]
        img (np.ndarray):输入图像
        color (Tuple[int, int, int],可选):用于绘制边界框的 BGR 格式颜色,未指定则随机选择
        keypoints (np.ndarray,可选):关键点格式为 [x1, y1, s],其中 x1, y1 为坐标,s 为置信度分数
        label (str,可选):边界框标签字符串
        line_thickness (int,可选):边界框绘制线条的厚度,默认值为5
    """
    # 计算线宽和字体厚度
    tl = line_thickness or round(0.002 * (img.shape[0] + img.shape[1]) / 2) + 1
    color = color or [random.randint(0, 255) for _ in range(3)]
    c1, c2 = (int(box[0]), int(box[1])), (int(box[2]), int(box[3]))
    cv2.rectangle(img, c1, c2, color, thickness=tl, lineType=cv2.LINE_AA)
    
    # 绘制标签
    if label:
        tf = max(tl - 1, 1)
        t_size = cv2.getTextSize(label, 0, fontScale=tl / 3, thickness=tf)[0]
        c2 = c1[0] + t_size[0], c1[1] - t_size[1] - 3
        cv2.rectangle(img, c1, c2, color, -1, cv2.LINE_AA)
        cv2.putText(img, label, (c1[0], c1[1] - 2), 0, tl / 3, [225, 255, 255], thickness=tf, lineType=cv2.LINE_AA)
    
    # 绘制关键点和骨架
    if keypoints is not None:
        # 关键点颜色配置
        kpt_color = colors.pose_palette[[16, 16, 16, 16, 16, 0, 0, 0, 0, 0, 0, 9, 9, 9, 9, 9, 9]]
        # 人体骨架连接关系
        skeleton = [[16, 14], [14, 12], [17, 15], [15, 13], [12, 13], [6, 12], [7, 13], [6, 7], [6, 8],
                    [7, 9], [8, 10], [9, 11], [2, 3], [1, 2], [1, 3], [2, 4], [3, 5], [4, 6], [5, 7]]
        limb_color = colors.pose_palette[[9, 9, 9, 9, 7, 7, 7, 0, 0, 0, 0, 0, 16, 16, 16, 16, 16, 16, 16]]
        shape = img.shape[:2]
        
        # 绘制关键点
        for i, k in enumerate(keypoints):
            color_k = [int(x) for x in kpt_color[i]]
            x_coord, y_coord = k[0], k[1]
            if x_coord % shape[1] != 0 and y_coord % shape[0] != 0:
                if len(k) == 3 and k[2] < 0.5:
                    continue
                cv2.circle(img, (int(x_coord), int(y_coord)), 5, color_k, -1, lineType=cv2.LINE_AA)
        
        # 绘制骨架连接
        ndim = keypoints.shape[-1]
        for i, sk in enumerate(skeleton):
            pos1 = (int(keypoints[(sk[0] - 1), 0]), int(keypoints[(sk[0] - 1), 1]))
            pos2 = (int(keypoints[(sk[1] - 1), 0]), int(keypoints[(sk[1] - 1), 1]))
            if ndim == 3:
                conf1 = keypoints[(sk[0] - 1), 2]
                conf2 = keypoints[(sk[1] - 1), 2]
                if conf1 < 0.5 or conf2 < 0.5:
                    continue
            if pos1[0] % shape[1] == 0 or pos1[1] % shape[0] == 0 or pos1[0] < 0 or pos1[1] < 0:
                continue
            if pos2[0] % shape[1] == 0 or pos2[1] % shape[0] == 0 or pos2[0] < 0 or pos2[1] < 0:
                continue
            cv2.line(img, pos1, pos2, [int(x) for x in limb_color[i]], thickness=2, lineType=cv2.LINE_AA)
    
    return img

def draw_results(results:Dict, source_image:np.ndarray, label_map:Dict):
    """
    辅助函数,用于在图像上绘制所有检测结果
    
    参数:
        results (Dict):检测结果,包含边界框和关键点信息
        source_image (np.ndarray):用于绘制的输入图像
        label_map (Dict[int, str]):标签 ID 到类别名称的映射
    """
    boxes = results["box"]
    keypoints = results.get("kpt")
    h, w = source_image.shape[:2]
    for idx, (*xyxy, conf, lbl) in enumerate(boxes):
        if conf < 0.4:  # 过滤低置信度结果
            continue
        label = f'{label_map[0]} {conf:.2f}'
        kp = keypoints[idx] if keypoints is not None else None
        source_image = plot_one_box(xyxy, source_image, keypoints=kp, label=label, color=colors(int(lbl)), line_thickness=1)
    return source_image

测试数据准备

下载测试图像用于后续模型推理验证:

# 下载测试样本
IMAGE_PATH = Path('./data/intel_rnb.jpg')
download_file(
    url='https://storage.openvinotoolkit.org/repositories/openvino_notebooks/data/data/image/intel_rnb.jpg',
    filename=IMAGE_PATH.name,
    directory=IMAGE_PATH.parent
)
'data/intel_rnb.jpg' 已存在。
PosixPath('/home/ea/work/openvino_notebooks/notebooks/230-yolov8-optimization/data/intel_rnb.jpg')

模型实例化与原始推理

加载预训练模型

YOLOv8 提供了便捷的模型加载接口,支持从本地路径或模型中心加载预训练模型。加载模型后,可直接对图像进行推理并可视化结果:

models_dir = Path('./models')
models_dir.mkdir(exist_ok=True)  # 创建模型保存目录
from ultralytics import YOLO

POSE_MODEL_NAME = "yolov8n-pose"

# 加载 YOLOv8 姿态估计模型
pose_model = YOLO(models_dir / f'{POSE_MODEL_NAME}.pt')
label_map = pose_model.model.names  # 获取类别标签映射

# 对测试图像进行推理
res = pose_model(IMAGE_PATH)
Image.fromarray(res[0].plot()[:, :, ::-1])  # 转换颜色通道并显示结果
image 1/1 /home/ea/work/openvino_notebooks/notebooks/230-yolov8-optimization/data/intel_rnb.jpg: 480x640 1 person, 52.6ms
Speed: 2.1ms preprocess, 52.6ms inference, 1.3ms postprocess per image at shape (1, 3, 480, 640)

YOLOv8 原始模型推理结果

模型转换为 OpenVINO IR 格式

OpenVINO IR(Intermediate Representation)是一种针对深度学习模型的中间表示格式,能够优化模型在 Intel 硬件上的推理性能。YOLOv8 提供了直接导出为 OpenVINO IR 格式的 API:

# 定义转换后的模型路径
pose_model_path = models_dir / f"{POSE_MODEL_NAME}_openvino_model/{POSE_MODEL_NAME}.xml"

# 如果模型未转换,则执行转换
if not pose_model_path.exists():
    pose_model.export(format="openvino", dynamic=True, half=False)

OpenVINO 模型推理验证

为了确保转换后的模型能够正常工作,需要构建完整的推理管道,包括图像预处理、模型推理和结果后处理三个关键步骤。

图像预处理

YOLOv8 模型对输入图像有特定要求,需要进行尺寸调整、通道转换和归一化等预处理操作:

from typing import Tuple
import torch
import numpy as np

def letterbox(img: np.ndarray, new_shape:Tuple[int, int] = (640, 640), color:Tuple[int, int, int] = (114, 114, 114), auto:bool = False, scale_fill:bool = False, scaleup:bool = False, stride:int = 32):
    """
    为检测任务调整图像大小并填充,保持原始宽高比
    
    参数:
      img (np.ndarray):输入图像
      new_shape (Tuple(int, int)):目标图像大小,格式为 [高度,宽度]
      color (Tuple(int, int, int)):填充区域的颜色
      auto (bool):是否使用动态输入大小,仅填充以满足步长约束
      scale_fill (bool):是否拉伸图像以填充新形状
      scaleup (bool):是否允许放大图像
      stride (int):输入填充步长
      
    返回:
      img (np.ndarray):预处理后的图像
      ratio (Tuple(float, float)):高度和宽度的缩放比例
      padding_size (Tuple(int, int)):高度和宽度的填充大小
    """
    shape = img.shape[:2]  # 当前形状 [高度,宽度]
    if isinstance(new_shape, int):
        new_shape = (new_shape, new_shape)

    # 计算缩放比例
    r = min(new_shape[0] / shape[0], new_shape[1] / shape[1])
    if not scaleup:  # 仅缩小,不放大以保持精度
        r = min(r, 1.0)

    # 计算填充大小
    ratio = r, r  # 宽度和高度比例
    new_unpad = int(round(shape[1] * r)), int(round(shape[0] * r))
    dw, dh = new_shape[1] - new_unpad[0], new_shape[0] - new_unpad[1]
    
    if auto:  # 最小矩形填充
        dw, dh = np.mod(dw, stride), np.mod(dh, stride)
    elif scale_fill:  # 拉伸填充
        dw, dh = 0.0, 0.0
        new_unpad = (new_shape[1], new_shape[0])
        ratio = new_shape[1] / shape[1], new_shape[0] / shape[0]

    dw /= 2  # 填充分为两侧
    dh /= 2

    # 调整图像大小
    if shape[::-1] != new_unpad:
        img = cv2.resize(img, new_unpad, interpolation=cv2.INTER_LINEAR)
    
    # 添加边界填充
    top, bottom = int(round(dh - 0.1)), int(round(dh + 0.1))
    left, right = int(round(dw - 0.1)), int(round(dw + 0.1))
    img = cv2.copyMakeBorder(img, top, bottom, left, right, cv2.BORDER_CONSTANT, value=color)
    return img, ratio, (dw, dh)

def preprocess_image(img0: np.ndarray):
    """
    根据 YOLOv8 输入要求预处理图像
    
    参数:
      img0 (np.ndarray):原始图像
      
    返回:
      img (np.ndarray):预处理后的图像,格式为 CHW
    """
    # 调整大小
    img = letterbox(img0)[0]
    # 转换通道布局 HWC -> CHW
    img = img.transpose(2, 0, 1)
    img = np.ascontiguousarray(img)
    return img

def image_to_tensor(image:np.ndarray):
    """
    将预处理后的图像转换为模型输入张量
    
    参数:
      image (np.ndarray):预处理后的图像
      
    返回:
      input_tensor (np.ndarray):输入张量,格式为 NCHW,值范围 [0, 1]
    """
    input_tensor = image.astype(np.float32)  # 转换为 float32
    input_tensor /= 255.0  # 归一化到 [0.0, 1.0]

    # 添加批量维度
    if input_tensor.ndim == 3:
        input_tensor = np.expand_dims(input_tensor, 0)
    return input_tensor

结果后处理

模型输出需要经过非最大抑制(NMS)等后处理步骤,以过滤冗余框并将坐标映射回原始图像尺寸:

from ultralytics.utils import ops

def postprocess(
    pred_boxes:np.ndarray,
    input_hw:Tuple[int, int],
    orig_img:np.ndarray,
    min_conf_threshold:float = 0.25,
    nms_iou_threshold:float = 0.45,
    agnostic_nms:bool = False,
    max_detections:int = 80,
):
    """
    YOLOv8 模型后处理函数
    
    参数:
        pred_boxes (np.ndarray):模型输出的预测框
        input_hw (np.ndarray):预处理后的图像尺寸
        orig_image (np.ndarray):原始图像
        min_conf_threshold (float):置信度阈值
        nms_iou_threshold (float):NMS 的 IoU 阈值
        agnostic_nms (bool):是否应用类别无关的 NMS
        max_detections (int):NMS 后保留的最大检测数量
        
    返回:
       pred (List[Dict[str, np.ndarray]]):包含检测框和关键点的字典列表
    """
    nms_kwargs = {"agnostic": agnostic_nms, "max_det": max_detections}
    # 应用非最大抑制
    preds = ops.non_max_suppression(
        torch.from_numpy(pred_boxes),
        min_conf_threshold,
        nms_iou_threshold,
        nc=1,
        **nms_kwargs
    )

    results = []
    kpt_shape = [17, 3]  # 17个关键点,每个包含x,y,score
    
    for i, pred in enumerate(preds):
        shape = orig_img[i].shape if isinstance(orig_img, list) else orig_img.shape
        # 将框坐标缩放回原始图像尺寸
        pred[:, :4] = ops.scale_boxes(input_hw, pred[:, :4], shape).round()
        # 处理关键点
        pred_kpts = pred[:, 6:].view(len(pred), *kpt_shape) if len(pred) else pred[:, 6:]
        pred_kpts = ops.scale_coords(input_hw, pred_kpts, shape)
        results.append({"box": pred[:, :6].numpy(), 'kpt': pred_kpts.numpy()})

    return results

推理设备选择

OpenVINO 支持多种硬件设备进行推理,可通过以下方式选择合适的设备:

import ipywidgets as widgets
import openvino as ov

core = ov.Core()

# 创建设备选择下拉菜单
device = widgets.Dropdown(
    options=core.available_devices + ["AUTO"],
    value='AUTO',
    description='Device:',
    disabled=False,
)

device
Dropdown(description='Device:', index=2, options=('CPU', 'GPU', 'AUTO'), value='AUTO')

单图像推理测试

构建完整的推理函数并测试转换后的 OpenVINO 模型:

core = ov.Core()
# 读取 OpenVINO 模型
pose_ov_model = core.read_model(pose_model_path)
# 对于非 CPU 设备,固定输入形状以优化性能
if device.value != "CPU":
    pose_ov_model.reshape({0: [1, 3, 640, 640]})
# 编译模型到指定设备
pose_compiled_model = core.compile_model(pose_ov_model, device.value)

def detect(image:np.ndarray, model:ov.Model):
    """
    OpenVINO YOLOv8 模型推理函数
    
    参数:
        image (np.ndarray):输入图像
        model (Model):OpenVINO 编译模型
        
    返回:
        detections (np.ndarray):包含检测框和关键点的字典列表
    """
    preprocessed_image = preprocess_image(image)
    input_tensor = image_to_tensor(preprocessed_image)
    result = model(input_tensor)
    boxes = result[model.output(0)]
    input_hw = input_tensor.shape[2:]
    detections = postprocess(pred_boxes=boxes, input_hw=input_hw, orig_img=image)
    return detections

# 加载测试图像并进行推理
input_image = np.array(Image.open(IMAGE_PATH))
detections = detect(input_image, pose_compiled_model)[0]
image_with_boxes = draw_results(detections, input_image, label_map)

Image.fromarray(image_with_boxes)

OpenVINO 模型推理结果

实验结果表明,转换后的 OpenVINO 模型能够产生与原始模型一致的推理结果,验证了模型转换的正确性。

模型精度评估

为了科学衡量模型性能,需要在标准数据集上进行精度评估。YOLOv8 预训练模型基于 COCO 数据集训练,因此使用该数据集的验证集进行评估。

验证数据集准备

下载 COCO 验证集及对应的标签文件:

from zipfile import ZipFile

# 数据集和配置文件 URL
DATA_URL = "http://images.cocodataset.org/zips/val2017.zip"
LABELS_URL = "https://github.com/ultralytics/yolov5/releases/download/v1.0/coco2017labels-segments.zip"
CFG_URL = "https://raw.githubusercontent.com/ultralytics/ultralytics/8ebe94d1e928687feaa1fee6d5668987df5e43be/ultralytics/datasets/coco-pose.yaml"

OUT_DIR = Path('./datasets')

# 定义文件路径
DATA_PATH = OUT_DIR / "val2017.zip"
LABELS_PATH = OUT_DIR / "coco2017labels-segments.zip"
CFG_PATH = OUT_DIR / "coco-pose.yaml"

# 下载文件
download_file(DATA_URL, DATA_PATH.name, DATA_PATH.parent)
download_file(LABELS_URL, LABELS_PATH.name, LABELS_PATH.parent)
download_file(CFG_URL, CFG_PATH.name, CFG_PATH.parent)

# 解压文件(如果尚未解压)
if not (OUT_DIR / "coco/labels").exists():
    with ZipFile(LABELS_PATH , "r") as zip_ref:
        zip_ref.extractall(OUT_DIR)
    with ZipFile(DATA_PATH , "r") as zip_ref:
        zip_ref.extractall(OUT_DIR / 'coco/images')
'datasets/val2017.zip' 已存在。
'datasets/coco2017labels-segments.zip' 已存在。
datasets/coco-pose.yaml:   0%|          | 0.00/781 [00:00<?, ?B/s]

精度评估函数

定义用于模型精度评估的函数,计算精确率(Precision)、召回率(Recall)和平均精度(mAP)等指标:

from tqdm.notebook import tqdm
from ultralytics.utils.metrics import ConfusionMatrix

def test(model:ov.Model, core:ov.Core, data_loader:torch.utils.data.DataLoader, validator, num_samples:int = None):
    """
    OpenVINO YOLOv8 模型精度验证函数
    
    参数:
        model (Model):OpenVINO 模型
        data_loader (torch.utils.data.DataLoader):数据集加载器
        validator:验证器类实例
        num_samples (int,可选):验证样本数量,None 表示使用全部样本
        
    返回:
        stats: (Dict[str, float]) - 包含聚合精度指标的字典
    """
    # 初始化验证器状态
    validator.seen = 0
    validator.jdict = []
    validator.stats = []
    validator.batch_i = 1
    validator.confusion_matrix = ConfusionMatrix(nc=validator.nc)
    
    # 调整模型输入形状并编译
    model.reshape({0: [1, 3, -1, -1]})
    compiled_model = core.compile_model(model)
    
    # 迭代数据集进行验证
    for batch_i, batch in enumerate(tqdm(data_loader, total=num_samples)):
        if num_samples is not None and batch_i == num_samples:
            break
        batch = validator.preprocess(batch)
        results = compiled_model(batch["img"])
        preds = torch.from_numpy(results[compiled_model.output(0)])
        preds = validator.postprocess(preds)
        validator.update_metrics(preds, batch)
    
    # 获取并返回统计结果
    stats = validator.get_stats()
    return stats

def print_stats(stats:np.ndarray, total_images:int, total_objects:int):
    """
    打印精度统计信息的辅助函数
    
    参数:
        stats: (Dict[str, float]) - 精度指标字典
        total_images (int) - 评估图像数量
        total_objects (int) - 总对象数量
    """
    print("Boxes:")
    mp, mr, map50, mean_ap = stats['metrics/precision(B)'], stats['metrics/recall(B)'], stats['metrics/mAP50(B)'], stats['metrics/mAP50-95(B)']
    # 打印表头
    s = ('%20s' + '%12s' * 6) % ('Class', 'Images', 'Labels', 'Precision', 'Recall', 'mAP@.5', 'mAP@.5:.95')
    print(s)
    # 打印结果
    pf = '%20s' + '%12i' * 2 + '%12.3g' * 4
    print(pf % ('all', total_images, total_objects, mp, mr, map50, mean_ap))
    
    # 如果存在关键点指标,则打印
    if 'metrics/precision(M)' in stats:
        s_mp, s_mr, s_map50, s_mean_ap = stats['metrics/precision(M)'], stats['metrics/recall(M)'], stats['metrics/mAP50(M)'], stats['metrics/mAP50-95(M)']
        s = ('%20s' + '%12s' * 6) % ('Class', 'Images', 'Labels', 'Precision', 'Recall', 'mAP@.5', 'mAP@.5:.95')
        print(s)
        pf = '%20s' + '%12i' * 2 + '%12.3g' * 4
        print(pf % ('all', total_images, total_objects, s_mp, s_mr, s_map50, s_mean_ap))

配置验证器和数据加载器

使用 YOLOv8 提供的验证器类进行精度评估,配置相关参数并创建数据加载器:

from ultralytics.utils import DEFAULT_CFG
from ultralytics.cfg import get_cfg
from ultralytics.data.utils import check_det_dataset

# 配置验证参数
args = get_cfg(cfg=DEFAULT_CFG)
args.data = 'coco8-pose.yaml'
args.model = 'yolov8n-pose.pt'
from ultralytics.models.yolo.pose import PoseValidator

# 创建姿态估计验证器实例
pose_validator = PoseValidator(args=args)
# 检查数据集并创建数据加载器
pose_validator.data = check_det_dataset(args.data)
pose_data_loader = pose_validator.get_dataloader("datasets/coco8-pose", 1)
val: Scanning datasets/coco8-pose/labels/train.cache... 8 images, 0 backgrounds, 0 corrupt: 100%|██████████| 8/8 [00:00<?, ?it/s]
from ultralytics.utils.metrics import OKS_SIGMA

# 配置验证器参数
pose_validator.is_coco = True
pose_validator.names = pose_model.model.names
pose_validator.metrics.names = pose_validator.names
pose_validator.nc = pose_model.model.model[-1].nc
pose_validator.sigma = OKS_SIGMA  # 关键点评估的 OKS  sigma 参数

执行精度评估

为了减少评估时间,可以指定评估样本数量。在实际应用中,建议使用完整数据集进行评估以获得准确结果:

NUM_TEST_SAMPLES = 300  # 评估样本数量,None 表示使用全部样本
# 对 OpenVINO 模型进行精度评估
fp_pose_stats = test(pose_ov_model, core, pose_data_loader, pose_validator, num_samples=NUM_TEST_SAMPLES)
0%|          | 0/300 [00:00<?, ?it/s]
# 打印评估结果
print_stats(fp_pose_stats, pose_validator.seen, pose_validator.nt_per_class.sum())
Boxes:
               Class      Images      Labels   Precision      Recall      mAP@.5  mAP@.5:.95
                 all           8          21           1         0.9       0.955       0.736

评估指标说明:

  • Precision(精确率):模型预测为正的样本中实际为正的比例,衡量模型识别相关对象的精确程度。
  • Recall(召回率):实际为正的样本中被模型正确预测的比例,衡量模型检测所有真实对象的能力。
  • mAP@.5:在 IoU 阈值为 0.5 时计算的平均精度均值,反映模型在中等重叠要求下的性能。
  • mAP@.5:.95:在 IoU 阈值从 0.5 到 0.95(步长 0.05)范围内计算的平均精度均值,综合反映模型在不同重叠要求下的性能。

使用 NNCF 进行模型量化优化

模型量化是一种有效的模型压缩和加速技术,能够在保持精度的同时显著提升推理性能。NNCF(Neural Network Compression Framework)提供了便捷的模型量化工具,支持 Post-training 量化模式(无需重新训练)。

量化数据集准备

量化过程需要少量校准数据来确定量化参数,这里使用验证数据集的子集作为量化校准集:

import nncf  # noqa: F811
from typing import Dict

def transform_fn(data_item:Dict):
    """
    量化转换函数,从数据加载器项中提取并预处理输入数据
    
    参数:
       data_item: Dict 数据加载器产生的数据项
       
    返回:
        input_tensor: 用于量化的输入数据
    """
    input_tensor = pose_validator.preprocess(data_item)['img'].numpy()
    return input_tensor


# 创建量化数据集
quantization_dataset = nncf.Dataset(pose_data_loader, transform_fn)
INFO:nncf:NNCF 初始化成功。检测到支持的框架:torch, tensorflow, onnx, openvino

模型量化

使用 NNCF 对 OpenVINO 模型进行 8 位量化。由于 YOLOv8 模型包含非 ReLU 激活函数,采用混合量化策略(权重对称量化,激活非对称量化)以获得更好的精度:

# 定义量化过程中需要忽略的操作范围
ignored_scope = nncf.IgnoredScope(
    types=["Multiply", "Subtract", "Sigmoid"],  # 按类型忽略的操作
    names=[
        "/model.22/dfl/conv/Conv",           # 后处理子图中的操作
        "/model.22/Add",
        "/model.22/Add_1",
        "/model.22/Add_2",
        "/model.22/Add_3",
        "/model.22/Add_4",
        "/model.22/Add_5",
        "/model.22/Add_6",
        "/model.22/Add_7",
        "/model.22/Add_8",
        "/model.22/Add_9",
        "/model.22/Add_10"
    ]
)

# 对姿态估计模型进行量化
quantized_pose_model = nncf.quantize(
    pose_ov_model,
    quantization_dataset,
    preset=nncf.QuantizationPreset.MIXED,
    ignored_scope=ignored_scope
)
INFO:nncf:12 ignored nodes was found by name in the NNCFGraph
INFO:nncf:12 ignored nodes was found by types in the NNCFGraph
INFO:nncf:Not adding activation input quantizer for operation: 134 /model.22/Mul_6
145 /model.22/Add_12

INFO:nncf:Not adding activation input quantizer for operation: 135 /model.22/Sigmoid_1
INFO:nncf:Not adding activation input quantizer for operation: 156 /model.22/Mul_7
INFO:nncf:Not adding activation input quantizer for operation: 144 /model.22/Sigmoid
INFO:nncf:Not adding activation input quantizer for operation: 174 /model.22/dfl/conv/Conv
INFO:nncf:Not adding activation input quantizer for operation: 
196 /model.22/Sub
INFO:nncf:Not adding activation input quantizer for operation: 197 /model.22/Add_10
INFO:nncf:Not adding activation input quantizer for operation: 212 /model.22/Sub_1
INFO:nncf:Not adding activation input quantizer for operation: 239 /model.22/Mul_5
Statistics collection:   3%|███▉                                                                                                                                              | 8/300 [00:01<00:38,  7.55it/s]
Applying Fast Bias correction: 100%|██████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████| 72/72 [00:03<00:00, 19.73it/s]

保存量化模型

将量化后的模型保存为 OpenVINO IR 格式,以便后续部署使用:

from openvino.runtime import serialize

# 定义量化模型保存路径
int8_model_pose_path = models_dir / f'{POSE_MODEL_NAME}_openvino_int8_model/{POSE_MODEL_NAME}.xml'
print(f"量化关键点检测模型将保存到 {int8_model_pose_path}")
serialize(quantized_pose_model, str(int8_model_pose_path))
量化关键点检测模型将保存到 models/yolov8n-pose_openvino_int8_model/yolov8n-pose.xml

量化模型推理验证

验证量化模型的推理功能是否正常,确保量化过程没有破坏模型的基本功能:

device
Dropdown(description='Device:', index=2, options=('CPU', 'GPU', 'AUTO'), value='AUTO')
# 配置量化模型并进行推理
if device.value != "CPU":
    quantized_pose_model.reshape({0: [1, 3, 640, 640]})
quantized_pose_compiled_model = core.compile_model(quantized_pose_model, device.value)

# 加载测试图像并执行推理
input_image = np.array(Image.open(IMAGE_PATH))
detections = detect(input_image, quantized_pose_compiled_model)[0]
image_with_boxes = draw_results(detections, input_image, label_map)

Image.fromarray(image_with_boxes)

量化模型推理结果

可视化结果表明,量化模型能够正确检测人体并估计关键点,与原始模型的输出保持一致。

模型性能对比

使用 OpenVINO 提供的 Benchmark Tool 对原始 FP32 模型和量化 INT8 模型的推理性能进行对比测试:

device
Dropdown(description='Device:', index=2, options=('CPU', 'GPU', 'AUTO'), value='AUTO')

FP32 模型性能测试

# 推理 FP32 模型(OpenVINO IR)
!benchmark_app -m $pose_model_path -d $device.value -api async -shape "[1,3,640,640]"
[Step 1/11] 解析和验证输入参数
[ INFO ] 解析输入参数
[Step 2/11] 加载 OpenVINO 运行时
[ WARNING ] 未知设备 AUTO 的默认持续时间 120 秒
[ INFO ] OpenVINO:
[ INFO ] 版本 ................................. 2023.2.0-12690-0ee0b4d9561
[ INFO ]
[ INFO ] 设备信息:
[ INFO ] AUTO
[ INFO ] 版本 ................................. 2023.2.0-12690-0ee0b4d9561
[ INFO ]
[ INFO ]
[Step 3/11] 设置设备配置
[ WARNING ] 性能提示未在命令行中明确指定。设备(AUTO)性能提示将设置为 PerformanceMode.THROUGHPUT。
[Step 4/11] 读取模型文件
[ INFO ] 加载模型文件
[ INFO ] 读取模型耗时 17.85 毫秒
[ INFO ] 原始模型 I/O 参数:
[ INFO ] 模型输入:
[ INFO ]     images (node: images) : f32 / [...] / [?,3,?,?]
[ INFO ] 模型输出:
[ INFO ]     output0 (node: output0) : f32 / [...] / [?,56,?]
[Step 5/11] 调整模型大小以匹配图像大小和给定批量
[ INFO ] 模型批量大小:1
[ INFO ] 调整模型形状:'images': [1,3,640,640]
[ INFO ] 调整模型形状耗时 11.94 毫秒
[Step 6/11] 配置模型输入
[ INFO ] 模型输入:
[ INFO ]     images (node: images) : u8 / [N,C,H,W] / [1,3,640,640]
[ INFO ] 模型输出:
[ INFO ]     output0 (node: output0) : f32 / [...] / [1,56,8400]
[Step 7/11] 将模型加载到设备
[ INFO ] 编译模型耗时 410.27 毫秒
[Step 8/11] 查询最佳运行时参数
[ INFO ] 模型:
[ INFO ]   NETWORK_NAME: torch_jit
[ INFO ]   EXECUTION_DEVICES: ['CPU']
[ INFO ]   PERFORMANCE_HINT: PerformanceMode.THROUGHPUT
[ INFO ]   OPTIMAL_NUMBER_OF_INFER_REQUESTS: 12
[ INFO ]   MULTI_DEVICE_PRIORITIES: CPU
[ INFO ]   CPU:
[ INFO ]     AFFINITY: Affinity.CORE
[ INFO ]     CPU_DENORMALS_OPTIMIZATION: False
[ INFO ]     CPU_SPARSE_WEIGHTS_DECOMPRESSION_RATE: 1.0
[ INFO ]     ENABLE_CPU_PINNING: True
[ INFO ]     ENABLE_HYPER_THREADING: True
[ INFO ]     EXECUTION_DEVICES: ['CPU']
[ INFO ]     EXECUTION_MODE_HINT: ExecutionMode.PERFORMANCE
[ INFO ]     INFERENCE_NUM_THREADS: 36
[ INFO ]     INFERENCE_PRECISION_HINT: <Type: 'float32'>
[ INFO ]     NETWORK_NAME: torch_jit
[ INFO ]     NUM_STREAMS: 12
[ INFO ]     OPTIMAL_NUMBER_OF_INFER_REQUESTS: 12
[ INFO ]     PERFORMANCE_HINT: PerformanceMode.THROUGHPUT
[ INFO ]     PERFORMANCE_HINT_NUM_REQUESTS: 0
[ INFO ]     PERF_COUNT: False
[ INFO ]     SCHEDULING_CORE_TYPE: SchedulingCoreType.ANY_CORE
[ INFO ]   MODEL_PRIORITY: Priority.MEDIUM
[ INFO ]   LOADED_FROM_CACHE: False
[Step 9/11] 创建推理请求并准备输入张量
[ WARNING ] 未为输入 'images' 提供输入文件!此输入将用随机值填充!
[ INFO ] 用随机值填充输入 'images'
[Step 10/11] 测量性能(开始异步推理,12 个推理请求,限制:120000 毫秒持续时间)
[ INFO ] 仅在推理模式下进行基准测试(输入填充不包含在测量循环中)。
[ INFO ] 首次推理耗时 33.91 毫秒
[Step 11/11] 转储统计报告
[ INFO ] 执行设备:['CPU']
[ INFO ] 计数:            18420 次迭代
[ INFO ] 持续时间:         120067.97 毫秒
[ INFO ] 延迟:
[ INFO ]    中位数:        74.24 毫秒
[ INFO ]    平均值:       78.05 毫秒
[ INFO ]    最小值:        39.74 毫秒
[ INFO ]    最大值:        165.06 毫秒
[ INFO ] 吞吐量:   153.41 FPS

INT8 模型性能测试

# 推理 INT8 模型(OpenVINO IR)
!benchmark_app -m $int8_model_pose_path -d $device.value -api async -shape "[1,3,640,640]" -t 15
[Step 1/11] 解析和验证输入参数
[ INFO ] 解析输入参数
[Step 2/11] 加载 OpenVINO 运行时
[ INFO ] OpenVINO:
[ INFO ] 版本 ................................. 2023.2.0-12690-0ee0b4d9561
[ INFO ]
[ INFO ] 设备信息:
[ INFO ] AUTO
[ INFO ] 版本 ................................. 2023.2.0-12690-0ee0b4d9561
[ INFO ]
[ INFO ]
[Step 3/11] 设置设备配置
[ WARNING ] 性能提示未在命令行中明确指定。设备(AUTO)性能提示将设置为 PerformanceMode.THROUGHPUT。
[Step 4/11] 读取模型文件
[ INFO ] 加载模型文件
[ INFO ] 读取模型耗时 29.51 毫秒
[ INFO ] 原始模型 I/O 参数:
[ INFO ] 模型输入:
[ INFO ]     images (node: images) : f32 / [...] / [1,3,?,?]
[ INFO ] 模型输出:
[ INFO ]     output0 (node: output0) : f32 / [...] / [1,56,21..]
[Step 5/11] 调整模型大小以匹配图像大小和给定批量
[ INFO ] 模型批量大小:1
[ INFO ] 调整模型形状:'images': [1,3,640,640]
[ INFO ] 调整模型形状耗时 16.46 毫秒
[Step 6/11] 配置模型输入
[ INFO ] 模型输入:
[ INFO ]     images (node: images) : u8 / [N,C,H,W] / [1,3,640,640]
[ INFO ] 模型输出:
[ INFO ]     output0 (node: output0) : f32 / [...] / [1,56,8400]
[Step 7/11] 将模型加载到设备
[ INFO ] 编译模型耗时 732.13 毫秒
[Step 8/11] 查询最佳运行时参数
[ INFO ] 模型:
[ INFO ]   NETWORK_NAME: torch_jit
[ INFO ]   EXECUTION_DEVICES: ['CPU']
[ INFO ]   PERFORMANCE_HINT: PerformanceMode.THROUGHPUT
[ INFO ]   OPTIMAL_NUMBER_OF_INFER_REQUESTS: 18
[ INFO ]   MULTI_DEVICE_PRIORITIES: CPU
[ INFO ]   CPU:
[ INFO ]     AFFINITY: Affinity.CORE
[ INFO ]     CPU_DENORMALS_OPTIMIZATION: False
[ INFO ]     CPU_SPARSE_WEIGHTS_DECOMPRESSION_RATE: 1.0
[ INFO ]     ENABLE_CPU_PINNING: True
[ INFO ]     ENABLE_HYPER_THREADING: True
[ INFO ]     EXECUTION_DEVICES: ['CPU']
[ INFO ]     EXECUTION_MODE_HINT: ExecutionMode.PERFORMANCE
[ INFO ]     INFERENCE_NUM_THREADS: 36
[ INFO ]     INFERENCE_PRECISION_HINT: <Type: 'float32'>
[ INFO ]     NETWORK_NAME: torch_jit
[ INFO ]     NUM_STREAMS: 18
[ INFO ]     OPTIMAL_NUMBER_OF_INFER_REQUESTS: 18
[ INFO ]     PERFORMANCE_HINT: PerformanceMode.THROUGHPUT
[ INFO ]     PERFORMANCE_HINT_NUM_REQUESTS: 0
[ INFO ]     PERF_COUNT: False
[ INFO ]     SCHEDULING_CORE_TYPE: SchedulingCoreType.ANY_CORE
[ INFO ]   MODEL_PRIORITY: Priority.MEDIUM
[ INFO ]   LOADED_FROM_CACHE: False
[Step 9/11] 创建推理请求并准备输入张量
[ WARNING ] 未为输入 'images' 提供输入文件!此输入将用随机值填充!
[ INFO ] 用随机值填充输入 'images'
[Step 10/11] 测量性能(开始异步推理,18 个推理请求,限制:15000 毫秒持续时间)
[ INFO ] 仅在推理模式下进行基准测试(输入填充不包含在测量循环中)。
[ INFO ] 首次推理耗时 26.46 毫秒
[Step 11/11] 转储统计报告
[ INFO ] 执行设备:['CPU']
[ INFO ] 计数:            6426 次迭代
[ INFO ] 持续时间:         15072.05 毫秒
[ INFO ] 延迟:
[ INFO ]    中位数:        40.12 毫秒
[ INFO ]    平均值:       42.00 毫秒
[ INFO ]    最小值:        27.49 毫秒

结论

本研究实现了 YOLOv8 姿态估计模型到 OpenVINO IR 格式的转换,并利用 NNCF 工具进行了 8 位量化优化。实验结果表明:

  1. 转换后的 OpenVINO 模型能够保持与原始模型相同的推理精度和可视化效果。
  2. 量化后的 INT8 模型在几乎不损失精度的前提下,显著提升了推理性能,吞吐量从 153.41 FPS 提升至约 426.36 FPS(根据 15 秒内 6426 次迭代计算),延迟中位数从 74.24 毫秒降低至 40.12 毫秒。
  3. 模型量化同时减少了模型存储空间(约 4 倍压缩),更适合部署在资源受限的边缘设备上。

本方案为 YOLOv8 姿态估计模型的高效部署提供了可行的技术路径,特别适用于需要实时性能的应用场景,如视频监控、运动分析等领域。

您可能感兴趣的与本文相关的镜像

Yolo-v5

Yolo-v5

Yolo

YOLO(You Only Look Once)是一种流行的物体检测和图像分割模型,由华盛顿大学的Joseph Redmon 和Ali Farhadi 开发。 YOLO 于2015 年推出,因其高速和高精度而广受欢迎

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

知来者逆

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值