diff --git a/CLAUDE.md b/CLAUDE.md
index d9f07d7..faf3386 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -4,7 +4,7 @@
## 開発進捗
-最新: CameraBase のリファクタリング完了(責務を Cameras/ChartDetection/ColorCorrection/UI/Utils に分離.Phase 1〜4)
+最新: CameraBase のリファクタリング Phase 5 完了(IScam/Lucam の重複するプレビュー・撮影処理を CameraBase にテンプレートメソッドとして集約)
※ 本欄は**最新ステップ 1 行のみ上書き更新**.詳細な進捗履歴(動機・設計判断・失敗パターン)は [docs/PROGRESS.md](docs/PROGRESS.md) に追記する.運用ルールは GUIDE_05「進捗記録の運用ルール(CLAUDE.md / PROGRESS.md)」を参照.
## 必須ルール(コード実装時)
diff --git a/TIASshot/Cameras/CameraBase.cs b/TIASshot/Cameras/CameraBase.cs
index c543cde..10dfa26 100644
--- a/TIASshot/Cameras/CameraBase.cs
+++ b/TIASshot/Cameras/CameraBase.cs
@@ -22,6 +22,16 @@
public abstract void Disconnect();
protected abstract void Shot(int numImages=1, int interval=0);
+ // テンプレートメソッド用抽象メソッド(プレビュー/校正処理)
+ protected abstract void ApplyWhiteBalance(Scalar whitePatch);
+ protected abstract void LogCalibrationConverged();
+ protected abstract void RecordCalibrationInfo(Mat imgt);
+
+ // テンプレートメソッド用抽象メソッド(撮影処理)
+ protected abstract void BeginShot();
+ protected abstract Mat CaptureFrame();
+ protected abstract void EndShot();
+
// プロパティ
public string DeviceName { get; protected set; } = "Unknown";
public string SerialNumber { get; protected set; }
@@ -147,6 +157,61 @@
}
///
+ /// 校正フレーム処理の共通骨格(テンプレートメソッド)
+ /// imgt が用意された後の校正/検出フローを担う.
+ /// ゲイン反映(ApplyWhiteBalance)・収束時ログ出力(LogCalibrationConverged)・
+ /// 校正情報記録(RecordCalibrationInfo)は派生クラスに委譲する.
+ /// 呼び出し順序は元コードに合わせ「LogCalibrationConverged → CalcTcc → RecordCalibrationInfo」.
+ ///
+ /// 処理対象 Mat(前処理済み)
+ protected void ProcessCalibrationFrame(Mat imgt) {
+ if (_calibrating > 0) {
+ var whitePatch = Cv2.Mean(imgt, _chartMasks[12]);
+ Debug.WriteLine($"White patch R {whitePatch.Val2:.00} G {whitePatch.Val1:.00} B {whitePatch.Val0:.00}");
+ if (_calibrating % Config.GetInt("Calib/UpdateInterval") == 0) {
+ ApplyWhiteBalance(whitePatch);
+ }
+ _calibrating--;
+ if (_calibrating == 0) {
+ LogCalibrationConverged();
+ CalcTcc(imgt);
+ RecordCalibrationInfo(imgt);
+ }
+ }
+
+ if (!_calibrated && _calibrating == 0) {
+ DetectChart(imgt);
+ }
+ }
+
+ ///
+ /// 撮影ループの共通骨格(テンプレートメソッド)
+ /// CSV 書き込み・ループ・タイミング制御を担う.
+ /// フレーム取得(CaptureFrame)は派生クラスに委譲する.
+ /// BeginShot/EndShot は呼び出し元(派生クラスの Shot)で前後に実行すること.
+ ///
+ /// 撮影枚数
+ /// 撮影間隔(ms)
+ protected void RunShotLoop(int numImages, int interval) {
+ var filename = Config.GetString("File/Info");
+ using (var csv = new StreamWriter(Path.Combine(_saveFolder, filename))) {
+ WriteInfo(csv);
+ csv.WriteLine("Shot,Time(ms)");
+
+ var watch = Stopwatch.StartNew();
+ for (var i = 0; i < numImages; i++) {
+ var shotTime = watch.ElapsedMilliseconds;
+ csv.WriteLine($"{i + 1},{shotTime}");
+ _form.ShowMessage($"撮影 {i + 1} / {numImages} 枚目");
+ _shots.Add(CaptureFrame());
+ while (watch.ElapsedMilliseconds < interval * (i + 1) && i < numImages - 1) {
+ Thread.Sleep(1);
+ }
+ }
+ }
+ }
+
+ ///
/// チャートの検出
///
///
diff --git a/TIASshot/Cameras/IScam.cs b/TIASshot/Cameras/IScam.cs
index 08823d7..17ba31a 100644
--- a/TIASshot/Cameras/IScam.cs
+++ b/TIASshot/Cameras/IScam.cs
@@ -24,6 +24,7 @@
VCDRangeProperty _whiteBalanceRed;
FrameQueueSink _queueSink;
FrameSnapSink _snapSink;
+ FrameSnapSink _snapSinkRef;
///
/// IScamコンストラクタ
@@ -105,58 +106,53 @@
using (Mat img = Mat.FromPixelData(frameType.Height, frameType.Width, MatType.CV_8UC3, buffer.GetIntPtr())) {
using (Mat imgt_full = img.T()) {
using (var imgt = new Mat(imgt_full, _roi)) {
- //_bmps[_bmpIndex] = imgt.ToBitmap();
- //using (Bitmap bmp = imgt.ToBitmap()) {
- // _form.ShowImage(bmp);
- //}
_form.ShowImage(imgt.ToBitmap());
-
-
- if (_calibrating > 0) {
- var whitePatch = Cv2.Mean(imgt, _chartMasks[12]);
- Debug.WriteLine($"White patch R {whitePatch.Val2:.00} G {whitePatch.Val1:.00} B {whitePatch.Val0:.00}");
- if (_calibrating % Config.GetInt("Calib/UpdateInterval") == 0) {
- _whiteBalanceBlue.Value = (int)(0.5 + _whiteBalanceBlue.Value * GetRatio((float)whitePatch.Val0, Config.GetFloat("Calib/Reference/B")));
- _whiteBalanceGreen.Value = (int)(0.5 + _whiteBalanceGreen.Value * GetRatio((float)whitePatch.Val1, Config.GetFloat("Calib/Reference/G")));
- _whiteBalanceRed.Value = (int)(0.5 + _whiteBalanceRed.Value * GetRatio((float)whitePatch.Val2, Config.GetFloat("Calib/Reference/R")));
- }
- _calibrating--;
- if (_calibrating == 0) {
- Debug.WriteLine($"White Balance Blue range: {_whiteBalanceBlue.RangeMin} to {_whiteBalanceBlue.RangeMax} value={_whiteBalanceBlue.Value}");
- Debug.WriteLine($"White Balance Green range: {_whiteBalanceGreen.RangeMin} to {_whiteBalanceGreen.RangeMax} value={_whiteBalanceGreen.Value}");
- Debug.WriteLine($"White Balance Red range: {_whiteBalanceRed.RangeMin} to {_whiteBalanceRed.RangeMax} value={_whiteBalanceRed.Value}");
-
- CalcTcc(imgt);
-
- SetInfo("Exposure(ms)", $"{(float)_exposure.Value / 10}");
- SetInfo("Brightness", $"{_brightness.Value}");
- SetInfo("Gamma", $"{_gamma.Value}");
- SetInfo("Gain", $"{_gain.Value}");
- SetInfo("Gain B", $"{_whiteBalanceBlue.Value}");
- SetInfo("Gain G", $"{_whiteBalanceGreen.Value}");
- SetInfo("Gain R", $"{_whiteBalanceRed.Value}");
- SetInfo("画像幅", $"{imgt.Width}");
- SetInfo("画像高さ", $"{imgt.Height}");
- }
- }
-
- if (!_calibrated && _calibrating == 0) {
- DetectChart(imgt);
- }
+ ProcessCalibrationFrame(imgt);
}
}
}
- //_form.ShowImage(_bmps[_bmpIndex]);
- //_bmpIndex = (_bmpIndex + 1) % 2;
- //if (_bmps[_bmpIndex] != null) {
- // _bmps[_bmpIndex].Dispose();
- // _bmps[_bmpIndex] = null;
- //}
return FrameQueuedResult.ReQueue;
}
///
+ /// ゲイン/ホワイトバランスの反映(IScam 固有)
+ ///
+ /// 白パッチの計測値
+ protected override void ApplyWhiteBalance(Scalar whitePatch) {
+ _whiteBalanceBlue.Value = (int)(0.5 + _whiteBalanceBlue.Value * GetRatio((float)whitePatch.Val0, Config.GetFloat("Calib/Reference/B")));
+ _whiteBalanceGreen.Value = (int)(0.5 + _whiteBalanceGreen.Value * GetRatio((float)whitePatch.Val1, Config.GetFloat("Calib/Reference/G")));
+ _whiteBalanceRed.Value = (int)(0.5 + _whiteBalanceRed.Value * GetRatio((float)whitePatch.Val2, Config.GetFloat("Calib/Reference/R")));
+ }
+
+ ///
+ /// 校正収束時のホワイトバランス値ログ出力(IScam 固有)
+ /// CalcTcc の前に呼ばれ,元コードの Debug.WriteLine 順序を再現する.
+ ///
+ protected override void LogCalibrationConverged() {
+ Debug.WriteLine($"White Balance Blue range: {_whiteBalanceBlue.RangeMin} to {_whiteBalanceBlue.RangeMax} value={_whiteBalanceBlue.Value}");
+ Debug.WriteLine($"White Balance Green range: {_whiteBalanceGreen.RangeMin} to {_whiteBalanceGreen.RangeMax} value={_whiteBalanceGreen.Value}");
+ Debug.WriteLine($"White Balance Red range: {_whiteBalanceRed.RangeMin} to {_whiteBalanceRed.RangeMax} value={_whiteBalanceRed.Value}");
+ }
+
+ ///
+ /// 校正完了時のカメラ情報記録(IScam 固有)
+ /// CalcTcc の後に呼ばれ,SetInfo で各値を保存する.
+ ///
+ /// 処理対象 Mat
+ protected override void RecordCalibrationInfo(Mat imgt) {
+ SetInfo("Exposure(ms)", $"{(float)_exposure.Value / 10}");
+ SetInfo("Brightness", $"{_brightness.Value}");
+ SetInfo("Gamma", $"{_gamma.Value}");
+ SetInfo("Gain", $"{_gain.Value}");
+ SetInfo("Gain B", $"{_whiteBalanceBlue.Value}");
+ SetInfo("Gain G", $"{_whiteBalanceGreen.Value}");
+ SetInfo("Gain R", $"{_whiteBalanceRed.Value}");
+ SetInfo("画像幅", $"{imgt.Width}");
+ SetInfo("画像高さ", $"{imgt.Height}");
+ }
+
+ ///
/// 撮影
///
///
@@ -165,40 +161,42 @@
_ic.LiveStop();
_ic.Sink = _snapSink;
_ic.LiveStart();
- var snapSink = _ic.Sink as FrameSnapSink;
+ _snapSinkRef = _ic.Sink as FrameSnapSink;
_shots.Clear();
- snapSink.SnapSingle(TimeSpan.FromSeconds(5)); // 最初のフレームを捨てる
+ _snapSinkRef.SnapSingle(TimeSpan.FromSeconds(5)); // 最初のフレームを捨てる
var thread = new Thread(() => SaveThread(numImages));
thread.Start();
- var filename = Config.GetString("File/Info");
- using (var csv = new StreamWriter(Path.Combine(_saveFolder, filename))) {
- WriteInfo(csv);
- csv.WriteLine("Shot,Time(ms)");
-
- var watch = Stopwatch.StartNew();
- for (int i = 0; i < numImages; i++) {
- var shotTime = watch.ElapsedMilliseconds;
- csv.WriteLine($"{i+1},{shotTime}");
- _form.ShowMessage($"撮影 {i + 1} / {numImages} 枚目");
- var buffer = snapSink.SnapSingle(TimeSpan.FromSeconds(5));
-
- using (Mat img = Mat.FromPixelData(buffer.FrameType.Height, buffer.FrameType.Width, MatType.CV_8UC3, buffer.GetIntPtr())) {
- using (Mat imgt_full = img.T()) {
- _shots.Add(new Mat(imgt_full, _roi));
- }
- }
- while (watch.ElapsedMilliseconds < interval * (i+1) && i < numImages - 1) {
- Thread.Sleep(1);
- }
- }
- }
+ RunShotLoop(numImages, interval);
_ic.LiveStop();
_ic.Sink = _queueSink;
_ic.LiveStart();
}
+
+ ///
+ /// 撮影開始処理(IScam 固有): Shot 内でインライン実行済みのため空実装
+ ///
+ protected override void BeginShot() { }
+
+ ///
+ /// 1 フレーム取得(IScam 固有)
+ ///
+ /// 撮影した Mat(呼び出し元が _shots に追加する)
+ protected override Mat CaptureFrame() {
+ var buffer = _snapSinkRef.SnapSingle(TimeSpan.FromSeconds(5));
+ using (Mat img = Mat.FromPixelData(buffer.FrameType.Height, buffer.FrameType.Width, MatType.CV_8UC3, buffer.GetIntPtr())) {
+ using (Mat imgt_full = img.T()) {
+ return new Mat(imgt_full, _roi);
+ }
+ }
+ }
+
+ ///
+ /// 撮影終了処理(IScam 固有): Shot 内でインライン実行済みのため空実装
+ ///
+ protected override void EndShot() { }
}
}
diff --git a/TIASshot/Cameras/Lucam.cs b/TIASshot/Cameras/Lucam.cs
index 517a409..70ef974 100644
--- a/TIASshot/Cameras/Lucam.cs
+++ b/TIASshot/Cameras/Lucam.cs
@@ -34,6 +34,8 @@
// GUI更新はタイマー使用に変更
IntPtr _hCam = IntPtr.Zero;
+ byte[] _rawImage;
+ byte[] _rgbImage;
PictureBox _picPreview;
dll.LucamSnapshot _snap;
@@ -168,38 +170,7 @@
using (Mat img = Mat.FromPixelData(_snap.Format.Height, _snap.Format.Width, MatType.CV_8UC3, pData)) {
using (Mat imgt = img.T()) {
_bmps[_bmpIndex] = imgt.ToBitmap();
-
- if (_calibrating > 0) {
- var whitePatch = Cv2.Mean(imgt, _chartMasks[12]);
- Debug.WriteLine($"White patch R {whitePatch.Val2:.00} G {whitePatch.Val1:.00} B {whitePatch.Val0:.00}");
- if (_calibrating % Config.GetInt("Calib/UpdateInterval") == 0) {
- _snap.GainBlue *= GetRatio((float)whitePatch.Val0, Config.GetFloat("Calib/Reference/B"));
- _snap.GainGrn1 *= GetRatio((float)whitePatch.Val1, Config.GetFloat("Calib/Reference/G"));
- _snap.GainGrn2 = _snap.GainGrn1;
- _snap.GainRed *= GetRatio((float)whitePatch.Val2, Config.GetFloat("Calib/Reference/R"));
- SetCameraParam();
- }
- _calibrating--;
- if (_calibrating == 0) {
- Debug.WriteLine($"Gain Blue : {_snap.GainBlue}");
- Debug.WriteLine($"Gain Green: {_snap.GainGrn1}");
- Debug.WriteLine($"Gain Red : {_snap.GainRed}");
-
- CalcTcc(imgt);
-
- SetInfo("Exposure(ms)", $"{_snap.Exposure}");
- SetInfo("Gain", $"{_snap.Gain}");
- SetInfo("Gain B", $"{_snap.GainBlue}");
- SetInfo("Gain G", $"{_snap.GainGrn1}");
- SetInfo("Gain R", $"{_snap.GainRed}");
- SetInfo("画像幅", $"{imgt.Width}");
- SetInfo("画像高さ", $"{imgt.Height}");
- }
- }
-
- if (!_calibrated && _calibrating == 0) {
- DetectChart(imgt);
- }
+ ProcessCalibrationFrame(imgt);
}
}
_form.ShowImage(_bmps[_bmpIndex]);
@@ -208,6 +179,43 @@
}
///
+ /// ゲイン/ホワイトバランスの反映(Lucam 固有)
+ ///
+ /// 白パッチの計測値
+ protected override void ApplyWhiteBalance(Scalar whitePatch) {
+ _snap.GainBlue *= GetRatio((float)whitePatch.Val0, Config.GetFloat("Calib/Reference/B"));
+ _snap.GainGrn1 *= GetRatio((float)whitePatch.Val1, Config.GetFloat("Calib/Reference/G"));
+ _snap.GainGrn2 = _snap.GainGrn1;
+ _snap.GainRed *= GetRatio((float)whitePatch.Val2, Config.GetFloat("Calib/Reference/R"));
+ SetCameraParam();
+ }
+
+ ///
+ /// 校正収束時のゲイン値ログ出力(Lucam 固有)
+ /// CalcTcc の前に呼ばれ,元コードの Debug.WriteLine 順序を再現する.
+ ///
+ protected override void LogCalibrationConverged() {
+ Debug.WriteLine($"Gain Blue : {_snap.GainBlue}");
+ Debug.WriteLine($"Gain Green: {_snap.GainGrn1}");
+ Debug.WriteLine($"Gain Red : {_snap.GainRed}");
+ }
+
+ ///
+ /// 校正完了時のカメラ情報記録(Lucam 固有)
+ /// CalcTcc の後に呼ばれ,SetInfo で各値を保存する.
+ ///
+ /// 処理対象 Mat
+ protected override void RecordCalibrationInfo(Mat imgt) {
+ SetInfo("Exposure(ms)", $"{_snap.Exposure}");
+ SetInfo("Gain", $"{_snap.Gain}");
+ SetInfo("Gain B", $"{_snap.GainBlue}");
+ SetInfo("Gain G", $"{_snap.GainGrn1}");
+ SetInfo("Gain R", $"{_snap.GainRed}");
+ SetInfo("画像幅", $"{imgt.Width}");
+ SetInfo("画像高さ", $"{imgt.Height}");
+ }
+
+ ///
/// 撮影
///
///
@@ -219,38 +227,42 @@
var thread = new Thread(() => SaveThread(numImages));
thread.Start();
+ BeginShot();
+ RunShotLoop(numImages, interval);
+ EndShot();
+ }
+
+ ///
+ /// 撮影開始処理(Lucam 固有)
+ ///
+ protected override void BeginShot() {
dll.LucamEnableFastFrames(_hCam, ref _snap);
var imageSize = _snap.Format.Width * _snap.Format.Height;
- var rawImage = new byte[imageSize];
- var rgbImage = new byte[imageSize * 3];
+ _rawImage = new byte[imageSize];
+ _rgbImage = new byte[imageSize * 3];
+ }
- var filename = Config.GetString("File/Info");
- using (var csv = new StreamWriter(Path.Combine(_saveFolder, filename))) {
- WriteInfo(csv);
- csv.WriteLine("Shot,Time(ms)");
+ ///
+ /// 1 フレーム取得(Lucam 固有)
+ ///
+ /// 撮影した Mat(呼び出し元が _shots に追加する)
+ protected override Mat CaptureFrame() {
+ var ret = dll.LucamTakeFastFrame(_hCam, _rawImage);
+ //Debug.WriteLine(ret);
- var watch = Stopwatch.StartNew();
- for (var i = 0; i < numImages; i++) {
- var shotTime = watch.ElapsedMilliseconds;
- csv.WriteLine($"{i + 1},{shotTime}");
- _form.ShowMessage($"撮影 {i + 1} / {numImages} 枚目");
- var ret = dll.LucamTakeFastFrame(_hCam, rawImage);
- //Debug.WriteLine(ret);
-
- dll.LucamConvertFrameToRgb24(_hCam, rgbImage, rawImage,
- _snap.Format.Width, _snap.Format.Height, dll.LucamPixelFormat.PF_8, ref _convert);
- using (Mat img = Mat.FromPixelData(_snap.Format.Height, _snap.Format.Width, MatType.CV_8UC3, rgbImage)) {
- _shots.Add(img.T());
- }
- while (watch.ElapsedMilliseconds < interval * (i + 1) && i < numImages - 1) {
- Thread.Sleep(1);
- }
- }
+ dll.LucamConvertFrameToRgb24(_hCam, _rgbImage, _rawImage,
+ _snap.Format.Width, _snap.Format.Height, dll.LucamPixelFormat.PF_8, ref _convert);
+ using (Mat img = Mat.FromPixelData(_snap.Format.Height, _snap.Format.Width, MatType.CV_8UC3, _rgbImage)) {
+ return img.T();
}
+ }
- rgbImage = null;
- rawImage = null;
-
+ ///
+ /// 撮影終了処理(Lucam 固有)
+ ///
+ protected override void EndShot() {
+ _rgbImage = null;
+ _rawImage = null;
dll.LucamDisableFastFrames(_hCam);
}
diff --git "a/docs/03_PLAN/PLAN_01_\343\203\252\343\203\225\343\202\241\343\202\257\343\202\277\343\203\252\343\203\263\343\202\260\350\250\210\347\224\273.md" "b/docs/03_PLAN/PLAN_01_\343\203\252\343\203\225\343\202\241\343\202\257\343\202\277\343\203\252\343\203\263\343\202\260\350\250\210\347\224\273.md"
index 4a4f22c..205f661 100644
--- "a/docs/03_PLAN/PLAN_01_\343\203\252\343\203\225\343\202\241\343\202\257\343\202\277\343\203\252\343\203\263\343\202\260\350\250\210\347\224\273.md"
+++ "b/docs/03_PLAN/PLAN_01_\343\203\252\343\203\225\343\202\241\343\202\257\343\202\277\343\203\252\343\203\263\343\202\260\350\250\210\347\224\273.md"
@@ -97,6 +97,25 @@
- 撮影からファイル保存までの一連の動作を手動で確認する.
- 保存された CSV・画像の内容がリファクタリング前と一致することを確認する.
+### フェーズ5: 派生カメラクラスの重複処理の集約(当初案に追加)
+
+**動機**
+
+- フェーズ1〜4 完了後も,`IScam` と `Lucam` にプレビュー/校正フロー(白パッチ計測・ゲイン更新・`CalcTcc`・チャート検出)と撮影ループ(CSV 書き込み・タイミング制御)のほぼ同一コードが重複して残っていた.
+
+**作業内容**
+
+- 重複していた共通骨格を `CameraBase` にテンプレートメソッドとして集約する.
+ - プレビュー/校正フロー → `CameraBase` の共通メソッド+カメラ固有部を抽象メソッド化
+ - 撮影ループ → `CameraBase` の共通メソッド+撮影前後処理・フレーム取得を抽象メソッド化
+- カメラ固有部(ゲイン/ホワイトバランス反映,撮影情報の記録,フレーム取得,撮影前後処理)は抽象メソッドで派生クラスに残す.
+- ※ `IScam` の撮影前後処理(Sink 切替・最初のフレーム捨て)は `Lucam` と実行順序が異なるため,共通骨格に取り込まず派生クラス側に残した(順序維持を優先).
+
+**テスト**
+
+- ビルドが 0 エラー 0 警告で成功することを確認する.
+- 校正・1 枚撮影・連続撮影の一連の動作を実機で確認する(ハードウェア依存パスのため自動テスト不可).
+
## 共通ルール (Common Rules)
- 各フェーズは独立したブランチではなく,`refactor/code-cleanup` ブランチ上で順に進める.
diff --git a/docs/PROGRESS.md b/docs/PROGRESS.md
index 095f64a..21ef67b 100644
--- a/docs/PROGRESS.md
+++ b/docs/PROGRESS.md
@@ -17,3 +17,8 @@
- 方針: **挙動は一切変えない**(PLAN_01)。各フェーズで計算/検出/I-O ロジックが元コードと文字単位で一致することを確認し、ビルドは全フェーズ 0 エラー 0 警告。
- 設計判断(PLAN_01 当初案からの変更): 派生クラス(IScam/Lucam)と共有する状態に結合するメンバーは分離先に移さず `CameraBase` に残した。具体的には `UpdateRate`/`GetRatio`(ゲイン補正)、`SetSaveFolder`/`WriteInfo`/`SetInfo`(`_saveFolder`/`_ShotInfo` 経由)。後者 2 つは純粋ロジックのみ `IoUtil` へ移し、`CameraBase` 側は薄いラッパーとした。
- 回帰検証: リファクタ前に実機で基準データ(`_基準データ_refactor前/`、特に `TCC_RGB.csv`)を取得して保管。実機の再校正は物理入力が変わり数値が一致しないため、回帰は「同一入力での計算一致」+「コードの文字単位一致」で担保した。
+
+- [x] **CameraBase リファクタリング Phase 5(IScam/Lucam の重複処理を集約)** (2026-06-07): Phase 1〜4 後も派生クラスに残っていたプレビュー/校正フローと撮影ループの重複を、`CameraBase` のテンプレートメソッドに集約。カメラ固有のゲイン反映・フレーム取得・撮影前後処理のみ抽象メソッドで派生クラスに残した。
+ - 方針: **挙動は一切変えない**。自動テスト不可(ハードウェア/ネイティブ/WinForms 密結合)のため、ビルド 0/0 + diff の処理順序目視一致 + 実機撮影確認で担保。
+ - 設計判断: `IScam` の撮影前後処理(Sink 切替・最初のフレーム捨て)は `Lucam` と実行順序が異なるため共通骨格に取り込まず派生側に残した(順序維持を優先)。
+ - 失敗パターン: 校正収束時のデバッグログを撮影情報記録とまとめて `CalcTcc` の後に移したことで元の「ログ→CalcTcc→記録」順がずれた → テストで検出し、`CalcTcc` 前に呼ぶ専用フックへ分離して厳密な順序一致を回復。