HumansXドキュメントセンター 公式サイトへ ↗

YOLO11物体検出システム -- モデルデプロイドキュメント

概要

本ドキュメントでは、YOLO 物体検出モデルをエッジデバイスへデプロイする手順について、前処理・モデル推論・後処理の具体的な実装を含めて詳しく説明します。このデプロイ方式は Horizon RDK-X5 などの NPU デバイス向けに最適化されており、3 クラス(orange、green、red)の効率的な物体検出機能を実現します。

🔧 デプロイ手順の概要

输入图像 → 前处理 → 模型推理 → 后处理 → 检测结果
    ↓         ↓         ↓         ↓         ↓
  BGR图像   NV12格式   BPU推理   解码NMS   物体框+类别

モデルのデプロイを行う前に、学習で得られた onnx モデルに対して RDK が提供する BPU 変換ツールチェーンを使用し、モデルの変換・量子化・コンパイルを行って、BPU 上で動作する専用の bin モデルファイルを生成する必要があります。

参考資料

YOLO 幾何形状検出の ONNX から BIN へのモデル変換の詳細については、以下の優れたチュートリアルを参照してください:

  1. RDK-X5 における YOLO11 のデプロイテスト
  • 出典:CSDN - YOLO11デプロイ実践
  • 内容:RDK-X5 プラットフォームでのデプロイ手順、性能テスト、幾何形状検出の実例
  1. ONNX から BIN モデルへの変換方法の詳細
  • 出典:博客园 - BPUモデル変換
  • 内容:モデル変換ツールチェーンの使い方、設定ファイルの詳細、幾何形状の量子化最適化
  1. YOLO BPU モデル変換完全ガイド
  • 出典:個人技術ブログ
  • 内容:ONNX から BIN への完全な変換手順、幾何形状検出のベストプラクティス

📊 前処理の流れ

1. 画像サイズの調整

機能: 入力画像をモデルが必要とするサイズに縮小・拡大します

実装方法 (utils/base_model.py:76):

def resizer(self, img: np.ndarray) -> np.ndarray:
    """
    将输入图像resize到模型输入大小,并记录缩放比例。
    Args:
        img (np.ndarray): 输入图像
    Returns:
        np.ndarray: resize后的图像
    """
    img_h, img_w = img.shape[0:2]
    self.y_scale, self.x_scale = img_h/self.model_input_height, img_w/self.model_input_weight
    return cv2.resize(img, (self.model_input_height, self.model_input_weight), interpolation=cv2.INTER_NEAREST)

主な特長:

  • 目標サイズ: 320x320 ピクセル
  • 後で座標を復元するためにスケール比を記録
  • 処理速度を確保するため INTER_NEAREST 補間を使用

2. カラーフォーマット変換

機能: BGR 形式の画像を NPU デバイスが対応する NV12 形式に変換します

実装方法 (utils/base_model.py:88):

