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` を基準データとして保管する