Newer
Older
TIASShot / TIASshot.Tests / PatchColorErrorTests.cs
#if SELFTEST
using System;
using System.IO;
using NUnit.Framework;
using OpenCvSharp;

namespace TIASshot.Tests {
    /// <summary>
    /// 色票の測定値と参照値(tcc_srgb)の差の集計(PatchColorError)のテスト(層①.純粋関数).
    ///
    /// 仕様: PLAN_06「チャートが必要」の calibrate(変換後の色票と参照値の色差)と合意済みの判定方法
    ///   - 値の並びは 1 行 = 1 色票,列 0 = B・列 1 = G・列 2 = R(8bit 階調)
    ///   - 色の誤差はチャネルごとの |測定 − 参照|.平均の分母は 色票の数 × 3.NaN があれば平均・最大とも NaN
    ///   - 白色票は参照値の BGR の和が最大の色票(同値なら先の行).tcc_srgb.csv では 0 始まりの 12(13 番)
    ///   - 白色票を除いた最大(範囲外の除外指定は何も除かない.残りが無ければ NaN と -1)
    ///   - 参考の ΔE は CIE76(sRGB D65 → CIELAB).0〜255 の外は丸めずにガンマの式を符号つきで延長する
    /// 本クラスは SELFTEST 構成(Debug|x64)でのみコンパイルされる.
    /// </summary>
    [TestFixture]
    public class PatchColorErrorTests {

        private const double Eps = 1e-9;

        // ================================================================
        // Compare
        // ================================================================

        private static readonly double[,] Measured2 = { { 10, 20, 30 }, { 40, 50, 60 } };
        // 差の絶対値: 1, 2, 0 / 0, 0, 6
        private static readonly double[,] Reference2 = { { 11, 18, 30 }, { 40, 50, 66 } };

        [Test]
        public void Compare_色票の数() {
            Assert.That(PatchColorError.Compare(Measured2, Reference2).Patches, Is.EqualTo(2));
        }

        [Test]
        public void Compare_平均_分母は色票の数かける3() {
            // (1 + 2 + 0 + 0 + 0 + 6) / (2 × 3) = 1.5
            Assert.That(PatchColorError.Compare(Measured2, Reference2).MeanAbs, Is.EqualTo(1.5).Within(Eps));
        }

        [Test]
        public void Compare_最大_チャネルごとの差の絶対値の最大() {
            Assert.That(PatchColorError.Compare(Measured2, Reference2).MaxAbs, Is.EqualTo(6.0).Within(Eps));
        }

        [Test]
        public void Compare_最大の色票_0始まりの番号() {
            Assert.That(PatchColorError.Compare(Measured2, Reference2).MaxAbsPatch, Is.EqualTo(1));
        }

        [Test]
        public void Compare_測定が参照より小さい_差は絶対値() {
            var s = PatchColorError.Compare(new double[,] { { 0, 0, 0 } }, new double[,] { { 3, 3, 3 } });
            Assert.That(s.MeanAbs, Is.EqualTo(3.0).Within(Eps));
        }

        [Test]
        public void Compare_一致_平均は0() {
            Assert.That(PatchColorError.Compare(Measured2, Measured2).MeanAbs, Is.EqualTo(0.0).Within(Eps));
        }

        [Test]
        public void Compare_一致_DeltaEの最大は0() {
            Assert.That(PatchColorError.Compare(Measured2, Measured2).MaxDeltaE, Is.EqualTo(0.0).Within(Eps));
        }

        [Test]
        public void Compare_白と黒_DeltaEの平均は100() {
            var s = PatchColorError.Compare(new double[,] { { 255, 255, 255 } }, new double[,] { { 0, 0, 0 } });
            Assert.That(s.MeanDeltaE, Is.EqualTo(100.0).Within(0.01));
        }

        [Test]
        public void Compare_DeltaEの最大の色票_0始まりの番号() {
            var s = PatchColorError.Compare(new double[,] { { 100, 100, 100 }, { 255, 255, 255 } },
                new double[,] { { 100, 100, 100 }, { 0, 0, 0 } });
            Assert.That(s.MaxDeltaEPatch, Is.EqualTo(1));
        }

