#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