Newer
Older
TIASshot / docs / 03_PLAN / PLAN_02_バグ修正計画.md

バグ修正計画 (Bug Fix Plan)

前提 (Prerequisites)

  • PLAN_01_リファクタリング計画.md の全フェーズ(Phase 1〜5)が完了し,動作がリファクタリング前と同一であることを確認してから着手する.
  • 修正は1件ずつ独立して行い,都度テストを実施する.
  • コミットメッセージは [fix] タグを使用する.
    • 例: [fix] _shots へのスレッドセーフなアクセスに修正
  • 本計画はリファクタリング(PLAN_01)前に作成されたため,各原因の該当箇所はリファクタ後の現構造に更新済み.撮影制御は Cameras/CameraBase.cs(共通骨格 RunShotLoopSaveThread 等)と Cameras/Lucam.csCameras/IScam.cs(派生クラスの Shot)に,色変換は ColorCorrection/ColorCorrector.cs に分離されている.

症状 (Symptom)

  • 連続撮影で4枚以上取得するとアプリがたまにクラッシュする.
  • 再現性は不安定(毎回ではなく「たまに」落ちる).

原因と修正方針 (Causes and Fix Policy)

優先度1: _shots へのスレッドセーフでないアクセス(根本原因)

状態: 完了(2026-06-07)_shotsBlockingCollection<Mat> 化して生産者-消費者パイプラインに変更(RunShotLoopAdd/CompleteAddingSaveThreadGetConsumingEnumerable).撮影ループは try/finallyCompleteAdding() を保証.詳細は docs/PROGRESS.md および git log を参照.

原因

_shotsCameraBaseList<Mat> フィールド)は撮影スレッドと保存スレッド(SaveThread)から同時にアクセスされる. List<T> はスレッドセーフではないため,以下の2点で競合が発生する.

  • リサイズ競合
    • List<T> の内部配列は容量が足りなくなると自動で拡張される(デフォルト容量 4 → 5枚目の Add 時に 8 に拡張).
    • 拡張中に SaveThread_shots[saveCount] を読むとメモリ破壊が起きる.
    • 「4枚以上でたまに落ちる」という症状に一致する.
  • TOCTOU(確認と使用の間の競合)
    • SaveThread_shots.Count チェックと _shots[saveCount] アクセスの間に撮影スレッドがリサイズを起こすと IndexOutOfRangeException が発生する.

該当箇所(リファクタ後)

  • 書き込み: Cameras/CameraBase.csRunShotLoop_shots.Add(CaptureFrame())),および各派生クラスの ShotCameras/Lucam.csCameras/IScam.cs_shots.Clear()).
  • 読み取り: Cameras/CameraBase.csSaveThread_shots.Count / _shots[saveCount]).
  • スレッド起動: 各派生クラスの Shotnew Thread(() => SaveThread(numImages))
  • SaveThread はインデックス(_shots[saveCount])でアクセスするため,置き換え先はインデックス参照可能な構造(ロック付き List<Mat> 等)が前提.ConcurrentQueue を使う場合は保存側を Dequeue ベースに作り替える必要がある点に注意.

修正方針

  • _shots へのアクセスをスレッドセーフ化する(ロック付きアクセス,または保存側を ConcurrentQueue<Mat> + Dequeue に作り替える).
  • SaveThreadThread.Sleep ポーリング待機をスレッドセーフな同期(SemaphoreSlimBlockingCollection 等)に変更する.

テスト

  • 連続撮影を10枚以上で複数回繰り返し,クラッシュしないことを確認する.

優先度2: ConvertImage 内の Mat 未 Dispose によるメモリ蓄積

状態: 完了(2026-06-07)ConvertImage(Mat src, Mat conv) の中間 Mat(flatten/extended/converted/convertedImage)を入れ子 using で確実に Dispose.返り値 convImg8using の外で返却(use-after-dispose なし).ColorCorrectorTests に回帰テストを追加.詳細は docs/PROGRESS.md および git log を参照.

原因

