diff --git a/CLAUDE.md b/CLAUDE.md index 01fa40a..9af22d2 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -4,7 +4,7 @@ ## 開発進捗 -最新: ゲイン補正式(GetRatio)を純粋メソッド GainCalc に抽出してテスト対象化(挙動不変・実機で校正収束確認済み) +最新: PLAN_02 優先度2 完了(ConvertImage の中間 Mat を using で解放し OOM クラッシュを修正・回帰テスト追加) ※ 本欄は**最新ステップ 1 行のみ上書き更新**.詳細な進捗履歴(動機・設計判断・失敗パターン)は [docs/PROGRESS.md](docs/PROGRESS.md) に追記する.運用ルールは GUIDE_05「進捗記録の運用ルール(CLAUDE.md / PROGRESS.md)」を参照. ## 必須ルール(コード実装時) diff --git a/TIASshot.Tests/ColorCorrectorTests.cs b/TIASshot.Tests/ColorCorrectorTests.cs index e0dfe91..21ad28f 100644 --- a/TIASshot.Tests/ColorCorrectorTests.cs +++ b/TIASshot.Tests/ColorCorrectorTests.cs @@ -286,5 +286,139 @@ conv.At(3, 0) = 1.0; conv.At(3, 1) = 0.0; conv.At(3, 2) = 0.0; return conv; } + + // --------------------------------------------------------------- + // ConvertImage(Mat src, Mat conv) テスト + // --------------------------------------------------------------- + + /// + /// ConvertImage: 2x2 の CV_8UC3 画像と 4x3 の conv 行列を与えたとき + /// 出力が CV_8UC3 かつ入力と同じ高さ×幅(2×2)になること + /// + [Test] + public void ConvertImage_2x2Input_OutputIsCV8UC3WithSameSize() { + // Arrange: 2行2列 CV_8UC3(各画素 B=50, G=100, R=150) + using (var src = new Mat(2, 2, MatType.CV_8UC3, new Scalar(50, 100, 150))) + using (var conv = CreateIdentityLikeConvMat()) { + var corrector = MakeCorrectorWith4Channels(); + + // Act + using (var dst = corrector.ConvertImage(src, conv)) { + // Assert: 型・サイズ + Assert.AreEqual(MatType.CV_8UC3, dst.Type(), "出力型は CV_8UC3"); + Assert.AreEqual(2, dst.Rows, "出力行数は入力と同じ"); + Assert.AreEqual(2, dst.Cols, "出力列数は入力と同じ"); + } + } + } + + /// + /// ConvertImage: 恒等変換行列を使ったとき出力ピクセル値が入力ピクセル値と一致すること + /// (use-after-dispose で出力が破損していないことの確認) + /// 手計算: + /// flatten 後 ExtendMat で各画素 → [1, R, G, B] + /// 恒等 conv = [[0,0,0],[0,0,1],[0,1,0],[1,0,0]] + /// 積 [1,R,G,B]*conv = [0+0+0+B, 0+0+G+0, 0+R+0+0] = [B, G, R] + /// ConvertTo CV_8UC3 → Vec3b(B, G, R) + /// + [Test] + public void ConvertImage_IdentityConv_OutputPixelMatchesInput() { + // Arrange: 1x1 CV_8UC3(B=50, G=100, R=150) + using (var src = new Mat(1, 1, MatType.CV_8UC3, new Scalar(50, 100, 150))) + using (var conv = CreateIdentityLikeConvMat()) { + var corrector = MakeCorrectorWith4Channels(); + + // Act + using (var dst = corrector.ConvertImage(src, conv)) { + // Assert: 出力ピクセルが入力と同一値 + var pixel = dst.At(0, 0); + Assert.AreEqual(50, pixel.Item0, "出力 B = 50"); + Assert.AreEqual(100, pixel.Item1, "出力 G = 100"); + Assert.AreEqual(150, pixel.Item2, "出力 R = 150"); + } + } + } + + /// + /// ConvertImage: 0 行列の conv を使ったとき全ピクセルが 0 になること + /// (出力が conv に依存して決まること、use-after-dispose で任意値が返らないことの確認) + /// + [Test] + public void ConvertImage_ZeroConv_AllOutputPixelsAreZero() { + // Arrange: 2x2 CV_8UC3(B=50, G=100, R=150)、conv は全ゼロ + using (var src = new Mat(2, 2, MatType.CV_8UC3, new Scalar(50, 100, 150))) + using (var conv = new Mat(4, 3, MatType.CV_64FC1, new Scalar(0.0))) { + var corrector = MakeCorrectorWith4Channels(); + + // Act + using (var dst = corrector.ConvertImage(src, conv)) { + // Assert: 全ピクセルが 0 + for (int row = 0; row < dst.Rows; row++) { + for (int col = 0; col < dst.Cols; col++) { + var pixel = dst.At(row, col); + Assert.AreEqual(0, pixel.Item0, $"row={row}, col={col}: B は 0"); + Assert.AreEqual(0, pixel.Item1, $"row={row}, col={col}: G は 0"); + Assert.AreEqual(0, pixel.Item2, $"row={row}, col={col}: R は 0"); + } + } + } + } + } + + /// + /// ConvertImage: 同一 ColorCorrector インスタンスで連続 3 回呼び出しても + /// 毎回正しい出力が得られ例外が発生しないこと + /// (using による解放が呼び出しを跨いで健全なことの確認=今回修正の核心) + /// 注意: src.ConvertTo(src, CV_64FC3) が in-place 書き換えを行うため(優先度4 の既知挙動)、 + /// 各呼び出しで新しい src を生成する + /// + [Test] + public void ConvertImage_CalledThreeTimesInSequence_EachCallReturnsCorrectOutput() { + var corrector = MakeCorrectorWith4Channels(); + + for (int call = 0; call < 3; call++) { + // Arrange: 呼び出しごとに新しい src を生成(in-place 書き換え副作用の回避) + using (var src = new Mat(2, 2, MatType.CV_8UC3, new Scalar(50, 100, 150))) + using (var conv = CreateIdentityLikeConvMat()) { + // Act: 例外が発生しないことも検証(解放済み Mat へのアクセスがないこと) + Mat dst = null; + Assert.DoesNotThrow(() => dst = corrector.ConvertImage(src, conv), + $"呼び出し {call + 1} 回目で例外が発生した"); + + // Assert: 出力が正しいこと(解放済み Mat からの読み出しでないことの確認) + using (dst) { + Assert.AreEqual(MatType.CV_8UC3, dst.Type(), $"呼び出し {call + 1} 回目: 型は CV_8UC3"); + var pixel = dst.At(0, 0); + Assert.AreEqual(50, pixel.Item0, $"呼び出し {call + 1} 回目: B = 50"); + Assert.AreEqual(100, pixel.Item1, $"呼び出し {call + 1} 回目: G = 100"); + Assert.AreEqual(150, pixel.Item2, $"呼び出し {call + 1} 回目: R = 150"); + } + } + } + } + + /// + /// ConvertImage: src.ConvertTo(src, CV_64FC3) の in-place 書き換えの既知挙動を文書化するテスト + /// 優先度4 のスコープ外バグ(今回未修正)。同一 src インスタンスを使い回すと + /// 1 回目の呼び出し後に src が CV_64FC3 に変化していることを確認する。 + /// このテストは「現在の挙動」を記録するもので、修正後は変更が必要になる。 + /// + [Test] + public void ConvertImage_SrcIsModifiedInPlace_KnownBehaviorPriority4() { + // Arrange: CV_8UC3 の src + using (var src = new Mat(1, 1, MatType.CV_8UC3, new Scalar(50, 100, 150))) + using (var conv = CreateIdentityLikeConvMat()) { + var corrector = MakeCorrectorWith4Channels(); + + Assert.AreEqual(MatType.CV_8UC3, src.Type(), "呼び出し前は CV_8UC3"); + + // Act + using (var dst = corrector.ConvertImage(src, conv)) { + // Assert: 1 回目の呼び出し後、src は CV_64FC3 に変化している(in-place 書き換えの既知挙動) + Assert.AreEqual(MatType.CV_64FC3, src.Type(), + "ConvertImage 呼び出し後、src は in-place で CV_64FC3 に変換される(優先度4 の既知挙動)"); + } + } + } } } diff --git a/TIASshot/ColorCorrection/ColorCorrector.cs b/TIASshot/ColorCorrection/ColorCorrector.cs index 1f12f72..42bb2ff 100644 --- a/TIASshot/ColorCorrection/ColorCorrector.cs +++ b/TIASshot/ColorCorrection/ColorCorrector.cs @@ -186,22 +186,23 @@ /// /// /// double型画像 - private Mat ConvertImage(Mat src, Mat conv) { + internal Mat ConvertImage(Mat src, Mat conv) { if (src.Type() != MatType.CV_64FC3) { src.ConvertTo(src, MatType.CV_64FC3); } - var flatten = src.Reshape(3, src.Height * src.Width); - var extended = ExtendMat(flatten, conv.Rows); - var converted = (extended * conv).ToMat(); - var convertedImage = converted.Reshape(3, src.Height); - //Clipping to 8bit range - //unsafe { - // convertedImage.ForEachAsVec3d(Clip8bit); - //} + using (var flatten = src.Reshape(3, src.Height * src.Width)) + using (var extended = ExtendMat(flatten, conv.Rows)) + using (var converted = (extended * conv).ToMat()) + using (var convertedImage = converted.Reshape(3, src.Height)) { + //Clipping to 8bit range + //unsafe { + // convertedImage.ForEachAsVec3d(Clip8bit); + //} - var convImg8 = new Mat(); - convertedImage.ConvertTo(convImg8, MatType.CV_8UC3); - return convImg8; + var convImg8 = new Mat(); + convertedImage.ConvertTo(convImg8, MatType.CV_8UC3); + return convImg8; + } } /// 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 eb98d15..a9b0a0f 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" @@ -49,6 +49,8 @@ ### 優先度2: `ConvertImage` 内の Mat 未 Dispose によるメモリ蓄積 +> **状態: 完了(2026-06-07)** — `ConvertImage(Mat src, Mat conv)` の中間 Mat(`flatten`/`extended`/`converted`/`convertedImage`)を入れ子 `using` で確実に Dispose.返り値 `convImg8` は `using` の外で返却(use-after-dispose なし).`ColorCorrectorTests` に回帰テストを追加.詳細は `docs/PROGRESS.md` および `git log` を参照. + **原因** `ColorCorrector.ConvertImage(Mat src, Mat conv)` 内で生成される中間 Mat(`flatten`,`extended`,`converted`,`convertedImage`)が Dispose されないまま残る. diff --git a/docs/PROGRESS.md b/docs/PROGRESS.md index c19f0a6..952b8b1 100644 --- a/docs/PROGRESS.md +++ b/docs/PROGRESS.md @@ -48,3 +48,8 @@ - [x] **ゲイン補正式(GetRatio)の純粋メソッド抽出+テスト** (2026-06-07): `CameraBase` にインラインだったゲイン更新比率の計算式を,ハードウェア非依存の純粋クラス `GainCalc.CalcRatio` に抽出し,`CameraBase.GetRatio` は委譲に変更。`TIASshot.Tests` でテスト対象化(GUIDE_08 の「ピュアロジック層を分離して自動テスト」方針の実践)。 - 方針: **挙動不変**。式・演算順序・`float` 比較・ゼロ除算挙動・`UpdateRate` の流れをすべて保持し,派生クラスの呼び出し箇所は無変更。実機で校正(ホワイトバランス自動調整)の収束を確認。 - テスト観点: 通常比率・`updateRate` 減衰・下降補正(`target