Newer
Older
TIASShot / TIASshot.Tests / SelfTestChartTests.cs
#if SELFTEST
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using NUnit.Framework;
using OpenCvSharp;
using OpenCvSharp.Aruco;

namespace TIASshot.Tests {
    /// <summary>
    /// セルフテスト用の合成チャート(SelfTestChart)のテスト.
    ///
    /// 仕様(SelfTestChart のクラス仕様・verify-profile の calib-save 行):
    ///   - ChartDetector のチャート座標系(810x1545,ArUco Dict4X4_50 の ID 40 が上・41 が下,24 色票)どおりに描く
    ///   - 色票の色は参照値をチャネルごとの 1 次式で歪めた「カメラ色」
    ///   - 生成は決定的(同じ入力から同じ画像).実物の ChartDetector で検出でき,マスク位置が色票 ROI と一致する
    ///   - プレビュー用の単色フレームはフレーム番号で色が変わる(どのフレームのコピーかを画素値で判別できる)
    /// 本クラスは SELFTEST 構成(Debug|x64)でのみコンパイルされる.
    /// </summary>
    [TestFixture]
    public class SelfTestChartTests {

        private Mat _tcc;

        [SetUp]
        public void SetUp() {
            _tcc = CreateTcc();
        }

        [TearDown]
        public void TearDown() {
            _tcc?.Dispose();
        }

        // ---------------------------------------------------------------
        // 生成画像の形式と決定性
        // ---------------------------------------------------------------

        [Test]
        public void CreateChartFrame_フレームサイズと型_定数どおりのBGR3ch() {
            using (var frame = SelfTestChart.CreateChartFrame(_tcc)) {
                Assert.That(frame.Width, Is.EqualTo(SelfTestChart.FrameWidth));
                Assert.That(frame.Height, Is.EqualTo(SelfTestChart.FrameHeight));
                Assert.That(frame.Type(), Is.EqualTo(MatType.CV_8UC3));
            }
        }

        [Test]
        public void CreateChartFrame_同じ入力で2回生成_全画素一致() {
            using (var a = SelfTestChart.CreateChartFrame(_tcc))
            using (var b = SelfTestChart.CreateChartFrame(_tcc))
            using (var diff = new Mat()) {
                Cv2.Absdiff(a, b, diff);
                using (var gray = diff.Reshape(1)) {
                    Assert.That(Cv2.CountNonZero(gray), Is.EqualTo(0));
                }
            }
        }

        [Test]
        public void CreateChartFrame_チャートがフレーム内に収まる() {
            Assert.That(SelfTestChart.OffsetX, Is.GreaterThanOrEqualTo(0));
            Assert.That(SelfTestChart.OffsetY, Is.GreaterThanOrEqualTo(0));
            Assert.That(SelfTestChart.OffsetX + SelfTestChart.ChartWidth, Is.LessThanOrEqualTo(SelfTestChart.FrameWidth));
            Assert.That(SelfTestChart.OffsetY + SelfTestChart.ChartHeight, Is.LessThanOrEqualTo(SelfTestChart.FrameHeight));
        }

        [Test]
        public void ChartSize_ChartDetectorの座標系810x1545() {
            Assert.That(SelfTestChart.ChartWidth, Is.EqualTo(810));
            Assert.That(SelfTestChart.ChartHeight, Is.EqualTo(1545));
        }

        // ---------------------------------------------------------------
        // マーカー配置
        // ---------------------------------------------------------------

        [Test]
        public void CreateChartFrame_ArUco検出_ID40と41が1個ずつ見つかる() {
            var markers = DetectMarkers();
            Assert.That(markers.Keys.OrderBy(x => x).ToArray(), Is.EqualTo(new[] { 40, 41 }));
        }

        [Test]
        public void CreateChartFrame_ArUco検出_ID40が上でID41が下() {
            var markers = DetectMarkers();
            Assume.That(markers.ContainsKey(40) && markers.ContainsKey(41), Is.True);
            Assert.That(markers[40][3].Y, Is.LessThan(markers[41][3].Y));
        }

        [TestCase(40, 345, 130)]
        [TestCase(41, 345, 1200)]
        public void CreateChartFrame_ArUco検出_マーカー左上がチャート座標どおり(int id, int chartX, int chartY) {
            var markers = DetectMarkers();
            Assume.That(markers.ContainsKey(id), Is.True);
            var topLeft = markers[id][0];
            Assert.That(topLeft.X, Is.EqualTo(SelfTestChart.OffsetX + chartX).Within(1.5));
            Assert.That(topLeft.Y, Is.EqualTo(SelfTestChart.OffsetY + chartY).Within(1.5));
        }

