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` | 別系統(現状潜在・クラッシュとは無関係) |