diff --git a/CLAUDE.md b/CLAUDE.md index ab43740..27cdcd4 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -13,6 +13,7 @@ 補足: **実機搭載 LED の物性確定**(LED Paradise LP-2835D90VW / Ra98 SunLightLED 2835 4000K、Vf 3.1V・If 60mA・Φv 16–19 lm @60mA・配光角 120°・規格通り 60mA 駆動前提).規格値から独立推定した radiance ≈ 2030 W/(m²·sr) は現状校正値 2807 と 1.38× 差で、反射率 0.85→0.89 への調整で物理範囲内に説明可能.既知差分 #3(LED 実 W 値未検証)クローズ.**形状指標への影響なし**(線形ゲイン).配光は厳密 Lambertian 仮定継続(詳細 IES 未提供、実 SMD 2835 のやや前方凸性は ISP LSC と打ち消し合う近似として現状妥当).詳細: [SPEC_03 LED セクション](docs/04_SPEC/SPEC_03_基準設計の物理構成.md#led光源). 補足: **輝度マップ「形」の追跡を導入**([TEST_03 輝度マップ形状追跡](docs/06_TEST/TEST_03_輝度マップ形状追跡.md) + `scripts/build_luminance_evolution_panel.py`).重要な発見: **これまでの全 7 改善ステップ (8-1〜8-7) で輝度マップの「形」は全く変わっていない**.OETF は単調変換で形を変えず,発光面サイズ変更も形を変えない.**実機は中央付近にピークを持つ楕円状,シミュは下端のみ明の Y 軸単調勾配**.**LSC モデル化は「数値残差縮小」と「形の構造的改善」の両方を担う唯一の改善候補**.以後の改善判断は「数値 + 形」の 2 軸で行う. 補足: **Step 8-8 完了**.linear 評価モードに mean 揃え正規化を導入(`evaluation/normalize.py` の `apply_mean_anchor` + `EvaluationConfig.normalization` スキーマ + CLI 統合 + OETF 排他バリデーション).OETF 露出アンカー(Step 8-3)と同じ思想を linear ドメインに対称展開.`configs/eval_baseline_linear.yaml` で有効化(target_mean=146.7146).形状指標 (CoV/勾配/C/P/max-min) は厳密不変,mean のみ target_mean に揃う.動作確認: α=0.961934 で baseline luminance.npy が mean=146.7146, CoV=0.182577 で評価可能.テスト 62 件追加(既存 441 + 新規 62 = 503 件全通過). +補足: **Step 8-9 完了**.LED 18 個の位置を Fusion 360 設計 STL (`configs/meshes/SmTIASv3.stl`) から抽出して `scene_baseline.yaml` を更新.`scripts/extract_led_positions_from_stl.py` で連結成分分割→ LED/カバー識別→ bottom/right/left 自動命名.主な変更: 下基板 LED 配置を「等間隔 8.7mm」→「3+ギャップ+3 (4.2mm+10.8mm+4.2mm)」へ訂正(実機写真の中央電流制限抵抗 2 個と整合).左右基板も実位置に修正.**形状指標は ~1% 以内のほぼ不変**(多重反射が支配的,LED 数 mm 誤差は ROI 上で平滑化).絶対 mean は +10% 増加(α 値 0.962→0.873).既知差分 #2 をクローズ.TEST_03 比較パネルに Step 8-9 を追加(5 枚並列). 次のアクション(キャリブ再開時): 残差に **LSC(ISP レンズシェーディング)順方向モデル**を当てる(物理動機づけ低自由度形、overfitting 回避・取得パラメータ数明記). 実装済みモジュール: diff --git a/configs/leds_extracted.yaml b/configs/leds_extracted.yaml new file mode 100644 index 0000000..17cb124 --- /dev/null +++ b/configs/leds_extracted.yaml @@ -0,0 +1,324 @@ +- id: led_bottom_0 + type: area + shape: rectangle + center: + - -10.0 + - -71.0 + - -66.1 + size: + - 2.8 + - 3.5 + direction: + - 0.0 + - 0.0 + - -1.0 + radiance: + - 2807.2469840551203 + - 2807.2469840551203 + - 2807.2469840551203 +- id: led_bottom_1 + type: area + shape: rectangle + center: + - -5.8 + - -71.0 + - -66.1 + size: + - 2.8 + - 3.5 + direction: + - 0.0 + - 0.0 + - -1.0 + radiance: + - 2807.2469840551203 + - 2807.2469840551203 + - 2807.2469840551203 +- id: led_bottom_2 + type: area + shape: rectangle + center: + - -1.6 + - -71.0 + - -66.1 + size: + - 2.8 + - 3.5 + direction: + - 0.0 + - 0.0 + - -1.0 + radiance: + - 2807.2469840551203 + - 2807.2469840551203 + - 2807.2469840551203 +- id: led_bottom_3 + type: area + shape: rectangle + center: + - 9.2 + - -71.0 + - -66.1 + size: + - 2.8 + - 3.5 + direction: + - 0.0 + - 0.0 + - -1.0 + radiance: + - 2807.2469840551203 + - 2807.2469840551203 + - 2807.2469840551203 +- id: led_bottom_4 + type: area + shape: rectangle + center: + - 13.4 + - -71.0 + - -66.1 + size: + - 2.8 + - 3.5 + direction: + - 0.0 + - 0.0 + - -1.0 + radiance: + - 2807.2469840551203 + - 2807.2469840551203 + - 2807.2469840551203 +- id: led_bottom_5 + type: area + shape: rectangle + center: + - 17.6 + - -71.0 + - -66.1 + size: + - 2.8 + - 3.5 + direction: + - 0.0 + - 0.0 + - -1.0 + radiance: + - 2807.2469840551203 + - 2807.2469840551203 + - 2807.2469840551203 +- id: led_right_0 + type: area + shape: rectangle + center: + - 65.9987 + - -24.9738 + - -66.0243 + size: + - 3.5 + - 2.8 + direction: + - 0.0 + - 0.0 + - -1.0 + radiance: + - 2807.2469840551203 + - 2807.2469840551203 + - 2807.2469840551203 +- id: led_right_1 + type: area + shape: rectangle + center: + - 65.9987 + - -20.7738 + - -66.0243 + size: + - 3.5 + - 2.8 + direction: + - 0.0 + - 0.0 + - -1.0 + radiance: + - 2807.2469840551203 + - 2807.2469840551203 + - 2807.2469840551203 +- id: led_right_2 + type: area + shape: rectangle + center: + - 65.9987 + - -16.5738 + - -66.0243 + size: + - 3.5 + - 2.8 + direction: + - 0.0 + - 0.0 + - -1.0 + radiance: + - 2807.2469840551203 + - 2807.2469840551203 + - 2807.2469840551203 +- id: led_right_3 + type: area + shape: rectangle + center: + - 65.9987 + - -5.7738 + - -66.0243 + size: + - 3.5 + - 2.8 + direction: + - 0.0 + - 0.0 + - -1.0 + radiance: + - 2807.2469840551203 + - 2807.2469840551203 + - 2807.2469840551203 +- id: led_right_4 + type: area + shape: rectangle + center: + - 65.9987 + - -1.5738 + - -66.0243 + size: + - 3.5 + - 2.8 + direction: + - 0.0 + - 0.0 + - -1.0 + radiance: + - 2807.2469840551203 + - 2807.2469840551203 + - 2807.2469840551203 +- id: led_right_5 + type: area + shape: rectangle + center: + - 65.9987 + - 2.6262 + - -66.0243 + size: + - 3.5 + - 2.8 + direction: + - 0.0 + - 0.0 + - -1.0 + radiance: + - 2807.2469840551203 + - 2807.2469840551203 + - 2807.2469840551203 +- id: led_left_0 + type: area + shape: rectangle + center: + - -66.0067 + - -32.5952 + - -66.0243 + size: + - 3.5 + - 2.8 + direction: + - 0.0 + - 0.0 + - -1.0 + radiance: + - 2807.2469840551203 + - 2807.2469840551203 + - 2807.2469840551203 +- id: led_left_1 + type: area + shape: rectangle + center: + - -66.0067 + - -28.3952 + - -66.0243 + size: + - 3.5 + - 2.8 + direction: + - 0.0 + - 0.0 + - -1.0 + radiance: + - 2807.2469840551203 + - 2807.2469840551203 + - 2807.2469840551203 +- id: led_left_2 + type: area + shape: rectangle + center: + - -66.0067 + - -24.1952 + - -66.0243 + size: + - 3.5 + - 2.8 + direction: + - 0.0 + - 0.0 + - -1.0 + radiance: + - 2807.2469840551203 + - 2807.2469840551203 + - 2807.2469840551203 +- id: led_left_3 + type: area + shape: rectangle + center: + - -66.0067 + - -13.3952 + - -66.0243 + size: + - 3.5 + - 2.8 + direction: + - 0.0 + - 0.0 + - -1.0 + radiance: + - 2807.2469840551203 + - 2807.2469840551203 + - 2807.2469840551203 +- id: led_left_4 + type: area + shape: rectangle + center: + - -66.0067 + - -9.1952 + - -66.0243 + size: + - 3.5 + - 2.8 + direction: + - 0.0 + - 0.0 + - -1.0 + radiance: + - 2807.2469840551203 + - 2807.2469840551203 + - 2807.2469840551203 +- id: led_left_5 + type: area + shape: rectangle + center: + - -66.0067 + - -4.9952 + - -66.0243 + size: + - 3.5 + - 2.8 + direction: + - 0.0 + - 0.0 + - -1.0 + radiance: + - 2807.2469840551203 + - 2807.2469840551203 + - 2807.2469840551203 diff --git a/configs/meshes/SmTIASv3.stl b/configs/meshes/SmTIASv3.stl new file mode 100644 index 0000000..ca24772 --- /dev/null +++ b/configs/meshes/SmTIASv3.stl Binary files differ diff --git a/configs/scene_baseline.yaml b/configs/scene_baseline.yaml index 1ffc76f..e65cc8c 100644 --- a/configs/scene_baseline.yaml +++ b/configs/scene_baseline.yaml @@ -1,6 +1,6 @@ name: baseline_minitias -description: MiniTIAS 基準設計シーン(Step 8).orthographic センサで評価板の裏面(-Z 側 = スマホ側)の照度分布マップを - 2D 画像として直接取得する. +description: MiniTIAS 基準設計シーン(Step 8-9).LED 位置を Fusion 360 設計 STL から抽出した正確な値に更新(configs/leds_extracted.yaml + 経由).orthographic センサで評価板の裏面(-Z 側 = スマホ側)の照度分布マップを 2D 画像として直接取得する. geometry: meshes: - id: housing_front @@ -51,9 +51,9 @@ type: area shape: rectangle center: - - -21.67 - - -70.2 - - -65.3 + - -10.0 + - -71.0 + - -66.1 size: - 2.8 - 3.5 @@ -69,9 +69,9 @@ type: area shape: rectangle center: - - -13.0 - - -70.2 - - -65.3 + - -5.8 + - -71.0 + - -66.1 size: - 2.8 - 3.5 @@ -87,9 +87,9 @@ type: area shape: rectangle center: - - -4.33 - - -70.2 - - -65.3 + - -1.6 + - -71.0 + - -66.1 size: - 2.8 - 3.5 @@ -105,9 +105,9 @@ type: area shape: rectangle center: - - 4.33 - - -70.2 - - -65.3 + - 9.2 + - -71.0 + - -66.1 size: - 2.8 - 3.5 @@ -123,9 +123,9 @@ type: area shape: rectangle center: - - 13.0 - - -70.2 - - -65.3 + - 13.4 + - -71.0 + - -66.1 size: - 2.8 - 3.5 @@ -141,9 +141,9 @@ type: area shape: rectangle center: - - 21.67 - - -70.2 - - -65.3 + - 17.6 + - -71.0 + - -66.1 size: - 2.8 - 3.5 @@ -159,12 +159,12 @@ type: area shape: rectangle center: - - 70.2 - - -21.67 - - -65.3 + - 65.9987 + - -24.9738 + - -66.0243 size: - - 2.8 - 3.5 + - 2.8 direction: - 0.0 - 0.0 @@ -177,12 +177,12 @@ type: area shape: rectangle center: - - 70.2 - - -13.0 - - -65.3 + - 65.9987 + - -20.7738 + - -66.0243 size: - - 2.8 - 3.5 + - 2.8 direction: - 0.0 - 0.0 @@ -195,12 +195,12 @@ type: area shape: rectangle center: - - 70.2 - - -4.33 - - -65.3 + - 65.9987 + - -16.5738 + - -66.0243 size: - - 2.8 - 3.5 + - 2.8 direction: - 0.0 - 0.0 @@ -213,12 +213,12 @@ type: area shape: rectangle center: - - 70.2 - - 4.33 - - -65.3 + - 65.9987 + - -5.7738 + - -66.0243 size: - - 2.8 - 3.5 + - 2.8 direction: - 0.0 - 0.0 @@ -231,12 +231,12 @@ type: area shape: rectangle center: - - 70.2 - - 13.0 - - -65.3 + - 65.9987 + - -1.5738 + - -66.0243 size: - - 2.8 - 3.5 + - 2.8 direction: - 0.0 - 0.0 @@ -249,12 +249,12 @@ type: area shape: rectangle center: - - 70.2 - - 21.67 - - -65.3 + - 65.9987 + - 2.6262 + - -66.0243 size: - - 2.8 - 3.5 + - 2.8 direction: - 0.0 - 0.0 @@ -267,12 +267,12 @@ type: area shape: rectangle center: - - -70.2 - - -21.67 - - -65.3 + - -66.0067 + - -32.5952 + - -66.0243 size: - - 2.8 - 3.5 + - 2.8 direction: - 0.0 - 0.0 @@ -285,12 +285,12 @@ type: area shape: rectangle center: - - -70.2 - - -13.0 - - -65.3 + - -66.0067 + - -28.3952 + - -66.0243 size: - - 2.8 - 3.5 + - 2.8 direction: - 0.0 - 0.0 @@ -303,12 +303,12 @@ type: area shape: rectangle center: - - -70.2 - - -4.33 - - -65.3 + - -66.0067 + - -24.1952 + - -66.0243 size: - - 2.8 - 3.5 + - 2.8 direction: - 0.0 - 0.0 @@ -321,12 +321,12 @@ type: area shape: rectangle center: - - -70.2 - - 4.33 - - -65.3 + - -66.0067 + - -13.3952 + - -66.0243 size: - - 2.8 - 3.5 + - 2.8 direction: - 0.0 - 0.0 @@ -339,12 +339,12 @@ type: area shape: rectangle center: - - -70.2 - - 13.0 - - -65.3 + - -66.0067 + - -9.1952 + - -66.0243 size: - - 2.8 - 3.5 + - 2.8 direction: - 0.0 - 0.0 @@ -357,12 +357,12 @@ type: area shape: rectangle center: - - -70.2 - - 21.67 - - -65.3 + - -66.0067 + - -4.9952 + - -66.0243 size: - - 2.8 - 3.5 + - 2.8 direction: - 0.0 - 0.0 diff --git "a/docs/04_SPEC/SPEC_03_\345\237\272\346\272\226\350\250\255\350\250\210\343\201\256\347\211\251\347\220\206\346\247\213\346\210\220.md" "b/docs/04_SPEC/SPEC_03_\345\237\272\346\272\226\350\250\255\350\250\210\343\201\256\347\211\251\347\220\206\346\247\213\346\210\220.md" index 8840ef1..62ae479 100644 --- "a/docs/04_SPEC/SPEC_03_\345\237\272\346\272\226\350\250\255\350\250\210\343\201\256\347\211\251\347\220\206\346\247\213\346\210\220.md" +++ "b/docs/04_SPEC/SPEC_03_\345\237\272\346\272\226\350\250\255\350\250\210\343\201\256\347\211\251\347\220\206\346\247\213\346\210\220.md" @@ -102,15 +102,15 @@ 実機の LED 基板は **前側のでっぱりの直角部分** に取り付けられている.3 基板を Z 軸まわりに ±90° 回転で配置: -| 基板 | 基板中心 [mm] | LED 並び方向 | 個数 | +| 基板 | LED 基板カバー中心 [mm] | LED 並び方向 | 個数 | | --- | --- | --- | --- | -| 下(Y-) | (0, -70.2, -65.3) | X 軸 | 6 | -| 右(X+) | (70.2, 0, -65.3) | Y 軸 | 6 | -| 左(X-) | (-70.2, 0, -65.3) | Y 軸 | 6 | +| 下(Y-) | (0, -70.25, -65.35) | X 軸 | 6 | +| 右(X+) | (+65.25, -14.97, -65.27) | Y 軸 | 6 | +| 左(X-) | (-65.26, -15.00, -65.27) | Y 軸 | 6 | -各基板上の LED 6 個は基板の長手方向に約 8.7 mm 間隔で等間隔配置.総数 **18 個**. +各基板上の LED 6 個は **3 個 + 中央ギャップ + 3 個** の配置(LED 同士 4.2 mm 間隔,中央 10.8 mm ギャップ).中央ギャップは電流制限抵抗 2 個分のスペースに対応(実機写真 `REFERENCE/led_board_closeup.jpg` で確認).total **18 個**. -※ 左右基板の中心座標は STL の下基板(`LED基板カバー.stl`、bbox 中心 (0, -70.2, -65.3))を基準に Z 軸まわり ±90° 回転で算出した推定値.Fusion 360 設計の実位置と微小なズレがある可能性あり. +LED 18 個の正確な座標は **Step 8-9 で Fusion 360 設計 (`configs/meshes/SmTIASv3.stl`) から抽出** し,`scripts/extract_led_positions_from_stl.py` 経由で `configs/scene_baseline.yaml` の lights セクションを更新済み.抽出値は `configs/leds_extracted.yaml` を参照.既知差分 #2 はこれで解消([TEST_01 Step 8-9](../06_TEST/TEST_01_キャリブレーション検証履歴.md#step-8-9-led-位置を-fusion-360-設計-stl-から抽出して反映)). ## カメラ(センサ) @@ -208,7 +208,7 @@ Step 8 進行中に把握した、実機との未解消の差分: 1. **3D プリンタ白素材の反射率の正確な値**: 暫定 `0.85` だが、実物のフィラメント(PLA / PETG / ABS のどれか)と分光反射率データで再校正が必要. -2. **左右 LED 基板の正確な位置・向き**: STL から推定した座標で、Fusion 360 設計とのズレがある可能性. +2. ~~**左右 LED 基板の正確な位置・向き**~~(**解消済み・Fusion 設計から正確化**): `configs/meshes/SmTIASv3.stl`(Fusion 360 アセンブリ全体エクスポート)から 18 LED 位置を `scripts/extract_led_positions_from_stl.py` で抽出し,`configs/scene_baseline.yaml` の lights セクションを更新.主な変更: 下基板内 LED 配置を「等間隔 8.7mm」→「3+ギャップ+3 (4.2mm+10.8mm+4.2mm)」へ訂正.左右基板も実位置 X=±66.0, Y=-12 付近に修正.direction は (0,0,-1) で不変(実機写真の中央電流制限抵抗 2 個と整合).形状指標は ~1% 以内のほぼ不変(多重反射が支配的,LED 数 mm 誤差は ROI 上で平滑化).[TEST_01 Step 8-9](../06_TEST/TEST_01_キャリブレーション検証履歴.md#step-8-9-led-位置を-fusion-360-設計-stl-から抽出して反映) 参照. 3. ~~**LED 1 個あたりの実 W 値**~~(**物性確定・物理整合確認済み、未対処項目は反射率の再校正のみ**): 実機搭載 LED は LED Paradise LP-2835D90VW(Ra98 SunLightLED 2835 4000K)と確定.規格値(Φv 16–19 lm @60 mA、Vf 3.1V)から独立推定した radiance は ≈ 2030 W/(m²·sr)、現状校正値 2807 との比 1.38× は反射率 0.85→0.89 への調整で物理範囲内(マット白プラ実用上限 0.92)に説明可能.ウォールプラグ効率 33.6% も実用 LED として妥当.[LED セクション「規格値に基づく radiance の独立推定」参照](#規格値に基づく-radiance-の独立推定).**形状指標への影響なし**(線形ゲイン).残る課題は反射率の実フィラメント分光データでの再校正(既知差分 #1). 4. **スマホホルダの実素材**: 推定 `holder_dark` (0.50).実物確認が必要. 5. ~~**linear vs sRGB のドメイン不一致(OETF 欠落)**~~(**最大要因・解消済み**): シミュ linear と実測 gamma 空間 luma を直接突合していた交絡で見かけ差の **約 2/3**.標準 sRGB OETF 順方向適用+実測 mean アンカーを `sm-evaluate` に正規実装して解消(`evaluation/oetf.py` + `EvaluationConfig.oetf`、`configs/eval_baseline.yaml` で有効化).以降の比較は OETF 後ドメインで行う.※ 旧版でここを「主因は ISP レンズシェーディング」としていたのは OETF を抜かした誤り. diff --git "a/docs/06_TEST/TEST_01_\343\202\255\343\203\243\343\203\252\343\203\226\343\203\254\343\203\274\343\202\267\343\203\247\343\203\263\346\244\234\350\250\274\345\261\245\346\255\264.md" "b/docs/06_TEST/TEST_01_\343\202\255\343\203\243\343\203\252\343\203\226\343\203\254\343\203\274\343\202\267\343\203\247\343\203\263\346\244\234\350\250\274\345\261\245\346\255\264.md" index 6299adf..906d1bc 100644 --- "a/docs/06_TEST/TEST_01_\343\202\255\343\203\243\343\203\252\343\203\226\343\203\254\343\203\274\343\202\267\343\203\247\343\203\263\346\244\234\350\250\274\345\261\245\346\255\264.md" +++ "b/docs/06_TEST/TEST_01_\343\202\255\343\203\243\343\203\252\343\203\226\343\203\254\343\203\274\343\202\267\343\203\247\343\203\263\346\244\234\350\250\274\345\261\245\346\255\264.md" @@ -289,6 +289,56 @@ - TEST_03 の比較パネル生成スクリプト (`scripts/build_luminance_evolution_panel.py`) が利用する Step 8-1 / 8-7 の入力を **linear + normalization** 評価結果に揃え,color scale を共有して「形」の比較を強化する(別フェーズ・別コミット). - 本筋 LSC モデル化([Step 8-N](#step-8-n-候補-lsc-順方向モデル未着手))は引き続き未着手. +### Step 8-9: LED 位置を Fusion 360 設計 STL から抽出して反映 + +- **日付**: 2026-05-28 +- **背景・問題意識**: [既知差分 #2](#既知の差分-実機との不一致)「左右 LED 基板の正確な位置・向き」が未解消だった.現 YAML の LED 18 個の座標は **「下基板を Z 軸まわり ±90° 回転」と推定**で算出していたため,Fusion 360 設計とのズレが残る可能性.特に下基板内の **LED 6 個の X 配置を等間隔 8.7mm と仮定** していたが,実機写真 (`REFERENCE/led_board_closeup.jpg`) を見ると **3 個 + 部品ギャップ + 3 個** の配置と疑われていた. +- **仮説(物理動機)**: Fusion 360 のアセンブリ全体を STL でエクスポートすれば,3 基板分の LED 18 個 + LED 基板カバー 3 枚をワールド座標で取得できる.それを真の値として YAML を更新すれば,既知差分 #2 をクローズできる.形状指標への影響は不明だが「物理表現の正確さ」が向上する. +- **試した変更**: + - `configs/meshes/SmTIASv3.stl` を Fusion 360 で全体エクスポート(File → Export,ワールド座標系) + - 抽出スクリプト `scripts/extract_led_positions_from_stl.py` を新規作成: 連結成分分割 (union-find) → LED 候補 (extents ≤4×4×1.5mm) / カバー候補 (extents 細長板状) で分類 → 各 LED に最寄りカバーを紐付け → bottom/right/left 自動命名 + 基板内ソート + - 抽出結果 `configs/leds_extracted.yaml` を `scene_baseline.yaml` の `lights` セクションへ置換(18 LED) +- **期待した効果**: + - 既知差分 #2 のクローズ + - 形状指標は **多重反射が支配的なら微変化のみ**,大きく動かない予想 + - LED 配置の物理的正確性が向上 +- **実際の結果**(`output/baseline_v3_leds/`,linear + normalization 評価): + + | 指標 | 旧 LED(推定値) | 新 LED(Fusion STL) | 差 | + | --- | --- | --- | --- | + | mean (pre-normalize) | 152.52 | **167.95** | **+10.1%** | + | α (mean 揃え係数) | 0.961934 | **0.873423** | -10% | + | mean (post-normalize) | 146.7146 | 146.7146 | 同(アンカー一致) | + | CoV | 0.18258 | 0.18151 | -0.6% | + | 勾配 % | 24.65 | 24.13 | -0.5pt | + | C/P | 1.327 | 1.318 | -0.7% | + | max/min | 4.2759 | 4.2713 | ≈ 0 | + | std | 26.79 | 26.63 | -0.6% | + +- **LED 位置の主な変更**: + + | 観点 | 旧 YAML | 新 STL(Fusion) | + | --- | --- | --- | + | 下基板 X 配置 | 等間隔 8.7mm × 6(-21.67〜+21.67) | **3+ギャップ+3**(4.2mm 間隔 + 中央 10.8mm + 4.2mm 間隔,-10.0〜+17.6) | + | 下基板 X 中心 | 0.0 | **+3.8(右オフセット)** | + | 左右基板 X 距離 | ±70.2 | **±66.0**(4.2mm 内側) | + | 左右基板 Y 中心 | 0.0 | **-12.0**(やや下寄り) | + | Z 位置 | -65.3 | **-66.1**(0.8mm スマホ側) | + | direction(全 LED) | (0, 0, -1) | (0, 0, -1)(不変) | + +- **解釈**: + - **形状指標は 1% 以内のほぼ不変**.多重反射が支配的で,LED 位置の数 mm レベルの誤差は ROI 上で平滑化される(評価板まで 65 mm に対し誤差 ~4 mm は約 6%、多重反射でさらに薄まる). + - **絶対 mean が +10% 増加**.新 LED 配置の方が評価板への光量効率が高い(理由: 不明だが,下基板 X 中心が +3.8mm 右オフセットしていて評価板中央への幾何関係が変わった可能性).mean アンカーで吸収されるため形状指標には影響しない. + - **形は出発点と同じ Y 軸単調勾配**([TEST_03 比較パネル](TEST_03_輝度マップ形状追跡.md) で確認).LED 位置の正確化も「形を変えない変更」の一例. + - 実機写真 (`REFERENCE/led_board_closeup.jpg`) で LED 配置が「3+ギャップ+3」と一致.これは中央の電流制限抵抗 2 個分のスペース(10.8mm)に対応. +- **学び**: + - **設計データ (Fusion 360) を真実の源にすると物理表現が正確になる**.手動推定座標はエラーが入りやすい. + - **形状指標は LED 位置の細かいズレに対し頑健**.多重反射が支配的なシーンでは数 mm の誤差は薄まる.これは Step 8-7 の発光面サイズ検証と同じ「点光源近似」の系の発見. + - これで既知差分 #2 がクローズ.既存の改善ステップ 8-1〜8-7 + 本 Step 8-8/9 で **「形を変えない変更」の集合が完成**.本筋 LSC モデル化(Step 8-N)に進む準備が整った. +- **次のアクション**: + - SPEC_03 既知差分 #2 をクローズ + - 本筋 LSC モデル化 ([Step 8-N](#step-8-n-候補-lsc-順方向モデル未着手)) に進む + ## Step 8-N 候補: LSC 順方向モデル(未着手) - **位置付け**: 8-3 OETF 適用後の残差 **1.6〜1.9×**(CoV 0.089 vs 実測 0.043,勾配 12.57 vs 6.10)の **主因候補**. diff --git "a/docs/06_TEST/TEST_02_\343\202\267\343\203\237\343\203\245\343\203\254\343\203\274\343\202\267\343\203\247\343\203\263\345\256\237\350\241\214\350\250\230\351\214\262.md" "b/docs/06_TEST/TEST_02_\343\202\267\343\203\237\343\203\245\343\203\254\343\203\274\343\202\267\343\203\247\343\203\263\345\256\237\350\241\214\350\250\230\351\214\262.md" index 6b9a47e..d582425 100644 --- "a/docs/06_TEST/TEST_02_\343\202\267\343\203\237\343\203\245\343\203\254\343\203\274\343\202\267\343\203\247\343\203\263\345\256\237\350\241\214\350\250\230\351\214\262.md" +++ "b/docs/06_TEST/TEST_02_\343\202\267\343\203\237\343\203\245\343\203\254\343\203\274\343\202\267\343\203\247\343\203\263\345\256\237\350\241\214\350\250\230\351\214\262.md" @@ -27,6 +27,7 @@ | `output/emitter_real/` | 8-7 検証 | `configs/scene_baseline_emitter_real.yaml` (size 1.5×1.5, radiance 12227, Φe 保存) | `configs/eval_baseline_linear.yaml` (OETF off) | 429.99 / 0.1863 / 26.21 / 1.355 | 点光源近似検証.形状指標 ≈ baseline_linear_npy で実証.輝度マップ追跡 (TEST_03) パネル右下 | | `output/sweep_reflectance/r075/` 〜 `r092/` | 8-2 反射率スイープ | `configs/scene_baseline.yaml` の `printer_white.reflectance` を 0.75 / 0.80 / 0.85 / 0.90 / 0.92 に振った 5 種 (d=12, spp=256) | linear 評価 | → 下表 `sweep_summary.csv` | TEST_01 / SPEC_03 の反射率スイープ表の元データ | | `output/sweep_reflectance/sweep_summary.csv` | 8-2 | — | — | 5 反射率の評価結果まとめ | 反射率スイープのサマリ表 | +| `output/baseline_v3_leds/` | 8-9 LED 位置正確化 | `configs/scene_baseline.yaml`(Fusion STL から抽出した新 LED 位置で更新済み, R=0.85, d=8, spp=1024) | `configs/eval_baseline_linear.yaml` (linear + normalization) | 146.71 / 0.1815 / 24.13 / 1.318 | TEST_01 Step 8-9 検証.輝度マップ追跡 (TEST_03) パネル右下(α=0.873434) | ### sweep_summary.csv の値(参考、TEST_01 line と一致確認用) @@ -49,6 +50,9 @@ | emitter_real 評価 | `uv run sm-evaluate --image output/emitter_real/luminance.npy --config configs/eval_baseline_linear.yaml --output output/emitter_real/` | | 反射率スイープ全 5 点 | `output/sweep_reflectance/run_sweep.py` を参考に再実装.元スクリプトは削除推奨 (下記) のため要再構築 | | 輝度マップ追跡パネル再生成 | `uv run python scripts/build_luminance_evolution_panel.py` | +| LED 位置を Fusion STL から再抽出 | `uv run python scripts/extract_led_positions_from_stl.py configs/meshes/SmTIASv3.stl --output configs/leds_extracted.yaml`(その後 scene_baseline.yaml の lights セクションを置換) | +| baseline_v3_leds レンダ生成 | `$env:DRJIT_LIBLLVM_PATH = "C:\Program Files\LLVM\bin\LLVM-C.dll"; uv run sm-render --scene configs/scene_baseline.yaml --output output/baseline_v3_leds/` | +| baseline_v3_leds 評価 | `uv run sm-evaluate --image output/baseline_v3_leds/luminance.npy --config configs/eval_baseline_linear.yaml --output output/baseline_v3_leds/` | ## 過去の整理実績 (Cleanup History) diff --git "a/docs/06_TEST/TEST_03_\350\274\235\345\272\246\343\203\236\343\203\203\343\203\227\345\275\242\347\212\266\350\277\275\350\267\241.md" "b/docs/06_TEST/TEST_03_\350\274\235\345\272\246\343\203\236\343\203\203\343\203\227\345\275\242\347\212\266\350\277\275\350\267\241.md" index e8afc23..19e2a83 100644 --- "a/docs/06_TEST/TEST_03_\350\274\235\345\272\246\343\203\236\343\203\203\343\203\227\345\275\242\347\212\266\350\277\275\350\267\241.md" +++ "b/docs/06_TEST/TEST_03_\350\274\235\345\272\246\343\203\236\343\203\203\343\203\227\345\275\242\347\212\266\350\277\275\350\267\241.md" @@ -43,6 +43,7 @@ | Step 8-1 (出発点,linear + mean 正規化) | ROI 下端のみ明るく,上端に向かって単調に減衰する **Y 軸単調勾配**.LED 配置の物理的事実(下基板が Y=-70.2 のみで上対応の基板なし)を素直に反映 | 下端は白く完全飽和,上端は黒く振り切れ.**実機色域に収まらない急峻な勾配**.CoV=0.1826 | | Step 8-3 (OETF 適用後,sRGB) | 数値指標は半減(CoV 0.183 → 0.089)したが,**輝度マップの形は出発点と区別がつかない**.OETF はピクセル値の単調変換で**相対順序を変えない**ため形は構造的に不変 | Step 8-1 と全く同じ形.CoV だけが半減(OETF の非線形圧縮で分布が縮む)が,飽和パターンは継続 | | Step 8-7 (発光面 size 1.5×1.5,linear + mean 正規化) | 絶対値スケールは 2.83× 上昇したが,**形は出発点と同じ Y 軸単調勾配**.形状指標がほぼ不変だったことと整合 | Step 8-1 と区別不能の形.mean 正規化で表示スケールが完全に揃う | +| Step 8-9 (LED 位置を Fusion STL から正確化,linear + mean 正規化) | LED 位置を「3+ギャップ+3」の実機配置に直したが,**形は依然として Y 軸単調勾配**.数値指標 CoV/勾配/C/P も 1% 以内の微変化 | Step 8-1 と区別不能の形.LED 位置の数 mm 誤差は多重反射で平滑化される | ## 学び (Findings) @@ -50,7 +51,7 @@ Step 8-3 OETF は「指標数値の符号化ドメイン不一致」を解消したのみで,**シミュの形は実機の形に近づいていない**.OETF は単調変換であり,ピクセル間の相対順序を保つため形は不変. -### 2. これまでの全 7 改善ステップは「形を変えない変更」だった +### 2. これまでの全改善ステップは「形を変えない変更」だった | Step | 種別 | 形への影響 | | --- | --- | --- | @@ -61,6 +62,8 @@ | 8-5 3D 可視化改善 | 表示側の変更(物理計算には不介入) | 形不変 | | 8-6 LED 物性確定 | 線形ゲイン(radiance 値の校正) | 形不変 | | 8-7 発光面サイズ検証 | 局所幾何との縮退(絶対値変化)+ 点光源近似 | 形不変 | +| 8-8 linear mean 正規化 | 表示用の線形ゲイン(指標は不変) | 形不変 | +| 8-9 LED 位置の Fusion STL 反映 | LED 数 mm 移動,多重反射で平滑化 | 形不変 | これらはすべて **線形ゲイン・単調変換・局所幾何依存のいずれか** であり,**「ピクセル間の空間構造」を変える物理を含まない**. diff --git a/scripts/build_luminance_evolution_panel.py b/scripts/build_luminance_evolution_panel.py index 96adedd..f62b4a4 100644 --- a/scripts/build_luminance_evolution_panel.py +++ b/scripts/build_luminance_evolution_panel.py @@ -118,6 +118,12 @@ "title": "Step 8-7: Real-chip Emitter Size", "subtitle": "size 1.5x1.5 mm (Φe preserved), linear + mean normalization", }, + { + "loader": _load_sim_linear, + "args": (ROOT / "output" / "baseline_v3_leds" / "luminance.npy",), + "title": "Step 8-9: LED Positions from Fusion STL", + "subtitle": "Accurate LED layout (3+gap+3 per board), linear + mean normalization", + }, ] @@ -155,9 +161,25 @@ vmin = float(np.percentile(real_roi, REAL_VMIN_PERCENTILE)) vmax = float(np.percentile(real_roi, REAL_VMAX_PERCENTILE)) - fig, axes = plt.subplots(2, 2, figsize=(14, 13)) + # PANELS 数に応じてグリッドを自動決定 + n = len(panels_data) + if n <= 2: + rows, cols = 1, n + elif n <= 4: + rows, cols = 2, 2 + elif n <= 6: + rows, cols = 2, 3 + elif n <= 9: + rows, cols = 3, 3 + else: + cols = 4 + rows = (n + cols - 1) // cols + + fig, axes = plt.subplots(rows, cols, figsize=(cols * 7, rows * 6.5)) + axes_flat = axes.flatten() if hasattr(axes, "flatten") else [axes] + last_im = None - for ax, (panel, roi, m) in zip(axes.flatten(), panels_data): + for ax, (panel, roi, m) in zip(axes_flat, panels_data): last_im = ax.imshow(roi, cmap="hot", vmin=vmin, vmax=vmax, aspect="auto") ax.set_axis_off() title = ( @@ -168,10 +190,14 @@ ) ax.set_title(title, fontsize=10, loc="left") + # 余ったマス目は非表示 + for ax in axes_flat[n:]: + ax.set_axis_off() + # 共有 colorbar cbar = fig.colorbar( last_im, - ax=axes.ravel().tolist(), + ax=axes_flat.tolist() if hasattr(axes_flat, "tolist") else list(axes_flat), shrink=0.7, label=f"Luminance (mean-anchored to {TARGET_MEAN})", ) diff --git a/scripts/extract_led_positions_from_stl.py b/scripts/extract_led_positions_from_stl.py new file mode 100644 index 0000000..0bd2350 --- /dev/null +++ b/scripts/extract_led_positions_from_stl.py @@ -0,0 +1,305 @@ +"""Fusion 360 から出力した LED ユニット STL から LED 位置を抽出する. + +STL ファイルに含まれる連結成分を解析し,カバー(最大の連結成分)と LED 群 +(小さい連結成分)を識別する.各 LED の中心座標・サイズ・法線方向(発光方向)を +抽出して scene_baseline.yaml の lights セクションに組み込める形式で出力する. + +使い方: + # 1 ファイル(複数ユニット含む) + uv run python scripts/extract_led_positions_from_stl.py \\ + configs/meshes/LEDユニット_all.stl + + # 複数 STL ファイル(各ユニット別ファイル) + uv run python scripts/extract_led_positions_from_stl.py \\ + configs/meshes/LEDユニット下.stl \\ + configs/meshes/LEDユニット右.stl \\ + configs/meshes/LEDユニット左.stl + + # 出力を YAML ファイルに保存 + uv run python scripts/extract_led_positions_from_stl.py \\ + configs/meshes/LEDユニット_all.stl \\ + --output configs/leds_extracted.yaml + +仕様: +- 連結成分のうち面数最大をカバー(光らない),残りを LED(発光面)と識別 +- 各 LED の法線方向は extents 最小軸(板の厚み方向)から推定 +- 法線の符号は「カバー中心 → LED 中心」のベクトルに基づき外向きに決定 +- LED 位置は (Y, X, Z) 順で sort(下基板は X 順,右・左基板は Y 順になる想定) +""" + +from __future__ import annotations + +import argparse +from collections import defaultdict +from pathlib import Path +from typing import Any + +import numpy as np +import trimesh +import yaml + +# 既存 scene_baseline.yaml の LED radiance(Step 8-6 で校正済み).新規 YAML 生成時に使う. +DEFAULT_RADIANCE = 2807.2469840551203 + + +def _split_connected_components(mesh: trimesh.Trimesh) -> list[list[int]]: + """頂点共有で連結する面群をグループ化(union-find). + + Returns: + 各グループは face index のリスト. + """ + n_verts = len(mesh.vertices) + parent = list(range(n_verts)) + + 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 face in mesh.faces: + union(int(face[0]), int(face[1])) + union(int(face[1]), int(face[2])) + + groups: dict[int, list[int]] = defaultdict(list) + for fi, face in enumerate(mesh.faces): + groups[find(int(face[0]))].append(fi) + return list(groups.values()) + + +def _component_stats(mesh: trimesh.Trimesh, face_indices: list[int]) -> dict[str, Any]: + """連結成分の統計(頂点・面数・bbox 中心・extents)を計算する.""" + face_verts_idx = np.unique(mesh.faces[face_indices].flatten()) + verts = mesh.vertices[face_verts_idx] + bmin = verts.min(axis=0) + bmax = verts.max(axis=0) + return { + "n_verts": len(face_verts_idx), + "n_faces": len(face_indices), + "center": (bmin + bmax) / 2.0, + "extents": bmax - bmin, + "bmin": bmin, + "bmax": bmax, + } + + +def _infer_led_direction(led_center: np.ndarray, led_extents: np.ndarray, + cover_center: np.ndarray) -> np.ndarray: + """LED 発光面の法線方向を推定する. + + LED は薄板状(extents の最小軸が板の厚み).発光方向はその最小軸方向で, + カバー中心から LED 中心への差分の符号で外向きを決める. + """ + min_axis = int(np.argmin(led_extents)) + direction = np.zeros(3) + diff = led_center - cover_center + sign = 1.0 if diff[min_axis] >= 0 else -1.0 + direction[min_axis] = sign + return direction + + +def _led_size_xy(led_extents: np.ndarray) -> tuple[float, float]: + """LED 発光面のサイズ(厚み以外の 2 軸)を取得する. + + パッケージ命名規則どおり (2.8, 3.5) mm 等が想定される. + 返り値は (短辺, 長辺) ではなく (X 方向幅, Y 方向幅) を意図しない. + AreaLight の size は (width, height) の 2 値で,向きは direction で別途指定する. + """ + min_axis = int(np.argmin(led_extents)) + other = [i for i in range(3) if i != min_axis] + return (float(led_extents[other[0]]), float(led_extents[other[1]])) + + +def _classify_components(comps: list[dict]) -> tuple[list[dict], list[dict]]: + """連結成分を「LED 候補」「カバー候補」に分類する. + + 分類基準(SMD 2835 LED と LED 基板カバーの想定形状に基づく): + - LED 候補: extents の最大 ≤ 4mm かつ最小 ≤ 1.5mm(板状で 2.8×3.5×1mm 想定) + - カバー候補: extents の最大 > 30mm かつ最小 ≤ 3mm(細長い板状、LED 基板カバー想定) + + 上記いずれにも該当しない大きな成分(筐体、評価板、スマホ等)は除外する. + """ + led_candidates = [] + cover_candidates = [] + for c in comps: + e = c["extents"] + if max(e) <= 4.0 and min(e) <= 1.5: + led_candidates.append(c) + elif max(e) > 30.0 and min(e) <= 3.0: + cover_candidates.append(c) + return led_candidates, cover_candidates + + +def extract_leds_from_mesh(mesh: trimesh.Trimesh) -> tuple[list[dict], list[dict]]: + """STL メッシュから LED 群とカバー群を抽出する. + + 複数の LED 基板カバー(下・右・左)を含む複合 STL に対応. + 各 LED について「最寄りカバー」を探し,そのカバー中心からの差分で + 発光方向(direction)の符号を決定する. + + Returns: + (leds, covers) のタプル. + - leds: 各 LED の {"center", "size", "direction", "extents", "nearest_cover_idx"} のリスト + - covers: 各カバーの統計(center, extents 等)のリスト + """ + groups = _split_connected_components(mesh) + if len(groups) < 2: + msg = f"STL に連結成分が {len(groups)} 個しかない.カバー + LED 群が必要" + raise ValueError(msg) + + comps = [_component_stats(mesh, g) for g in groups] + led_components, cover_components = _classify_components(comps) + + if len(cover_components) == 0: + msg = "カバー候補が見つからない(細長い板状の連結成分が必要)" + raise ValueError(msg) + if len(led_components) == 0: + msg = "LED 候補が見つからない(2.8x3.5x1mm 想定の小さな板状成分が必要)" + raise ValueError(msg) + + cover_centers = np.array([c["center"] for c in cover_components]) + leds = [] + for c in led_components: + # 最寄りカバー(中心間距離が最小)を選ぶ + distances = np.linalg.norm(cover_centers - c["center"], axis=1) + nearest_idx = int(np.argmin(distances)) + nearest_cover = cover_components[nearest_idx] + + size = _led_size_xy(c["extents"]) + direction = _infer_led_direction(c["center"], c["extents"], nearest_cover["center"]) + leds.append({ + "center": c["center"].tolist(), + "size": list(size), + "direction": direction.tolist(), + "extents": c["extents"].tolist(), + "nearest_cover_idx": nearest_idx, + }) + return leds, cover_components + + +def _classify_cover_position(cover_center: np.ndarray) -> str: + """カバー中心位置から `bottom` / `right` / `left` / `top` を判定する. + + 判定基準: |Y| > |X| なら上下軸(Y 符号で bottom/top),そうでなければ左右軸(X 符号で left/right). + MiniTIAS の 3 基板配置 (下基板 Y<0, 右基板 X>0, 左基板 X<0) に対応. + """ + x, y, _ = cover_center + if abs(y) > abs(x): + return "bottom" if y < 0 else "top" + return "right" if x > 0 else "left" + + +def _assign_led_ids(leds: list[dict], covers: list[dict]) -> list[dict]: + """各 LED に id を割り当てて並べ替える. + + - 各 LED の所属基板を cover の位置(bottom/right/left/top)で判定 + - 同基板内では「下基板: X 昇順」「右/左基板: Y 昇順」「上基板: X 昇順」で並べる + - id 命名: `led_{label}_{index}` 例: `led_bottom_0`, `led_right_5` + """ + cover_labels = [_classify_cover_position(np.array(c["center"])) for c in covers] + grouped: dict[str, list[dict]] = defaultdict(list) + for led in leds: + label = cover_labels[led["nearest_cover_idx"]] + grouped[label].append(led) + + # 並べ替えキー: bottom/top は X 昇順,right/left は Y 昇順 + sort_keys = { + "bottom": lambda l: l["center"][0], + "top": lambda l: l["center"][0], + "right": lambda l: l["center"][1], + "left": lambda l: l["center"][1], + } + # 出力順: bottom → right → left → top(読みやすい固定順) + label_order = ["bottom", "right", "left", "top"] + result = [] + for label in label_order: + if label not in grouped: + continue + group = sorted(grouped[label], key=sort_keys[label]) + for i, led in enumerate(group): + led_with_id = dict(led) + led_with_id["assigned_id"] = f"led_{label}_{i}" + result.append(led_with_id) + return result + + +def _format_led_yaml_entries(leds: list[dict], radiance: float = DEFAULT_RADIANCE) -> list[dict]: + """LED リストを scene_baseline.yaml の lights セクション形式に整形する. + + `assigned_id` フィールド(_assign_led_ids で付与)が無い場合は単純な番号で代用. + """ + entries = [] + for i, led in enumerate(leds): + led_id = led.get("assigned_id", f"led_{i:02d}") + entries.append({ + "id": led_id, + "type": "area", + "shape": "rectangle", + "center": [round(c, 4) for c in led["center"]], + "size": [round(s, 4) for s in led["size"]], + "direction": [round(d, 4) for d in led["direction"]], + "radiance": [radiance, radiance, radiance], + }) + return entries + + +def main() -> None: + parser = argparse.ArgumentParser(description=__doc__.splitlines()[0]) + parser.add_argument("stl_files", nargs="+", type=Path, + help="解析する STL ファイル(複数可)") + parser.add_argument("--output", type=Path, default=None, + help="出力 YAML パス(省略時は標準出力)") + parser.add_argument("--radiance", type=float, default=DEFAULT_RADIANCE, + help=f"各 LED の radiance 値(既定 {DEFAULT_RADIANCE})") + parser.add_argument("--summary", action="store_true", + help="LED 位置サマリーを stderr に表示") + args = parser.parse_args() + + all_leds: list[dict] = [] + for stl in args.stl_files: + if not stl.is_file(): + msg = f"STL ファイルが見つかりません: {stl}" + raise FileNotFoundError(msg) + print(f"# Processing {stl}", flush=True) + mesh = trimesh.load_mesh(str(stl)) + leds, covers = extract_leds_from_mesh(mesh) + print(f" detected {len(covers)} cover(s):", flush=True) + for i, cover in enumerate(covers): + label = _classify_cover_position(cover["center"]) + print(f" cover[{i}] label={label:<6} center=({cover['center'][0]:+.2f}, " + f"{cover['center'][1]:+.2f}, {cover['center'][2]:+.2f}) " + f"extents=({cover['extents'][0]:.2f}, {cover['extents'][1]:.2f}, " + f"{cover['extents'][2]:.2f})", flush=True) + + # ID 付与(bottom/right/left/top + 並び順) + leds_with_ids = _assign_led_ids(leds, covers) + print(f" detected {len(leds_with_ids)} LED(s) (id assigned):", flush=True) + for led in leds_with_ids: + print(f" {led['assigned_id']:<16} " + f"center=({led['center'][0]:+.2f}, {led['center'][1]:+.2f}, " + f"{led['center'][2]:+.2f}) size={led['size']} dir={led['direction']}", + flush=True) + all_leds.extend(leds_with_ids) + + print(f"\n# Total: {len(all_leds)} LED(s) from {len(args.stl_files)} STL file(s)", flush=True) + + # YAML 形式で出力 + entries = _format_led_yaml_entries(all_leds, radiance=args.radiance) + yaml_str = yaml.safe_dump(entries, default_flow_style=False, + allow_unicode=True, sort_keys=False) + + if args.output is None: + print("\n# --- lights セクション(scene_baseline.yaml にコピペ可) ---") + print(yaml_str) + else: + args.output.write_text(yaml_str, encoding="utf-8") + print(f"\n# Wrote {len(entries)} LED entries to {args.output}", flush=True) + + +if __name__ == "__main__": + main()