        [TestCase(40)]
        [TestCase(41)]
        public void CreateChartFrame_ArUco検出_マーカーの一辺が120px(int id) {
            var markers = DetectMarkers();
            Assume.That(markers.ContainsKey(id), Is.True);
            var c = markers[id];
            Assert.That(c[1].X - c[0].X, Is.EqualTo(120).Within(2.0));
            Assert.That(c[3].Y - c[0].Y, Is.EqualTo(120).Within(2.0));
        }

        // ---------------------------------------------------------------
        // 色票の配置と色
        // ---------------------------------------------------------------

        [Test]
        public void PatchRoi_24個_すべてチャート内に収まる() {
            var chart = new Rect(SelfTestChart.OffsetX, SelfTestChart.OffsetY, SelfTestChart.ChartWidth, SelfTestChart.ChartHeight);
            for (var i = 0; i < 24; i++) {
                var roi = SelfTestChart.PatchRoi(i);
                Assert.That((roi & chart) == roi, Is.True, $"色票 {i} の ROI {roi} がチャート内にあること");
            }
        }

        [Test]
        public void PatchRoi_24個_互いに重ならない() {
            var rois = Enumerable.Range(0, 24).Select(SelfTestChart.PatchRoi).ToArray();
            for (var i = 0; i < 24; i++) {
                for (var j = i + 1; j < 24; j++) {
                    Assert.That((rois[i] & rois[j]).Width * (rois[i] & rois[j]).Height, Is.EqualTo(0), $"色票 {i} と {j}");
                }
            }
        }

        [Test]
        public void PatchRoi_ArUcoマーカーと重ならない() {
            var m40 = new Rect(SelfTestChart.OffsetX + 345, SelfTestChart.OffsetY + 130, 120, 120);
            var m41 = new Rect(SelfTestChart.OffsetX + 345, SelfTestChart.OffsetY + 1200, 120, 120);
            for (var i = 0; i < 24; i++) {
                var roi = SelfTestChart.PatchRoi(i);
                Assert.That((roi & m40).Width * (roi & m40).Height, Is.EqualTo(0), $"色票 {i} と ID40");
                Assert.That((roi & m41).Width * (roi & m41).Height, Is.EqualTo(0), $"色票 {i} と ID41");
            }
        }

        [Test]
        public void CreateChartFrame_各色票ROIの全画素_カメラ色で塗られている() {
            using (var frame = SelfTestChart.CreateChartFrame(_tcc)) {
                for (var i = 0; i < 24; i++) {
                    var expected = SelfTestChart.CameraColor(_tcc, i);
                    using (var roi = new Mat(frame, SelfTestChart.PatchRoi(i))) {
                        Cv2.MeanStdDev(roi, out var mean, out var stddev);
                        for (var ch = 0; ch < 3; ch++) {
                            Assert.That(mean[ch], Is.EqualTo(Math.Round(expected[ch])).Within(0.51), $"色票 {i} ch{ch} の平均");
                            Assert.That(stddev[ch], Is.EqualTo(0).Within(1e-9), $"色票 {i} ch{ch} は単色");
                        }
                    }
                }
            }
        }

        [Test]
        public void CameraColor_チャネルごとに1次式_等間隔の入力で等間隔の出力() {
            using (var tcc = new Mat(3, 3, MatType.CV_64FC1)) {
                for (var c = 0; c < 3; c++) {
                    tcc.Set(0, c, 20.0);
                    tcc.Set(1, c, 100.0);
                    tcc.Set(2, c, 180.0);
                }
                var v0 = SelfTestChart.CameraColor(tcc, 0);
                var v1 = SelfTestChart.CameraColor(tcc, 1);
                var v2 = SelfTestChart.CameraColor(tcc, 2);
                for (var c = 0; c < 3; c++) {
                    Assert.That(v2[c] - v1[c], Is.EqualTo(v1[c] - v0[c]).Within(1e-9), $"ch{c} の線形性");
                    Assert.That(v1[c], Is.GreaterThan(v0[c]), $"ch{c} は単調増加");
                }
            }
        }

        [Test]
        public void CameraColor_参照値を歪める_恒等変換ではない() {
            // 恒等だと色変換行列の検算にならない
            var differs = false;
            for (var i = 0; i < 24; i++) {
                var v = SelfTestChart.CameraColor(_tcc, i);
                for (var c = 0; c < 3; c++) {
                    if (Math.Abs(v[c] - _tcc.At<double>(i, c)) > 1.0) differs = true;
                }
            }
            Assert.That(differs, Is.True);
        }

        [TestCase(0.0)]
        [TestCase(255.0)]
        [TestCase(-500.0)]
        [TestCase(5000.0)]
        public void CameraColor_画素値の範囲0から255に収まる(double refValue) {
            using (var tcc = new Mat(1, 3, MatType.CV_64FC1, new Scalar(refValue))) {
                var v = SelfTestChart.CameraColor(tcc, 0);
                for (var c = 0; c < 3; c++) {
                    Assert.That(v[c], Is.InRange(0.0, 255.0), $"ch{c}");
                }
            }
        }

