diff --git a/TIASshot.Tests/ChartDetectorTests.cs b/TIASshot.Tests/ChartDetectorTests.cs new file mode 100644 index 0000000..99eb72a --- /dev/null +++ b/TIASshot.Tests/ChartDetectorTests.cs @@ -0,0 +1,278 @@ +using System.Collections.Generic; +using System.IO; +using NUnit.Framework; +using OpenCvSharp; +using OpenCvSharp.Aruco; +using ArucoDictionary = OpenCvSharp.Aruco.Dictionary; + +namespace TIASshot.Tests { + /// + /// ChartDetector のテスト(AR マーカー検出・上下判定・固定判定・マスク生成) + /// + /// fixture は実写画像ではなく,Dict4X4_50 の ID 40 / 41 をコードで描画した合成画像を使う. + /// DetectChart は「画像入力 → マスク出力」の純粋処理で,外部依存は + /// Config の Calib/ChartSetCriteria・Calib/ChartSetCount と showMessage コールバックだけであるため, + /// 合成画像で検出 API の等価性(ID・コーナー順序・幾何)を検証できる. + /// 実写のノイズ・照明ムラ・レンズ歪みは再現しないので,実機確認の代替ではない. + /// + /// Config は静的シングルトンのため [NonParallelizable] とし,各テストの SetUp で必ず読み直す. + /// + [TestFixture] + [NonParallelizable] + public class ChartDetectorTests { + + // 合成 fixture の寸法.ChartDetector が内部で使うモデル空間(810 x 1545)と同じにして + // ホモグラフィがほぼ恒等変換になるようにする + private const int ImageWidth = 810; + private const int ImageHeight = 1545; + + // マーカーの描画サイズ.ChartDetector の PointsDst40 / PointsDst41 が示す 120px 四方に合わせる + private const int MarkerSizePx = 120; + + // モデル空間上のマーカー位置(PointsDst40 / PointsDst41 の左上座標) + private const int Marker40X = 345; + private const int Marker40Y = 130; + private const int Marker41X = 345; + private const int Marker41Y = 1200; + + private const int MarkerId40 = 40; + private const int MarkerId41 = 41; + + // テスト用 Config の値 + private const int ChartSetCount = 3; + private const float ChartSetCriteria = 4.0f; + + // ChartSetCriteria を確実に超える移動量 + private const int MovementOverCriteria = 20; + + // ChartDetector が返すマスクの枚数(カラーチャートのパッチ数) + private const int ExpectedMaskCount = 24; + + private const string UpsideDownMessage = "舌診チャートが上下逆方向です"; + + private static readonly string TestConfigXml = + "" + + "" + + " " + + " " + ChartSetCriteria.ToString("0.0", System.Globalization.CultureInfo.InvariantCulture) + "" + + " " + ChartSetCount + "" + + " " + + ""; + + private string _tempConfigPath; + private List _messages; + + [SetUp] + public void SetUp() { + var tempPath = Path.GetTempFileName(); + _tempConfigPath = tempPath + ".xml"; + File.Move(tempPath, _tempConfigPath); + File.WriteAllText(_tempConfigPath, TestConfigXml, System.Text.Encoding.UTF8); + Config.Load(_tempConfigPath); + _messages = new List(); + } + + [TearDown] + public void TearDown() { + if (_tempConfigPath != null && File.Exists(_tempConfigPath)) File.Delete(_tempConfigPath); + } + + // --------------------------------------------------------------- + // テスト本体 + // --------------------------------------------------------------- + + /// + /// AR マーカーが 1 つも写っていない画像では検出が成立せず null を返す + /// + [Test] + public void DetectChart_マーカーが無い画像_nullを返す() { + var detector = new ChartDetector(RecordMessage); + using (var img = CreateBlankImage()) { + var masks = detector.DetectChart(img); + DisposeMasks(masks); + Assert.That(masks, Is.Null); + } + } + + /// + /// ID 40 だけが写っている場合はマーカー座標が 8 点に満たないため null を返す + /// + [Test] + public void DetectChart_ID40のみの画像_nullを返す() { + var detector = new ChartDetector(RecordMessage); + using (var img = CreateBlankImage()) { + DrawMarker(img, MarkerId40, Marker40X, Marker40Y); + var masks = detector.DetectChart(img); + DisposeMasks(masks); + Assert.That(masks, Is.Null); + } + } + + /// + /// ID 40 が ID 41 より下にある(チャートが上下逆)場合は null を返す + /// + [Test] + public void DetectChart_チャートが上下逆_nullを返す() { + var detector = new ChartDetector(RecordMessage); + using (var img = CreateUpsideDownChartImage()) { + var masks = detector.DetectChart(img); + DisposeMasks(masks); + Assert.That(masks, Is.Null); + } + } + + /// + /// ID 40 が ID 41 より下にある場合は上下逆である旨が呼び出し側に通知される + /// + [Test] + public void DetectChart_チャートが上下逆_上下逆を通知する() { + var detector = new ChartDetector(RecordMessage); + using (var img = CreateUpsideDownChartImage()) { + DisposeMasks(detector.DetectChart(img)); + Assert.That(_messages, Does.Contain(UpsideDownMessage)); + } + } + + /// + /// 固定判定が成立するまで(Calib/ChartSetCount フレーム分)は null を返す + /// + /// 1 フレーム目は _lastPosition が原点のため移動量が閾値を超えてカウントが 0 に落ちる. + /// したがってマスクが返るのは ChartSetCount + 1 フレーム目となる. + /// + [Test] + public void DetectChart_固定判定が成立するまで_nullを返す() { + var detector = new ChartDetector(RecordMessage); + using (var img = CreateChartImage(Marker40X, Marker40Y, Marker41X, Marker41Y)) { + for (int frame = 1; frame <= ChartSetCount; frame++) { + var masks = detector.DetectChart(img); + DisposeMasks(masks); + Assert.That(masks, Is.Null, $"{frame} フレーム目ではまだ固定判定は成立しないはず"); + } + } + } + + /// + /// マーカーが安定して検出され続けると,固定判定の成立フレームで 24 枚のマスクが返る + /// + [Test] + public void DetectChart_マーカーが安定検出_24枚のマスクを返す() { + var detector = new ChartDetector(RecordMessage); + using (var img = CreateChartImage(Marker40X, Marker40Y, Marker41X, Marker41Y)) { + var masks = DetectUntilStable(detector, img); + try { + Assert.That(masks, Has.Count.EqualTo(ExpectedMaskCount)); + } finally { + DisposeMasks(masks); + } + } + } + + /// + /// 返るマスクはすべて入力画像と同じサイズである(入力画像座標系のマスクとして使える) + /// + [Test] + public void DetectChart_返るマスク_入力画像と同じサイズ() { + var detector = new ChartDetector(RecordMessage); + using (var img = CreateChartImage(Marker40X, Marker40Y, Marker41X, Marker41Y)) { + var masks = DetectUntilStable(detector, img); + try { + Assert.That(masks, Is.All.Matches( + m => m.Width == ImageWidth && m.Height == ImageHeight)); + } finally { + DisposeMasks(masks); + } + } + } + + /// + /// マーカーが Calib/ChartSetCriteria を超えて動くと固定判定カウントがリセットされる + /// + /// 安定フレームを ChartSetCount 回与えた直後にマーカーを動かすと, + /// リセットが無ければそのフレームでマスクが返るが,リセットされるため null になる. + /// + [Test] + public void DetectChart_基準を超えて動く_固定判定がリセットされる() { + var detector = new ChartDetector(RecordMessage); + using (var stillImg = CreateChartImage(Marker40X, Marker40Y, Marker41X, Marker41Y)) + using (var movedImg = CreateChartImage( + Marker40X + MovementOverCriteria, Marker40Y, + Marker41X + MovementOverCriteria, Marker41Y)) { + for (int frame = 1; frame <= ChartSetCount; frame++) { + DisposeMasks(detector.DetectChart(stillImg)); + } + var masks = detector.DetectChart(movedImg); + DisposeMasks(masks); + Assert.That(masks, Is.Null); + } + } + + // --------------------------------------------------------------- + // ヘルパー + // --------------------------------------------------------------- + + private void RecordMessage(string message) { + _messages.Add(message); + } + + /// + /// 固定判定が成立するまで同じ画像を与え,成立フレームの結果を返す + /// + private static List DetectUntilStable(ChartDetector detector, Mat img) { + List masks = null; + for (int frame = 0; frame <= ChartSetCount; frame++) { + DisposeMasks(masks); + masks = detector.DetectChart(img); + } + return masks; + } + + private static void DisposeMasks(List masks) { + if (masks == null) return; + foreach (var mask in masks) mask.Dispose(); + } + + /// + /// マーカーを含まない白い BGR 画像を作る + /// + private static Mat CreateBlankImage() { + return new Mat(ImageHeight, ImageWidth, MatType.CV_8UC3, Scalar.All(255)); + } + + /// + /// ID 40(上)と ID 41(下)を指定位置に描いた合成チャート画像を作る + /// + private static Mat CreateChartImage(int x40, int y40, int x41, int y41) { + var img = CreateBlankImage(); + try { + DrawMarker(img, MarkerId40, x40, y40); + DrawMarker(img, MarkerId41, x41, y41); + return img; + } catch { + img.Dispose(); + throw; + } + } + + /// + /// ID 40 と ID 41 の上下を入れ替えた(チャートが上下逆の)合成画像を作る + /// + private static Mat CreateUpsideDownChartImage() { + return CreateChartImage(Marker40X, Marker41Y, Marker41X, Marker40Y); + } + + /// + /// Dict4X4_50 のマーカーを画像上の指定位置に描画する + /// + private static void DrawMarker(Mat canvas, int markerId, int x, int y) { + using (ArucoDictionary dict = CvAruco.GetPredefinedDictionary(PredefinedDictionaryType.Dict4X4_50)) + using (var marker = new Mat()) + using (var markerBgr = new Mat()) + using (var roi = new Mat(canvas, new Rect(x, y, MarkerSizePx, MarkerSizePx))) { + // borderBits = 1(Dict4X4_50 の既定).sidePixels には黒枠を含んだ全体サイズを指定する + dict.GenerateImageMarker(markerId, MarkerSizePx, marker, 1); + Cv2.CvtColor(marker, markerBgr, ColorConversionCodes.GRAY2BGR); + markerBgr.CopyTo(roi); + } + } + } +} diff --git a/TIASshot.Tests/TIASshot.Tests.csproj b/TIASshot.Tests/TIASshot.Tests.csproj index 5aec33b..f78955e 100644 --- a/TIASshot.Tests/TIASshot.Tests.csproj +++ b/TIASshot.Tests/TIASshot.Tests.csproj @@ -1,6 +1,6 @@ - + @@ -15,6 +15,8 @@ 512 + true + true true @@ -38,11 +40,23 @@ ..\packages\NUnit.3.14.0\lib\net45\nunit.framework.dll - - ..\packages\OpenCvSharp4.4.10.0.20241108\lib\net48\OpenCvSharp.dll + + ..\packages\OpenCvSharp4.4.13.0.20260627\lib\netstandard2.0\OpenCvSharp.dll + + ..\packages\System.Buffers.4.6.1\lib\net462\System.Buffers.dll + + + ..\packages\System.Memory.4.6.3\lib\net462\System.Memory.dll + + + ..\packages\System.Numerics.Vectors.4.6.1\lib\net462\System.Numerics.Vectors.dll + + + ..\packages\System.Runtime.CompilerServices.Unsafe.6.1.2\lib\net462\System.Runtime.CompilerServices.Unsafe.dll + @@ -65,6 +79,7 @@ + @@ -78,7 +93,9 @@ このプロジェクトは、このコンピューター上にない NuGet パッケージを参照しています。それらのパッケージをダウンロードするには、[NuGet パッケージの復元] を使用します。詳細については、http://go.microsoft.com/fwlink/?LinkID=322105 を参照してください。見つからないファイルは {0} です。 - + + + diff --git a/TIASshot.Tests/packages.config b/TIASshot.Tests/packages.config index 4648879..112e7d6 100644 --- a/TIASshot.Tests/packages.config +++ b/TIASshot.Tests/packages.config @@ -3,6 +3,10 @@ - - + + + + + + diff --git a/TIASshot/App.config b/TIASshot/App.config index 91126fc..f0f212b 100644 --- a/TIASshot/App.config +++ b/TIASshot/App.config @@ -6,8 +6,12 @@ + + + + - + diff --git a/TIASshot/Cameras/Lucam.cs b/TIASshot/Cameras/Lucam.cs index 176c590..f425ace 100644 --- a/TIASshot/Cameras/Lucam.cs +++ b/TIASshot/Cameras/Lucam.cs @@ -12,7 +12,6 @@ using System.Drawing; using System.Windows.Interop; using System.Threading; -using OpenCvSharp.Aruco; using System.Windows.Media.Animation; using System.Configuration; using System.Web.ModelBinding; diff --git a/TIASshot/ChartDetection/ChartDetector.cs b/TIASshot/ChartDetection/ChartDetector.cs index 94597b8..1b9fafd 100644 --- a/TIASshot/ChartDetection/ChartDetector.cs +++ b/TIASshot/ChartDetection/ChartDetector.cs @@ -10,7 +10,9 @@ /// internal class ChartDetector { - readonly Dictionary ARDict = CvAruco.GetPredefinedDictionary(PredefinedDictionaryName.Dict4X4_50); + // ARマーカー検出器(OpenCvSharp 4.13 以降は検出 API が ArucoDetector クラスに集約された). + // 辞書・検出パラメータは旧 CvAruco.DetectMarkers 呼び出し時と同一(Dict4X4_50 + DetectorParameters の既定値). + readonly ArucoDetector _arucoDetector; readonly Point2f[] PointsDst40 = new Point2f[] { new Point2f(345, 130),new Point2f(465, 130),new Point2f(465, 250),new Point2f(345, 250), }; @@ -25,6 +27,10 @@ public ChartDetector(Action showMessage) { _showMessage = showMessage; + // ArucoDetector は辞書を内部にコピーして保持するため,元の Dictionary はここで解放してよい + using (var dict = CvAruco.GetPredefinedDictionary(PredefinedDictionaryType.Dict4X4_50)) { + _arucoDetector = new ArucoDetector(dict, new DetectorParameters(), new RefineParameters()); + } } /// @@ -36,8 +42,7 @@ public List DetectChart(Mat img) { // ARマーカー検出 - CvAruco.DetectMarkers(img, ARDict, out var corners, out var ids, - new DetectorParameters(), out var rejectedImgPoints); + _arucoDetector.DetectMarkers(img, out var corners, out var ids, out var rejectedImgPoints); if (ids.Length < 1) return null; // マーカー座標格納 diff --git a/TIASshot/TIASshot.csproj b/TIASshot/TIASshot.csproj index 5d011ee..c7d2973 100644 --- a/TIASshot/TIASshot.csproj +++ b/TIASshot/TIASshot.csproj @@ -1,6 +1,6 @@  - + Debug @@ -66,35 +66,35 @@ .\lumenera.api.dll - - ..\packages\OpenCvSharp4.4.10.0.20241108\lib\net48\OpenCvSharp.dll + + ..\packages\OpenCvSharp4.4.13.0.20260627\lib\netstandard2.0\OpenCvSharp.dll - ..\packages\OpenCvSharp4.Extensions.4.10.0.20241108\lib\net48\OpenCvSharp.Extensions.dll + ..\packages\OpenCvSharp4.Extensions.4.13.0.20260213\lib\net48\OpenCvSharp.Extensions.dll - ..\packages\OpenCvSharp4.WpfExtensions.4.10.0.20241108\lib\net48\OpenCvSharp.WpfExtensions.dll + ..\packages\OpenCvSharp4.WpfExtensions.4.13.0.20260213\lib\net48\OpenCvSharp.WpfExtensions.dll - - ..\packages\System.Buffers.4.5.1\lib\net461\System.Buffers.dll + + ..\packages\System.Buffers.4.6.1\lib\net462\System.Buffers.dll ..\packages\System.Drawing.Common.8.0.0\lib\net462\System.Drawing.Common.dll - - ..\packages\System.Memory.4.5.5\lib\net461\System.Memory.dll + + ..\packages\System.Memory.4.6.3\lib\net462\System.Memory.dll - - ..\packages\System.Numerics.Vectors.4.5.0\lib\net46\System.Numerics.Vectors.dll + + ..\packages\System.Numerics.Vectors.4.6.1\lib\net462\System.Numerics.Vectors.dll - - ..\packages\System.Runtime.CompilerServices.Unsafe.6.0.0\lib\net461\System.Runtime.CompilerServices.Unsafe.dll + + ..\packages\System.Runtime.CompilerServices.Unsafe.6.1.2\lib\net462\System.Runtime.CompilerServices.Unsafe.dll @@ -199,7 +199,8 @@ このプロジェクトは、このコンピューター上にない NuGet パッケージを参照しています。それらのパッケージをダウンロードするには、[NuGet パッケージの復元] を使用します。詳細については、http://go.microsoft.com/fwlink/?LinkID=322105 を参照してください。見つからないファイルは {0} です。 - + + copy /Y $(ProjectDir)config.xml $(TargetDir) @@ -210,4 +211,5 @@ copy /Y $(ProjectDir)*.wav $(TargetDir) - \ No newline at end of file + + diff --git a/TIASshot/packages.config b/TIASshot/packages.config index 83f0b5d..86ea2ce 100644 --- a/TIASshot/packages.config +++ b/TIASshot/packages.config @@ -1,14 +1,14 @@  - - - - - - + + + + + + - - - + + + \ No newline at end of file diff --git "a/docs/02_ENV/ENV_01_\346\212\200\350\241\223\343\202\271\343\202\277\343\203\203\343\202\257.md" "b/docs/02_ENV/ENV_01_\346\212\200\350\241\223\343\202\271\343\202\277\343\203\203\343\202\257.md" index 7a9aff8..1d7af4a 100644 --- "a/docs/02_ENV/ENV_01_\346\212\200\350\241\223\343\202\271\343\202\277\343\203\203\343\202\257.md" +++ "b/docs/02_ENV/ENV_01_\346\212\200\350\241\223\343\202\271\343\202\277\343\203\203\343\202\257.md" @@ -11,21 +11,25 @@ **OpenCvSharp4 関連** -- `OpenCvSharp4` (4.10.0): OpenCV の C# ラッパー本体 -- `OpenCvSharp4.Extensions` (4.10.0): Bitmap 変換等の拡張機能 -- `OpenCvSharp4.runtime.win` (4.10.0): Windows 向けネイティブランタイム -- `OpenCvSharp4.Windows` (4.10.0): Windows Forms 向け統合パッケージ -- `OpenCvSharp4.WpfExtensions` (4.10.0): WPF 向け拡張機能 +- `OpenCvSharp4` (4.13.0): OpenCV の C# ラッパー本体 +- `OpenCvSharp4.Extensions` (4.13.0): Bitmap 変換等の拡張機能 +- `OpenCvSharp4.runtime.win` (4.13.0): Windows 向けネイティブランタイム +- `OpenCvSharp4.Windows` (4.13.0): Windows Forms 向け統合パッケージ +- `OpenCvSharp4.WpfExtensions` (4.13.0): WPF 向け拡張機能 + +※ 4.13.0 から本体アセンブリの供給が `lib/net48` から `lib/netstandard2.0` に変わった.これにより `System.Memory` 等への推移的依存が新たに発生するため,下記の補完ライブラリを**テストプロジェクトにも**明示的に参照させる必要がある(無いと `Mat` 操作時に実行時 `FileNotFoundException` になる). **.NET 補完ライブラリ** -- `System.Buffers` (4.5.1) +- `System.Buffers` (4.6.1) - `System.Drawing.Common` (8.0.0) -- `System.Memory` (4.5.5) -- `System.Numerics.Vectors` (4.5.0) -- `System.Runtime.CompilerServices.Unsafe` (6.0.0) +- `System.Memory` (4.6.3) +- `System.Numerics.Vectors` (4.6.1) +- `System.Runtime.CompilerServices.Unsafe` (6.1.2) - `System.ValueTuple` (4.5.0) +※ `System.Drawing.Common` と `System.ValueTuple` を除く 4 件は OpenCvSharp 4.13 の推移的依存であり,バージョンは相互に不可分(`System.Memory` 4.6.3 が他 3 件の下限を指定している).個別に上げ下げしないこと. + ## テストプロジェクト TIASshot.Tests (Test Project) テストプロジェクトも `packages.config` で管理する(本体と同じレガシー形式・x64). @@ -33,7 +37,8 @@ - `NUnit` (3.14.0): テストフレームワーク - `NUnit3TestAdapter` (4.5.0): テストランナー連携(Visual Studio / vstest) - `Microsoft.NET.Test.Sdk` (17.8.0): テスト実行基盤 -- ※ `OpenCvSharp4` / `OpenCvSharp4.runtime.win` (4.10.0) も参照する.`Mat` 演算はネイティブ DLL に依存するため,テストプロジェクトも **x64** でビルドする. +- ※ `OpenCvSharp4` / `OpenCvSharp4.runtime.win` (4.13.0) も参照する.`Mat` 演算はネイティブ DLL に依存するため,テストプロジェクトも **x64** でビルドする. +- ※ あわせて `System.Buffers` / `System.Memory` / `System.Numerics.Vectors` / `System.Runtime.CompilerServices.Unsafe` を本体と同一バージョンで参照する(OpenCvSharp 4.13 の推移的依存.`packages.config` 方式では推移的依存が自動解決されないため明示が要る). ## 外部 SDK(別途インストールが必要) (External SDKs) diff --git "a/docs/06_TEST/TEST_01_\343\203\206\343\202\271\343\203\210\344\273\225\346\247\230.md" "b/docs/06_TEST/TEST_01_\343\203\206\343\202\271\343\203\210\344\273\225\346\247\230.md" index b7c4650..0af3d99 100644 --- "a/docs/06_TEST/TEST_01_\343\203\206\343\202\271\343\203\210\344\273\225\346\247\230.md" +++ "b/docs/06_TEST/TEST_01_\343\203\206\343\202\271\343\203\210\344\273\225\346\247\230.md" @@ -54,6 +54,8 @@ 保存済みの実画像を入力として,チャートマスクの位置を比較する. +※ 2026-08-29 に自動テスト `ChartDetectorTests` を追加した(合成した AR マーカー画像を入力し,早期 return・上下逆判定・固定判定・24 枚のマスク生成を検証する).**本節の実機による目視比較を置き換えるものではない**.自動テストが担保するのは検出ロジックの分岐と幾何であり,実写のノイズ・照明ムラ・レンズ歪み・実際のマーカー印刷品質に対する検出性能は実機でしか確認できない.OpenCvSharp のネイティブ側更新(例: 4.10 → 4.13)を行ったときは,自動テストが緑でも本節の実機比較を必ず実施すること. + ### 基準データの作成(リファクタリング前に実施) 1. キャリブレーション時に保存される `TCC_ROIs.jpg` を基準データとして保管する