Chuyển đến nội dung chính

第 16 課:身材估算 — 根據照片和測量結果預測體型

輸入處理:身高/體重→身材估算、詳細測量值(胸部/腰/肩)或真人照片。 MediaPipe Pose、OpenPose、SMPL 身體模型。

🧠 人工智慧與機器學習 — 第 15 課 第 16 課:身體估計 — 身體預測 根據照片和測量得出形狀

人工智慧在行動:建構時尚和按需印刷的人工智慧平台

第 5 部分:虛擬試戴與電腦視覺

亞洲開發網

簡介

虛擬試穿從了解使用者的體型開始。本文介紹了三種身體資料輸入方法——從最簡單的(身高/體重)到最精確的(上傳影像)——以及如何將它們全部轉換為 3D 身體參數。


1. 三種輸入模式

Mode 1: Basic Measurements (accuracy: ~70%)
├── Input: height (cm), weight (kg)
├── Process: Statistical regression → body shape params
└── Output: Estimated SMPL β parameters

Mode 2: Detailed Measurements (accuracy: ~85%)
├── Input: height, weight, chest, waist, shoulder
├── Process: Direct mapping → body shape params
└── Output: Accurate SMPL β parameters

Mode 3: Photo-based (accuracy: ~90%)
├── Input: 1-2 photos (front + side)
├── Process: Pose detection → body shape regression
└── Output: Precise SMPL β parameters + pose

2. 模式1——統計身體估計

class BasicBodyEstimator:
    """Estimate body shape từ height/weight"""

    # Regression coefficients (trained trên body scan dataset)
    BODY_RATIOS = {
        "male": {
            "chest": lambda h, bmi: 76 + bmi * 1.3 + (h - 170) * 0.15,
            "waist": lambda h, bmi: 62 + bmi * 1.8 + (h - 170) * 0.1,
            "shoulder": lambda h, bmi: h * 0.256 + bmi * 0.1,
            "hip": lambda h, bmi: 80 + bmi * 1.0 + (h - 170) * 0.1,
            "arm_length": lambda h, bmi: h * 0.44,
            "torso_length": lambda h, bmi: h * 0.30,
        },
        "female": {
            "chest": lambda h, bmi: 72 + bmi * 1.1 + (h - 160) * 0.12,
            "waist": lambda h, bmi: 56 + bmi * 1.6 + (h - 160) * 0.08,
            "shoulder": lambda h, bmi: h * 0.238 + bmi * 0.08,
            "hip": lambda h, bmi: 82 + bmi * 1.4 + (h - 160) * 0.12,
            "arm_length": lambda h, bmi: h * 0.43,
            "torso_length": lambda h, bmi: h * 0.29,
        },
    }

    def estimate(
        self,
        height_cm: float,
        weight_kg: float,
        gender: str = "neutral",
    ) -> BodyMeasurements:
        bmi = weight_kg / (height_cm / 100) ** 2

        if gender == "neutral":
            # Average male & female
            male = self._compute("male", height_cm, bmi)
            female = self._compute("female", height_cm, bmi)
            measurements = {
                k: (male[k] + female[k]) / 2
                for k in male
            }
        else:
            measurements = self._compute(gender, height_cm, bmi)

        return BodyMeasurements(
            height=height_cm,
            weight=weight_kg,
            **measurements,
            estimation_method="basic",
            confidence=0.7,
        )

    def _compute(
        self, gender: str, height: float, bmi: float
    ) -> dict:
        ratios = self.BODY_RATIOS[gender]
        return {
            key: round(func(height, bmi), 1)
            for key, func in ratios.items()
        }

3. 模式 2 — 詳細測量

class DetailedBodyMapper:
    """Map chi tiết measurements sang SMPL parameters"""

    def map_to_smpl(
        self, measurements: BodyMeasurements
    ) -> SMPLParams:
        """
        Convert body measurements → SMPL β parameters

        SMPL uses 10 shape parameters (β) that control:
        β[0]: overall body size (height)
        β[1]: weight/mass
        β[2]: chest-to-waist ratio
        β[3]: shoulder width
        β[4]: hip width
        β[5-9]: finer shape details
        """
        # Normalize measurements
        norm = self._normalize(measurements)

        # Linear mapping to β parameters
        # (trained via regression on CAESAR dataset)
        betas = np.array([
            norm.height * 2.5 - 4.2,        # β0: height
            norm.bmi * 1.8 - 2.0,            # β1: mass
            norm.chest_waist_ratio * 1.2,     # β2: proportions
            norm.shoulder * 0.8 - 1.0,        # β3: shoulders
            norm.hip * 0.6 - 0.5,             # β4: hips
            0, 0, 0, 0, 0,                    # β5-9: default
        ], dtype=np.float32)

        return SMPLParams(
            betas=betas,
            gender=measurements.gender or "neutral",
            confidence=0.85,
        )

4. 模式 3 — 基於照片的身體估計

