PLAN_01_リファクタリング計画.md の全フェーズ(Phase 1〜5)が完了し,動作がリファクタリング前と同一であることを確認してから着手する.[fix] タグを使用する.
[fix] _shots へのスレッドセーフなアクセスに修正Cameras/CameraBase.cs(共通骨格 RunShotLoop・SaveThread 等)と Cameras/Lucam.cs・Cameras/IScam.cs(派生クラスの Shot)に,色変換は ColorCorrection/ColorCorrector.cs に分離されている._shots へのスレッドセーフでないアクセス(根本原因)状態: 完了(2026-06-07) —
_shotsをBlockingCollection<Mat>化して生産者-消費者パイプラインに変更(RunShotLoopでAdd/CompleteAdding、SaveThreadでGetConsumingEnumerable).撮影ループはtry/finallyでCompleteAdding()を保証.詳細はdocs/PROGRESS.mdおよびgit logを参照.
原因
_shots(CameraBase の List<Mat> フィールド)は撮影スレッドと保存スレッド(SaveThread)から同時にアクセスされる.
List<T> はスレッドセーフではないため,以下の2点で競合が発生する.
List<T> の内部配列は容量が足りなくなると自動で拡張される(デフォルト容量 4 → 5枚目の Add 時に 8 に拡張).SaveThread が _shots[saveCount] を読むとメモリ破壊が起きる.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<Mat> 等)が前提.ConcurrentQueue を使う場合は保存側を Dequeue ベースに作り替える必要がある点に注意.修正方針
_shots へのアクセスをスレッドセーフ化する(ロック付きアクセス,または保存側を ConcurrentQueue<Mat> + Dequeue に作り替える).SaveThread の Thread.Sleep ポーリング待機をスレッドセーフな同期(SemaphoreSlim・BlockingCollection 等)に変更する.テスト
ConvertImage 内の Mat 未 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 オーバーロード).修正方針
flatten,extended,converted,convertedImage を using ブロックで管理して確実に Dispose する.テスト
Parallel.For 内での未捕捉例外原因
ColorCorrector.ExtendMat の Parallel.For 内で OOM 等の例外が発生した場合,AggregateException としてラップされて SaveThread に伝播する.
SaveThread で捕捉されなければアプリがクラッシュする.
※ 優先度2の修正でメモリ蓄積が解消されれば,発生頻度は大幅に下がる.
該当箇所(リファクタ後)
ColorCorrection/ColorCorrector.cs の ExtendMat(Parallel.For).Cameras/CameraBase.cs の SaveThread(SaveImages → ColorCorrector.ConvertImage → ExtendMat の呼び出し経路).修正方針
SaveThread に例外ハンドラを追加し,クラッシュではなくエラーメッセージを表示して処理を中断するようにする.ExtendMat の Parallel.For に try-catch を追加する.テスト
ConvertImage による _shots[i] の in-place 書き換え原因
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 を直接書き換えない).テスト
4,10,17)で撮影し,各チャンネルの sRGB 変換画像が正しく出力されることを確認する.原因 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().修正方針
CameraBase 側に共通ヘルパーを置くのが妥当).テスト
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<string> / Func<int>)で Invoke し,その結果を返すように修正する.
return (string)Invoke(new Func<string>(() => txtDataName.Text));テスト
| 順位 | 原因 | 該当ファイル | クラッシュとの関連 |
|---|---|---|---|
| 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 | 別系統(現状潜在・クラッシュとは無関係) |