def bgr2nv12(self, bgr_img: np.ndarray) -> np.ndarray:
    """
    将BGR格式图像转换为NV12格式(YUV420半平面),用于模型推理。
    Args:
        bgr_img (np.ndarray): BGR格式图像
    Returns:
        np.ndarray: NV12格式一维数组
    """
    begin_time = time.time()
    bgr_img = self.resizer(bgr_img)  # 先调整尺寸
    height, width = bgr_img.shape[0], bgr_img.shape[1]
    area = height * width
    yuv420p = cv2.cvtColor(bgr_img, cv2.COLOR_BGR2YUV_I420).reshape((area * 3 // 2,))
    y = yuv420p[:area]
    uv_planar = yuv420p[area:].reshape((2, area // 4))
    uv_packed = uv_planar.transpose((1, 0)).reshape((area // 2,))
    nv12 = np.zeros_like(yuv420p)
    nv12[:height * width] = y
    nv12[height * width:] = uv_packed

    process_time = time.time() - begin_time
    logger.debug(f"BGR转NV12耗时: {process_time*1000:.2f}ms")
    return nv12

変換の流れ:

  1. 画像のスケーリング: resizer を呼び出して画像を 320x320 にスケーリング
  2. BGR から YUV_I420 へ: OpenCV を使用して色空間を変換
  3. YUV の分離: Y チャンネルと UV チャンネルを分離
  4. UV の並べ替え: UV 成分をプレーナー形式からインターリーブ形式に変換
  5. NV12 の合成: 最終的な NV12 形式のデータに合成

パフォーマンス最適化:

  • 最適化分析のためのパフォーマンス計測を含む
  • numpy 配列演算を使用して効率を向上

⚡ モデル推論の流れ

1. モデルの読み込み

機能: Horizon BPU 量子化モデルを読み込みます

実装方法 (utils/base_model.py:47):

def _load_hobot_model(self, model_file: str):
    """加载地平线BPU模型"""
    try:
        begin_time = time.time()
        self.quantize_model = dnn.load(model_file)
        load_time = time.time() - begin_time
        logger.info(f"✅ 模型加载成功,耗时: {load_time:.2f}秒")

        # 打印输入张量信息
        logger.info("-> 输入张量信息")
        for i, quantize_input in enumerate(self.quantize_model[0].inputs):
            logger.info(f"input[{i}]: name={quantize_input.name}, "
                       f"type={quantize_input.properties.dtype}, "
                       f"shape={quantize_input.properties.shape}")

        # 打印输出张量信息
        logger.info("-> 输出张量信息")
        for i, quantize_output in enumerate(self.quantize_model[0].outputs):
            logger.info(f"output[{i}]: name={quantize_output.name}, "
                       f"type={quantize_output.properties.dtype}, "
                       f"shape={quantize_output.properties.shape}")

        # 记录模型输入的高和宽
        self.model_input_height, self.model_input_weight = self.quantize_model[0].inputs[0].properties.shape[2:4]

    except Exception as e:
        logger.error(f"❌ 模型加载失败: {e}")
        raise

モデル情報:

  • 対応形式: Horizon BPU 量子化モデル (.bin)
  • 入力テンソル: NV12 形式、320x320 サイズ
  • 出力テンソル: 6 つの出力層(3 つのスケールの境界ボックスと分類に対応)

2. モデル推論

機能: ハードウェアアクセラレーションによる推論を実行します

実装方法 (utils/base_model.py:112):

def forward(self, input_tensor: np.array):
    """
    执行模型推理。
    Args:
        input_tensor (np.array): 输入张量
    Returns:
        推理输出张量列表
    """
    if not HAS_HOBOT_DNN or self.quantize_model is None:
        logger.warning("模型未加载或运行在CPU模拟模式下")
        # CPU模拟模式,返回模拟输出
        return self._simulate_inference(input_tensor)

    begin_time = time.time()
    quantize_outputs = self.quantize_model[0].forward(input_tensor)
    inference_time = time.time() - begin_time
    logger.debug(f"模型推理耗时: {inference_time*1000:.2f}ms")
    return quantize_outputs

推論の特長:

  • ハードウェアアクセラレーション: Horizon BPU を利用した NPU 推論
  • パフォーマンス監視: 推論時間の統計を含む
  • フォールバック対応: 開発テスト用の CPU シミュレーションモードに対応

3. 出力形式の変換

機能: 推論出力を numpy 配列に変換します

実装方法 (utils/base_model.py:137):

def c2numpy(self, outputs):
    """
    将C类型的推理输出转为numpy数组。
    Args:
        outputs: 推理输出张量列表
    Returns:
        list[np.array]: numpy数组列表
    """
    if not HAS_HOBOT_DNN or not outputs:
        return []

    begin_time = time.time()
    numpy_outputs = [dnnTensor.buffer for dnnTensor in outputs]
    convert_time = time.time() - begin_time
    logger.debug(f"C转numpy耗时: {convert_time*1000:.2f}ms")
    return numpy_outputs

🎯 後処理の流れ

1. マルチスケール特徴マップの処理

機能: モデルが出力するマルチスケール特徴マップを処理します

実装方法 (utils/yolo_shape_detector.py:184-190):

# reshape到对应的尺寸 (根据实际模型输出调整顺序)
s_bboxes = outputs[0].reshape(-1, 64)    # 40x40 -> 1600
m_bboxes = outputs[1].reshape(-1, 64)    # 20x20 -> 400
l_bboxes = outputs[2].reshape(-1, 64)    # 10x10 -> 100
s_clses = outputs[3].reshape(-1, 3)      # 3个类别
m_clses = outputs[4].reshape(-1, 3)      # 3个类别
l_clses = outputs[5].reshape(-1, 3)      # 3个类别

マルチスケール構成:

  • 小スケール (40x40): stride=8、小さな物体を検出
  • 中スケール (20x20): stride=16、中程度の物体を検出
  • 大スケール (10x10): stride=32、大きな物体を検出

出力順序の特徴: face プロジェクトとは異なり、object プロジェクトでは境界ボックスの出力が先、分類の出力が後になります

2. 信頼度によるフィルタリング

機能: 信頼度しきい値に基づいて有効な検出を選別します

実装方法 (utils/yolo_shape_detector.py:193-211):

# 获取置信度最高的索引和分数
s_max_scores = np.max(s_clses, axis=1)
s_valid_indices = np.flatnonzero(s_max_scores >= self.conf_inverse)
s_ids = np.argmax(s_clses[s_valid_indices, :], axis=1)
s_scores = s_max_scores[s_valid_indices]

# Sigmoid激活函数计算置信度
s_scores = 1 / (1 + np.exp(-s_scores))

フィルタリングのロジック:

  1. 各位置における最大分類信頼度を計算(3 クラスの中の最大値を採用)
  2. 信頼度しきい値を用いて有効な位置を選別
  3. 対応するクラス ID と元の信頼度を取得
  4. sigmoid 関数を適用して最終的な信頼度を算出

マルチクラスの特徴: orange、green、red の 3 クラスの分類検出に対応

3. DFL 境界ボックスのデコード

機能: Distribution Focal Loss を用いて境界ボックスの座標をデコードします

実装方法 (utils/yolo_shape_detector.py:216-241):

if len(s_valid_indices) > 0:
    s_bboxes_float32 = s_bboxes[s_valid_indices, :]
    # DFL解码: 使用softmax计算期望值
    s_ltrb_indices = np.sum(softmax(s_bboxes_float32.reshape(-1, 4, 16), axis=2) * self.weights_static, axis=2)
    s_anchor_indices = self.s_anchor[s_valid_indices, :]
    # 基于anchor点计算边界框坐标
    s_x1y1 = s_anchor_indices - s_ltrb_indices[:, 0:2]
    s_x2y2 = s_anchor_indices + s_ltrb_indices[:, 2:4]
    # 乘以stride还原到原始尺度
    s_dbboxes = np.hstack([s_x1y1, s_x2y2]) * 8

DFL デコードの手順:

  1. Softmax 計算: 16 個の bin に softmax を適用して確率分布を求める
  2. 期待値計算: 境界ボックス各辺までの距離の期待値を計算
  3. 座標変換: anchor 点を基準に絶対座標を計算
  4. スケール復元: 対応する stride を掛けて最終座標を得る

4. 非最大値抑制 (NMS)

機能: 重複した検出を除去し、最適な検出枠を残します

実装方法 (utils/yolo_shape_detector.py:252-262):

# NMS非极大值抑制
if len(dbboxes) > 0:
    indices = cv2.dnn.NMSBoxes(dbboxes, scores, self.conf, self.iou)
    if len(indices) > 0:
        # 还原到原始图像尺度
        bboxes = dbboxes[indices] * np.array([self.x_scale, self.y_scale, self.x_scale, self.y_scale])
        bboxes = bboxes.astype(np.int32)

        process_time = time.time() - begin_time
        logger.debug(f"后处理耗时: {process_time*1000:.2f}ms")

        return ids[indices], scores[indices], bboxes

NMS 処理:

  • OpenCV の dnn.NMSBoxes 関数を使用
  • パラメータ: 信頼度しきい値 (conf=0.6)、IOU しきい値 (iou=0.45)
  • 座標復元: 検出結果を入力画像のサイズに復元

5. 検出処理の全体フロー

メイン検出関数 (utils/yolo_shape_detector.py:87):

def detect_shapes(self, frame: np.ndarray) -> Tuple[List[List[int]], List[int], List[float], float, int]:
    """
    检测物体
    Args:
        frame: 输入图像帧
    Returns:
        Tuple[List[List[int]], List[int], List[float], float, int]: (边界框列表, 类别ID列表, 置信度列表, 推理时间, 物体数量)
    """
    start_time = time.time()

    try:
        # 1. 预处理:BGR转NV12
        input_tensor = self.bgr2nv12(frame)

        # 2. 模型推理
        outputs = self.forward(input_tensor)

        # 3. 转换输出格式
        numpy_outputs = self.c2numpy(outputs)

        # 4. 后处理:解码、NMS等
        if not numpy_outputs:
            return self._simulate_detection(frame)

        class_ids, confidences, bboxes = self.postProcess(numpy_outputs)

        # 5. 结果格式化
        inference_time = time.time() - start_time
        # ... 格式化输出

        return shape_boxes, shape_class_ids, shape_confidences, inference_time, shape_count

    except Exception as e:
        logger.error(f"物体检测失败: {e}")
        return [], [], [], 0.0, 0

🔧 アプリケーション層のラッパー

1. ObjectDetector ラッパークラス

機能: 統一された物体検出インターフェースを提供します

実装方法 (app/object_detector.py:58):

def detect(self, frame: np.ndarray) -> dict:
    """
    检测物体

    Args:
        frame: 输入图像帧

    Returns:
        dict: 检测结果,包含 boxes, class_ids, confidences, inference_time, object_count
    """
    if self.detector is None:
        logger.warning("检测器未初始化")
        return {
            'boxes': [],
            'class_ids': [],
            'confidences': [],
            'inference_time': 0.0,
            'object_count': 0,
            'success': False,
            'error': '检测器未初始化'
        }

    try:
        # 执行检测
        boxes, class_ids, confidences, inference_time, object_count = self.detector.detect_shapes(frame)

        return {
            'boxes': boxes,
            'class_ids': class_ids,
            'confidences': confidences,
            'inference_time': inference_time,
            'object_count': object_count,
            'success': True,
            'error': None
        }

    except Exception as e:
        logger.error(f"物体检测失败: {e}")
        return {
            'boxes': [],
            'class_ids': [],
            'confidences': [],
            'inference_time': 0.0,
            'object_count': 0,
            'success': False,
            'error': str(e)
        }

ラッパーの利点:

  • 統一された結果形式とエラー処理
  • 標準化された API インターフェース
  • 充実した例外処理機構

2. ユーティリティ関数

最大の物体を取得 (app/object_detector.py:150):

def get_largest_object(self, boxes: List[List[int]]) -> Optional[List[int]]:
    """
    获取最大的物体框

    Args:
        boxes: 物体边界框列表

    Returns:
        Optional[List[int]]: 最大物体框,如果没有则返回None
    """
    if not boxes:
        return None

    largest_area = 0
    largest_box = None

    for box in boxes:
        x1, y1, x2, y2 = box
        area = (x2 - x1) * (y2 - y1)
        if area > largest_area:
            largest_area = area
            largest_box = box

    return largest_box

物体の中心点を取得 (app/object_detector.py:175):

def get_object_center(self, box: List[int]) -> Tuple[int, int]:
    """
    获取物体框的中心点

    Args:
        box: 物体边界框 [x1, y1, x2, y2]

    Returns:
        Tuple[int, int]: 中心点坐标 (x, y)
    """
    x1, y1, x2, y2 = box
    center_x = (x1 + x2) // 2
    center_y = (y1 + y2) // 2
    return center_x, center_y

🎨 可視化機能

1. 結果の描画

機能: 画像上に検出結果を描画します

実装方法 (utils/yolo_shape_detector.py:287):

def draw_detections(self, frame: np.ndarray, shape_boxes: List[List[int]], 
                   class_ids: List[int] = None,
                   confidences: List[float] = None,
                   show_boxes: bool = True,
                   show_center: bool = True, 
                   show_stats: bool = True) -> np.ndarray:
    """
    在图像上绘制检测结果
    """
    # 类别颜色定义
    class_colors = [
        (0, 165, 255),  # 橙色 - orange
        (0, 255, 0),    # 绿色 - green
        (0, 0, 255)     # 红色 - red
    ]

    for i, bbox in enumerate(shape_boxes):
        x1, y1, x2, y2 = bbox

        # 获取类别ID和颜色
        class_id = class_ids[i] if class_ids and i < len(class_ids) else 0
        color = class_colors[class_id % len(class_colors)]

        # 绘制边界框
        cv2.rectangle(result_frame, (x1, y1), (x2, y2), color, 2)

        # 绘制标签和置信度
        confidence = confidences[i] if confidences and i < len(confidences) else 0.0
        class_name = self.class_names[class_id] if class_id < len(self.class_names) else f"Class {class_id}"
        label = f"{class_name}: {confidence:.2f}"

可視化の特長:

  • クラスごとに異なる色を使用(オレンジ、緑、赤)
  • クラス名と信頼度を表示
  • 表示オプションを設定可能(枠線、中心点、統計情報)

📈 パフォーマンス特性

時間計測

  • 前処理: BGR から NV12 への変換時間を含む
  • 推論: BPU ハードウェアアクセラレーションによる推論時間
  • 後処理: マルチスケールデコード、NMS などの処理時間
  • 合計時間: 検出処理全体の時間

メモリ最適化

  • 入力/出力バッファの再利用
  • 静的データ(anchor 点、DFL 重み)の事前計算
  • numpy 配列による効率的な演算

フォールバック対応

  • 開発テスト用の CPU シミュレーションモード
  • 充実した例外処理機構
  • 詳細なログ記録

🔧 デプロイ要件

ハードウェア要件

  • Horizon RDK-X5 または互換性のある BPU デバイス
  • INT8 量子化に対応した NPU アクセラレータ

ソフトウェア依存関係

# 核心依赖
hobot_dnn        # 地平线BPU推理库
opencv-python    # 图像处理
numpy            # 数值计算
scipy            # 数学函数(softmax)

モデルファイル

  • 形式: Horizon BPU 量子化モデル (.bin)
  • 入力: NV12 形式、320x320
  • 出力: 6 つのテンソル(3 スケールの境界ボックス + 分類)

検出クラス

  • クラス 0: orange(オレンジ)
  • クラス 1: green(緑)
  • クラス 2: red(赤)

📝 使用例

from app.object_detector import ObjectDetector

# 初始化检测器
detector = ObjectDetector(
    model_path="models/object_detect.bin",
    conf_threshold=0.7,
    iou_threshold=0.45
)

# 读取图像
frame = cv2.imread("test.jpg")

# 执行检测
result = detector.detect(frame)

if result['success']:
    print(f"检测到 {result['object_count']} 个物体")
    print(f"耗时: {result['inference_time']:.3f}秒")

    # 获取最大物体
    largest_box = detector.get_largest_object(result['boxes'])
    if largest_box:
        center = detector.get_object_center(largest_box)
        print(f"最大物体中心点: {center}")

    # 绘制结果
    result_frame = detector.draw_results(frame, result)
    cv2.imwrite("result.jpg", result_frame)
else:
    print(f"检测失败: {result['error']}")

🔄 Face 検出プロジェクトとの違い

1. モデル出力構造の違い

Face プロジェクト:

  • 出力順序: [分類, 境界ボックス]
  • 分類の次元: 1 チャンネル(2 値分類)

Object プロジェクト:

  • 出力順序: [境界ボックス, 分類]
  • 分類の次元: 3 チャンネル (orange/green/red)

2. 後処理の違い

Face プロジェクト: 単一の信頼度計算

s_max_scores = np.max(s_clses, axis=1)  # 单分类

Object プロジェクト: マルチクラスの信頼度計算

s_max_scores = np.max(s_clses, axis=1)  # 多分类取最大值
s_ids = np.argmax(s_clses[s_valid_indices, :], axis=1)  # 获取类别ID

3. アプリケーション層の違い

Face プロジェクト: 検出器を直接使用 Object プロジェクト: ObjectDetector ラッパー層を追加

  • エラー処理の統一
  • 結果形式の標準化
  • ユーティリティ関数の追加

🎯 まとめ

本デプロイ方式は完全な YOLO マルチクラス物体検出パイプラインを実現しており、特に 3 クラス(orange、green、red)の物体検出向けに最適化されています。プロジェクトはモジュール化設計を採用し、BaseModel 基底クラスを継承して固有の後処理ロジックを実装するとともに、より使いやすい API を提供するためのアプリケーション層ラッパーを追加しています。Face 検出プロジェクトと同じ前処理および推論フローを維持しつつ、マルチクラス検出の特性に合わせた調整を行い、効率的で信頼性の高い物体検出システムを構成しています。