0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

「犬」って打ったら犬を検出する — YOLO-WorldをiPhoneで動かす

0
Posted at

何ができるのか

「person, red car, coffee cup」とテキストを入力すると、カメラに映ったそれらの物体をリアルタイムに検出する。クラスの一覧は不要。好きな言葉を好きなだけ指定できる

これがYOLO-Worldの「Open-Vocabulary Detection」。CVPR 2024で発表され、従来の「80クラス固定」のYOLOとは根本的に異なるアプローチです。

仕組み

テキスト入力 ──→ CLIP Text Encoder ──→ テキスト特徴量 [1,80,512]
                                              │
カメラ映像   ──→ YOLO-World Detector ─────────┤──→ boxes [1,4,8400]
                                              └──→ scores [1,80,8400]
                                                       │
                                                   NMS + Filter ──→ バウンディングボックス

CLIPの言語理解力とYOLOの検出速度を組み合わせた二刀流。テキストをベクトルに変換し、画像から抽出した特徴量とのマッチングスコアで検出する。

クエリテキストの変更ではCLIP encoderだけ再実行すればよく、カメラフレームの推論にはVisual detectorだけを使う。テキスト変更のたびに重い再計算は走らない。

CoreMLモデルの準備

ダウンロード(すぐ使える)

CoreML-Modelsリポジトリのrelease assetsから3ファイルをダウンロード:

ファイル サイズ 役割
yoloworld_detector.mlpackage 25 MB YOLO-World V2-S (画像→boxes+scores)
clip_text_encoder.mlpackage 121 MB CLIP ViT-B/32 (テキスト→埋め込み)
clip_vocab.json 1.6 MB BPEトークナイザの語彙

自分で変換

pip install ultralytics open_clip_torch coremltools==8.1
python convert_models.py --size s  # s/m/l/x

変換スクリプトでは以下を行っている:

  1. YOLO-World V2のDetect headをアンラップboxes [1,4,8400]scores [1,NC,8400] を直接出力
  2. CLIPのText Encoderを単体で変換 — MultiheadAttentionをCoreML互換にパッチ
  3. BPE語彙をJSON化 — Swift側のトークナイザ用

iOSでの実装

アーキテクチャ概要

TextGroundingDetector (ObservableObject)
├── visualModel: MLModel    — YOLO-World detector
├── textEncoder: MLModel    — CLIP text encoder
├── tokenizer: CLIPTokenizer — BPEトークナイザ
└── cachedTxtFeats: MLMultiArray — テキスト特徴量キャッシュ

テキストのエンコード

ユーザーがクエリを変更したときだけ実行。結果はキャッシュされる。

func updateQueries(_ queryString: String) {
    let queries = queryString.split(separator: ",")
        .map { $0.trimmingCharacters(in: .whitespaces) }

    // 各クエリをトークナイズ → CLIP encoder → 512次元ベクトル
    let txtFeats = try MLMultiArray(shape: [1, 80, 512], dataType: .float32)

    for (i, query) in queries.prefix(80).enumerated() {
        let tokens = tokenizer.tokenize(query)
        // ... MLDictionaryFeatureProvider で textEncoder.prediction() ...
        // 結果を L2正規化して txtFeats[i] に格納
    }
    cachedTxtFeats = txtFeats
}

ポイント:

  • 最大80クエリを同時に検出可能
  • L2正規化が重要 — CLIPの出力は正規化されたコサイン類似度空間で動作する
  • vDSP_svesq + vDSP_vsmul でAccelerate使って高速正規化

画像の前処理

YOLO-Worldはletterbox前処理(アスペクト比保持 + パディング)を要求する:

func preprocessImage(_ cgImage: CGImage) throws -> MLMultiArray {
    let scale = Float(640) / Float(max(imgW, imgH))
    let scaledW = Int(Float(imgW) * scale)
    let scaledH = Int(Float(imgH) * scale)
    let padX = (640 - scaledW) / 2
    let padY = (640 - scaledH) / 2

    // グレー (0.5) でパディングされた640x640キャンバスに描画
    ctx.setFillColor(gray: 0.5, alpha: 1.0)
    ctx.fill(CGRect(x: 0, y: 0, width: 640, height: 640))
    ctx.draw(cgImage, in: CGRect(x: padX, y: padY, width: scaledW, height: scaledH))

    // RGBA → CHW Float32 [0,1]
    for i in 0..<(640*640) {
        dst[0 * hw + i] = Float(src[i * 4 + 0]) / 255  // R
        dst[1 * hw + i] = Float(src[i * 4 + 1]) / 255  // G
        dst[2 * hw + i] = Float(src[i * 4 + 2]) / 255  // B
    }
}

.scaleFill は使えない — 座標がletterboxのパディング分だけずれるので、出力座標からパディングを引き戻す必要がある。