        [Test]
        public void Compare_NaNを含む_平均はNaN() {
            var measured = new double[,] { { 10, double.NaN, 30 }, { 40, 50, 60 } };
            Assert.That(PatchColorError.Compare(measured, Reference2).MeanAbs, Is.NaN);
        }

        [Test]
        public void Compare_NaNを含む_最大はNaN() {
            // NaN の後に大きい差(6)があっても最大は NaN(誤差を見逃さない)
            var measured = new double[,] { { 10, double.NaN, 30 }, { 40, 50, 60 } };
            Assert.That(PatchColorError.Compare(measured, Reference2).MaxAbs, Is.NaN);
        }

        [Test]
        public void Compare_最後の色票にNaN_最大はNaN() {
            var measured = new double[,] { { 0, 0, 0 }, { 40, 50, double.NaN } };
            var reference = new double[,] { { 100, 100, 100 }, { 40, 50, 60 } };
            Assert.That(PatchColorError.Compare(measured, reference).MaxAbs, Is.NaN);
        }

        [Test]
        public void Compare_参照値にNaN_最大はNaN() {
            var reference = new double[,] { { 11, 18, 30 }, { double.NaN, 50, 66 } };
            Assert.That(PatchColorError.Compare(Measured2, reference).MaxAbs, Is.NaN);
        }

        [Test]
        public void Compare_列が3でない_ArgumentException() {
            Assert.Throws<ArgumentException>((Action)(() =>
                PatchColorError.Compare(new double[2, 4], new double[2, 4])));
        }

        [Test]
        public void Compare_行数が違う_ArgumentException() {
            Assert.Throws<ArgumentException>((Action)(() =>
                PatchColorError.Compare(new double[2, 3], new double[3, 3])));
        }

        [Test]
        public void Compare_0行_ArgumentException() {
            Assert.Throws<ArgumentException>((Action)(() =>
                PatchColorError.Compare(new double[0, 3], new double[0, 3])));
        }

        [Test]
        public void Compare_測定がnull_ArgumentNullException() {
            Assert.Throws<ArgumentNullException>((Action)(() => PatchColorError.Compare(null, Reference2)));
        }

        [Test]
        public void Compare_参照がnull_ArgumentNullException() {
            Assert.Throws<ArgumentNullException>((Action)(() => PatchColorError.Compare(Measured2, null)));
        }

        // ================================================================
        // SrgbToLab・DeltaE76
        // ================================================================

        [Test]
        public void SrgbToLab_白_Lは100() {
            PatchColorError.SrgbToLab(255, 255, 255, out var l, out _, out _);
            Assert.That(l, Is.EqualTo(100.0).Within(0.01));
        }

        [Test]
        public void SrgbToLab_白_aとbは0() {
            PatchColorError.SrgbToLab(255, 255, 255, out _, out var a, out var b);
            Assert.That(new[] { a, b }, Is.EqualTo(new[] { 0.0, 0.0 }).Within(0.01));
        }

        [Test]
        public void SrgbToLab_黒_Labはすべて0() {
            PatchColorError.SrgbToLab(0, 0, 0, out var l, out var a, out var b);
            Assert.That(new[] { l, a, b }, Is.EqualTo(new[] { 0.0, 0.0, 0.0 }).Within(1e-6));
        }

        [Test]
        public void SrgbToLab_中間の灰色128_Lは約53_59() {
            // sRGB (128,128,128) → L* 53.585(D65)
            PatchColorError.SrgbToLab(128, 128, 128, out var l, out _, out _);
            Assert.That(l, Is.EqualTo(53.585).Within(0.01));
        }

        [Test]
        public void SrgbToLab_赤_既知のLab() {
            // sRGB (255,0,0) → L* 53.24, a* 80.09, b* 67.20(D65)
            PatchColorError.SrgbToLab(255, 0, 0, out var l, out var a, out var b);
            Assert.That(new[] { l, a, b }, Is.EqualTo(new[] { 53.24, 80.09, 67.20 }).Within(0.05));
        }