import mediapipe as mp
import numpy as np
from PIL import Image

class PhotoBodyEstimator:
    """Estimate body từ 1-2 ảnh user"""

    def __init__(self):
        self.mp_pose = mp.solutions.pose
        self.pose = self.mp_pose.Pose(
            static_image_mode=True,
            model_complexity=2,
            min_detection_confidence=0.5,
        )

    def estimate_from_photo(
        self,
        front_photo: Image.Image,
        side_photo: Image.Image | None = None,
        known_height: float | None = None,
    ) -> BodyMeasurements:
        # 1. Detect pose landmarks
        front_landmarks = self._detect_landmarks(front_photo)

        # 2. Calculate proportions from landmarks
        proportions = self._calculate_proportions(
            front_landmarks
        )

        # 3. If side photo, improve depth estimation
        if side_photo:
            side_landmarks = self._detect_landmarks(side_photo)
            proportions = self._refine_with_side(
                proportions, side_landmarks
            )

        # 4. Convert proportions to measurements
        if known_height:
            scale = known_height / proportions["body_height_ratio"]
        else:
            scale = 170  # Default height assumption

        measurements = self._proportions_to_measurements(
            proportions, scale
        )

        return measurements

    def _detect_landmarks(
        self, photo: Image.Image
    ) -> dict:
        """Detect 33 pose landmarks"""
        img_array = np.array(photo.convert("RGB"))
        results = self.pose.process(img_array)

        if not results.pose_landmarks:
            raise ValueError("Không phát hiện được người trong ảnh")

        landmarks = {}
        for idx, lm in enumerate(results.pose_landmarks.landmark):
            name = self.mp_pose.PoseLandmark(idx).name
            landmarks[name] = {
                "x": lm.x, "y": lm.y, "z": lm.z,
                "visibility": lm.visibility,
            }
        return landmarks

    def _calculate_proportions(
        self, landmarks: dict
    ) -> dict:
        """Tính body proportions từ landmarks"""
        # Shoulder width
        l_shoulder = landmarks["LEFT_SHOULDER"]
        r_shoulder = landmarks["RIGHT_SHOULDER"]
        shoulder_width = abs(l_shoulder["x"] - r_shoulder["x"])

        # Hip width
        l_hip = landmarks["LEFT_HIP"]
        r_hip = landmarks["RIGHT_HIP"]
        hip_width = abs(l_hip["x"] - r_hip["x"])

        # Torso length
        mid_shoulder_y = (l_shoulder["y"] + r_shoulder["y"]) / 2
        mid_hip_y = (l_hip["y"] + r_hip["y"]) / 2
        torso_length = abs(mid_hip_y - mid_shoulder_y)

        # Body height (top of head to ankles)
        nose = landmarks.get("NOSE", landmarks["LEFT_EAR"])
        l_ankle = landmarks["LEFT_ANKLE"]
        body_height = abs(l_ankle["y"] - nose["y"])

        return {
            "shoulder_width": shoulder_width,
            "hip_width": hip_width,
            "torso_length": torso_length,
            "body_height_ratio": body_height,
            "shoulder_hip_ratio": shoulder_width / max(hip_width, 0.01),
        }

5. 統一體管道

class BodyEstimationPipeline:
    """Pipeline thống nhất cho 3 modes"""

    def __init__(self):
        self.basic = BasicBodyEstimator()
        self.detailed = DetailedBodyMapper()
        self.photo = PhotoBodyEstimator()

    async def estimate(
        self,
        height: float | None = None,
        weight: float | None = None,
        chest: float | None = None,
        waist: float | None = None,
        shoulder: float | None = None,
        front_photo: Image.Image | None = None,
        side_photo: Image.Image | None = None,
    ) -> BodyEstimationResult:
        # Determine best mode
        if front_photo:
            measurements = self.photo.estimate_from_photo(
                front_photo, side_photo, known_height=height
            )
            smpl_params = self.detailed.map_to_smpl(measurements)
        elif chest and waist and shoulder:
            measurements = BodyMeasurements(
                height=height, weight=weight,
                chest=chest, waist=waist, shoulder=shoulder,
            )
            smpl_params = self.detailed.map_to_smpl(measurements)
        elif height and weight:
            measurements = self.basic.estimate(height, weight)
            smpl_params = self.detailed.map_to_smpl(measurements)
        else:
            raise ValueError("Cần ít nhất height + weight")

        return BodyEstimationResult(
            measurements=measurements,
            smpl_params=smpl_params,
        )

總結

身體估計模組:

  1. 3種輸入模式 — 基本(身高/體重)、詳細(測量值)、照片
  2. 統計估計 — BMI 和身高迴歸
  3. MediaPipe Pose — 來自真實人物照片的 33 個地標
  4. SMPL 映射 — 轉換測量值 → 10 個 β 參數
  5. 統一管道 — 根據可用資料自動選擇最佳模式

下一篇文章:3D 頭像產生 — 根據 SMPL 參數建立頭像。