"""
=============================================================================
CS230 · 第 14 讲工具模块：合成的"人脸"——身份决定五官的几何，姿态 / 光照 / 表情是干扰
=============================================================================

对应讲义：lectures/lecture14_detection_segmentation_faces_style.md §1.3（关键点检测）、§10（人脸识别）

-----------------------------------------------------------------------------
【为什么需要它】
-----------------------------------------------------------------------------
第 5 讲 §3 的人脸世界是 100 维的"像素向量"（x = C·tanh(Az + Bu) + 噪声），用来讲清 triplet loss 的道理。
这一讲要用**卷积编码器在图像上**做人脸验证，还要做关键点检测（landmark detection），所以需要真正的"图"。
这里用 numpy 画 32×32 的灰度卡通脸：

    身份（identity，每个人固定，同一个人的所有照片都一样）：
        脸的宽和高、肤色深浅、发际线的高低和头发的颜色、两眼间距、眼睛的高低和大小、
        眉毛的倾斜和离眼睛的距离、鼻子的长短、嘴的位置和宽度          —— 共 12 个连续参数
    干扰（nuisance，每张照片重新抽）：
        姿态：平移 ±2.5 像素、旋转 ±12°、缩放 0.9–1.1
        光照：整体亮度增益 0.6–1.3、一个方向随机的亮度渐变（侧光）、背景亮度
        表情：微笑 / 撇嘴的弯曲程度、眼睛睁开的程度（眯眼）
        噪声：σ = 0.06 的像素噪声

每张图同时给出 5 个关键点（landmarks）的像素坐标：左眼中心、右眼中心、鼻尖、左嘴角、右嘴角
（"左 / 右"按图像里的左右，即 x 小的是左）。

【诚实的说明】身份参数的范围和干扰的强度，是第一次写这个世界时凭"照片之间看起来差别够大"定下的，之后没有为了实验结果再改。
    结果这个世界比第 5 讲的难得多：像素距离的验证准确率只比瞎猜高一点（代码 04 演示 2），主要是因为背景亮度、整体增益
    和侧光让同一个人的两张照片在像素上差得很远。

-----------------------------------------------------------------------------
【形状约定】图像 (m, 1, 32, 32) float32（单通道灰度，PyTorch 的习惯）；关键点 (m, 5, 2)，每行 (x, y) 像素坐标
-----------------------------------------------------------------------------

【注释标签】【思路】【参数】【返回】【边界】【DL 联系】【numpy 小知识】【Python 小知识】

直接运行本文件：画几个人、每人几张照片的字符画，并打印关键点（约 1 秒）。
    python3 face_world.py
"""

import numpy as np

FACE = 32
LANDMARKS = ["左眼", "右眼", "鼻尖", "左嘴角", "右嘴角"]

# 每个身份参数的范围 (低, 高)：均匀分布
IDENTITY_RANGES = {
    "rx": (8.5, 11.5),          # 脸的半宽（像素）
    "ry": (10.5, 13.5),         # 脸的半高
    "skin": (0.5, 0.85),        # 肤色亮度
    "hair_line": (-0.8, -0.35), # 发际线：v < hair_line · ry 的部分是头发（越接近 0 头发越多）
    "hair": (0.05, 0.4),        # 头发亮度
    "eye_dx": (2.8, 5.0),       # 两眼中心到中线的距离
    "eye_y": (-3.0, 0.5),       # 眼睛的高度（相对脸的中心，向下为正）
    "eye_r": (0.9, 1.7),        # 眼睛半径
    "brow_tilt": (-0.35, 0.35), # 眉毛的斜率（外侧高 / 低）
    "brow_gap": (1.8, 3.2),     # 眉毛在眼睛上方多远
    "nose_len": (2.0, 4.5),     # 鼻子长度（从眼睛下方 1 像素开始）
    "mouth_y": (4.0, 7.5),      # 嘴的高度
    "mouth_w": (2.5, 5.0),      # 嘴的半宽
}


def sample_identities(n, rng):
    """
    抽 n 个人的身份参数。

    【参数】n：人数；rng：np.random.Generator
    【返回】dict：每个参数名 → (n,) 数组
    【Python 小知识】字典推导式 {k: f(v) for k, v in d.items()}：对每一对键值算一个新值，组成新字典。
            items() 的顺序就是字典写下的顺序（Python 3.7 起保证），所以同一个种子总是抽出同样的人。
    """
    return {k: rng.uniform(lo, hi, n) for k, (lo, hi) in IDENTITY_RANGES.items()}


