diff --git a/TIASshot/Cameras/CameraBase.cs b/TIASshot/Cameras/CameraBase.cs index a44c352..62a08a7 100644 --- a/TIASshot/Cameras/CameraBase.cs +++ b/TIASshot/Cameras/CameraBase.cs @@ -463,11 +463,18 @@ /// 新しいインスタンスを作成してから古い BlockingCollection を破棄する /// (順序を逆にすると,破棄と生成の間に例外が発生した場合に _shots が破棄済みインスタンスを /// 指したままとなり,後段の CompleteAdding が ObjectDisposedException になる). + /// BlockingCollection の Dispose は中身の Mat を解放しないため,破棄の前に残っている Mat を + /// 取り出して解放する(DrainShots が待機の上限で打ち切った場合等の取り残し対策). + /// 前回の SaveThread は WaitPreviousSaveThread で終了済みのため,ここで取り出しても競合しない. /// protected void ResetShotQueue() { var previousShots = _shots; _shots = new BlockingCollection(); - previousShots?.Dispose(); + if (previousShots == null) return; + while (previousShots.TryTake(out var remaining)) { + remaining?.Dispose(); + } + previousShots.Dispose(); } /// @@ -598,7 +605,8 @@ } catch (CaptureFailedException ex) { // 破損データを正常画像として保存しないよう,この枚数以降は撮影しない. Debug.WriteLine($"撮影中止({i + 1}/{numImages} 枚目): {ex.Message}"); - csv.WriteLine($"{i + 1},{shotTime},撮影失敗のため中止({ex.Message})"); + // 例外メッセージのカンマ・改行で列や行が崩れないよう,Note 欄は RFC 4180 でエスケープする + csv.WriteLine($"{i + 1},{shotTime},{IoUtil.EscapeCsvField($"撮影失敗のため中止({ex.Message})")}"); return ex.Message; } csv.WriteLine($"{i + 1},{shotTime}"); @@ -619,7 +627,8 @@ var masks = _chartDetector.DetectChart(img); if (masks == null) return; - _chartMasks.Clear(); + // 旧マスク(校正の途中で失敗して再検出する場合等)を解放してから差し替える + ReleaseChartMasks(); _chartMasks.AddRange(masks); _calibrating = Config.GetInt("Calib/Frames"); #if MEMMONITOR @@ -628,6 +637,18 @@ } /// + /// チャートマスク(_chartMasks の各 Mat)を解放して空にする. + /// _chartMasks はカメラのコールバックスレッド(ProcessCalibrationFrame)だけが読むため, + /// 切断時(Disconnect)はストリーム停止の後に呼ぶこと(コールバックが動いている間に解放しない). + /// + protected void ReleaseChartMasks() { + foreach (var mask in _chartMasks) { + mask?.Dispose(); + } + _chartMasks.Clear(); + } + + /// /// ファイル名にチャネル数を追加する /// /// diff --git a/TIASshot/Cameras/IScam.cs b/TIASshot/Cameras/IScam.cs index 5470817..3718187 100644 --- a/TIASshot/Cameras/IScam.cs +++ b/TIASshot/Cameras/IScam.cs @@ -95,6 +95,8 @@ /// public override void Disconnect() { _ic.LiveStop(); + // コールバック(ProcessCalibrationFrame)が止まった後にチャートマスクを解放する + ReleaseChartMasks(); } /// diff --git a/TIASshot/Cameras/Lucam.cs b/TIASshot/Cameras/Lucam.cs index f425ace..2f4dcf3 100644 --- a/TIASshot/Cameras/Lucam.cs +++ b/TIASshot/Cameras/Lucam.cs @@ -329,6 +329,8 @@ } api.CameraClose(_hCam); _hCam = IntPtr.Zero; + // カメラを閉じてコールバック(ProcessCalibrationFrame)が止まった後にチャートマスクを解放する + ReleaseChartMasks(); Debug.WriteLine("カメラ切断"); } } diff --git a/TIASshot/SelfTest/SelfTestCamera.cs b/TIASshot/SelfTest/SelfTestCamera.cs index 0098e76..062b302 100644 --- a/TIASshot/SelfTest/SelfTestCamera.cs +++ b/TIASshot/SelfTest/SelfTestCamera.cs @@ -61,6 +61,14 @@ /// プレビュースレッドが動作中か public bool IsPreviewRunning => _previewThread != null && _previewThread.IsAlive; + /// + /// 現在のチャートマスク(_chartMasks)の参照のコピー(古い順.検出前・切断後は空). + /// 再検出・切断でマスクが解放された(Mat.IsDisposed)ことを確かめるために使う. + /// _chartMasks はプレビュースレッドが書き換えるため,プレビュー停止中(IsPreviewRunning が false)に読むこと. + /// 返した Mat の所有権は本クラスに残る(呼び出し側で Dispose しない). + /// + public IReadOnlyList ChartMasksSnapshot => _chartMasks.ToArray(); + public override bool Connect() { if (!BootCheck()) return false; DeviceName = "SelfTestCamera"; @@ -81,8 +89,7 @@ _bmps[i]?.Dispose(); _bmps[i] = null; } - foreach (var mask in _chartMasks) mask.Dispose(); - _chartMasks.Clear(); + ReleaseChartMasks(); _isPreview = false; } diff --git a/TIASshot/UI/Form1.cs b/TIASshot/UI/Form1.cs index b717f2b..ba043a2 100644 --- a/TIASshot/UI/Form1.cs +++ b/TIASshot/UI/Form1.cs @@ -309,7 +309,7 @@ /// public string GetDataName() { if (InvokeRequired) { - return (string)Invoke((MethodInvoker)delegate { GetDataName(); }); + return (string)Invoke(new Func(GetDataName)); } return txtDataName.Text; } @@ -320,7 +320,7 @@ /// public int GetNumMultiShots() { if (InvokeRequired) { - return (int)Invoke((MethodInvoker)delegate { GetNumMultiShots(); }); + return (int)Invoke(new Func(GetNumMultiShots)); } if (int.TryParse(txtNumMultiShots.Text, out int result)) return result; return 1; @@ -332,7 +332,7 @@ /// public int GetMultiShotsInterval() { if (InvokeRequired) { - return (int)Invoke((MethodInvoker)delegate { GetMultiShotsInterval(); }); + return (int)Invoke(new Func(GetMultiShotsInterval)); } if (int.TryParse(txtMultiShotsInterval.Text, out int result)) return result; return 1000; diff --git a/TIASshot/Utils/IoUtil.cs b/TIASshot/Utils/IoUtil.cs index a731ece..77e3748 100644 --- a/TIASshot/Utils/IoUtil.cs +++ b/TIASshot/Utils/IoUtil.cs @@ -148,10 +148,11 @@ /// CSV の 1 フィールドを RFC 4180 に従ってエスケープする. /// カンマ・ダブルクォート・CR・LF を含む場合のみ全体をダブルクォートで囲み, /// 内部のダブルクォートを二重化する.それ以外はそのまま返す(null は空文字). + /// WriteInfo のほか,撮影ログ(CameraBase.CaptureShots の Note 欄)でも使う. /// /// フィールド値(null 可) /// エスケープ済みのフィールド - private static string EscapeCsvField(string field) { + internal static string EscapeCsvField(string field) { if (string.IsNullOrEmpty(field)) return ""; if (field.IndexOfAny(new[] { ',', '"', '\r', '\n' }) < 0) return field; return "\"" + field.Replace("\"", "\"\"") + "\"";