ColorCorrector.ConvertImage(Mat src, Mat conv) 内で生成される中間 Mat(flattenextendedconvertedconvertedImage)が Dispose されないまま残る. 1回の呼び出しで数百 MB〜1 GB 超が蓄積し,画像枚数×チャンネル数分だけ積み重なる. (例: IScam 1840×2440,17次元,4枚連続撮影 → 数十 GB 規模) ※ 返り値 convImg8 は呼び出し側(CameraBase.SaveImages)で using により Dispose されているが,ConvertImage 内部の中間 Mat は解放されていない.

該当箇所(リファクタ後)

  • ColorCorrection/ColorCorrector.csConvertImage(Mat src, Mat conv)(private オーバーロード).

修正方針

  • flattenextendedconvertedconvertedImageusing ブロックで管理して確実に Dispose する.

テスト

  • タスクマネージャでメモリ使用量を監視しながら連続撮影を繰り返し,撮影ごとにメモリが増加し続けないことを確認する.

優先度3: Parallel.For 内での未捕捉例外

原因

ColorCorrector.ExtendMatParallel.For 内で OOM 等の例外が発生した場合,AggregateException としてラップされて SaveThread に伝播する. SaveThread で捕捉されなければアプリがクラッシュする. ※ 優先度2の修正でメモリ蓄積が解消されれば,発生頻度は大幅に下がる.

該当箇所(リファクタ後)

  • 例外発生源: ColorCorrection/ColorCorrector.csExtendMatParallel.For).
  • 捕捉すべき箇所: Cameras/CameraBase.csSaveThreadSaveImagesColorCorrector.ConvertImageExtendMat の呼び出し経路).

修正方針

  • SaveThread に例外ハンドラを追加し,クラッシュではなくエラーメッセージを表示して処理を中断するようにする.
  • ExtendMatParallel.For に try-catch を追加する.

テスト

  • 例外発生時にアプリが落ちずエラーが表示されることを確認する.

優先度4: ConvertImage による _shots[i] の in-place 書き換え

原因

ColorCorrector.ConvertImage(Mat src, Mat conv) 内の src.ConvertTo(src, MatType.CV_64FC3) が,引数で渡された _shots[i] 自体を CV_8UC3 から CV_64FC3 に書き換える副作用がある(srcCameraBase.SaveImages から _shots[saveCount] がそのまま渡される). 特に IScam の new Mat(imgt_full, _roi)(サブマトリクス)に対して in-place の型変換を行うと不正な動作を引き起こす可能性がある. また複数チャンネルを処理する際,2回目以降は変換済みのデータに対して処理をかけることになり,変換結果が誤る.

該当箇所(リファクタ後)

  • ColorCorrection/ColorCorrector.csConvertImage(Mat src, Mat conv) 冒頭の src.ConvertTo(src, MatType.CV_64FC3)
  • 呼び出し元: Cameras/CameraBase.csSaveImages_colorCorrector.ConvertImage(img, channel)img = _shots[saveCount]).

修正方針

  • ConvertImage の冒頭で src をコピーし,コピー側を変換するようにする(src を直接書き換えない).

テスト

  • 複数チャンネル(例: 4,10,17)で撮影し,各チャンネルの sRGB 変換画像が正しく出力されることを確認する.

優先度5: Mat 未 Dispose によるメモリリーク

原因 A: _shots.Clear() で Mat が解放されない

  • 撮影ごとに _shots.Clear() が呼ばれるが,格納された Mat は Dispose されない.
  • 該当箇所: 各派生クラスの ShotCameras/Lucam.csCameras/IScam.cs)の _shots.Clear()

原因 B: _chartMasks.Clear() で Mat が解放されない

  • チャート再検出時に _chartMasks.Clear() が呼ばれるが,同様に Dispose されない.
  • 該当箇所: Cameras/CameraBase.csDetectChart_chartMasks.Clear()

修正方針

  • Clear 前に各 Mat を Dispose するヘルパーを追加する(CameraBase 側に共通ヘルパーを置くのが妥当).

テスト

  • タスクマネージャでメモリ使用量を監視し,撮影・キャリブを繰り返してもメモリが増加し続けないことを確認する.

優先度6: Form1InvokeRequired 分岐で Invoke の戻り値を返していない(潜在バグ)