def _smooth_step(f):
    """把"有符号距离"（像素，> 0 在里面）变成 0–1 的覆盖度，边界上有 1 个像素的过渡（抗锯齿）。"""
    return np.clip(0.5 + f, 0.0, 1.0)


def render_faces(ident, person, rng, size=FACE):
    """
    给 person 里列出的每个人各画一张照片（每张照片的干扰重新抽）。

    【思路】
        1. 抽这张照片的干扰：平移 (tx, ty)、旋转 θ、缩放 s、表情（微笑弯曲 curv、睁眼程度 open）、光照；
        2. 对每个像素算出它在"脸的局部坐标" (u, v) 里的位置：先减去脸的中心、再转回 −θ、再除以 s
           （这是"把脸摆正"的逆变换，所以画五官时可以假装脸是正的）；
        3. 按"背景 → 脸 → 头发 → 眉毛 → 眼睛 → 鼻子 → 嘴"的顺序一层层画上去；
        4. 乘上光照（增益 + 侧光渐变），加噪声；
        5. 五个关键点先在局部坐标里写出来，再用**正**变换（乘 s、转 θ、加中心）变回像素坐标。
    【参数】ident：sample_identities 的结果；person：(m,) 整数数组，每张照片是谁；rng；size：边长
    【返回】(X, L)：X (m, 1, size, size) float32；L (m, 5, 2) float32 关键点的 (x, y)
    【numpy 小知识】所有照片一起算（向量化）：每个参数取 [person] 之后是 (m,) 数组，
            再用 [:, None, None] 变成 (m, 1, 1)，和 (size, size) 的像素网格广播成 (m, size, size)。
    """
    m = len(person)
    p = {k: v[person][:, None, None] for k, v in ident.items()}   # 每个参数 → (m, 1, 1)
    c0 = size / 2
    tx, ty = rng.uniform(-2.5, 2.5, m), rng.uniform(-2.5, 2.5, m)
    th = np.deg2rad(rng.uniform(-12, 12, m))
    sc = rng.uniform(0.9, 1.1, m)
    curv = rng.uniform(-0.12, 0.25, m)[:, None, None]             # 嘴的弯曲：> 0 嘴角上翘（微笑）
    open_ = rng.uniform(0.35, 1.0, m)[:, None, None]              # 眼睛睁开的程度
    gain = rng.uniform(0.6, 1.3, m)[:, None, None]
    light_dir = rng.uniform(0, 2 * np.pi, m)
    light_amp = rng.uniform(0.0, 0.5, m)[:, None, None]
    bg = rng.uniform(0.1, 0.9, m)[:, None, None]

    pix = np.arange(size) + 0.5
    X0, Y0 = pix[None, None, :], pix[None, :, None]               # (1, 1, size)、(1, size, 1)
    cx, cy = (c0 + tx)[:, None, None], (c0 + ty)[:, None, None]
    c, s = np.cos(th)[:, None, None], np.sin(th)[:, None, None]
    S = sc[:, None, None]
    u = (c * (X0 - cx) + s * (Y0 - cy)) / S                       # 逆变换：像素 → 脸的局部坐标
    v = (-s * (X0 - cx) + c * (Y0 - cy)) / S

    img = np.broadcast_to(bg, (m, size, size)).copy()
    # 脸：椭圆。(1 − 归一化半径) · 半短轴 ≈ 到边界的像素距离
    face_f = (1 - np.hypot(u / p["rx"], v / p["ry"])) * np.minimum(p["rx"], p["ry"]) * S
    a_face = _smooth_step(face_f)
    img = img * (1 - a_face) + p["skin"] * a_face
    # 头发：脸的上部（v < hair_line · ry）
    a_hair = a_face * _smooth_step((p["hair_line"] * p["ry"] - v) * S)
    img = img * (1 - a_hair) + p["hair"] * a_hair
    for side in (-1, 1):                                          # 左右各一只眼睛、一条眉毛
        ex = side * p["eye_dx"]
        # 眉毛：一条细的斜线段，|u − ex| < 1.6，厚 0.7 像素
        brow_v = p["eye_y"] - p["brow_gap"] + side * p["brow_tilt"] * (u - ex)
        brow_f = np.minimum(1.6 - np.abs(u - ex), 0.7 - np.abs(v - brow_v)) * S
        a = _smooth_step(brow_f)
        img = img * (1 - a) + 0.12 * a
        # 眼睛：横向半径 eye_r、纵向半径 eye_r · open 的椭圆
        ry_eye = p["eye_r"] * open_
        eye_f = (1 - np.hypot((u - ex) / p["eye_r"], (v - p["eye_y"]) / ry_eye)) * ry_eye * S
        a = _smooth_step(eye_f)
        img = img * (1 - a) + 0.08 * a
    # 鼻子：一条竖线，比皮肤暗一些
    top = p["eye_y"] + 1.0
    nose_f = np.minimum(0.5 - np.abs(u), np.minimum(v - top, top + p["nose_len"] - v)) * S
    a = _smooth_step(nose_f)
    img = img * (1 - a) + 0.6 * p["skin"] * a
    # 嘴：一段抛物线 v = mouth_y − curv · u²（curv > 0 时嘴角上翘），|u| < mouth_w，厚 0.6 像素
    mouth_v = p["mouth_y"] - curv * u ** 2
    mouth_f = np.minimum(p["mouth_w"] - np.abs(u), 0.6 - np.abs(v - mouth_v)) * S
    a = _smooth_step(mouth_f)
    img = img * (1 - a) + 0.1 * a
    # 光照：增益 × (1 + 侧光渐变)
    ramp = ((X0 - c0) * np.cos(light_dir)[:, None, None] + (Y0 - c0) * np.sin(light_dir)[:, None, None]) / size
    img = img * gain * (1 + light_amp * 2 * ramp)
    img = img + rng.normal(0.0, 0.06, img.shape)

    # 关键点：局部坐标 → 像素坐标（正变换）
    ident_p = {k: v[person] for k, v in ident.items()}
    mw = ident_p["mouth_w"]
    mouth_corner_v = ident_p["mouth_y"] - curv[:, 0, 0] * mw ** 2
    local = np.stack([
        np.stack([-ident_p["eye_dx"], ident_p["eye_y"]], -1),              # 左眼
        np.stack([ident_p["eye_dx"], ident_p["eye_y"]], -1),               # 右眼
        np.stack([np.zeros(m), ident_p["eye_y"] + 1.0 + ident_p["nose_len"]], -1),   # 鼻尖
        np.stack([-mw, mouth_corner_v], -1),                                # 左嘴角
        np.stack([mw, mouth_corner_v], -1),                                 # 右嘴角
    ], axis=1)                                                              # (m, 5, 2)
    lu, lv = local[..., 0] * sc[:, None], local[..., 1] * sc[:, None]
    lx = c0 + tx[:, None] + np.cos(th)[:, None] * lu - np.sin(th)[:, None] * lv
    ly = c0 + ty[:, None] + np.sin(th)[:, None] * lu + np.cos(th)[:, None] * lv
    L = np.stack([lx, ly], axis=-1)
    return img[:, None].astype(np.float32), L.astype(np.float32)


