Newer
Older
SmTIAS-LightSim / scripts / highlight_led_unit.py
"""SmTIASv3.stl の LED ユニットを基板別に色分けして可視化する.

連結成分を解析して LED / カバーを抽出し,bottom/right/left の各基板を色分け,
シミュと同じ視点(+X が右・+Y が上 = orthographic カメラ Z=-50→+Z)で投影した
画像を出力する.「右の LED だけ」を強調確認するのが目的.

実行方法:
    uv run python scripts/highlight_led_unit.py
出力:
    output/_slide_panels/led_unit_highlight.png
"""

from __future__ import annotations

from collections import defaultdict
from pathlib import Path

import matplotlib
import numpy as np
import trimesh

matplotlib.use("Agg")
import matplotlib.font_manager as fm
import matplotlib.pyplot as plt

ROOT = Path(__file__).resolve().parents[1]
STL = ROOT / "configs" / "meshes" / "SmTIASv3.stl"
OUT = ROOT / "output" / "_slide_panels" / "led_unit_highlight.png"


def _setup_font() -> None:
    for path in ("C:/Windows/Fonts/meiryo.ttc", "C:/Windows/Fonts/YuGothM.ttc"):
        if Path(path).exists():
            fm.fontManager.addfont(path)
            plt.rcParams["font.family"] = fm.FontProperties(fname=path).get_name()
            return


def _components(mesh: trimesh.Trimesh) -> list[list[int]]:
    n = len(mesh.vertices)
    parent = list(range(n))

    def find(x: int) -> int:
        while parent[x] != x:
            parent[x] = parent[parent[x]]
            x = parent[x]
        return x

    def union(a: int, b: int) -> None:
        ra, rb = find(a), find(b)
        if ra != rb:
            parent[ra] = rb

    for f in mesh.faces:
        union(int(f[0]), int(f[1]))
        union(int(f[1]), int(f[2]))
    groups: dict[int, list[int]] = defaultdict(list)
    for fi, f in enumerate(mesh.faces):
        groups[find(int(f[0]))].append(fi)
    return sorted(groups.values(), key=min)


def _stats(mesh: trimesh.Trimesh, faces: list[int]) -> dict:
    vi = np.unique(mesh.faces[faces].flatten())
    v = mesh.vertices[vi]
    bmin, bmax = v.min(0), v.max(0)
    return {"center": (bmin + bmax) / 2, "extents": bmax - bmin, "faces": faces}


def _label(center: np.ndarray) -> str:
    x, y, _ = center
    if abs(y) > abs(x):
        return "bottom" if y < 0 else "top"
    return "right" if x > 0 else "left"


def main() -> int:
    _setup_font()
    mesh = trimesh.load_mesh(str(STL))
    comps = [_stats(mesh, g) for g in _components(mesh)]

    # LED とカバーを分類(extract スクリプトと同基準)
    leds, covers = [], []
    for c in comps:
        e = c["extents"]
        if max(e) <= 4.0 and min(e) <= 1.5:
            leds.append(c)
        elif max(e) > 30.0 and min(e) <= 3.0:
            covers.append(c)

    colors = {"bottom": "#2ca02c", "right": "#d62728", "left": "#1f77b4", "top": "#9467bd"}

    # ---- 投影プロット: シミュカメラ視点 (+X 右, +Y 上) ----
    fig, axes = plt.subplots(1, 2, figsize=(16, 8))

    # 左: 全体うっすら + LED/カバーを基板別に色分け
    ax = axes[0]
    # 全体の頂点を薄いグレーで散布(間引き)
    allv = mesh.vertices[::37]
    ax.scatter(allv[:, 0], allv[:, 1], s=0.5, c="#cccccc", alpha=0.3)

    summary: dict[str, list[np.ndarray]] = defaultdict(list)
    for grp, marker, size in ((covers, "s", 12), (leds, "o", 40)):
        for c in grp:
            lab = _label(c["center"])
            summary[lab].append(c["center"])
            vi = np.unique(mesh.faces[c["faces"]].flatten())
            v = mesh.vertices[vi]
            ax.scatter(v[:, 0], v[:, 1], s=size, c=colors[lab], marker=marker,
                       edgecolors="none", alpha=0.7)

    ax.set_xlabel("物理空間 X+ (mm) → 画像右")
    ax.set_ylabel("物理空間 Y+ (mm) ↑ 画像上")
    ax.set_aspect("equal")
    ax.set_title("LED ユニット基板別カラー (シミュ視点 +X右/+Y上)\n"
                 "緑=下 / 赤=右(X>0) / 青=左(X<0)", fontsize=11)
    ax.grid(alpha=0.3)
    ax.axvline(0, color="k", lw=0.5, alpha=0.5)
    ax.axhline(0, color="k", lw=0.5, alpha=0.5)

    # 右: 右基板だけ強調(他は非表示)
    ax2 = axes[1]
    ax2.scatter(allv[:, 0], allv[:, 1], s=0.5, c="#eeeeee", alpha=0.3)
    for grp, marker, size in ((covers, "s", 14), (leds, "o", 55)):
        for c in grp:
            lab = _label(c["center"])
            if lab != "right":
                continue
            vi = np.unique(mesh.faces[c["faces"]].flatten())
            v = mesh.vertices[vi]
            ax2.scatter(v[:, 0], v[:, 1], s=size, c=colors["right"], marker=marker,
                        edgecolors="k", linewidths=0.3, alpha=0.85)
    ax2.set_xlabel("物理空間 X+ (mm) → 画像右")
    ax2.set_ylabel("物理空間 Y+ (mm) ↑ 画像上")
    ax2.set_aspect("equal")
    ax2.set_title("右 LED ユニットのみ強調 (X>0)", fontsize=11)
    ax2.grid(alpha=0.3)
    ax2.axvline(0, color="k", lw=0.5, alpha=0.5)
    ax2.axhline(0, color="k", lw=0.5, alpha=0.5)

    fig.suptitle("SmTIASv3.stl LED ユニット 右/左/下 の確認", fontsize=13, fontweight="bold")
    plt.tight_layout()
    OUT.parent.mkdir(parents=True, exist_ok=True)
    plt.savefig(OUT, dpi=110, bbox_inches="tight")

    # コンソールサマリ
    print(f"saved: {OUT}")
    for lab in ("bottom", "right", "left", "top"):
        if lab in summary:
            cs = np.array(summary[lab])
            print(f"  {lab:6s}: {len(cs)} comp(s), center mean=("
                  f"{cs[:,0].mean():+.1f}, {cs[:,1].mean():+.1f}, {cs[:,2].mean():+.1f})")
    return 0


if __name__ == "__main__":
    raise SystemExit(main())