# 実機自動テスト計画 (Hardware-in-the-Loop Test Plan)

## 目的 (Purpose)

[PLAN_05](PLAN_05_テスト網羅性向上計画.md) の 4 層モデルで「層④: 実機」に残っている確認（SDK の実呼び出し・実画像）を，**Claude が実機を操作して自動で確認する**ようにする．人間の役割を「機材をつなぐ・チャートを置く・テストしてよいと合図する」という物理的な準備だけに絞る．

## 前提 (Preconditions)

2026-10-06 にユーザーと確認した．

| 項目 | 内容 |
| --- | --- |
| 接続 | カメラ（ImagingSource・Lumenera）と光源は，Claude が動作している開発 PC に接続できる |
| 機材 | 両機種がそろっている |
| 対象機種 | **ImagingSource だけを対象にし，Lumenera の実機テストは行わない**（2026-10-08 にユーザーが決定．Lumenera は解像度が相対的に低く今後あまり使わない見込みのため）．`--camera lucam` の実装は残すが，検査の対象にしない |
| 実行してよい条件 | **ユーザーが会話の中で明示的に合図したときだけ**．撮影の本番中に機材を操作しないため，合図なしに実機テストを実行しない |

## 仕組み (Design)

既存のセルフテスト（`TIASshot.exe --selftest`．偽カメラ・ヘッドレス）と同じ考え方で，実機版の検査モードを追加する．

