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` 前に呼ぶ専用フックへ分離して厳密な順序一致を回復。