        [Test]
        public void SrgbToLab_青_既知のLab() {
            // sRGB (0,0,255) → L* 32.30, a* 79.19, b* -107.86(D65)
            PatchColorError.SrgbToLab(0, 0, 255, out var l, out var a, out var b);
            Assert.That(new[] { l, a, b }, Is.EqualTo(new[] { 32.30, 79.19, -107.86 }).Within(0.05));
        }

        [Test]
        public void SrgbToLab_255を超える値_丸めずにLが100を超える() {
            PatchColorError.SrgbToLab(300, 300, 300, out var l, out _, out _);
            Assert.That(l, Is.GreaterThan(100.0));
        }

        [Test]
        public void SrgbToLab_0未満の値_丸めずにLが負() {
            PatchColorError.SrgbToLab(-10, -10, -10, out var l, out _, out _);
            Assert.That(l, Is.LessThan(0.0));
        }

        [Test]
        public void SrgbToLab_0未満の値_符号つきで延長する() {
            // 暗部(L* の線形部分)では,符号を反転した入力の L* は符号が反転する
            PatchColorError.SrgbToLab(10, 10, 10, out var lPos, out _, out _);
            PatchColorError.SrgbToLab(-10, -10, -10, out var lNeg, out _, out _);
            Assert.That(lNeg, Is.EqualTo(-lPos).Within(1e-9));
        }

        [Test]
        public void DeltaE76_同じ色_0() {
            Assert.That(PatchColorError.DeltaE76(50, 120, 200, 50, 120, 200), Is.EqualTo(0.0).Within(Eps));
        }

        [Test]
        public void DeltaE76_白と黒_100() {
            Assert.That(PatchColorError.DeltaE76(255, 255, 255, 0, 0, 0), Is.EqualTo(100.0).Within(0.01));
        }

        [Test]
        public void DeltaE76_引数はBGRの順_赤と黒() {
            // BGR (0,0,255) = 赤.Lab (53.24, 80.09, 67.20) と黒の距離 ≒ 117.34
            Assert.That(PatchColorError.DeltaE76(0, 0, 255, 0, 0, 0), Is.EqualTo(117.34).Within(0.1));
        }

        [Test]
        public void DeltaE76_対称() {
            Assert.That(PatchColorError.DeltaE76(10, 200, 30, 90, 80, 70),
                Is.EqualTo(PatchColorError.DeltaE76(90, 80, 70, 10, 200, 30)).Within(Eps));
        }

        // ================================================================
        // ToArray
        // ================================================================

        [Test]
        public void ToArray_nかける3のMat_同じ値の配列() {
            using (var m = new Mat(2, 3, MatType.CV_64FC1)) {
                m.Set(0, 0, 1.5); m.Set(0, 1, 2.5); m.Set(0, 2, 3.5);
                m.Set(1, 0, 4.5); m.Set(1, 1, 5.5); m.Set(1, 2, 6.5);
                Assert.That(PatchColorError.ToArray(m), Is.EqualTo(new double[,] { { 1.5, 2.5, 3.5 }, { 4.5, 5.5, 6.5 } }));
            }
        }

        [Test]
        public void ToArray_列が3でない_ArgumentException() {
            using (var m = new Mat(2, 4, MatType.CV_64FC1, Scalar.All(0))) {
                Assert.Throws<ArgumentException>((Action)(() => PatchColorError.ToArray(m)));
            }
        }

        [Test]
        public void ToArray_3チャネルのMat_ArgumentException() {
            using (var m = new Mat(2, 3, MatType.CV_64FC3, Scalar.All(0))) {
                Assert.Throws<ArgumentException>((Action)(() => PatchColorError.ToArray(m)));
            }
        }

        [Test]
        public void ToArray_null_ArgumentNullException() {
            Assert.Throws<ArgumentNullException>((Action)(() => PatchColorError.ToArray(null)));
        }