- **起動**: `TIASshot.exe --hwtest --camera <lucam|iscam> [--with-chart] [--hwtest-keep] --hwtest-out <結果ファイル>`．ランナー `scripts/verify/run_hwtest.py` がビルドから実行・判定までを行う（`run_selftest.py` と同じ形）
- **`--with-chart`**: チャートが必要な検査（connect → calibrate → shot → close-saving → close-calibrating → cleanup）を行う．校正は止めない．チャート不要の検査は行わないため，`--camera iscam` と `--camera iscam --with-chart` をカラーチャートをはめたまま続けて実行する．全体の上限はチャート不要 600 秒・`--with-chart` 1200 秒，ランナーの打ち切りはそれぞれ 720 秒・1320 秒
- **`--keep`（ランナー）＝ `--hwtest-keep`**: 合格時も作業フォルダ（`%TEMP%\TIASshot-hwtest\work-*`）を残す（不合格・タイムアウト時は常に残る）．`--with-chart` では校正後のフレームとその変換画像を証拠の画像（`evidence\calibrated-frame-raw.png`・`evidence\calibrated-frame-srgbNN.png`．`NN` は変換の次元）として保存する．撮影データ・校正データは作業フォルダの `data\` に保存され，合格時は作業フォルダごと削除される
- **ビルドフラグ**: `HWTEST`（Debug 系の構成のみ）．診断・評価機能を Release に含めない方針（CLAUDE.md・[ENV_06](../02_ENV/ENV_06_ビルド構成とビルドフラグ.md)）に従う
- **フォーム**: ImagingSource は SDK の画面部品（`ICImagingControl`）を使うため，ヘッドレスではなく**実際の `Form1` を表示して**動かす．人間がその場にいる前提なので表示してよい
- **カメラの選択**: 両機種を同時につないでも，`--camera` で指定した機種だけを使う（通常起動の `Form1_Load` は Lumenera を優先して探すため，そのままでは ImagingSource を検査できない）
- **結果**: 項目ごとの PASS / FAIL と測定値を結果ファイルに書き，終了コード（0=合格，1=不合格あり，2=実機テスト自体の異常，3=タイムアウト）で返す．セルフテストの結果ファイルと同じ書式にする

### 守ること (Safety)

- 保存先は一時フォルダ（`%TEMP%` 配下）にする．本番の保存先（`C:\TIAS_Data` 等）には書き込まない
- 同梱の `config.xml` は変更しない（一時フォルダに複製して保存先等を差し替える）
- カメラのデバイス状態ファイル（`{DeviceName}.xml`）は読むだけで，上書きしない．露光・ゲイン等を検査で変えた場合は，終了時に元の値へ戻す
  - ImagingSource の露光・ゲインは絶対値（`VCDAbsoluteValueProperty`．露光は秒，ゲインは dB）で読み書きし，元の絶対値そのものへ戻して読み戻しで確かめる（整数の段（`VCDRangeProperty`）で扱うと元の値に戻せないため）
- チャート不要の検査の間は自動校正（チャート検出・ホワイトバランスの調整・変換行列の算出）を止める（`CameraBase` の校正停止フラグ．`HWTEST` ビルド限定）．はめたチャートで校正が走ると露光・ゲイン・ホワイトバランスが検査中に変わるため
- 光源は検査の最後に必ずポートを閉じる（失敗した場合も）．消灯はしない（アプリ終了時に光源を消さない仕様に合わせる．`light` 項目の最後に再点灯した状態で終わる）
- 待機には必ずタイムアウトを付ける

## 検査項目 (Test Items)

### チャート不要（`--with-chart` なし）

※ 穴に何もはめないと中が暗く明るさの変化を評価しにくいため，**カラーチャート（または白板）をはめた状態で行う**．この検査の間は，チャートが写っても自動校正を走らせない（テストモードだけの処理．2026-10-08 にユーザーと合意）．

| ID | 確認内容 | 判定の仕方 |
| --- | --- | --- |
| `connect` | 指定した機種に接続でき，機種名・シリアル番号が取れる | `Connect()` の成否 |
| `preview` | プレビューが流れる | 一定時間に届いたフレーム数（フレームレートの下限） |
| `exposure-gain` | 露光・ゲインの設定がフレームに効く | 基準の明るさを探してから，露光を 1 段（2^n 秒の段で半分・2 倍），ゲインを範囲の幅の ±10% 上げ下げしたときの平均輝度の変化の向きと量．飽和した向きは確認しない．終了時に元の値へ戻ったことを読み戻しで確かめる |
| `light` | 光源が点灯・消灯し，閉じた後に開き直せる | 点灯時と消灯時のフレームの平均輝度の差，ポートの再オープンの成否 |
| `close-preview` | プレビュー中に閉じる処理を呼んでもすぐ戻る | 後始末の所要時間（上限つき）と例外の有無，閉じた後に届いたフレーム数 |
| `reconnect` | 切断した後に再接続できる | 2 回目の `Connect()` の成否とフレームの到着 |
| `cleanup` | 後始末と書き込み禁止 | 露光・ゲインを戻す → カメラ切断 → 光源のポートを閉じる（消灯しない）の成否，設定・デバイス状態ファイル・本番の保存先が変わっていないこと |

露光・ゲインの基準探し: 元の露光に最も近い 2^n 秒の段に合わせ，平均輝度が中間（30〜220）かつ飽和画素（254 以上）の割合が 1% 以下になるまで露光を 1 段ずつ動かす（最大 12 段）．実測で露光 1 段は 0.497 倍（黒レベル 15.2 を引いた値）≈ 0.5 倍，ゲイン 1 dB は 1.122 倍 ≈ 10^(1/20) と理論どおりに効いた．

### チャートが必要（`--with-chart`．人間がチャートをカメラの前に置いてから実行する）

※ チャート不要の検査と同じくカラーチャートをはめたまま続けて実行できる（付け外しは不要）．こちらは自動校正を走らせて結果を確かめる．`--with-chart` ではチャート不要の検査（`preview`〜`reconnect`）は行わない（`connect`・`cleanup` は共通．`cleanup` ではホワイトバランスを戻さない．校正で変わるのはカメラ側の値だけで，次の接続でデバイス状態ファイルの値に戻る）．

| ID | 確認内容 | 判定の仕方 |
| --- | --- | --- |
| `calibrate` | 実物の舌診チャートで自動校正が完了し，校正後のフレームを変換した色票が参照値に近い | 校正の待ち時間（最初のフレーム → チャート検出 → 校正完了．上限 60 秒），校正完了後に撮影ボタンが有効になること，校正データが一時フォルダに保存されること．校正後のフレームを校正の変換行列で変換し，24 色票の平均と参照値（`tcc_srgb`）の差を**保存画像と同じ 8bit（切り詰め後）・チャネルごとの差の絶対値**で判定する（平均，白色票を除いた最大，白色票の最大）．白色票は参照値の BGR の和が最大の色票（13 番）．ΔE76・白飛びの割合・当てはめの誤差は参考 |
| `shot` | 単発・連続撮影で保存される | 単発 1 枚・連続 3 枚（間隔 1000 ms）．撮影直後は保存中で撮影ボタンが無効，保存完了（上限つき）後に再有効化されること，保存されたファイル（撮影画像・変換画像・Info.csv）と撮影ログの行数・備考，撮影時刻の間隔（設定値との差），エラー通知の有無 |
| `close-saving` | 保存中に閉じようとすると取り消され，保存後は閉じられる | 連続 5 枚（間隔 0 ms．保存が先に終わったら 10 枚で再試行）の保存中に閉じる処理を呼び，取り消しの成否と表示文・後始末が走っていないこと．保存完了後に閉じる処理の所要時間・後始末の順序（closing → timer → camera → light）・閉じた後に届いたフレーム数 |
| `close-calibrating` | 校正中に閉じてもすぐ戻る | 再接続して校正中になってから 500 ms 後に閉じる処理を呼び，所要時間・後始末の順序・例外の有無・閉じた後に届いたフレーム数 |

※ 判定のしきい値はチャート不要の検査はフェーズ 1，チャートが必要な検査はフェーズ 2 の実測から決めた（下記「確定したしきい値と実測値」）．

### 確定したしきい値と実測値 (Thresholds and Measurements)

フェーズ 1 の実測（ImagingSource DFK 33UX178．シリアル 35620307・10620446．カラーチャートをはめた状態）と，フェーズ 2 の実測（同機種．シリアル 10620446．2026-10-08 の 2 回）から決めた．しきい値の本体は `HwTest/HwTestJudge.cs`．

| 項目 | しきい値 | 実測値 |
| --- | --- | --- |
| `preview` のフレームレート | 10 fps 以上 | 13〜18 fps |
| `exposure-gain` の露光 | 1 段下げで比 0.8 以下，1 段上げで比 1.25 以上（向きごと．飽和した向きは確認しない） | 下げ 0.545（2026-10-08 13:38 の実行．上げは比 1.322 だが飽和画素 70% のため確認しない向き） |
| `exposure-gain` のゲイン | 平均輝度の差 3 以上（片側の向きだけ期待どおりでも合格） | 下げ −15.6（同上．上げは +47.1 だが飽和画素 68% のため確認しない向き） |
| 基準の明るさ | 平均輝度 30〜220，飽和画素（254 以上）の割合 1% 以下 | 元の露光に近い段（0.015625 秒）で 158.9 |
| 飽和の判定 | 254 以上の画素の割合が 1% を超える | — |
| `light` の点灯と消灯の差 | 平均輝度の差 100 以上 | 153〜196 |
| `close-preview` の閉じる処理 | 1000 ms 以内，閉じた後に届くフレーム 2 以下 | 75〜221 ms，0 フレーム |
| `reconnect` | 再接続後に 5 フレーム以上 | 964 ms で 5 フレーム（2026-10-08 13:38 の実行） |
| 露光・ゲインの復元 | 元の値との相対誤差 1% 以内 | 一致 |
| `calibrate` の校正の待ち時間 | 60 秒以内 | 5.0〜5.6 秒（チャート検出まで 1.6〜1.8 秒＋校正 3.4〜3.8 秒） |
| `calibrate` の色の誤差（8bit・チャネルごとの \|測定−参照\|．17 次元）の平均 | 5 以下 | 1.57 |
| `calibrate` の色の誤差の白色票を除いた最大 | 10 以下 | 3.47〜3.5（10 番） |
| `calibrate` の色の誤差の白色票（13 番）の最大 | 40 以下（**暫定**．下記「17 次元の変換のノイズ」） | 24.65〜27.35 |
| `calibrate` の ΔE76 | 参考（判定しない） | 平均 1.20〜1.26，最大 10.11〜12.04（白色票） |
| `shot` の撮影間隔のずれ | 設定値との差 100 ms 以下 | 4〜22 ms |
| `shot`・`close-saving` の保存の待ち時間 | 180 秒以内（2026-10-09 に 90 秒から延ばした） | 1 枚あたり約 3.3 秒（5 枚で 16〜20 秒）．重いアプリが動いていると約 11 秒（5 枚で 55 秒） |
| `close-saving` の閉じる処理 | 保存中は取り消される．保存の完了後は 1000 ms 以内，閉じた後に届くフレーム 2 以下 | 取り消しを確認，33〜72 ms，0 フレーム |
| `close-calibrating` の閉じる処理 | 校正中になるまで 60 秒以内，閉じる処理 1000 ms 以内，閉じた後に届くフレーム 2 以下 | 53〜98 ms，0 フレーム |

### 17 次元の変換のノイズ (Noise of the 17-Term Conversion)

フェーズ 2 の実機テストで，白色票の色の誤差だけが大きい（24.65〜27.35．他の色票は 3.5 以下）ことが分かった．**製品の性質であり，対策は要判断**．対策は保留中の検討事項として [TECH_03「保留中の検討事項」](../05_TECH/TECH_03_色補正処理仕様.md) で管理する（2026-10-09 に保留と決定）．

- 事実: 白色票の入力（約 226．標準偏差 3〜5．飽和なし）が 17 次元（3 次）の変換で約 10 倍に増幅され（変換後の標準偏差 約 50，8bit に切り詰める前の値 -249〜621），8bit の範囲を外れて点状のノイズ・白飛びになる（白色票の 255 の画素 約 73%）．チャート外の白い台紙・枠でも点状のノイズが出る．平均の色は合っている（切り詰め前の値で平均すると参照値との差は 2 以内）．白など色票の範囲の端の明るい色で，高次の多項式の外挿がノイズを増幅するためと考えられる
- **回帰ではない（上流 v1.6 でも同じ）**: 2026-10-09 に v1.6 と現行で同じチャートを撮影し，同じ入力を両方のコードに通すと，マスク・色票の値・変換行列・変換後の画像（PNG/JPEG）がビット単位で一致した（色変換はリファクタリングで変わっていない）．白色票の変換後の標準偏差も v1.6 51/39/23・現行 50/36/24（B/G/R）で同程度
- 要判断: 対策（使う次元を下げる，入力・出力に制限をかける，色票の範囲外を別扱いにする等）は色の再現性に関わるため，研究室・上流と相談して決める．それまで白色票のしきい値 40 は暫定とし，色の誤差は白色票とそれ以外に分けて判定する
- 付随して確かめたこと: CSV に保存する変換行列（小数 7 桁）は 17 次元の 3 次の係数では有効数字が約 3 桁しかなく，CSV から画像を再現すると 8bit で最大 2〜4 ずれる

## 進め方 (Workflow)

1. 人間が機材をつなぎ，カラーチャート（または白板）を穴にはめて，「実機テストして」等と合図する
2. Claude（verifier）が `python scripts/verify/run_hwtest.py --camera <機種>` と `python scripts/verify/run_hwtest.py --camera <機種> --with-chart` を続けて実行し，結果ファイルと終了コードで判定する
3. 対象は ImagingSource（`--camera iscam`）だけ．Lumenera は行わない（上記「対象機種」）
4. 人間に残る確認は，画面の見た目の最終確認など，数値で置き換えられないものだけにする

## 変更が必要なもの (Changes)

| 対象 | 変更内容 |
| --- | --- |
| `TIASshot`（本体） | `HWTEST` フラグの追加，`--hwtest` の実装（検査項目・結果ファイル），カメラを機種指定で接続する口 |
| `scripts/verify/run_hwtest.py` | ビルド・実行・判定のランナー |
| `.claude/verify-profile.md` | 実機カメラ・光源・シリアルポートへの接続を，「ユーザーが会話の中で合図したとき」に限って許可する．合図がなければ実機テストは実行せず「検証不能（合図なし）」と報告する |
| `.claude/rules/testing.md` | 層④のうち実機テストで確認できる項目を明記する |
| [TEST_01](../06_TEST/TEST_01_テスト仕様.md) | 手動テストの項目を，実機テストで置き換えた分だけ減らす |
| [ENV_04](../02_ENV/ENV_04_開発コマンド.md)・[ENV_06](../02_ENV/ENV_06_ビルド構成とビルドフラグ.md) | 実機テストのコマンドと `HWTEST` フラグの説明 |

## フェーズ (Phases)

### フェーズ 1: 基盤とチャート不要の検査

- `HWTEST` フラグ，`--hwtest`，機種指定の接続，結果ファイル，ランナーを作る
- チャート不要の検査（`connect`・`preview`・`exposure-gain`・`light`・`close-preview`・`reconnect`）を実装する
- 検証プロファイルを更新する
- 完了条件: ImagingSource で人間の合図のもと実行でき，しきい値を実測から決めて全項目が合格する（当初は両機種としていたが，2026-10-08 に Lumenera を対象外とした）
- **状態: ImagingSource で完了（2026-10-08．DFK 33UX178 の 2 個体で 7/7 合格）．Lumenera の実機テストは行わない（対象外）**
- 実機テストで見つかった不具合: 同梱の `DFK33UX178.xml` は別個体（シリアル 41520093）のものを指しており，旧 `IScam.Connect` はファイルのデバイスを開いていたため，別個体では接続できず例外が外へ漏れていた．機種名でデバイスを開いてから設定だけを適用し，失敗は `ErrorMsg`＋`false` で返すように直した（`Cameras/IScamDeviceSelector`．[TECH_02](../05_TECH/TECH_02_カメラ制御仕様.md)）
- 仕様変更: アプリ終了時に光源を消灯しない（ポートを閉じるだけ．上流 v1.7 と同じ．2026-10-08 にユーザーが確定）．実機テストの後始末も同じく消灯しない

### フェーズ 2: チャートが必要な検査

- `calibrate`・`shot`・`close-saving`・`close-calibrating` を実装する
- 完了条件: ImagingSource で実物のチャートを使って全項目が合格する
- **状態: ImagingSource で完了（2026-10-08．DFK 33UX178（シリアル 10620446）で 2 回とも 6/6 合格）**
- 実機テストで見つかったこと: 17 次元の変換で白色票・白い台紙に点状のノイズ・白飛びが出る（上流 v1.6 から同じ．上記「17 次元の変換のノイズ」．対策は要判断）

### フェーズ 3: 手動テストの縮小

- TEST_01 の手動テストと PLAN_05 の「手動確認の目標形」を，実機テストで置き換えた分だけ縮める
- **状態: 完了（2026-10-09）**．手動テストに残したのは，実機テストで確かめられない項目だけ: 通常起動で機種を自動で選ぶ流れ，プレビューの見た目，マウスでのボタン操作，撮影した画像の色の目視，撮影の途中で閉じる場合（未自動化），閉じた後も光源が点いたまま残ること，Lumenera を使う場合の確認
- 後段の候補: 撮影の途中で閉じる場合の検査を実機テストに追加すれば，手動テストからさらに外せる

## PLAN_05 との関係 (Relation to PLAN_05)

- PLAN_05 フェーズ 3b（SDK アダプタからの判断ロジックの切り出し）とは独立に進められる．3b で切り出したロジックは層①で，SDK の呼び出しそのものは本計画の実機テストで確認する，という分担になる
- PLAN_05 フェーズ 4（実機録画データによる回帰）の撮影は，本計画の実機テストの仕組みを使って自動化できる可能性がある（録画フレームの保存を検査モードに加える）

## リスクと対策 (Risks)

| リスク | 対策 |
| --- | --- |
| 撮影の本番中に Claude が機材を操作してしまう | 実行はユーザーの合図があったときだけ．検証プロファイルで明記し，合図がなければ検証不能として報告する |
| 検査でカメラの設定が変わったまま残る | デバイス状態ファイルは上書きしない．変えた露光・ゲインは終了時に元に戻す |
| 光源のポートが開いたまま残る | 検査の最後（失敗時も）にポートを閉じる．光源は点いたままでよい（アプリ終了時と同じ仕様） |
| 実機の個体差・照明で判定が不安定になる | しきい値は実測から余裕を持って決める．輝度は絶対値ではなく変化の向きで判定する |
| SDK がハングして検査が終わらない | 検査全体と各項目にタイムアウトを付け，ランナー側でもプロセスを打ち切る |
