diff --git a/TIASshot.Tests/ColorCorrectorTests.cs b/TIASshot.Tests/ColorCorrectorTests.cs
index f06f12e..b06447b 100644
--- a/TIASshot.Tests/ColorCorrectorTests.cs
+++ b/TIASshot.Tests/ColorCorrectorTests.cs
@@ -474,6 +474,118 @@
}
}
+ // ---------------------------------------------------------------
+ // CalcConvertMatrix / ConvertColor の中間 Mat 解放テスト
+ // ---------------------------------------------------------------
+
+ ///
+ /// ExtendMat の戻り値(中間 Mat)を捕捉するテスト用サブクラス.
+ /// 挙動は変えず,戻り値の参照だけを記録する.
+ ///
+ private class ExtendMatCapturingCorrector : ColorCorrector {
+ public readonly List CapturedExtended = new List();
+
+ public ExtendMatCapturingCorrector() : base(null, null, new int[] { 4 }) { }
+
+ internal override Mat ExtendMat(Mat src, int channels) {
+ var result = base.ExtendMat(src, channels);
+ CapturedExtended.Add(result);
+ return result;
+ }
+ }
+
+ ///
+ /// CalcConvertMatrix: 通常入力(Nx3)では ExtendMat が新規確保した中間 Mat を
+ /// 呼び出し完了時に解放し,入力 src は解放しないこと(Mat リーク防止)
+ ///
+ [Test]
+ public void CalcConvertMatrix_通常入力_中間拡張Matを解放し入力は保持する() {
+ // Arrange
+ using (var src = CreateLinearColorChart(rows: 24))
+ using (var target = CreateScaledColorChart(rows: 24, scale: 1.1)) {
+ var corrector = new ExtendMatCapturingCorrector();
+
+ // Act
+ using (var conv = corrector.CalcConvertMatrix(src, target, channels: 4)) {
+ // Assert
+ Assert.AreEqual(1, corrector.CapturedExtended.Count, "ExtendMat は 1 回呼ばれる");
+ var extended = corrector.CapturedExtended[0];
+ Assert.AreNotSame(src, extended, "通常入力では新規 Mat が確保される(前提確認)");
+ Assert.IsTrue(extended.IsDisposed, "中間拡張 Mat は CalcConvertMatrix 完了時に解放される");
+ Assert.IsFalse(src.IsDisposed, "入力 src は解放されない(所有権は呼び出し側)");
+ }
+ }
+ }
+
+ ///
+ /// ConvertColor: 通常入力(Nx3)では ExtendMat が新規確保した中間 Mat を
+ /// 呼び出し完了時に解放し,入力 src は解放しないこと(Mat リーク防止)
+ ///
+ [Test]
+ public void ConvertColor_通常入力_中間拡張Matを解放し入力は保持する() {
+ // Arrange
+ using (var src = new Mat(1, 3, MatType.CV_64FC1))
+ using (var conv = CreateIdentityLikeConvMat()) {
+ SetBGR(src, row: 0, b: 10.0, g: 20.0, r: 30.0);
+ var corrector = new ExtendMatCapturingCorrector();
+
+ // Act
+ using (var dst = corrector.ConvertColor(src, conv)) {
+ // Assert
+ Assert.AreEqual(1, corrector.CapturedExtended.Count, "ExtendMat は 1 回呼ばれる");
+ var extended = corrector.CapturedExtended[0];
+ Assert.AreNotSame(src, extended, "通常入力では新規 Mat が確保される(前提確認)");
+ Assert.IsTrue(extended.IsDisposed, "中間拡張 Mat は ConvertColor 完了時に解放される");
+ Assert.IsFalse(src.IsDisposed, "入力 src は解放されない(所有権は呼び出し側)");
+ }
+ }
+ }
+
+ ///
+ /// CalcConvertMatrix: ExtendMat のパススルー入力(3 列相当でない Mat は src をそのまま返す)でも
+ /// 入力 src を解放しないこと(無条件 Dispose による入力破壊の回帰防止)
+ ///
+ [Test]
+ public void CalcConvertMatrix_パススルー入力_入力Matを解放しない() {
+ // Arrange: 24x4 → Cols*Channels() = 4 != 3 なので ExtendMat は src をそのまま返す
+ using (var src = new Mat(24, 4, MatType.CV_64FC1, new Scalar(1.0)))
+ using (var target = CreateScaledColorChart(rows: 24, scale: 1.1)) {
+ for (int row = 0; row < 24; row++) {
+ src.At(row, 1) = row + 1.0;
+ src.At(row, 2) = (row + 1) * 2.0;
+ src.At(row, 3) = (row + 1) * 3.0;
+ }
+ var corrector = new ExtendMatCapturingCorrector();
+
+ // Act
+ using (var conv = corrector.CalcConvertMatrix(src, target, channels: 4)) {
+ // Assert
+ Assert.AreSame(src, corrector.CapturedExtended[0], "パススルー入力では src がそのまま返る(前提確認)");
+ Assert.IsFalse(src.IsDisposed, "パススルー入力の src は解放されない(所有権は呼び出し側)");
+ }
+ }
+ }
+
+ ///
+ /// ConvertColor: ExtendMat のパススルー入力(3 列相当でない Mat は src をそのまま返す)でも
+ /// 入力 src を解放しないこと(無条件 Dispose による入力破壊の回帰防止)
+ ///
+ [Test]
+ public void ConvertColor_パススルー入力_入力Matを解放しない() {
+ // Arrange: 1x4 → Cols*Channels() = 4 != 3 なので ExtendMat は src をそのまま返す
+ using (var src = new Mat(1, 4, MatType.CV_64FC1, new Scalar(1.0)))
+ using (var conv = CreateIdentityLikeConvMat()) {
+ var corrector = new ExtendMatCapturingCorrector();
+
+ // Act
+ using (var dst = corrector.ConvertColor(src, conv)) {
+ // Assert
+ Assert.AreSame(src, corrector.CapturedExtended[0], "パススルー入力では src がそのまま返る(前提確認)");
+ Assert.IsFalse(src.IsDisposed, "パススルー入力の src は解放されない(所有権は呼び出し側)");
+ }
+ }
+ }
+
///
/// CalcTcc テスト用に,参照される File/* キーを持つ最小 Config を読み込む.
/// ファイル名は種類ごとに識別可能な文字列にし,コールバックの path で判別する.
diff --git a/TIASshot/ColorCorrection/ColorCorrector.cs b/TIASshot/ColorCorrection/ColorCorrector.cs
index 5ee6184..74bd7e8 100644
--- a/TIASshot/ColorCorrection/ColorCorrector.cs
+++ b/TIASshot/ColorCorrection/ColorCorrector.cs
@@ -217,11 +217,13 @@
}
///
- /// 行列の拡張 3次元→高次元(指定チャンネル数)
+ /// 行列の拡張 3次元→高次元(指定チャンネル数).
+ /// 3 列相当でない入力は拡張せず src をそのまま返す(呼び出し側は参照同一性で解放を判断する).
+ /// virtual はテスト用シーム(戻り値 Mat の解放検証のため.挙動は不変).
///
///
///
- internal Mat ExtendMat(Mat src, int channels) {
+ internal virtual Mat ExtendMat(Mat src, int channels) {
if (src.Cols * src.Channels() != 3) return src;
var dst = new Mat(src.Rows, channels, MatType.CV_64FC1);
Parallel.For(0, src.Rows, row => {