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..d90bce6 100644 --- a/TIASshot/Cameras/CameraBase.cs +++ b/TIASshot/Cameras/CameraBase.cs @@ -62,6 +62,8 @@ protected BlockingCollection _shots = new BlockingCollection(); protected Dictionary _ShotInfo = new Dictionary(); // 設定値保存用 protected Rect _roi; + protected Thread _saveThread = null; // SaveThreadへの参照を保持(前回完了待機用) + protected ManualResetEventSlim _saveThreadReady = new ManualResetEventSlim(false); // SaveThread起動完了の同期 // プライベートメンバ readonly float UpdateRate; @@ -142,7 +144,12 @@ Cv2.ImWrite(Path.Combine(_saveFolder, filename), img); foreach (var channel in _colorCorrector.Channels) { - using (var converted = _colorCorrector.ConvertImage(img, channel)) { + // ConvertImage は src を in-place で CV_64FC3 に変換するため、 + // 各チャンネルで独立したクローンを作成して元画像を保護する。 + // 【重要】Clone を作成しないと、同一 Mat を複数チャンネルで変換時に + // 2チャンネル目以降が破壊された型(CV_64FC3)を処理してクラッシュする。 + using (var imgClone = img.Clone()) + using (var converted = _colorCorrector.ConvertImage(imgClone, channel)) { filename = GetFilenameWithChannel("File/SrgbImage", channel); filename = filename.Replace("{NO}", $"{idx + 1:0000}"); Cv2.ImWrite(Path.Combine(_saveFolder, filename), converted); @@ -155,9 +162,14 @@ /// /// protected void SaveThread(int numImages) { + // スレッド起動完了を通知(producer が待機している) + _saveThreadReady.Set(); + int saveCount = 0; foreach (var img in _shots.GetConsumingEnumerable()) { - SaveImages(img, saveCount); + using (img) { + SaveImages(img, saveCount); + } saveCount++; _form.ShowMessage($"画像{saveCount}/{numImages}枚目保存"); } @@ -269,18 +281,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/Cameras/IScam.cs b/TIASshot/Cameras/IScam.cs index 1652a83..da3b09a 100644 --- a/TIASshot/Cameras/IScam.cs +++ b/TIASshot/Cameras/IScam.cs @@ -166,12 +166,29 @@ _ic.LiveStart(); _snapSinkRef = _ic.Sink as FrameSnapSink; + // 前回の SaveThread が完了するまで待機(低スペックPCでの競合回避) + if (_saveThread != null && _saveThread.IsAlive) { + _saveThread.Join(); + } + + // 古い BlockingCollection を破棄してから新しいインスタンスを作成 + _shots?.Dispose(); _shots = new BlockingCollection(); _snapSinkRef.SnapSingle(TimeSpan.FromSeconds(5)); // 最初のフレームを捨てる + // ManualResetEventSlim をリセットして SaveThread の起動完了を待機できるようにする + _saveThreadReady.Reset(); + var thread = new Thread(() => SaveThread(numImages)); + _saveThread = thread; // 参照を保持(次回の完了待機用) thread.Start(); + // SaveThread が起動完了するまで待機(consumer 準備完了を保証) + // タイムアウト設定で無限待機を回避(通常は即座に完了するはず) + if (!_saveThreadReady.Wait(TimeSpan.FromSeconds(5))) { + Debug.WriteLine("警告: SaveThread の起動完了待機がタイムアウトしました"); + } + RunShotLoop(numImages, interval); _ic.LiveStop(); diff --git a/TIASshot/Cameras/Lucam.cs b/TIASshot/Cameras/Lucam.cs index ea5b953..961d32d 100644 --- a/TIASshot/Cameras/Lucam.cs +++ b/TIASshot/Cameras/Lucam.cs @@ -224,10 +224,27 @@ protected override void Shot(int numImages=1, int interval=0) { SetSnapParam(); + // 前回の SaveThread が完了するまで待機(低スペックPCでの競合回避) + if (_saveThread != null && _saveThread.IsAlive) { + _saveThread.Join(); + } + + // 古い BlockingCollection を破棄してから新しいインスタンスを作成 + _shots?.Dispose(); _shots = new BlockingCollection(); + + // ManualResetEventSlim をリセットして SaveThread の起動完了を待機できるようにする + _saveThreadReady.Reset(); + var thread = new Thread(() => SaveThread(numImages)); + _saveThread = thread; // 参照を保持(次回の完了待機用) thread.Start(); + // SaveThread が起動完了するまで待機(consumer 準備完了を保証) + if (!_saveThreadReady.Wait(TimeSpan.FromSeconds(5))) { + Debug.WriteLine("警告: SaveThread の起動完了待機がタイムアウトしました"); + } + BeginShot(); RunShotLoop(numImages, interval); EndShot(); 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); } } diff --git a/TIASshot/DBK33UX178.xml b/TIASshot/DBK33UX178.xml new file mode 100644 index 0000000..6c33e9c --- /dev/null +++ b/TIASshot/DBK33UX178.xml @@ -0,0 +1,141 @@ + + + RGB24 (3072x2048) + 30.000030 + 0 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/TIASshot/DFK33UX178.xml b/TIASshot/DFK33UX178.xml index 6c33e9c..5806f31 100644 --- a/TIASshot/DFK33UX178.xml +++ b/TIASshot/DFK33UX178.xml @@ -1,9 +1,11 @@ - + RGB24 (3072x2048) 30.000030 0 + 0 + @@ -42,10 +44,10 @@ - + - + @@ -58,6 +60,9 @@ + + + @@ -67,7 +72,10 @@ - + + + + @@ -88,6 +96,11 @@ + + + + + @@ -123,7 +136,7 @@ - + diff --git a/TIASshot/Properties/AssemblyInfo.cs b/TIASshot/Properties/AssemblyInfo.cs index 308d4a7..2a98256 100644 --- a/TIASshot/Properties/AssemblyInfo.cs +++ b/TIASshot/Properties/AssemblyInfo.cs @@ -30,5 +30,5 @@ // ビルド番号 // リビジョン // -[assembly: AssemblyVersion("1.6.0.0")] -[assembly: AssemblyFileVersion("1.6.0.0")] +[assembly: AssemblyVersion("1.8.0.0")] +[assembly: AssemblyFileVersion("1.8.0.0")]