        // ================================================================
        // WhitePatchIndex
        // ================================================================

        [Test]
        public void WhitePatchIndex_BGRの和が最大の行() {
            var reference = new double[,] { { 100, 100, 100 }, { 200, 210, 220 }, { 250, 10, 10 } };
            Assert.That(PatchColorError.WhitePatchIndex(reference), Is.EqualTo(1));
        }

        [Test]
        public void WhitePatchIndex_和が同じ_先の行() {
            var reference = new double[,] { { 10, 10, 10 }, { 200, 200, 200 }, { 100, 200, 300 } };
            Assert.That(PatchColorError.WhitePatchIndex(reference), Is.EqualTo(1));
        }

        [Test]
        public void WhitePatchIndex_1行_0() {
            Assert.That(PatchColorError.WhitePatchIndex(new double[,] { { 1, 2, 3 } }), Is.EqualTo(0));
        }

        [Test]
        public void WhitePatchIndex_同梱のtcc_srgb_12() {
            var path = Path.GetFullPath(Path.Combine(TestContext.CurrentContext.TestDirectory, "..", "..", "..", "..", "TIASshot", "tcc_srgb.csv"));
            Assert.That(File.Exists(path), Is.True, $"tcc_srgb.csv が見つからない: {path}");
            using (var m = IoUtil.LoadMatFromCsv(path)) {
                Assert.That(PatchColorError.WhitePatchIndex(PatchColorError.ToArray(m)), Is.EqualTo(12));
            }
        }

        [Test]
        public void WhitePatchIndex_null_ArgumentNullException() {
            Assert.Throws<ArgumentNullException>((Action)(() => PatchColorError.WhitePatchIndex(null)));
        }

        [Test]
        public void WhitePatchIndex_列が3でない_ArgumentException() {
            Assert.Throws<ArgumentException>((Action)(() => PatchColorError.WhitePatchIndex(new double[2, 2])));
        }

        [Test]
        public void WhitePatchIndex_0行_ArgumentException() {
            Assert.Throws<ArgumentException>((Action)(() => PatchColorError.WhitePatchIndex(new double[0, 3])));
        }

        [Test]
        public void WhitePatchIndex_NaNを含む_ArgumentException() {
            var reference = new double[,] { { 100, 100, 100 }, { double.NaN, 0, 0 } };
            Assert.Throws<ArgumentException>((Action)(() => PatchColorError.WhitePatchIndex(reference)));
        }

        // ================================================================
        // PatchMaxAbs・MaxAbsExcluding
        // ================================================================

        // 色票ごとの差の最大: 2(0 番)・24.65(1 番=白色票に見立てる)・3.47(2 番)
        private static readonly double[,] Ref3 = { { 100, 100, 100 }, { 221.5, 220.1, 228.5 }, { 50, 60, 70 } };
        private static readonly double[,] Meas3 = { { 101, 98, 100 }, { 196.85, 212.2, 211.9 }, { 50, 63.47, 69 } };

        [Test]
        public void PatchMaxAbs_その色票のチャネルごとの差の最大() {
            Assert.That(PatchColorError.PatchMaxAbs(Meas3, Ref3, 1), Is.EqualTo(24.65).Within(1e-9));
        }

        [Test]
        public void PatchMaxAbs_一致_0() {
            Assert.That(PatchColorError.PatchMaxAbs(Ref3, Ref3, 0), Is.EqualTo(0.0));
        }

        [Test]
        public void PatchMaxAbs_NaNを含む_NaN() {
            var measured = new double[,] { { double.NaN, 0, 0 } };
            Assert.That(PatchColorError.PatchMaxAbs(measured, new double[,] { { 0, 0, 100 } }, 0), Is.NaN);
        }

        [TestCase(-1)]
        [TestCase(3)]
        public void PatchMaxAbs_番号が範囲外_ArgumentOutOfRangeException(int patch) {
            Assert.Throws<ArgumentOutOfRangeException>((Action)(() => PatchColorError.PatchMaxAbs(Meas3, Ref3, patch)));
        }

