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/03_PLAN/PLAN_02_\343\203\220\343\202\260\344\277\256\346\255\243\350\250\210\347\224\273.md" "b/docs/03_PLAN/PLAN_02_\343\203\220\343\202\260\344\277\256\346\255\243\350\250\210\347\224\273.md" index 2d3d5fb..1b74f0e 100644 --- "a/docs/03_PLAN/PLAN_02_\343\203\220\343\202\260\344\277\256\346\255\243\350\250\210\347\224\273.md" +++ "b/docs/03_PLAN/PLAN_02_\343\203\220\343\202\260\344\277\256\346\255\243\350\250\210\347\224\273.md" @@ -2,10 +2,11 @@ ## 前提 (Prerequisites) -- `PLAN_01_リファクタリング計画.md` の全フェーズが完了し,動作がリファクタリング前と同一であることを確認してから着手する. +- `PLAN_01_リファクタリング計画.md` の全フェーズ(Phase 1〜5)が完了し,動作がリファクタリング前と同一であることを確認してから着手する. - 修正は1件ずつ独立して行い,都度テストを実施する. - コミットメッセージは `[fix]` タグを使用する. - 例: `[fix] _shots へのスレッドセーフなアクセスに修正` +- 本計画はリファクタリング(PLAN_01)前に作成されたため,各原因の**該当箇所はリファクタ後の現構造に更新済み**.撮影制御は `Cameras/CameraBase.cs`(共通骨格 `RunShotLoop`・`SaveThread` 等)と `Cameras/Lucam.cs`・`Cameras/IScam.cs`(派生クラスの `Shot`)に,色変換は `ColorCorrection/ColorCorrector.cs` に分離されている. ## 症状 (Symptom) @@ -18,20 +19,27 @@ **原因** -`_shots`(`List`)は Shot スレッドと SaveThread から同時にアクセスされる. +`_shots`(`CameraBase` の `List` フィールド)は撮影スレッドと保存スレッド(`SaveThread`)から同時にアクセスされる. `List` はスレッドセーフではないため,以下の2点で競合が発生する. - リサイズ競合 - `List` の内部配列は容量が足りなくなると自動で拡張される(デフォルト容量 4 → 5枚目の Add 時に 8 に拡張). - - 拡張中に SaveThread が `_shots[saveCount]` を読むとメモリ破壊が起きる. + - 拡張中に `SaveThread` が `_shots[saveCount]` を読むとメモリ破壊が起きる. - 「4枚以上でたまに落ちる」という症状に一致する. - TOCTOU(確認と使用の間の競合) - - SaveThread の `_shots.Count` チェックと `_shots[saveCount]` アクセスの間に Shot スレッドがリサイズを起こすと `IndexOutOfRangeException` が発生する. + - `SaveThread` の `_shots.Count` チェックと `_shots[saveCount]` アクセスの間に撮影スレッドがリサイズを起こすと `IndexOutOfRangeException` が発生する. + +**該当箇所(リファクタ後)** + +- 書き込み: `Cameras/CameraBase.cs` の `RunShotLoop`(`_shots.Add(CaptureFrame())`),および各派生クラスの `Shot`(`Cameras/Lucam.cs`・`Cameras/IScam.cs` の `_shots.Clear()`). +- 読み取り: `Cameras/CameraBase.cs` の `SaveThread`(`_shots.Count` / `_shots[saveCount]`). +- スレッド起動: 各派生クラスの `Shot` 内 `new Thread(() => SaveThread(numImages))`. +- ※ `SaveThread` はインデックス(`_shots[saveCount]`)でアクセスするため,置き換え先はインデックス参照可能な構造(ロック付き `List` 等)が前提.`ConcurrentQueue` を使う場合は保存側を Dequeue ベースに作り替える必要がある点に注意. **修正方針** -- `_shots` を `List` から `System.Collections.Concurrent.ConcurrentQueue` またはロック付きアクセスに変更する. -- SaveThread の待機ロジックをスレッドセーフな方法(`SemaphoreSlim` 等)に変更する. +- `_shots` へのアクセスをスレッドセーフ化する(ロック付きアクセス,または保存側を `ConcurrentQueue` + Dequeue に作り替える). +- `SaveThread` の `Thread.Sleep` ポーリング待機をスレッドセーフな同期(`SemaphoreSlim`・`BlockingCollection` 等)に変更する. **テスト** @@ -41,13 +49,18 @@ **原因** -`ConvertImage` 内で生成される中間 Mat(`extended`,`converted`,`convertedImage`)が Dispose されないまま残る. +`ColorCorrector.ConvertImage(Mat src, Mat conv)` 内で生成される中間 Mat(`flatten`,`extended`,`converted`,`convertedImage`)が Dispose されないまま残る. 1回の呼び出しで数百 MB〜1 GB 超が蓄積し,画像枚数×チャンネル数分だけ積み重なる. (例: IScam 1840×2440,17次元,4枚連続撮影 → 数十 GB 規模) +※ 返り値 `convImg8` は呼び出し側(`CameraBase.SaveImages`)で `using` により Dispose されているが,`ConvertImage` 内部の中間 Mat は解放されていない. + +**該当箇所(リファクタ後)** + +- `ColorCorrection/ColorCorrector.cs` の `ConvertImage(Mat src, Mat conv)`(private オーバーロード). **修正方針** -- `extended`,`converted`,`convertedImage` を `using` ブロックで管理して確実に Dispose する. +- `flatten`,`extended`,`converted`,`convertedImage` を `using` ブロックで管理して確実に Dispose する. **テスト** @@ -57,13 +70,18 @@ **原因** -`ExtendMat` の `Parallel.For` 内で OOM 等の例外が発生した場合,`AggregateException` としてラップされて SaveThread に伝播する. -SaveThread で捕捉されなければアプリがクラッシュする. +`ColorCorrector.ExtendMat` の `Parallel.For` 内で OOM 等の例外が発生した場合,`AggregateException` としてラップされて `SaveThread` に伝播する. +`SaveThread` で捕捉されなければアプリがクラッシュする. ※ 優先度2の修正でメモリ蓄積が解消されれば,発生頻度は大幅に下がる. +**該当箇所(リファクタ後)** + +- 例外発生源: `ColorCorrection/ColorCorrector.cs` の `ExtendMat`(`Parallel.For`). +- 捕捉すべき箇所: `Cameras/CameraBase.cs` の `SaveThread`(`SaveImages` → `ColorCorrector.ConvertImage` → `ExtendMat` の呼び出し経路). + **修正方針** -- SaveThread に例外ハンドラを追加し,クラッシュではなくエラーメッセージを表示して処理を中断するようにする. +- `SaveThread` に例外ハンドラを追加し,クラッシュではなくエラーメッセージを表示して処理を中断するようにする. - `ExtendMat` の `Parallel.For` に try-catch を追加する. **テスト** @@ -74,10 +92,15 @@ **原因** -`ConvertImage` 内の `src.ConvertTo(src, CV_64FC3)` が `_shots[i]` 自体を CV_8UC3 から CV_64FC3 に書き換える副作用がある. +`ColorCorrector.ConvertImage(Mat src, Mat conv)` 内の `src.ConvertTo(src, MatType.CV_64FC3)` が,引数で渡された `_shots[i]` 自体を CV_8UC3 から CV_64FC3 に書き換える副作用がある(`src` は `CameraBase.SaveImages` から `_shots[saveCount]` がそのまま渡される). 特に IScam の `new Mat(imgt_full, _roi)`(サブマトリクス)に対して in-place の型変換を行うと不正な動作を引き起こす可能性がある. また複数チャンネルを処理する際,2回目以降は変換済みのデータに対して処理をかけることになり,変換結果が誤る. +**該当箇所(リファクタ後)** + +- `ColorCorrection/ColorCorrector.cs` の `ConvertImage(Mat src, Mat conv)` 冒頭の `src.ConvertTo(src, MatType.CV_64FC3)`. +- 呼び出し元: `Cameras/CameraBase.cs` の `SaveImages`(`_colorCorrector.ConvertImage(img, channel)`,`img` = `_shots[saveCount]`). + **修正方針** - `ConvertImage` の冒頭で `src` をコピーし,コピー側を変換するようにする(`src` を直接書き換えない). @@ -91,25 +114,52 @@ **原因 A: `_shots.Clear()` で Mat が解放されない** - 撮影ごとに `_shots.Clear()` が呼ばれるが,格納された Mat は Dispose されない. +- 該当箇所: 各派生クラスの `Shot`(`Cameras/Lucam.cs`・`Cameras/IScam.cs`)の `_shots.Clear()`. **原因 B: `_chartMasks.Clear()` で Mat が解放されない** - チャート再検出時に `_chartMasks.Clear()` が呼ばれるが,同様に Dispose されない. +- 該当箇所: `Cameras/CameraBase.cs` の `DetectChart` の `_chartMasks.Clear()`. **修正方針** -- Clear 前に各 Mat を Dispose するヘルパーを追加する. +- Clear 前に各 Mat を Dispose するヘルパーを追加する(`CameraBase` 側に共通ヘルパーを置くのが妥当). **テスト** - タスクマネージャでメモリ使用量を監視し,撮影・キャリブを繰り返してもメモリが増加し続けないことを確認する. +### 優先度6: `Form1` の `InvokeRequired` 分岐で `Invoke` の戻り値を返していない(潜在バグ) + +※ 上記の連続撮影クラッシュ(優先度1〜5)とは**独立した別系統のバグ**.現状はワーカースレッドから呼ばれていないため顕在化していないが,将来撮影呼び出しをスレッド化した際にデータ名取得失敗等の不具合になりうる. + +**原因** + +`Form1` の `GetDataName` / `GetNumMultiShots` / `GetMultiShotsInterval` は,`InvokeRequired` が true のとき `Invoke((MethodInvoker)delegate { GetDataName(); })` のように**戻り値を捨てている**.`MethodInvoker` は `void` デリゲートのため `Invoke` の戻り値は常に `null`/既定値となり,UI スレッド外から呼ぶと正しい値(テキストボックスの内容)が返らない. + +- 現状: これらは `CameraBase.ShotOne` / `ShotMulti` から呼ばれ,その起点はボタンクリックハンドラ(UI スレッド)のため `InvokeRequired` が false 側を通り顕在化していない. +- リスク: 撮影処理をワーカースレッド化(連続撮影のマルチスレッド化)すると,このパスを通って `GetDataName` が `null` を返し,保存フォルダ名や撮影枚数が不正になる. + +**該当箇所** + +- `UI/Form1.cs` の `GetDataName` / `GetNumMultiShots` / `GetMultiShotsInterval` の `InvokeRequired` 分岐. + +**修正方針** + +- 戻り値を返すデリゲート(`Func` / `Func`)で `Invoke` し,その結果を返すように修正する. + - 例: `return (string)Invoke(new Func(() => txtDataName.Text));` + +**テスト** + +- 各メソッドが UI スレッド・ワーカースレッドのいずれから呼ばれても正しい値を返すことを確認する. + ## 修正順序まとめ (Summary) -| 順位 | 原因 | クラッシュとの関連 | -| --- | --- | --- | -| 1 | `_shots` のスレッドセーフでないアクセス | 直接原因(「たまに落ちる」に一致) | -| 2 | `ConvertImage` 内の Mat 未 Dispose | 直接原因(OOM によるクラッシュ) | -| 3 | `Parallel.For` 内の未捕捉例外 | 間接原因(2の修正で頻度減) | -| 4 | `ConvertImage` の in-place 書き換え | 誤動作(変換結果の誤り) | -| 5 | `_shots` / `_chartMasks` の Mat 未 Dispose | メモリリーク(蓄積) | +| 順位 | 原因 | 該当ファイル | クラッシュとの関連 | +| --- | --- | --- | --- | +| 1 | `_shots` のスレッドセーフでないアクセス | `CameraBase.cs` / 各 `Shot` | 直接原因(「たまに落ちる」に一致) | +| 2 | `ConvertImage` 内の Mat 未 Dispose | `ColorCorrector.cs` | 直接原因(OOM によるクラッシュ) | +| 3 | `Parallel.For` 内の未捕捉例外 | `ColorCorrector.cs` / `CameraBase.cs` | 間接原因(2の修正で頻度減) | +| 4 | `ConvertImage` の in-place 書き換え | `ColorCorrector.cs` | 誤動作(変換結果の誤り) | +| 5 | `_shots` / `_chartMasks` の Mat 未 Dispose | 各 `Shot` / `CameraBase.cs` | メモリリーク(蓄積) | +| 6 | `Form1` の `InvokeRequired` 戻り値欠落 | `Form1.cs` | 別系統(現状潜在・クラッシュとは無関係) | 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` 前に呼ぶ専用フックへ分離して厳密な順序一致を回復。