何ができるのか
「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
変換スクリプトでは以下を行っている:
-
YOLO-World V2のDetect headをアンラップ —
boxes [1,4,8400]とscores [1,NC,8400]を直接出力 - CLIPのText Encoderを単体で変換 — MultiheadAttentionをCoreML互換にパッチ
- 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
参考リンク
- YOLO-World 論文 (CVPR 2024)
- AILab-CVC/YOLO-World (GitHub)
- Ultralytics YOLO-World Docs
- CoreML-Models リポジトリ
- OpenAI CLIP
🐣
最新のAI機能を使ったアプリやサービスを最速で試作したい。
そんなご要望にお応えします。
ご相談はこちらまで。
rockyshikoku@gmail.com