        // ---------------------------------------------------------------
        // 実物の ChartDetector との整合
        // ---------------------------------------------------------------

        [Test]
        public void ChartDetector_合成チャート_検出でき24枚のマスクが色票ROIと一致する() {
            var cfg = Path.GetTempFileName() + ".xml";
            File.WriteAllText(cfg,
                "<?xml version=\"1.0\" encoding=\"utf-8\"?><Config><Calib>" +
                "<ChartSetCriteria>4.0</ChartSetCriteria><ChartSetCount>3</ChartSetCount>" +
                "</Calib></Config>", System.Text.Encoding.UTF8);
            List<Mat> masks = null;
            try {
                Config.Load(cfg);
                var detector = new ChartDetector(_ => { });
                using (var frame = SelfTestChart.CreateChartFrame(_tcc)) {
                    // 固定判定(同じ位置が ChartSetCount 回続く)を満たすまで同じフレームを与える
                    for (var n = 0; n < 10 && masks == null; n++) masks = detector.DetectChart(frame);
                }
                Assert.That(masks, Is.Not.Null, "合成チャートが検出されること");
                Assert.That(masks.Count, Is.EqualTo(24));
                for (var i = 0; i < 24; i++) {
                    var expected = SelfTestChart.PatchRoi(i);
                    var actual = Cv2.BoundingRect(masks[i]);
                    Assert.That(actual.X, Is.EqualTo(expected.X).Within(3), $"色票 {i} の X");
                    Assert.That(actual.Y, Is.EqualTo(expected.Y).Within(3), $"色票 {i} の Y");
                    Assert.That(actual.Width, Is.EqualTo(expected.Width).Within(3), $"色票 {i} の幅");
                    Assert.That(actual.Height, Is.EqualTo(expected.Height).Within(3), $"色票 {i} の高さ");
                }
            } finally {
                if (masks != null) foreach (var m in masks) m.Dispose();
                if (File.Exists(cfg)) File.Delete(cfg);
            }
        }

        // ---------------------------------------------------------------
        // プレビュー用の単色フレーム
        // ---------------------------------------------------------------

        [Test]
        public void PlainFrameColor_プレビュー12フレーム分_すべて異なる色() {
            var colors = Enumerable.Range(0, 12).Select(SelfTestChart.PlainFrameColor)
                .Select(s => $"{s.Val0},{s.Val1},{s.Val2}").ToList();
            Assert.That(colors, Is.Unique);
        }

        [Test]
        public void PlainFrameColor_画素値の範囲に収まる() {
            for (var i = 0; i < 100; i++) {
                var s = SelfTestChart.PlainFrameColor(i);
                Assert.That(new[] { s.Val0, s.Val1, s.Val2 }, Is.All.InRange(0.0, 255.0), $"index {i}");
            }
        }

        [Test]
        public void CreatePlainFrame_指定サイズの単色BGR画像() {
            using (var m = SelfTestChart.CreatePlainFrame(5, 64, 48)) {
                Assert.That(m.Width, Is.EqualTo(64));
                Assert.That(m.Height, Is.EqualTo(48));
                Assert.That(m.Type(), Is.EqualTo(MatType.CV_8UC3));
                var expected = SelfTestChart.PlainFrameColor(5);
                var px = m.At<Vec3b>(47, 63);
                Assert.That(new double[] { px.Item0, px.Item1, px.Item2 },
                    Is.EqualTo(new[] { expected.Val0, expected.Val1, expected.Val2 }).Within(0.51));
            }
        }

        // ---------------------------------------------------------------
        // ヘルパ
        // ---------------------------------------------------------------

        /// <summary>TCC の sRGB 参照値に相当する 24x3(B,G,R・CV_64F)の値(決定的な擬似値)</summary>
        private static Mat CreateTcc() {
            var m = new Mat(24, 3, MatType.CV_64FC1);
            for (var i = 0; i < 24; i++) {
                m.Set(i, 0, 30.0 + (i * 37) % 200);
                m.Set(i, 1, 40.0 + (i * 53) % 190);
                m.Set(i, 2, 50.0 + (i * 71) % 180);
            }
            return m;
        }

        private Dictionary<int, Point2f[]> DetectMarkers() {
            var result = new Dictionary<int, Point2f[]>();
            using (var frame = SelfTestChart.CreateChartFrame(_tcc))
            using (var dict = CvAruco.GetPredefinedDictionary(PredefinedDictionaryName.Dict4X4_50)) {
                CvAruco.DetectMarkers(frame, dict, out var corners, out var ids, new DetectorParameters(), out _);
                for (var i = 0; i < ids.Length; i++) {
                    Assert.That(result.ContainsKey(ids[i]), Is.False, $"ID {ids[i]} が重複検出された");
                    result[ids[i]] = corners[i];
                }
            }
            return result;
        }
    }
}
#endif