推論と後処理

let input = try MLDictionaryFeatureProvider(dictionary: [
    "image": tensor,
    "txt_feats": cachedTxtFeats,  // キャッシュ済みテキスト特徴量
])
let output = try visualModel.prediction(from: input)

let boxes = output.featureValue(for: "boxes")!.multiArrayValue!   // [1,4,8400]
let scores = output.featureValue(for: "scores")!.multiArrayValue! // [1,NC,8400]

for qi in 0..<queryCount {
    for anchor in 0..<8400 {
        let score = scores[qi * 8400 + anchor]
        guard score >= threshold else { continue }

        let cx = boxes[0 * 8400 + anchor]
        let cy = boxes[1 * 8400 + anchor]
        let bw = boxes[2 * 8400 + anchor]
        let bh = boxes[3 * 8400 + anchor]

        // パディングを除去して正規化座標に変換
        let nx = (cx - bw/2 - padX) / (imgW * scale)
        let ny = (cy - bh/2 - padY) / (imgH * scale)
    }
}

出力のscoresはBNContrastiveHeadで計算済みのsigmoid値なので、そのまま信頼度として使える。

NMS

クエリごと(per-class)にNMSを適用:

allDets.sort { $0.confidence > $1.confidence }
var kept: [Int] = []
for i in allDets.indices {
    var suppress = false
    for ki in kept {
        if allDets[i].classIndex == allDets[ki].classIndex
            && iou(allDets[i].rect, allDets[ki].rect) > 0.5 {
            suppress = true; break
        }
    }
    if !suppress { kept.append(i) }
}

BPEトークナイザ (Swift)

CLIPのトークナイザをSwiftで実装する必要がある。clip_vocab.json からBPEのmergeルールと語彙を読み込む:

class CLIPTokenizer {
    let contextLength: Int  // 77
    private let encoder: [String: Int]
    private let bpeRanks: [(String, String): Int]

    func tokenize(_ text: String) -> [Int] {
        var tokens = [encoder["<|startoftext|>"]!]
        // テキストを小文字化 → 文字単位に分解 → BPE merge → トークンIDに変換
        // ...
        tokens.append(encoder["<|endoftext|>"]!)
        // contextLength (77) にパディング
        return tokens + Array(repeating: 0, count: contextLength - tokens.count)
    }
}

通常のYOLOとの比較

YOLO-World (Open-Vocabulary) YOLO26 (固定クラス)
検出対象 任意のテキスト COCO 80クラス固定
モデル構成 Detector + CLIP Encoder + Vocab 1モデルのみ
合計サイズ ~148 MB ~18 MB
NMS アプリ側で実装 不要 (End-to-End)
適用先 柔軟な検出・検索・グラウンディング 汎用物体検出
速度 やや遅い(CLIP分のオーバーヘッド) 最速

実用シナリオ

  • 「赤いスニーカー」で検索 — ECアプリのビジュアル検索
  • 「ひび割れ」で検出 — インフラ点検
  • 「犬, 猫, ハムスター」で同時検出 — ペットトラッキング
  • ユーザーが検出対象を自由に指定 — カスタマイズ不要のデプロイ

固定クラスのYOLOでは「ひび割れ」を検出するためにデータセットを集めて再学習が必要だった。YOLO-Worldならテキストを変えるだけ。

サンプルアプリ

CoreML-Models リポジトリの sample_apps/YOLOWorldDemo/ に完全なサンプルアプリがあります。

  • カメラ / 写真 / 動画の3モード
  • テキストフィールドでクエリを自由に変更
  • 信頼度スライダーでリアルタイムフィルタリング
  • モデルはrelease assetsからダウンロード、Xcodeにドラッグするだけ

変換時のTips

  • coremltools 8.1 を使う(9.0はバグあり)
  • torch.nn.MultiheadAttention.forward のパッチが必要 — CoreMLはデフォルトのPyTorch MHAをうまく変換できない。F.multi_head_attention_forward を直接呼ぶようにモンキーパッチする
  • YOLO-World V2 を使う(V1より高速で精度も高い)
  • compute_precision=ct.precision.FLOAT16 でモデルサイズを半減

まとめ

YOLO-Worldは「検出したいものをテキストで指定する」という、直感的で強力な物体検出を実現する。iPhoneのNeural Engineで動かせば、サーバー不要・オフライン・低レイテンシで使える。

固定クラスのYOLOと使い分けるなら:

  • 速度優先・COCO 80クラスで十分 → YOLO26
  • 検出対象を柔軟に変えたい → YOLO-World

参考リンク

🐣


最新のAI機能を使ったアプリやサービスを最速で試作したい。
そんなご要望にお応えします。
ご相談はこちらまで。
rockyshikoku@gmail.com

Twitter
Medium
GitHub

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?