diff --git a/TIASshot.Tests/ColorCorrectorTests.cs b/TIASshot.Tests/ColorCorrectorTests.cs index 21ad28f..f06f12e 100644 --- a/TIASshot.Tests/ColorCorrectorTests.cs +++ b/TIASshot.Tests/ColorCorrectorTests.cs @@ -1,3 +1,6 @@ +using System.Collections.Generic; +using System.IO; +using System.Linq; using NUnit.Framework; using OpenCvSharp; @@ -420,5 +423,80 @@ } } } + + // --------------------------------------------------------------- + // CalcTcc の資源解放テスト(v1.7 の Mat リーク修正を内包することの確認) + // --------------------------------------------------------------- + + /// + /// CalcTcc: 中間 Mat(辞書に保持しないもの)は呼び出し完了時に解放され, + /// 入力 tccRgb(呼び出し側所有)は解放されず,再校正時に旧変換行列が破棄されること. + /// v1.7 が塞いだ校正経路の Mat リークを本実装が内包していることを保証する. + /// + [Test] + public void CalcTcc_再校正_中間Matと旧変換行列を解放し入力は保持する() { + LoadCalcTccConfig(); + + using (var tccSrgb = CreateScaledColorChart(rows: 24, scale: 1.0)) + using (var tccXyz = CreateScaledColorChart(rows: 24, scale: 0.8)) + using (var tccRgb = CreateLinearColorChart(rows: 24)) { + // channel=4(通常経路)と 17(convOld 特殊経路)の両方を通す + var corrector = new ColorCorrector(tccSrgb, tccXyz, new int[] { 4, 17 }); + + // 1回目の校正:コールバックに渡る Mat を全て捕捉する + var firstSaved = new List>(); + corrector.CalcTcc(tccRgb, "dummy", + (path, mat) => firstSaved.Add(new KeyValuePair(path, mat))); + + // 中間 Mat(ConvRgb2Xyz / ConvRgb2XyzOld / TccSrgb / ConvSrgb2Xyz)は解放済み + foreach (var kv in firstSaved) { + var name = Path.GetFileName(kv.Key); + if (name.Contains("convrgb2xyz") || name.Contains("tccsrgb") || name.Contains("convsrgb2xyz")) { + Assert.IsTrue(kv.Value.IsDisposed, $"中間 Mat は解放済みのはず: {name}"); + } + } + + // 入力 tccRgb は ColorCorrector に破棄されない(所有権は呼び出し側) + var tccRgbSaved = firstSaved.First(kv => Path.GetFileName(kv.Key).Contains("tccrgb")).Value; + Assert.IsFalse(tccRgbSaved.IsDisposed, "入力 tccRgb は破棄されない(所有権は呼び出し側)"); + Assert.IsFalse(tccRgb.IsDisposed, "入力 tccRgb 本体も生存している"); + + // 保持される変換行列(ConvRgb2Srgb, ch4)は 1 回目校正後に生存 + var keptCh4 = firstSaved.First(kv => { + var n = Path.GetFileName(kv.Key); + return n.Contains("convrgb2srgb") && n.Contains("04"); + }).Value; + Assert.IsFalse(keptCh4.IsDisposed, "保持中の変換行列は 1 回目校正後に生存している"); + + // 2回目の校正:旧エントリ(1回目の保持行列)が破棄される + corrector.CalcTcc(tccRgb, "dummy", (path, mat) => { }); + Assert.IsTrue(keptCh4.IsDisposed, "再校正で古い変換行列(旧エントリ)が破棄される"); + } + } + + /// + /// CalcTcc テスト用に,参照される File/* キーを持つ最小 Config を読み込む. + /// ファイル名は種類ごとに識別可能な文字列にし,コールバックの path で判別する. + /// + private static void LoadCalcTccConfig() { + const string xml = + "" + + "" + + " tccrgb.csv" + + " convrgb2srgb_{CN}.csv" + + " convrgb2xyz_{CN}.csv" + + " convrgb2xyzold_{CN}.csv" + + " tccsrgb_{CN}.csv" + + " convsrgb2xyz_{CN}.csv" + + ""; + + var path = Path.GetTempFileName() + ".xml"; + File.WriteAllText(path, xml, System.Text.Encoding.UTF8); + try { + Config.Load(path); + } finally { + if (File.Exists(path)) File.Delete(path); + } + } } } diff --git a/TIASshot/Cameras/CameraBase.cs b/TIASshot/Cameras/CameraBase.cs index e521dc2..e3e7154 100644 --- a/TIASshot/Cameras/CameraBase.cs +++ b/TIASshot/Cameras/CameraBase.cs @@ -269,18 +269,19 @@ /// protected void CalcTcc(Mat img) { - // 変換行列の計算 - var tccRgb = GetTccRgb(img); - var imgRois = GetTccRoisImage(img); + // 変換行列の計算(tccRgb・imgRois はここで所有し,使用後に確実に解放する) + using (var tccRgb = GetTccRgb(img)) + using (var imgRois = GetTccRoisImage(img)) { - // データ保存 - SetSaveFolder("校正"); - SetInfo("校正データ", _saveFolder); - Cv2.ImWrite(Path.Combine(_saveFolder, Config.GetString("File/TccImage")), img); - Cv2.ImWrite(Path.Combine(_saveFolder, Config.GetString("File/TccRoisImage")), imgRois); + // データ保存 + SetSaveFolder("校正"); + SetInfo("校正データ", _saveFolder); + Cv2.ImWrite(Path.Combine(_saveFolder, Config.GetString("File/TccImage")), img); + Cv2.ImWrite(Path.Combine(_saveFolder, Config.GetString("File/TccRoisImage")), imgRois); - // 変換行列の計算・保存は ColorCorrector に委譲する - _colorCorrector.CalcTcc(tccRgb, _saveFolder, IoUtil.SaveMatToCsv); + // 変換行列の計算・保存は ColorCorrector に委譲する(tccRgb の所有権は渡さない) + _colorCorrector.CalcTcc(tccRgb, _saveFolder, IoUtil.SaveMatToCsv); + } IoUtil.PlaySound(Config.GetString("Sound/CalibDone")); _form.ShowMessage("自動校正完了"); diff --git a/TIASshot/ColorCorrection/ColorCorrector.cs b/TIASshot/ColorCorrection/ColorCorrector.cs index 16388b0..eadebea 100644 --- a/TIASshot/ColorCorrection/ColorCorrector.cs +++ b/TIASshot/ColorCorrection/ColorCorrector.cs @@ -91,34 +91,45 @@ saveMatToCsv(Path.Combine(saveFolder, Config.GetString("File/TccRgbValues")), tccRgb); + // 再校正時,古い変換行列のネイティブ資源を解放してから作り直す(Mat リーク防止) + foreach (var mat in _convRGB2SRGB.Values) { + mat.Dispose(); + } _convRGB2SRGB.Clear(); + foreach (var channel in ChannelList) { var convRGB2SRGB = CalcConvertMatrix(tccRgb, TCC_SRGB, channel); saveMatToCsv(Path.Combine(saveFolder, GetFilenameWithChannel("File/ConvRgb2Srgb", channel)), convRGB2SRGB); - var convRGB2XYZ = CalcConvertMatrix(tccRgb, TCC_XYZ, channel); - saveMatToCsv(Path.Combine(saveFolder, GetFilenameWithChannel("File/ConvRgb2Xyz", channel)), convRGB2XYZ); - if (channel == 17) { - var convOld = new Mat(convRGB2XYZ.Size(), MatType.CV_64FC1); - for (var row = 0; row < convRGB2XYZ.Rows; row++) { - var rowFrom = (row + 1) % convRGB2XYZ.Rows; // 1行ずらす - for (var col = 0; col < convRGB2XYZ.Cols; col++) { - convOld.At(row, col) = convRGB2XYZ.At(rowFrom, col); + // 以降の中間 Mat は辞書に保持しないため,使用後に確実に解放する + using (var convRGB2XYZ = CalcConvertMatrix(tccRgb, TCC_XYZ, channel)) { + saveMatToCsv(Path.Combine(saveFolder, GetFilenameWithChannel("File/ConvRgb2Xyz", channel)), convRGB2XYZ); + if (channel == 17) { + using (var convOld = new Mat(convRGB2XYZ.Size(), MatType.CV_64FC1)) { + for (var row = 0; row < convRGB2XYZ.Rows; row++) { + var rowFrom = (row + 1) % convRGB2XYZ.Rows; // 1行ずらす + for (var col = 0; col < convRGB2XYZ.Cols; col++) { + convOld.At(row, col) = convRGB2XYZ.At(rowFrom, col); + } + } + saveMatToCsv(Path.Combine(saveFolder, GetFilenameWithChannel("File/ConvRgb2XyzOld", channel)), convOld); } } - saveMatToCsv(Path.Combine(saveFolder, GetFilenameWithChannel("File/ConvRgb2XyzOld", channel)), convOld); } - var tccSrgb = ConvertColor(tccRgb, convRGB2SRGB); - saveMatToCsv(Path.Combine(saveFolder, GetFilenameWithChannel("File/TccSrgbValues", channel)), tccSrgb); + using (var tccSrgb = ConvertColor(tccRgb, convRGB2SRGB)) { + saveMatToCsv(Path.Combine(saveFolder, GetFilenameWithChannel("File/TccSrgbValues", channel)), tccSrgb); - var convSrgb2Xyz = CalcConvertMatrix(tccSrgb, TCC_XYZ, channel); - saveMatToCsv(Path.Combine(saveFolder, GetFilenameWithChannel("File/ConvSrgb2Xyz", channel)), convSrgb2Xyz); + using (var convSrgb2Xyz = CalcConvertMatrix(tccSrgb, TCC_XYZ, channel)) { + saveMatToCsv(Path.Combine(saveFolder, GetFilenameWithChannel("File/ConvSrgb2Xyz", channel)), convSrgb2Xyz); + } - // 変換精度検証 - var diff = Math.Sqrt(Cv2.Norm(TCC_SRGB, tccSrgb, NormTypes.L2)); - Debug.WriteLine($"{channel}次元 RGB→SRGB 変換行列の誤差 = {diff:.000}"); + // 変換精度検証 + var diff = Math.Sqrt(Cv2.Norm(TCC_SRGB, tccSrgb, NormTypes.L2)); + Debug.WriteLine($"{channel}次元 RGB→SRGB 変換行列の誤差 = {diff:.000}"); + } + // convRGB2SRGB は辞書に保持するため解放しない(ConvertImage で使用) _convRGB2SRGB.Add(channel, convRGB2SRGB); } }