diff --git a/CLAUDE.md b/CLAUDE.md
index 7918e2d..01fa40a 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -4,7 +4,7 @@
## 開発進捗
-最新: テストプロジェクト(TIASshot.Tests, NUnit/x64)を立ち上げ,ピュアロジック層(ColorCorrector/Config/IoUtil)に初期テストを追加
+最新: ゲイン補正式(GetRatio)を純粋メソッド GainCalc に抽出してテスト対象化(挙動不変・実機で校正収束確認済み)
※ 本欄は**最新ステップ 1 行のみ上書き更新**.詳細な進捗履歴(動機・設計判断・失敗パターン)は [docs/PROGRESS.md](docs/PROGRESS.md) に追記する.運用ルールは GUIDE_05「進捗記録の運用ルール(CLAUDE.md / PROGRESS.md)」を参照.
## 必須ルール(コード実装時)
diff --git a/TIASshot.Tests/GainCalcTests.cs b/TIASshot.Tests/GainCalcTests.cs
new file mode 100644
index 0000000..121ecaf
--- /dev/null
+++ b/TIASshot.Tests/GainCalcTests.cs
@@ -0,0 +1,147 @@
+using NUnit.Framework;
+
+namespace TIASshot.Tests {
+ ///
+ /// GainCalc.CalcRatio のテスト
+ ///
+ [TestFixture]
+ public class GainCalcTests {
+
+ // ---------------------------------------------------------------
+ // 通常ケース
+ // ---------------------------------------------------------------
+
+ ///
+ /// CalcRatio_通常ケースupdateRate0_5_正しい比率を返す:
+ /// value=100, target=200, updateRate=0.5 → (200/100 - 1) * 0.5 + 1 = 1.5
+ ///
+ [Test]
+ public void CalcRatio_通常ケースupdateRate0_5_正しい比率を返す() {
+ float result = GainCalc.CalcRatio(100.0f, 200.0f, 0.5f);
+ Assert.AreEqual(1.5f, result, 1e-6f);
+ }
+
+ // ---------------------------------------------------------------
+ // updateRate が変わると比率が変わること
+ // ---------------------------------------------------------------
+
+ ///
+ /// CalcRatio_updateRate1_0_係数1倍の比率を返す:
+ /// value=100, target=200, updateRate=1.0 → (200/100 - 1) * 1.0 + 1 = 2.0
+ ///
+ [Test]
+ public void CalcRatio_updateRate1_0_係数1倍の比率を返す() {
+ float result = GainCalc.CalcRatio(100.0f, 200.0f, 1.0f);
+ Assert.AreEqual(2.0f, result, 1e-6f);
+ }
+
+ ///
+ /// CalcRatio_updateRate0_1_updateRateが小さいほど変化量が小さい:
+ /// value=100, target=200, updateRate=0.1 → (200/100 - 1) * 0.1 + 1 = 1.1
+ ///
+ [Test]
+ public void CalcRatio_updateRate0_1_updateRateが小さいほど変化量が小さい() {
+ float result = GainCalc.CalcRatio(100.0f, 200.0f, 0.1f);
+ Assert.AreEqual(1.1f, result, 1e-6f);
+ }
+
+ // ---------------------------------------------------------------
+ // value == target のとき比率は 1.0
+ // ---------------------------------------------------------------
+
+ ///
+ /// CalcRatio_valueがtargetと等しい_updateRateによらず比率は1:
+ /// value == target のとき ratio = target/value = 1、(1 - 1) * anything + 1 = 1
+ ///
+ [Test]
+ public void CalcRatio_valueがtargetと等しい_updateRate0_5_比率は1() {
+ float result = GainCalc.CalcRatio(150.0f, 150.0f, 0.5f);
+ Assert.AreEqual(1.0f, result, 1e-6f);
+ }
+
+ ///
+ /// CalcRatio_valueがtargetと等しい_updateRateを変えても結果が同じ:
+ /// value == target のとき updateRate=0.5 と updateRate=1.0 の結果が一致する
+ /// (updateRate に依存しないことを比較で検証する)
+ ///
+ [Test]
+ public void CalcRatio_valueがtargetと等しい_updateRateを変えても結果が同じ() {
+ float result05 = GainCalc.CalcRatio(150.0f, 150.0f, 0.5f);
+ float result10 = GainCalc.CalcRatio(150.0f, 150.0f, 1.0f);
+ Assert.AreEqual(result05, result10, 1e-6f, "value==target のとき updateRate によらず結果が同じ");
+ }
+
+ // ---------------------------------------------------------------
+ // value == 255.0f の特別扱い
+ // ---------------------------------------------------------------
+
+ ///
+ /// CalcRatio_value255_updateRate0_5_係数は常に1_0になる:
+ /// value=255 のとき updateRate を無視して係数 1.0 を使う
+ /// → (200/255 - 1) * 1.0 + 1 = 200/255
+ ///
+ [Test]
+ public void CalcRatio_value255_updateRate0_5_係数は常に1_0になる() {
+ float expected = 200.0f / 255.0f;
+ float result = GainCalc.CalcRatio(255.0f, 200.0f, 0.5f);
+ Assert.AreEqual(expected, result, 1e-6f);
+ }
+
+ ///
+ /// CalcRatio_value255_updateRateを変えても結果が同じ:
+ /// value=255 では updateRate=0.5 と updateRate=1.0 の結果が一致する
+ ///
+ [Test]
+ public void CalcRatio_value255_updateRateを変えても結果が同じ() {
+ float result05 = GainCalc.CalcRatio(255.0f, 200.0f, 0.5f);
+ float result10 = GainCalc.CalcRatio(255.0f, 200.0f, 1.0f);
+ Assert.AreEqual(result05, result10, 1e-6f, "value==255 のとき updateRate によらず結果が同じ");
+ }
+
+ // ---------------------------------------------------------------
+ // ゲイン減少方向(target < value)
+ // ---------------------------------------------------------------
+
+ ///
+ /// CalcRatio_targetがvalueより小さい_比率が1未満になる:
+ /// value=200, target=100, updateRate=0.5 → (100/200 - 1) * 0.5 + 1 = 0.75
+ /// ゲインが目標より高すぎる場合に比率が 1.0 未満(下降補正)になることを確認する
+ ///
+ [Test]
+ public void CalcRatio_targetがvalueより小さい_updateRate0_5_比率が1未満になる() {
+ float result = GainCalc.CalcRatio(200.0f, 100.0f, 0.5f);
+ Assert.AreEqual(0.75f, result, 1e-6f);
+ }
+
+ // ---------------------------------------------------------------
+ // updateRate=0 のとき比率は常に 1.0
+ // ---------------------------------------------------------------
+
+ ///
+ /// CalcRatio_updateRate0_比率は常に1:
+ /// value != target でも updateRate=0 なら (ratio-1)*0+1 = 1.0
+ /// 更新が停止する境界値として確認する
+ ///
+ [Test]
+ public void CalcRatio_updateRate0_比率は常に1() {
+ float result = GainCalc.CalcRatio(100.0f, 200.0f, 0.0f);
+ Assert.AreEqual(1.0f, result, 1e-6f);
+ }
+
+ // ---------------------------------------------------------------
+ // ゼロ除算の挙動(現状記録)
+ // ---------------------------------------------------------------
+
+ ///
+ /// CalcRatio_value0_戻り値はInfinityまたはNaN_例外は出ない:
+ /// float では 0 除算は例外にならず Infinity / NaN になる(現状挙動の固定)
+ ///
+ [Test]
+ public void CalcRatio_value0_戻り値はInfinityまたはNaN_例外は出ない() {
+ float result = GainCalc.CalcRatio(0.0f, 200.0f, 0.5f);
+ // float(0 除算) は例外を投げずに Infinity / NaN を返す
+ Assert.IsTrue(float.IsInfinity(result) || float.IsNaN(result),
+ "value=0 のとき戻り値は Infinity または NaN になる(例外は出ない)");
+ }
+ }
+}
diff --git a/TIASshot.Tests/TIASshot.Tests.csproj b/TIASshot.Tests/TIASshot.Tests.csproj
index 3e5f4d6..b2dbf0a 100644
--- a/TIASshot.Tests/TIASshot.Tests.csproj
+++ b/TIASshot.Tests/TIASshot.Tests.csproj
@@ -54,6 +54,7 @@
+
diff --git a/TIASshot/Cameras/CameraBase.cs b/TIASshot/Cameras/CameraBase.cs
index db839c2..cf2b263 100644
--- a/TIASshot/Cameras/CameraBase.cs
+++ b/TIASshot/Cameras/CameraBase.cs
@@ -297,12 +297,11 @@
///
/// ゲイン値の更新比率計算
///
- ///
- ///
+ /// 現在の計測値
+ /// 目標値
+ /// 更新比率
protected float GetRatio(float value, float target) {
- float ratio = target / value;
- ratio = (ratio - 1.0f) * (value == 255.0f ? 1.0f : UpdateRate) + 1.0f;
- return ratio;
+ return GainCalc.CalcRatio(value, target, UpdateRate);
}
///
diff --git a/TIASshot/Cameras/GainCalc.cs b/TIASshot/Cameras/GainCalc.cs
new file mode 100644
index 0000000..2695fd5
--- /dev/null
+++ b/TIASshot/Cameras/GainCalc.cs
@@ -0,0 +1,21 @@
+namespace TIASshot {
+ ///
+ /// ゲイン更新比率の計算ロジック(ピュアロジック層).
+ /// ハードウェアに依存しない純粋な計算のみを担う.
+ ///
+ internal static class GainCalc {
+ ///
+ /// ゲイン値の更新比率を計算する.
+ /// value が 255.0f のときは updateRate によらず係数を 1.0 として比率を求める.
+ ///
+ /// 現在の計測値
+ /// 目標値
+ /// 更新レート(0 より大きい値を想定)
+ /// 更新比率
+ internal static float CalcRatio(float value, float target, float updateRate) {
+ float ratio = target / value;
+ ratio = (ratio - 1.0f) * (value == 255.0f ? 1.0f : updateRate) + 1.0f;
+ return ratio;
+ }
+ }
+}
diff --git a/TIASshot/TIASshot.csproj b/TIASshot/TIASshot.csproj
index 17d62b6..eb9c3d4 100644
--- a/TIASshot/TIASshot.csproj
+++ b/TIASshot/TIASshot.csproj
@@ -119,6 +119,7 @@
+
diff --git "a/docs/02_ENV/ENV_02_\343\203\207\343\202\243\343\203\254\343\202\257\343\203\210\343\203\252\346\247\213\346\210\220.md" "b/docs/02_ENV/ENV_02_\343\203\207\343\202\243\343\203\254\343\202\257\343\203\210\343\203\252\346\247\213\346\210\220.md"
index c0c3f8e..c85f6bc 100644
--- "a/docs/02_ENV/ENV_02_\343\203\207\343\202\243\343\203\254\343\202\257\343\203\210\343\203\252\346\247\213\346\210\220.md"
+++ "b/docs/02_ENV/ENV_02_\343\203\207\343\202\243\343\203\254\343\202\257\343\203\210\343\203\252\346\247\213\346\210\220.md"
@@ -25,7 +25,8 @@
├── Cameras/ ← カメラ制御
│ ├── CameraBase.cs ← カメラ制御の基底クラス(共通処理・撮影/校正の骨格)
│ ├── IScam.cs ← ImagingSource カメラの実装
-│ └── Lucam.cs ← Lumenera カメラの実装
+│ ├── Lucam.cs ← Lumenera カメラの実装
+│ └── GainCalc.cs ← ゲイン更新比率の計算(純粋ロジック)
├── ChartDetection/ ← チャート検出
│ └── ChartDetector.cs ← ArUco マーカー検出・マスク生成
├── ColorCorrection/ ← 色補正
diff --git "a/docs/05_TECH/TECH_01_\343\202\257\343\203\251\343\202\271\350\250\255\350\250\210.md" "b/docs/05_TECH/TECH_01_\343\202\257\343\203\251\343\202\271\350\250\255\350\250\210.md"
index d8c6ad6..d3998c6 100644
--- "a/docs/05_TECH/TECH_01_\343\202\257\343\203\251\343\202\271\350\250\255\350\250\210.md"
+++ "b/docs/05_TECH/TECH_01_\343\202\257\343\203\251\343\202\271\350\250\255\350\250\210.md"
@@ -60,6 +60,12 @@
- ファイル I/O(CSV 読み書き・保存フォルダ生成・撮影情報出力)とサウンド再生.状態を持たない.
- `CameraBase` から委譲される.
+**GainCalc(静的クラス)**
+
+- ファイル: `Cameras/GainCalc.cs`
+- ゲイン更新比率の計算(ホワイトバランス自動調整).ハードウェアに依存しない純粋計算.
+- `CameraBase.GetRatio` から委譲される(テスト可能なピュアロジック層).
+
## 継承関係 (Inheritance)
```text
@@ -90,7 +96,7 @@
- `RunShotLoop(numImages, interval)`: 撮影ループの共通骨格(テンプレートメソッド)
- `SaveImages(img, idx)`: 撮影画像の保存(RGB・sRGB 変換画像.色変換は `ColorCorrector` に委譲)
- `SaveThread(numImages)`: 画像保存スレッド処理
-- `GetRatio(value, target) : float`: ゲイン更新比率の計算
+- `GetRatio(value, target) : float`: ゲイン更新比率の計算(`GainCalc.CalcRatio` に委譲)
### 主なフィールド
diff --git a/docs/PROGRESS.md b/docs/PROGRESS.md
index 48f8b0c..c19f0a6 100644
--- a/docs/PROGRESS.md
+++ b/docs/PROGRESS.md
@@ -42,3 +42,9 @@
- テスト容易化のシーム(挙動不変): 本体に `[InternalsVisibleTo("TIASshot.Tests")]` 追加,`ColorCorrector` の純粋メソッドを private→internal・チャネル配列を受け取る internal コンストラクタ追加,`Config.Load(string path)` オーバーロード追加。
- ハマり所: テストでも OpenCvSharp ネイティブ DLL の **x64 ロード**が必須(`IoUtil` の CSV テストが通る=ネイティブ動作の試金石)。`.gitignore` を汎用 `bin/`・`obj/` に変更し,テストプロジェクトの成果物も除外。
- 後段: `GetRatio`(ゲイン補正式)は `CameraBase`(ハードウェア結合層)にインラインのため未テスト。抽出すればテスト候補。ENV_02 のメインソース一覧がリファクタ前構成のままドリフトしており,別途修正が必要。
+
+- [x] **docs ドリフト修正(ENV_02・TECH_01)** (2026-06-07): リファクタリング(Phase 1〜5)後も旧構成のまま残っていたドキュメントを実構成に整合。ENV_02 のソース一覧をサブフォルダ構成に、TECH_01 のファイルパス・クラス一覧(`ChartDetector`/`ColorCorrector`/`IoUtil` 追加)・`CameraBase` メンバ(移動した `ConvertImage`/`_convRGB2SRGB` 削除)を修正。
+
+- [x] **ゲイン補正式(GetRatio)の純粋メソッド抽出+テスト** (2026-06-07): `CameraBase` にインラインだったゲイン更新比率の計算式を,ハードウェア非依存の純粋クラス `GainCalc.CalcRatio` に抽出し,`CameraBase.GetRatio` は委譲に変更。`TIASshot.Tests` でテスト対象化(GUIDE_08 の「ピュアロジック層を分離して自動テスト」方針の実践)。
+ - 方針: **挙動不変**。式・演算順序・`float` 比較・ゼロ除算挙動・`UpdateRate` の流れをすべて保持し,派生クラスの呼び出し箇所は無変更。実機で校正(ホワイトバランス自動調整)の収束を確認。
+ - テスト観点: 通常比率・`updateRate` 減衰・下降補正(`target