バーチャル試着
AIとコンピュータビジョン技術を活用したバーチャル試着システムの仕組みを、人体推定・衣服変形・リアルタイムレンダリングの観点から解説し、ECサイトやアパレル業界での実装手法を示す
バーチャル試着
AIとコンピュータビジョン技術を活用したバーチャル試着システムの仕組みを、人体推定・衣服変形・リアルタイムレンダリングの観点から解説し、ECサイトやアパレル業界での実装手法を示す
この章で学ぶこと
- バーチャル試着の技術基盤 -- 人体ポーズ推定、セマンティックセグメンテーション、衣服変形アルゴリズム
- 主要アプローチの比較 -- 2D画像ベース vs 3Dモデルベース vs AR ベースの手法と精度
- 実装パイプラインと課題 -- データ準備、モデル訓練、リアルタイム推論の実現
前提知識
このガイドを読む前に、以下の知識があると理解が深まります:
- 基本的なプログラミングの知識
- 関連する基礎概念の理解
- AIによるゲームアセット生成 実践ガイド の内容を理解していること
1. バーチャル試着の全体像
1.1 システムアーキテクチャ
バーチャル試着パイプライン
入力 処理 出力
+----------+ +-------------------+ +----------+
| ユーザー | | 1. 人体推定 | | 試着結果 |
| 写真/動画 | -------> | 2. セグメンテーション| --> | 画像/動画 |
+----------+ | 3. 衣服変形 | +----------+
+----------+ | 4. 合成・レンダリング|
| 衣服画像 | -------> | |
| (カタログ) | +-------------------+
+----------+
1.2 技術スタック
レイヤー構成
フロントエンド
├── WebGL / Three.js --- 3D レンダリング
├── MediaPipe --- リアルタイム人体推定
└── WebRTC --- カメラ入力
AI モデル
├── DWPose / OpenPose --- 人体ポーズ推定
├── SAM (Meta) --- セグメンテーション
├── HR-VITON --- 画像ベース試着
├── CatVTON --- カテゴリ対応試着
└── StableVITON --- Diffusion ベース試着
バックエンド
├── ONNX Runtime --- モデル推論
├── TensorRT --- GPU 最適化推論
└── Triton Server --- 推論サーバー
1.3 3つのアプローチ
【2D 画像ベース(Image-based VTON)】
ユーザー写真 + 衣服画像 → AI が合成
精度: 中〜高
速度: 中(1-3秒)
用途: EC サイト、カタログ
【3D モデルベース】
3Dボディスキャン + 3D衣服モデル → 物理シミュレーション
精度: 最高
速度: 低(数秒〜数十秒)
用途: 高級アパレル、オーダーメイド
【AR リアルタイム】
カメラ映像 + AR オーバーレイ → リアルタイム合成
精度: 低〜中
速度: リアルタイム(30fps)
用途: 店舗ミラー、モバイルアプリ
1.4 技術進化のタイムライン
2018 VITON (Han et al.)
│ └─ 画像ベース試着の原点、Thin Plate Spline 変形
│
2019 CP-VTON (Wang et al.)
│ └─ Geometric Matching Module の導入
│
2020 ACGPN (Yang et al.)
│ └─ セマンティックセグメンテーションの精緻化
│
2021 PF-AFN (Ge et al.)
│ └─ Parser-Free アプローチ(パーシング不要化)
│
2022 HR-VITON (Lee et al.)
│ └─ 高解像度対応、条件付き正規化フロー
│
2023 StableVITON (Kim et al.)
│ └─ Stable Diffusion ベース、高品質合成
│ CatVTON (Zheng et al.)
│ └─ カテゴリ認識型試着
│
2024 OOTDiffusion (Xu et al.)
│ └─ Outfitting Fusion、全身対応
│ IDM-VTON
│ └─ Identity-preserving 試着
│
2025 マルチモーダル統合
└─ テキスト指示による衣服変更、3D統合
2. 2D 画像ベース試着の実装
2.1 HR-VITON パイプライン
# HR-VITON による試着画像生成 (擬似コード)
import torch
from hr_viton import HRVITONModel
from utils import load_image, preprocess
# モデルロード
model = HRVITONModel.from_pretrained("hr-viton-checkpoint")
model.to("cuda")
# 入力準備
person_image = load_image("person.jpg") # ユーザー写真
garment_image = load_image("tshirt_catalog.jpg") # 衣服カタログ画像
# 前処理: 人体パーシング + ポーズ推定
person_parse = segment_person(person_image) # 体のパーツ分割
person_pose = estimate_pose(person_image) # 関節位置推定
garment_mask = segment_garment(garment_image) # 衣服領域抽出
# 試着画像生成
result = model.inference(
person_image=person_image,
garment_image=garment_image,
person_parse=person_parse,
person_pose=person_pose,
garment_mask=garment_mask,
)
result.save("try_on_result.jpg")2.2 DensePose による人体表面推定
# DensePose: 人体表面の UV マッピング
from detectron2 import model_zoo
from detectron2.engine import DefaultPredictor
from detectron2.config import get_cfg
from densepose.config import add_densepose_config
cfg = get_cfg()
add_densepose_config(cfg)
cfg.merge_from_file(model_zoo.get_config_file(
"densepose_rcnn_R_101_FPN_DL_s1x.yaml"
))
cfg.MODEL.WEIGHTS = "densepose_model.pkl"
predictor = DefaultPredictor(cfg)
outputs = predictor(person_image)
# DensePose は人体の表面を UV 座標にマッピング
# → 衣服テクスチャを人体表面に正確に貼り付け可能2.3 セグメンテーション
# 人体パーシング (LIP / ATR フォーマット)
# 各ピクセルを体のパーツに分類
# パーツラベル:
# 0: 背景, 1: 帽子, 2: 髪, 3: サングラス
# 4: 上着, 5: スカート, 6: パンツ, 7: ドレス
# 8: ベルト, 9: 左靴, 10: 右靴, 11: 顔
# 12: 左足, 13: 右足, 14: 左腕, 15: 右腕
# 16: バッグ, 17: スカーフ
from transformers import SegformerForSemanticSegmentation, SegformerImageProcessor
processor = SegformerImageProcessor.from_pretrained(
"mattmdjaga/segformer_b2_clothes"
)
model = SegformerForSemanticSegmentation.from_pretrained(
"mattmdjaga/segformer_b2_clothes"
)
inputs = processor(images=person_image, return_tensors="pt")
outputs = model(**inputs)
# 各ピクセルのパーツラベルを取得
parse_map = outputs.logits.argmax(dim=1).squeeze()2.4 OOTDiffusion による高品質試着
# OOTDiffusion: Outfitting Fusion ベースの試着モデル
# Stable Diffusion をベースとした最新の VTON 手法
import torch
from ootd.inference import OOTDInference
from PIL import Image
class OOTDiffusionPipeline:
"""OOTDiffusion による高品質バーチャル試着"""
def __init__(self, model_path: str = "levihsu/OOTDiffusion"):
self.model = OOTDInference(
model_path=model_path,
model_type="hd", # "hd" (半身) or "dc" (全身)
)
def try_on(
self,
person_image_path: str,
garment_image_path: str,
category: str = "upperbody",
num_samples: int = 1,
num_steps: int = 20,
guidance_scale: float = 2.0,
seed: int = 42,
) -> list:
"""
バーチャル試着を実行
category:
- "upperbody": 上半身(Tシャツ、シャツ、ジャケット等)
- "lowerbody": 下半身(パンツ、スカート等)
- "dress": ワンピース、ドレス
guidance_scale:
- 1.0-2.0: 自然な仕上がり
- 2.0-3.0: 衣服のディテール重視
- 3.0+: 過度に強調(アーティファクト注意)
"""
person_img = Image.open(person_image_path).resize((768, 1024))
garment_img = Image.open(garment_image_path).resize((768, 1024))
results = self.model(
category=category,
image_garm=garment_img,
image_vton=person_img,
n_samples=num_samples,
n_steps=num_steps,
image_scale=guidance_scale,
seed=seed,
)
return results
def batch_try_on(
self,
person_image_path: str,
garment_dir: str,
output_dir: str,
category: str = "upperbody",
) -> dict:
"""複数の衣服を一括で試着"""
from pathlib import Path
Path(output_dir).mkdir(parents=True, exist_ok=True)
results = {}
for garment_path in Path(garment_dir).glob("*.{jpg,png,jpeg}"):
try:
output = self.try_on(
person_image_path=person_image_path,
garment_image_path=str(garment_path),
category=category,
)
out_path = f"{output_dir}/{garment_path.stem}_tryon.png"
output[0].save(out_path)
results[garment_path.stem] = out_path
except Exception as e:
results[garment_path.stem] = f"Error: {e}"
return results
# 使用例
pipeline = OOTDiffusionPipeline()
# 単一衣服の試着
result = pipeline.try_on(
person_image_path="user_photo.jpg",
garment_image_path="blue_tshirt.jpg",
category="upperbody",
num_samples=3, # 3つのバリエーションを生成
guidance_scale=2.0,
)
# 結果の保存
for i, img in enumerate(result):
img.save(f"tryon_result_{i}.png")2.5 衣服変形アルゴリズムの詳細
# Thin Plate Spline (TPS) 変形
# 衣服画像をユーザーの体型に合わせて変形する手法
import numpy as np
import cv2
class ThinPlateSplineWarper:
"""TPS (薄板スプライン) による衣服変形"""
def __init__(self, source_points: np.ndarray, target_points: np.ndarray):
"""
source_points: 衣服画像上の制御点 (N, 2)
target_points: ユーザー画像上の対応点 (N, 2)
"""
self.source = source_points
self.target = target_points
self.n = len(source_points)
# TPS パラメータの計算
self._compute_parameters()
def _compute_parameters(self):
"""TPS 変形パラメータを計算"""
n = self.n
K = np.zeros((n, n))
for i in range(n):
for j in range(n):
if i != j:
r = np.linalg.norm(self.source[i] - self.source[j])
K[i, j] = r ** 2 * np.log(r + 1e-6)
# 線形部分の行列
P = np.hstack([np.ones((n, 1)), self.source])
# 連立方程式の構築
L = np.zeros((n + 3, n + 3))
L[:n, :n] = K
L[:n, n:] = P
L[n:, :n] = P.T
# 各軸について解く
self.params_x = np.linalg.solve(
L + np.eye(n + 3) * 1e-6,
np.concatenate([self.target[:, 0], [0, 0, 0]])
)
self.params_y = np.linalg.solve(
L + np.eye(n + 3) * 1e-6,
np.concatenate([self.target[:, 1], [0, 0, 0]])
)
def warp_image(self, image: np.ndarray) -> np.ndarray:
"""画像を TPS 変形する"""
h, w = image.shape[:2]
output = np.zeros_like(image)
for y in range(h):
for x in range(w):
point = np.array([x, y], dtype=float)
# TPS による座標変換
new_x = self._transform_point(point, self.params_x)
new_y = self._transform_point(point, self.params_y)
# バイリニア補間でピクセル値を取得
if 0 <= new_x < w and 0 <= new_y < h:
output[y, x] = self._bilinear_interpolate(
image, new_x, new_y
)
return output
def _transform_point(self, point, params):
"""1点の座標変換"""
result = params[self.n] + params[self.n + 1] * point[0] + params[self.n + 2] * point[1]
for i in range(self.n):
r = np.linalg.norm(point - self.source[i])
if r > 0:
result += params[i] * r ** 2 * np.log(r)
return result
def _bilinear_interpolate(self, image, x, y):
"""バイリニア補間"""
x0 = int(np.floor(x))
x1 = min(x0 + 1, image.shape[1] - 1)
y0 = int(np.floor(y))
y1 = min(y0 + 1, image.shape[0] - 1)
dx = x - x0
dy = y - y0
return (
image[y0, x0] * (1 - dx) * (1 - dy)
+ image[y0, x1] * dx * (1 - dy)
+ image[y1, x0] * (1 - dx) * dy
+ image[y1, x1] * dx * dy
).astype(np.uint8)3. AR リアルタイム試着
# MediaPipe + Three.js でリアルタイム AR 試着 (概念コード)
import mediapipe as mp
import cv2
mp_pose = mp.solutions.pose
pose = mp_pose.Pose(
static_image_mode=False,
model_complexity=1,
min_detection_confidence=0.5,
min_tracking_confidence=0.5,
)
cap = cv2.VideoCapture(0)
while cap.isOpened():
ret, frame = cap.read()
if not ret:
break
# ポーズ推定
results = pose.process(cv2.cvtColor(frame, cv2.COLOR_BGR2RGB))
if results.pose_landmarks:
landmarks = results.pose_landmarks.landmark
# 肩、腰、腕の座標を取得
left_shoulder = landmarks[mp_pose.PoseLandmark.LEFT_SHOULDER]
right_shoulder = landmarks[mp_pose.PoseLandmark.RIGHT_SHOULDER]
# 衣服画像をポーズに合わせて変形・合成
garment_overlay = warp_garment_to_pose(
garment_image, landmarks, frame.shape
)
frame = overlay_transparent(frame, garment_overlay)
cv2.imshow('Virtual Try-On', frame)3.1 WebAR 実装パターン
// WebAR を使ったブラウザベース試着
// Three.js + MediaPipe Holistic
class WebARTryOn {
constructor(videoElement, canvasElement) {
this.video = videoElement;
this.canvas = canvasElement;
// Three.js のセットアップ
this.scene = new THREE.Scene();
this.camera = new THREE.PerspectiveCamera(75, 16 / 9, 0.1, 1000);
this.renderer = new THREE.WebGLRenderer({
canvas: this.canvas,
alpha: true,
});
// MediaPipe Pose の初期化
this.pose = new Pose({
locateFile: (file) => {
return `https://cdn.jsdelivr.net/npm/@mediapipe/pose/${file}`;
},
});
this.pose.setOptions({
modelComplexity: 1,
smoothLandmarks: true,
minDetectionConfidence: 0.5,
minTrackingConfidence: 0.5,
});
this.pose.onResults(this.onPoseResults.bind(this));
// 衣服メッシュ
this.garmentMesh = null;
}
async loadGarment(glbPath) {
const loader = new THREE.GLTFLoader();
const gltf = await loader.loadAsync(glbPath);
this.garmentMesh = gltf.scene;
this.scene.add(this.garmentMesh);
}
onPoseResults(results) {
if (!results.poseLandmarks || !this.garmentMesh) return;
const landmarks = results.poseLandmarks;
// 肩の座標からスケールと位置を計算
const leftShoulder = landmarks[11];
const rightShoulder = landmarks[12];
const leftHip = landmarks[23];
const rightHip = landmarks[24];
// 肩幅から衣服のスケーリング
const shoulderWidth = Math.sqrt(
Math.pow(rightShoulder.x - leftShoulder.x, 2) +
Math.pow(rightShoulder.y - leftShoulder.y, 2)
);
// 体の中心座標
const centerX = (leftShoulder.x + rightShoulder.x) / 2;
const centerY = (leftShoulder.y + leftHip.y) / 2;
// 体の傾きを計算
const angle = Math.atan2(
rightShoulder.y - leftShoulder.y,
rightShoulder.x - leftShoulder.x
);
// メッシュの位置・回転・スケールを更新
this.garmentMesh.position.set(
(centerX - 0.5) * 4,
-(centerY - 0.5) * 4,
0
);
this.garmentMesh.rotation.z = angle;
this.garmentMesh.scale.setScalar(shoulderWidth * 5);
this.renderer.render(this.scene, this.camera);
}
async start() {
const stream = await navigator.mediaDevices.getUserMedia({ video: true });
this.video.srcObject = stream;
const cameraLoop = new Camera(this.video, {
onFrame: async () => {
await this.pose.send({ image: this.video });
},
width: 1280,
height: 720,
});
cameraLoop.start();
}
}
// 使用例
const tryOn = new WebARTryOn(
document.getElementById('video'),
document.getElementById('canvas')
);
await tryOn.loadGarment('/models/tshirt.glb');
await tryOn.start();3.2 サイズ推薦システム
# AI ベースのサイズ推薦システム
import numpy as np
from dataclasses import dataclass
@dataclass
class BodyMeasurements:
"""体型計測データ"""
height_cm: float
weight_kg: float
chest_cm: float
waist_cm: float
hip_cm: float
shoulder_width_cm: float
arm_length_cm: float
inseam_cm: float
class SizeRecommender:
"""体型データに基づくサイズ推薦"""
# ブランドごとのサイズチャート(例)
SIZE_CHARTS = {
"standard_jp": {
"S": {"chest": (80, 88), "waist": (68, 76), "hip": (82, 90)},
"M": {"chest": (88, 96), "waist": (76, 84), "hip": (90, 98)},
"L": {"chest": (96, 104), "waist": (84, 92), "hip": (98, 106)},
"XL": {"chest": (104, 112), "waist": (92, 100), "hip": (106, 114)},
},
}
def __init__(self, brand: str = "standard_jp"):
self.size_chart = self.SIZE_CHARTS.get(brand, self.SIZE_CHARTS["standard_jp"])
def estimate_body_from_image(self, image_path: str, height_cm: float) -> BodyMeasurements:
"""
写真と身長から体型を推定
実際の実装では:
1. DensePose で体表面の UV マッピング
2. SMPL / SMPL-X で 3D 体型パラメータ推定
3. パラメータから各部位の寸法を計算
"""
# SMPL モデルによる体型推定(擬似コード)
from smpl_estimation import estimate_smpl_params
smpl_params = estimate_smpl_params(image_path, height_cm)
measurements = BodyMeasurements(
height_cm=height_cm,
weight_kg=smpl_params.estimated_weight,
chest_cm=smpl_params.chest_circumference,
waist_cm=smpl_params.waist_circumference,
hip_cm=smpl_params.hip_circumference,
shoulder_width_cm=smpl_params.shoulder_width,
arm_length_cm=smpl_params.arm_length,
inseam_cm=smpl_params.inseam,
)
return measurements
def recommend_size(
self,
measurements: BodyMeasurements,
garment_type: str = "top",
fit_preference: str = "regular",
) -> dict:
"""
サイズを推薦
fit_preference: "slim", "regular", "loose"
"""
# フィット調整(cm)
fit_adjustment = {
"slim": -2,
"regular": 0,
"loose": 4,
}
adj = fit_adjustment.get(fit_preference, 0)
# 各サイズとの適合度を計算
scores = {}
for size, ranges in self.size_chart.items():
score = 0
if garment_type in ("top", "outerwear"):
chest_mid = (ranges["chest"][0] + ranges["chest"][1]) / 2
score += 1.0 - abs(measurements.chest_cm + adj - chest_mid) / 20
if garment_type in ("bottom",):
waist_mid = (ranges["waist"][0] + ranges["waist"][1]) / 2
score += 1.0 - abs(measurements.waist_cm + adj - waist_mid) / 20
hip_mid = (ranges["hip"][0] + ranges["hip"][1]) / 2
score += 1.0 - abs(measurements.hip_cm + adj - hip_mid) / 20
scores[size] = max(0, score)
# 最適サイズ
best_size = max(scores, key=scores.get)
confidence = scores[best_size] / max(sum(scores.values()), 1e-6)
return {
"recommended_size": best_size,
"confidence": round(confidence, 2),
"scores": scores,
"fit_preference": fit_preference,
"note": f"バスト{measurements.chest_cm}cm に基づく推薦"
if garment_type == "top"
else f"ウエスト{measurements.waist_cm}cm に基づく推薦",
}
# 使用例
recommender = SizeRecommender(brand="standard_jp")
measurements = BodyMeasurements(
height_cm=170,
weight_kg=65,
chest_cm=92,
waist_cm=78,
hip_cm=94,
shoulder_width_cm=44,
arm_length_cm=58,
inseam_cm=76,
)
result = recommender.recommend_size(
measurements=measurements,
garment_type="top",
fit_preference="regular",
)
print(f"推薦サイズ: {result['recommended_size']}")
print(f"信頼度: {result['confidence']}")4. EC サイト向け実装ガイド
4.1 バックエンド API 設計
# FastAPI を使ったバーチャル試着 API
from fastapi import FastAPI, File, UploadFile, HTTPException
from pydantic import BaseModel
from typing import Optional
import uuid
app = FastAPI(title="Virtual Try-On API")
class TryOnRequest(BaseModel):
person_image_id: str
garment_id: str
category: str = "upperbody" # upperbody, lowerbody, dress
guidance_scale: float = 2.0
num_samples: int = 1
class TryOnResponse(BaseModel):
request_id: str
status: str
result_urls: list[str] = []
processing_time_ms: float = 0
size_recommendation: Optional[dict] = None
class VTONService:
"""バーチャル試着サービス"""
def __init__(self):
self.model = None # 遅延ロード
self._load_model()
def _load_model(self):
"""モデルの遅延ロード"""
import torch
self.device = "cuda" if torch.cuda.is_available() else "cpu"
# ここで VTON モデルをロード
print(f"Model loaded on {self.device}")
async def process_try_on(self, request: TryOnRequest) -> TryOnResponse:
"""試着リクエストを処理"""
import time
start = time.time()
request_id = str(uuid.uuid4())
try:
# 画像取得
person_img = await self._fetch_image(request.person_image_id)
garment_img = await self._fetch_garment(request.garment_id)
# 試着実行
results = self._run_inference(
person_img, garment_img,
category=request.category,
guidance_scale=request.guidance_scale,
num_samples=request.num_samples,
)
# 結果保存
result_urls = []
for i, result in enumerate(results):
url = await self._save_result(request_id, i, result)
result_urls.append(url)
elapsed = (time.time() - start) * 1000
return TryOnResponse(
request_id=request_id,
status="completed",
result_urls=result_urls,
processing_time_ms=round(elapsed, 1),
)
except Exception as e:
return TryOnResponse(
request_id=request_id,
status=f"error: {str(e)}",
)
def _run_inference(self, person_img, garment_img, **kwargs):
"""推論実行"""
# 実際の推論ロジック
pass
async def _fetch_image(self, image_id):
"""S3 等から画像を取得"""
pass
async def _fetch_garment(self, garment_id):
"""カタログ DB から衣服画像を取得"""
pass
async def _save_result(self, request_id, index, image):
"""結果を S3 に保存"""
pass
vton_service = VTONService()
@app.post("/api/v1/try-on", response_model=TryOnResponse)
async def try_on(request: TryOnRequest):
"""バーチャル試着 API エンドポイント"""
return await vton_service.process_try_on(request)
@app.post("/api/v1/upload-photo")
async def upload_photo(file: UploadFile = File(...)):
"""ユーザー写真のアップロード"""
if file.content_type not in ["image/jpeg", "image/png"]:
raise HTTPException(400, "JPEG または PNG のみ対応")
image_id = str(uuid.uuid4())
# S3 に保存
return {"image_id": image_id, "status": "uploaded"}4.2 フロントエンド統合パターン
// React コンポーネントでの統合例
interface TryOnResult {
requestId: string;
status: string;
resultUrls: string[];
processingTimeMs: number;
}
async function virtualTryOn(
personImageId: string,
garmentId: string,
category: 'upperbody' | 'lowerbody' | 'dress' = 'upperbody'
): Promise<TryOnResult> {
const response = await fetch('/api/v1/try-on', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
person_image_id: personImageId,
garment_id: garmentId,
category: category,
guidance_scale: 2.0,
num_samples: 1,
}),
});
if (!response.ok) {
throw new Error(`Try-on failed: ${response.statusText}`);
}
return response.json();
}
// UX パターン: プログレッシブ表示
// 1. ローディングスケルトン表示
// 2. 低解像度のプレビューを先に表示(512px)
// 3. 高解像度の結果に差し替え(1024px+)5. パフォーマンス最適化
5.1 推論高速化テクニック
# TensorRT による推論高速化
import tensorrt as trt
import numpy as np
class TRTOptimizedVTON:
"""TensorRT で最適化されたバーチャル試着モデル"""
def __init__(self, engine_path: str):
self.logger = trt.Logger(trt.Logger.WARNING)
with open(engine_path, "rb") as f:
self.engine = trt.Runtime(self.logger).deserialize_cuda_engine(f.read())
self.context = self.engine.create_execution_context()
@staticmethod
def convert_to_trt(
onnx_path: str,
output_path: str,
fp16: bool = True,
max_batch: int = 4,
):
"""ONNX モデルを TensorRT エンジンに変換"""
builder = trt.Builder(trt.Logger(trt.Logger.WARNING))
network = builder.create_network(
1 << int(trt.NetworkDefinitionCreationFlag.EXPLICIT_BATCH)
)
parser = trt.OnnxParser(network, trt.Logger(trt.Logger.WARNING))
with open(onnx_path, "rb") as f:
parser.parse(f.read())
config = builder.create_builder_config()
config.max_workspace_size = 4 << 30 # 4GB
if fp16:
config.set_flag(trt.BuilderFlag.FP16)
# ダイナミックバッチサイズ
profile = builder.create_optimization_profile()
input_shape = network.get_input(0).shape
profile.set_shape(
network.get_input(0).name,
min=(1, *input_shape[1:]),
opt=(2, *input_shape[1:]),
max=(max_batch, *input_shape[1:]),
)
config.add_optimization_profile(profile)
engine = builder.build_engine(network, config)
with open(output_path, "wb") as f:
f.write(engine.serialize())
print(f"TRT engine saved: {output_path}")
# 高速化の効果比較
# ┌──────────────┬──────────┬──────────┬──────────┐
# │ 手法 │ 推論時間 │ VRAM │ 品質 │
# ├──────────────┼──────────┼──────────┼──────────┤
# │ PyTorch FP32 │ 3.5秒 │ 8GB │ 最高 │
# │ PyTorch FP16 │ 1.8秒 │ 4GB │ ほぼ同等 │
# │ ONNX Runtime │ 1.2秒 │ 4GB │ ほぼ同等 │
# │ TensorRT FP16│ 0.6秒 │ 3GB │ ほぼ同等 │
# │ TensorRT INT8│ 0.3秒 │ 2GB │ やや低下 │
# └──────────────┴──────────┴──────────┴──────────┘5.2 バッチ処理とキャッシング
# Redis を使った試着結果のキャッシング
import hashlib
import redis
import json
class TryOnCache:
"""試着結果のキャッシュ管理"""
def __init__(self, redis_url: str = "redis://localhost:6379"):
self.redis = redis.from_url(redis_url)
self.ttl = 3600 * 24 # 24時間
def _generate_key(self, person_hash: str, garment_id: str, params: dict) -> str:
"""キャッシュキーの生成"""
param_str = json.dumps(params, sort_keys=True)
raw = f"{person_hash}:{garment_id}:{param_str}"
return f"vton:{hashlib.sha256(raw.encode()).hexdigest()}"
def get_cached_result(self, person_hash: str, garment_id: str, params: dict):
"""キャッシュから結果を取得"""
key = self._generate_key(person_hash, garment_id, params)
result = self.redis.get(key)
if result:
return json.loads(result)
return None
def cache_result(self, person_hash: str, garment_id: str, params: dict, result_urls: list):
"""結果をキャッシュに保存"""
key = self._generate_key(person_hash, garment_id, params)
self.redis.setex(key, self.ttl, json.dumps(result_urls))
def get_cache_stats(self) -> dict:
"""キャッシュ統計"""
info = self.redis.info("stats")
return {
"hits": info.get("keyspace_hits", 0),
"misses": info.get("keyspace_misses", 0),
"hit_rate": info.get("keyspace_hits", 0) /
max(info.get("keyspace_hits", 0) + info.get("keyspace_misses", 0), 1),
}6. 比較表
| 手法 | 精度 | 速度 | コスト | 要件 |
|---|---|---|---|---|
| 2D 画像ベース (HR-VITON) | 高 | 1-3秒 | 中 | GPU サーバー |
| Diffusion ベース (StableVITON) | 最高 | 5-15秒 | 高 | 高性能 GPU |
| OOTDiffusion | 最高 | 3-8秒 | 高 | 高性能 GPU |
| 3D モデルベース | 最高 | 10-30秒 | 最高 | 3D スキャンデータ |
| AR リアルタイム (MediaPipe) | 中 | リアルタイム | 低 | カメラのみ |
| 簡易オーバーレイ | 低 | リアルタイム | 最低 | なし |
| ユースケース | 推奨手法 | 理由 |
|---|---|---|
| EC 商品ページ | 2D 画像ベース | バッチ処理可能、高品質 |
| 店舗の AR ミラー | AR リアルタイム | 即時フィードバック |
| 高級ブランド | 3D モデルベース | 最高品質、サイズ感再現 |
| モバイルアプリ | AR リアルタイム | GPU 軽量、UX 良好 |
| ルックブック自動生成 | OOTDiffusion | 高品質、全身対応 |
VTON モデル世代別比較
| 世代 | 代表モデル | 特徴 | 制限 |
|---|---|---|---|
| 第1世代 (2018) | VITON, CP-VTON | TPS 変形 + 合成 | 変形の不自然さ |
| 第2世代 (2020) | ACGPN, PF-AFN | セグメンテーション改善 | 複雑な衣服に弱い |
| 第3世代 (2022) | HR-VITON | 高解像度、正規化フロー | 特定ポーズに限定 |
| 第4世代 (2023-) | StableVITON, OOTDiffusion | Diffusion ベース | 処理速度が遅い |
7. トラブルシューティング
7.1 よくある問題と対処法
問題1: 衣服のテクスチャが潰れる
──────────────────────────────
原因: 入力画像の解像度が低い、または衣服画像に影や折り目が多い
対処:
1. 衣服画像は白背景・フラットな状態で撮影(推奨: 1024x1024以上)
2. 影除去の前処理を適用
3. guidance_scale を下げる(1.5-2.0)
問題2: 体型が不自然に変形する
──────────────────────────────
原因: 人体パーシングの精度が低い、ポーズ推定の失敗
対処:
1. 正面・全身が写った写真を使用
2. 背景がシンプルな写真を選択
3. 複数の推定結果をアンサンブル
問題3: 衣服と肌の境界が不自然
──────────────────────────────
原因: セグメンテーションの精度不足、ブレンド処理の欠如
対処:
1. マスクにフェザリング(ぼかし)を適用
2. 境界部分の色調を統一する後処理
3. DensePose の UV マップを活用した精密な合成
問題4: AR モードでちらつき(フリッカー)
──────────────────────────────
原因: ポーズ推定のフレーム間ジッター
対処:
1. One Euro Filter でランドマークを平滑化
2. min_tracking_confidence を上げる(0.7-0.8)
3. 指数移動平均でポジションをスムージング
問題5: 処理速度が遅い(5秒以上)
──────────────────────────────
原因: モデルの最適化不足、大きすぎる解像度
対処:
1. TensorRT / ONNX Runtime で推論を最適化
2. FP16 精度に変更
3. 入力解像度を 768x1024 に統一
4. バッチ処理でスループットを向上
7.2 衣服画像の品質チェックリスト
撮影条件チェックリスト:
□ 白または単色の背景
□ 均一な照明(影なし)
□ 正面から撮影(平置きまたはマネキン)
□ 衣服全体が収まっている
□ 最低 1024x1024 ピクセル
□ JPEG 品質 90% 以上
□ 色の正確性(ホワイトバランス調整済み)
□ シワや折り目が最小限
前処理パイプライン:
1. 背景除去 → 白背景に統一
2. ホワイトバランス補正
3. 影除去(Intrinsic Image Decomposition)
4. 解像度統一(1024x1024 にリサイズ)
5. 衣服マスクの生成(SAM or U2-Net)
8. アンチパターン
アンチパターン 1: 体型の多様性を無視する
BAD:
標準体型のモデルでのみ学習
→ 多様な体型のユーザーで試着結果が不自然
→ 衣服がはみ出す、伸びる、変形する
GOOD:
- 多様な体型のデータで学習
- 体型パラメータ(身長、体重、バスト/ウエスト/ヒップ)を入力として使用
- ユーザーの体型に応じたサイズ推薦機能を併設
アンチパターン 2: 照明・色味の不一致
BAD:
スタジオ照明の衣服画像 + 屋外自然光のユーザー写真
→ 明らかな合成感、色味の不一致
→ ユーザーの購買判断に悪影響
GOOD:
- 照明推定(Light Estimation)で環境光を分析
- 衣服の色温度・明度をユーザー写真に合わせて調整
- 影の方向を統一(シャドウハーモナイゼーション)
アンチパターン 3: 単一アングルのみの対応
BAD:
正面写真のみ対応し、横向き・後ろ姿は完全に非対応
→ ユーザーが衣服の全体像を把握できない
→ 「背中のデザインが見えない」というクレーム
GOOD:
- マルチビュー対応: 正面、側面、背面の試着画像を生成
- 3D ボディモデル推定 → 複数視点からのレンダリング
- ユーザーに対して「正面写真」の撮影をガイドする UI
- 将来的には動画入力から 3D 再構成
アンチパターン 4: エラーハンドリングの欠如
BAD:
ポーズ推定の失敗時に何も表示せず、変形した試着結果を返す
→ ユーザーが不自然な画像を見て離脱
GOOD:
- ポーズ推定の信頼度スコアをチェック
- 信頼度が低い場合は「撮り直し」を促すメッセージ
- フォールバック: 標準モデルでの試着を代替表示
- 各処理ステージの品質ゲートを設定
実践演習
演習1: 基本的な実装
以下の要件を満たすコードを実装してください。
要件:
- 入力データの検証を行うこと
- エラーハンドリングを適切に実装すること
- テストコードも作成すること
# 演習1: 基本実装のテンプレート
class Exercise1:
"""基本的な実装パターンの演習"""
def __init__(self):
self.data = []
def validate_input(self, value):
"""入力値の検証"""
if value is None:
raise ValueError("入力値がNoneです")
return True
def process(self, value):
"""データ処理のメインロジック"""
self.validate_input(value)
self.data.append(value)
return self.data
def get_results(self):
"""処理結果の取得"""
return {
'count': len(self.data),
'data': self.data
}
# テスト
def test_exercise1():
ex = Exercise1()
assert ex.process(1) == [1]
assert ex.process(2) == [1, 2]
assert ex.get_results()['count'] == 2
try:
ex.process(None)
assert False, "例外が発生するべき"
except ValueError:
pass
print("全テスト合格!")
test_exercise1()演習2: 応用パターン
基本実装を拡張して、以下の機能を追加してください。
# 演習2: 応用パターン
from typing import List, Dict, Optional
from datetime import datetime
class AdvancedExercise:
"""応用パターンの演習"""
def __init__(self, max_size: int = 100):
self._items: List[Dict] = []
self._max_size = max_size
self._created_at = datetime.now()
def add(self, key: str, value: any) -> bool:
"""アイテムの追加(サイズ制限付き)"""
if len(self._items) >= self._max_size:
return False
self._items.append({
'key': key,
'value': value,
'timestamp': datetime.now().isoformat()
})
return True
def find(self, key: str) -> Optional[Dict]:
"""キーによる検索"""
for item in reversed(self._items):
if item['key'] == key:
return item
return None
def remove(self, key: str) -> bool:
"""キーによる削除"""
for i, item in enumerate(self._items):
if item['key'] == key:
self._items.pop(i)
return True
return False
def stats(self) -> Dict:
"""統計情報"""
return {
'total_items': len(self._items),
'max_size': self._max_size,
'usage_percent': len(self._items) / self._max_size * 100,
'uptime': str(datetime.now() - self._created_at)
}
# テスト
def test_advanced():
ex = AdvancedExercise(max_size=3)
assert ex.add("a", 1) == True
assert ex.add("b", 2) == True
assert ex.add("c", 3) == True
assert ex.add("d", 4) == False # サイズ制限
assert ex.find("b")['value'] == 2
assert ex.remove("b") == True
assert ex.find("b") is None
stats = ex.stats()
assert stats['total_items'] == 2
print("応用テスト全合格!")
test_advanced()演習3: パフォーマンス最適化
以下のコードのパフォーマンスを改善してください。
# 演習3: パフォーマンス最適化
import time
from functools import lru_cache
# 最適化前(O(n^2))
def slow_search(data: list, target: int) -> int:
"""非効率な検索"""
for i in range(len(data)):
for j in range(i + 1, len(data)):
if data[i] + data[j] == target:
return (i, j)
return (-1, -1)
# 最適化後(O(n))
def fast_search(data: list, target: int) -> tuple:
"""ハッシュマップを使った効率的な検索"""
seen = {}
for i, num in enumerate(data):
complement = target - num
if complement in seen:
return (seen[complement], i)
seen[num] = i
return (-1, -1)
# ベンチマーク
def benchmark():
import random
data = list(range(5000))
random.shuffle(data)
target = data[100] + data[4000]
start = time.time()
result1 = slow_search(data, target)
slow_time = time.time() - start
start = time.time()
result2 = fast_search(data, target)
fast_time = time.time() - start
print(f"非効率版: {slow_time:.4f}秒")
print(f"効率版: {fast_time:.6f}秒")
print(f"高速化率: {slow_time/fast_time:.0f}倍")
benchmark()ポイント:
- アルゴリズムの計算量を意識する
- 適切なデータ構造を選択する
- ベンチマークで効果を測定する
設計判断ガイド
選択基準マトリクス
技術選択を行う際の判断基準を以下にまとめます。
| 判断基準 | 重視する場合 | 妥協できる場合 |
|---|---|---|
| パフォーマンス | リアルタイム処理、大規模データ | 管理画面、バッチ処理 |
| 保守性 | 長期運用、チーム開発 | プロトタイプ、短期プロジェクト |
| スケーラビリティ | 成長が見込まれるサービス | 社内ツール、固定ユーザー |
| セキュリティ | 個人情報、金融データ | 公開データ、社内利用 |
| 開発速度 | MVP、市場投入スピード | 品質重視、ミッションクリティカル |
アーキテクチャパターンの選択
| アーキテクチャ選択フロー |
|---|
| ① チーム規模は? |
| ├─ 小規模(1-5人)→ モノリス |
| └─ 大規模(10人+)→ ②へ |
| ② デプロイ頻度は? |
| ├─ 週1回以下 → モノリス + モジュール分割 |
| └─ 毎日/複数回 → ③へ |
| ③ チーム間の独立性は? |
| ├─ 高い → マイクロサービス |
| └─ 中程度 → モジュラーモノリス |
トレードオフの分析
技術的な判断には必ずトレードオフが伴います。以下の観点で分析を行いましょう:
1. 短期 vs 長期のコスト
- 短期的に速い方法が長期的には技術的負債になることがある
- 逆に、過剰な設計は短期的なコストが高く、プロジェクトの遅延を招く
2. 一貫性 vs 柔軟性
- 統一された技術スタックは学習コストが低い
- 多様な技術の採用は適材適所が可能だが、運用コストが増加
3. 抽象化のレベル
- 高い抽象化は再利用性が高いが、デバッグが困難になる場合がある
- 低い抽象化は直感的だが、コードの重複が発生しやすい
# 設計判断の記録テンプレート
class ArchitectureDecisionRecord:
"""ADR (Architecture Decision Record) の作成"""
def __init__(self, title: str):
self.title = title
self.context = ""
self.decision = ""
self.consequences = []
self.alternatives = []
def set_context(self, context: str):
"""背景と課題の記述"""
self.context = context
return self
def set_decision(self, decision: str):
"""決定内容の記述"""
self.decision = decision
return self
def add_consequence(self, consequence: str, positive: bool = True):
"""結果の追加"""
self.consequences.append({
'description': consequence,
'type': 'positive' if positive else 'negative'
})
return self
def add_alternative(self, name: str, reason_rejected: str):
"""却下した代替案の追加"""
self.alternatives.append({
'name': name,
'reason_rejected': reason_rejected
})
return self
def to_markdown(self) -> str:
"""Markdown形式で出力"""
md = f"# ADR: {self.title}\n\n"
md += f"## 背景\n{self.context}\n\n"
md += f"## 決定\n{self.decision}\n\n"
md += "## 結果\n"
for c in self.consequences:
icon = "✅" if c['type'] == 'positive' else "⚠️"
md += f"- {icon} {c['description']}\n"
md += "\n## 却下した代替案\n"
for a in self.alternatives:
md += f"- **{a['name']}**: {a['reason_rejected']}\n"
return md実務での適用シナリオ
シナリオ1: スタートアップでのMVP開発
状況: 限られたリソースで素早くプロダクトをリリースする必要がある
アプローチ:
- シンプルなアーキテクチャを選択
- 必要最小限の機能に集中
- 自動テストはクリティカルパスのみ
- モニタリングは早期から導入
学んだ教訓:
- 完璧を求めすぎない(YAGNI原則)
- ユーザーフィードバックを早期に取得
- 技術的負債は意識的に管理する
シナリオ2: レガシーシステムのモダナイゼーション
状況: 10年以上運用されているシステムを段階的に刷新する
アプローチ:
- Strangler Fig パターンで段階的に移行
- 既存のテストがない場合はCharacterization Testを先に作成
- APIゲートウェイで新旧システムを共存
- データ移行は段階的に実施
| フェーズ | 作業内容 | 期間目安 | リスク |
|---|---|---|---|
| 1. 調査 | 現状分析、依存関係の把握 | 2-4週間 | 低 |
| 2. 基盤 | CI/CD構築、テスト環境 | 4-6週間 | 低 |
| 3. 移行開始 | 周辺機能から順次移行 | 3-6ヶ月 | 中 |
| 4. コア移行 | 中核機能の移行 | 6-12ヶ月 | 高 |
| 5. 完了 | 旧システム廃止 | 2-4週間 | 中 |
シナリオ3: 大規模チームでの開発
状況: 50人以上のエンジニアが同一プロダクトを開発する
アプローチ:
- ドメイン駆動設計で境界を明確化
- チームごとにオーナーシップを設定
- 共通ライブラリはInner Source方式で管理
- APIファーストで設計し、チーム間の依存を最小化
# チーム間のAPI契約定義
from dataclasses import dataclass
from typing import List, Optional
from enum import Enum
class Priority(Enum):
LOW = "low"
MEDIUM = "medium"
HIGH = "high"
CRITICAL = "critical"
@dataclass
class APIContract:
"""チーム間のAPI契約"""
endpoint: str
method: str
owner_team: str
consumers: List[str]
sla_ms: int # レスポンスタイムSLA
priority: Priority
def validate_sla(self, actual_ms: int) -> bool:
"""SLA準拠の確認"""
return actual_ms <= self.sla_ms
def to_openapi(self) -> dict:
"""OpenAPI形式で出力"""
return {
'path': self.endpoint,
'method': self.method,
'x-owner': self.owner_team,
'x-consumers': self.consumers,
'x-sla-ms': self.sla_ms
}
# 使用例
contracts = [
APIContract(
endpoint="/api/v1/users",
method="GET",
owner_team="user-team",
consumers=["order-team", "notification-team"],
sla_ms=200,
priority=Priority.HIGH
),
APIContract(
endpoint="/api/v1/orders",
method="POST",
owner_team="order-team",
consumers=["payment-team", "inventory-team"],
sla_ms=500,
priority=Priority.CRITICAL
)
]シナリオ4: パフォーマンスクリティカルなシステム
状況: ミリ秒単位のレスポンスが求められるシステム
最適化ポイント:
- キャッシュ戦略(L1: インメモリ、L2: Redis、L3: CDN)
- 非同期処理の活用
- コネクションプーリング
- クエリ最適化とインデックス設計
| 最適化手法 | 効果 | 実装コスト | 適用場面 |
|---|---|---|---|
| インメモリキャッシュ | 高 | 低 | 頻繁にアクセスされるデータ |
| CDN | 高 | 低 | 静的コンテンツ |
| 非同期処理 | 中 | 中 | I/O待ちが多い処理 |
| DB最適化 | 高 | 高 | クエリが遅い場合 |
| コード最適化 | 低-中 | 高 | CPU律速の場合 |
9. FAQ
Q1. バーチャル試着の精度はどの程度実用的か?
A. 2D 画像ベースの最新手法(StableVITON 等)は、正面写真での上半身衣服の試着において実用レベルに達している。ただし、(1) 複雑な柄やテクスチャの再現、(2) 横向き・後ろ向きのポーズ、(3) レイヤード(重ね着)のシナリオではまだ課題がある。EC サイトでの「参考イメージ」としては十分な品質。
Q2. バーチャル試着は返品率の削減に効果があるか?
A. 複数の調査で返品率の25-35%削減が報告されている。特にサイズ感の不一致による返品が大幅に減少する。AR 試着を導入したアパレルECでは、試着機能を使ったユーザーの購入率が2-3倍高いというデータもある。ただし、色の正確な再現がディスプレイ依存であるため、色味理由の返品には効果が限定的。
Q3. 自社で実装する場合の最低要件は?
A. (1) GPU サーバー: NVIDIA A10G 以上(AWS: g5.xlarge 相当)。(2) データ: 衣服のカタログ画像(白背景、正面)。(3) モデル: HR-VITON や CatVTON のオープンソースモデルを出発点にする。(4) 推論時間: バッチ処理なら2-5秒/枚が現実的。リアルタイム AR が必要なら MediaPipe + 簡易合成から始める。最小構成で PoC を作り、効果を検証してから本格投資する。
Q4. バーチャル試着のプライバシー対策は?
A. ユーザーの体型写真は極めて機密性の高い個人情報である。(1) オンデバイス処理: 可能な限りユーザーのデバイス上で推論する(WebGPU / Core ML)。(2) サーバー送信時: TLS 暗号化必須、処理完了後に即時削除。(3) 保存ポリシー: ユーザー写真は原則保存しない。必要な場合は明示的同意を取得。(4) アクセス制御: 試着結果へのアクセスはユーザー本人のみに限定。(5) GDPR / 個人情報保護法: データ処理の法的根拠を明確にし、プライバシーポリシーに明記。
Q5. アクセサリー(帽子、メガネ、靴)の試着は可能か?
A. アクセサリーの試着はカテゴリごとに成熟度が異なる。メガネ: AR ベースが非常に成熟しており、Warby Parker 等で実用化済み。顔のランドマーク検出が正確なため高精度。帽子: 頭部のサイズ推定が課題だが、AR で一定の品質を実現可能。靴: 足のサイズ推定 + AR が進展中(Nike Fit 等)。アクセサリー全般: 3D モデルベースの AR が最も適している。
FAQ
Q1: このトピックを学ぶ上で最も重要なポイントは何ですか?
実践的な経験を積むことが最も重要です。理論だけでなく、実際にコードを書いて動作を確認することで理解が深まります。
Q2: 初心者がよく陥る間違いは何ですか?
基礎を飛ばして応用に進むことです。このガイドで説明している基本概念をしっかり理解してから、次のステップに進むことをお勧めします。
Q3: 実務ではどのように活用されていますか?
このトピックの知識は、日常的な開発業務で頻繁に活用されます。特にコードレビューやアーキテクチャ設計の際に重要になります。
まとめ
| 項目 | ポイント |
|---|---|
| 技術基盤 | 人体ポーズ推定 + セグメンテーション + 衣服変形 + 合成 |
| 2D 画像ベース | EC サイト向け。HR-VITON、StableVITON が主要手法 |
| AR リアルタイム | 店舗・モバイル向け。MediaPipe で軽量実装可能 |
| 3D モデルベース | 最高精度だがコスト高。高級ブランド向け |
| ビジネス効果 | 返品率25-35%削減、購入率2-3倍向上の報告 |
| 課題 | 体型多様性、照明一致、複雑な衣服の再現 |
| 最適化 | TensorRT で 6 倍高速化、キャッシング必須 |
| プライバシー | ユーザー写真は即時削除、オンデバイス処理推奨 |
次に読むべきガイド
参考文献
- HR-VITON -- Lee et al. (ECCV 2022) -- 高解像度バーチャル試着
- StableVITON -- Kim et al. (2024) -- Diffusion ベースの試着モデル
- DensePose -- Guler et al. (CVPR 2018) -- 人体表面の密なマッピング
- OOTDiffusion -- Xu et al. (2024) -- Outfitting Fusion ベースの VTON
- SMPL -- Loper et al. (SIGGRAPH Asia 2015) -- パラメトリック人体モデル
- MediaPipe Pose -- Google (2020) -- リアルタイム人体ポーズ推定