        [Test]
        public void PatchMaxAbs_行数が違う_ArgumentException() {
            Assert.Throws<ArgumentException>((Action)(() => PatchColorError.PatchMaxAbs(new double[2, 3], new double[3, 3], 0)));
        }

        [Test]
        public void MaxAbsExcluding_白色票を除いた最大() {
            Assert.That(PatchColorError.MaxAbsExcluding(Meas3, Ref3, 1, out _), Is.EqualTo(3.47).Within(1e-9));
        }

        [Test]
        public void MaxAbsExcluding_白色票を除いた最大の色票() {
            PatchColorError.MaxAbsExcluding(Meas3, Ref3, 1, out var maxPatch);
            Assert.That(maxPatch, Is.EqualTo(2));
        }

        [TestCase(-1)]
        [TestCase(3)]
        public void MaxAbsExcluding_範囲外の除外指定_何も除かない(int excluded) {
            Assert.That(PatchColorError.MaxAbsExcluding(Meas3, Ref3, excluded, out _), Is.EqualTo(24.65).Within(1e-9));
        }

        [Test]
        public void MaxAbsExcluding_範囲外の除外指定_最大の色票は全体の最大() {
            PatchColorError.MaxAbsExcluding(Meas3, Ref3, -1, out var maxPatch);
            Assert.That(maxPatch, Is.EqualTo(1));
        }

        [Test]
        public void MaxAbsExcluding_除外の結果残りが無い_NaN() {
            Assert.That(PatchColorError.MaxAbsExcluding(new double[,] { { 1, 2, 3 } }, new double[,] { { 0, 0, 0 } }, 0, out _), Is.NaN);
        }

        [Test]
        public void MaxAbsExcluding_除外の結果残りが無い_最大の色票はマイナス1() {
            PatchColorError.MaxAbsExcluding(new double[,] { { 1, 2, 3 } }, new double[,] { { 0, 0, 0 } }, 0, out var maxPatch);
            Assert.That(maxPatch, Is.EqualTo(-1));
        }

        [Test]
        public void MaxAbsExcluding_残りにNaN_NaN() {
            var measured = new double[,] { { 101, 98, 100 }, { 0, 0, 0 }, { double.NaN, 60, 70 } };
            Assert.That(PatchColorError.MaxAbsExcluding(measured, Ref3, 1, out _), Is.NaN);
        }

        [Test]
        public void MaxAbsExcluding_除いた色票にだけNaN_NaNにならない() {
            var measured = new double[,] { { 101, 98, 100 }, { double.NaN, 0, 0 }, { 50, 63.47, 69 } };
            Assert.That(PatchColorError.MaxAbsExcluding(measured, Ref3, 1, out _), Is.EqualTo(3.47).Within(1e-9));
        }

        [Test]
        public void MaxAbsExcluding_除いた色票が全体の最大_白色票と白色票以外の最大の組がCompareの最大と一致する() {
            // 白色票を除いた最大と白色票の最大の大きい方 = 全体の最大
            var whole = PatchColorError.Compare(Meas3, Ref3).MaxAbs;
            var other = PatchColorError.MaxAbsExcluding(Meas3, Ref3, 1, out _);
            var white = PatchColorError.PatchMaxAbs(Meas3, Ref3, 1);
            Assert.That(Math.Max(other, white), Is.EqualTo(whole).Within(1e-9));
        }

        [Test]
        public void MaxAbsExcluding_形状が違う_ArgumentException() {
            Assert.Throws<ArgumentException>((Action)(() => PatchColorError.MaxAbsExcluding(new double[2, 3], new double[2, 4], 0, out _)));
        }

        [Test]
        public void MaxAbsExcluding_null_ArgumentNullException() {
            Assert.Throws<ArgumentNullException>((Action)(() => PatchColorError.MaxAbsExcluding(null, Ref3, 0, out _)));
        }
    }
}
#endif