def make_people(n_people, n_per_person, rng):
    """
    抽 n_people 个人，每人拍 n_per_person 张照片。

    【参数】n_people、n_per_person：人数、每人张数；rng：随机数生成器（身份和照片都从它来）
    【返回】(X, person, L, ident)：X (m, 1, 32, 32)；person (m,) 每张照片是谁（0..n_people−1）；
            L (m, 5, 2) 关键点；ident 身份参数字典。m = n_people · n_per_person，同一个人的照片排在一起。
    【DL 联系】训练编码器的人（"公开人脸库"）和测试时的新同学要用**不同的人**（不同的 rng），
            这样测出来的才是"认识从没见过的人"的本领（第 5 讲 §3.5）。
    """
    ident = sample_identities(n_people, rng)
    person = np.repeat(np.arange(n_people), n_per_person)
    X, L = render_faces(ident, person, rng)
    return X, person, L, ident


def ascii_gray(img, chars=" .:-=+*#%@"):
    """(H, W) 灰度图 → 字符画（按这张图自己的最小 / 最大值缩放）。返回 list[str]。"""
    lo, hi = float(img.min()), float(img.max())
    idx = np.clip(((img - lo) / (hi - lo + 1e-12) * len(chars)).astype(int), 0, len(chars) - 1)
    return ["".join(chars[k] for k in row) for row in idx]


if __name__ == "__main__":
    X, person, L, _ = make_people(3, 3, np.random.default_rng(0))
    for who in range(2):
        arts = [ascii_gray(X[k, 0]) for k in np.nonzero(person == who)[0]]
        print(f"第 {who} 个人的 3 张照片（同一个身份，不同的姿态 / 光照 / 表情）：")
        for r in range(FACE):
            print("   ".join(a[r] for a in arts))
        print()
    print("第 0 张照片的关键点 (x, y)：" + "；".join(f"{n} ({x:.1f}, {y:.1f})" for n, (x, y) in zip(LANDMARKS, L[0])))