※ 上記の連続撮影クラッシュ(優先度1〜5)とは独立した別系統のバグ.現状はワーカースレッドから呼ばれていないため顕在化していないが,将来撮影呼び出しをスレッド化した際にデータ名取得失敗等の不具合になりうる.

原因

Form1GetDataName / GetNumMultiShots / GetMultiShotsInterval は,InvokeRequired が true のとき Invoke((MethodInvoker)delegate { GetDataName(); }) のように戻り値を捨てているMethodInvokervoid デリゲートのため Invoke の戻り値は常に null/既定値となり,UI スレッド外から呼ぶと正しい値(テキストボックスの内容)が返らない.

  • 現状: これらは CameraBase.ShotOne / ShotMulti から呼ばれ,その起点はボタンクリックハンドラ(UI スレッド)のため InvokeRequired が false 側を通り顕在化していない.
  • リスク: 撮影処理をワーカースレッド化(連続撮影のマルチスレッド化)すると,このパスを通って GetDataNamenull を返し,保存フォルダ名や撮影枚数が不正になる.

該当箇所

  • UI/Form1.csGetDataName / GetNumMultiShots / GetMultiShotsIntervalInvokeRequired 分岐.

修正方針

  • 戻り値を返すデリゲート(Func<string> / Func<int>)で Invoke し,その結果を返すように修正する.
    • 例: return (string)Invoke(new Func<string>(() => txtDataName.Text));

テスト

  • 各メソッドが UI スレッド・ワーカースレッドのいずれから呼ばれても正しい値を返すことを確認する.

修正順序まとめ (Summary)

順位原因該当ファイルクラッシュとの関連
1_shots のスレッドセーフでないアクセスCameraBase.cs / 各 Shot直接原因(「たまに落ちる」に一致)
2ConvertImage 内の Mat 未 DisposeColorCorrector.cs直接原因(OOM によるクラッシュ)
3Parallel.For 内の未捕捉例外ColorCorrector.cs / CameraBase.cs間接原因(2の修正で頻度減)
4ConvertImage の in-place 書き換えColorCorrector.cs誤動作(変換結果の誤り)
5_shots / _chartMasks の Mat 未 DisposeShot / CameraBase.csメモリリーク(蓄積)
6Form1InvokeRequired 戻り値欠落Form1.cs別系統(現状潜在・クラッシュとは無関係)

効果検証(メモリ修正)の方法 (Verifying the Memory Fixes)

優先度2(ConvertImage の中間 Mat 解放)の効果を、定量・デモの両面で検証できるようにしてある.いずれも専用のコンパイル記号で出し分け,本番(Release)には含めない.

定量比較(自動・数値)

  • TIASshot.Tests/ConvertImageMemoryBenchmark.cs(NUnit・[Explicit])で,修正前(リーク版を忠実に再現)と修正後(実 ConvertImage)を同条件で反復し,Process.PrivateMemorySize64 / WorkingSet64 の baseline・peak・増分を比較する.通常テスト実行では走らず,明示指定で実行する(実行コマンドはファイル冒頭コメント参照).
  • Mat はネイティブメモリのため GC.GetTotalMemory では捕捉できない.計測ループ中は GC を呼ばない(本番のリーク機序を再現するため).

デモ(実アプリのメモリをライブ可視化)

  • コンパイル記号 MEMMONITOR: 定義してビルドすると,TIASshot 起動時に「メモリ観測ウィンドウ」(UI/MemoryMonitorForm)が自動で開く.System.Windows.Forms.Timer で実プロセスのメモリを定期サンプリングし,ライブ折れ線グラフ+現在値で表示,CSV 保存も可能.実際に撮影しながら実使用時のメモリ挙動を録画できる.Release には含めない(既定で Debug 構成のみ定義).
  • コンパイル記号 LEAKY_REPRO: 定義してビルドすると,ColorCorrector.ConvertImage が優先度2修正前(中間 Mat 未解放)の挙動に戻る.観測ウィンドウのモード表示も「リーク再現(修正前)」に切り替わる.既定ではどの構成にも未定義(=修正後)で,対比録画したいときだけ一時的に定義する.
  • 依存: 観測ウィンドウは System.Windows.Forms.DataVisualization(Chart)を使用する.