diff --git a/CLAUDE.md b/CLAUDE.md
index 5bdb8c6..bb332a1 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -4,7 +4,7 @@
## 開発進捗
-最新: ヘッドレスのセルフテスト(`--selftest`)と検証プロファイルを導入し,`/verify`・Phase 1b で依存ロード・プレビュー経路・校正〜保存を自動確認できるようにした.#31(ArUco API 移行)は実機確認待ちの PR #36 として引き続き保留中
+最新: OpenCvSharp 4.13 の ArUco API 移行(#31・PR #36)を main に追従させて実機確認のうえマージした
※ 本欄は**最新ステップ 1 行のみ上書き更新**.詳細な進捗履歴(動機・設計判断・失敗パターン)は [docs/PROGRESS.md](docs/PROGRESS.md) に追記する.運用ルールは `.claude/rules/progress-log.md` を参照(該当ファイル編集時に自動ロードされる).
## 必須ルール(コード実装時)
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/SelfTestChartTests.cs b/TIASshot.Tests/SelfTestChartTests.cs
index 4d29ac5..a46dfa3 100644
--- a/TIASshot.Tests/SelfTestChartTests.cs
+++ b/TIASshot.Tests/SelfTestChartTests.cs
@@ -288,8 +288,9 @@
private Dictionary DetectMarkers() {
var result = new Dictionary();
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 _);
+ using (var dict = CvAruco.GetPredefinedDictionary(PredefinedDictionaryType.Dict4X4_50))
+ using (var detector = new ArucoDetector(dict, new DetectorParameters(), new RefineParameters())) {
+ detector.DetectMarkers(frame, out var corners, out var ids, 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];
diff --git a/TIASshot.Tests/TIASshot.Tests.csproj b/TIASshot.Tests/TIASshot.Tests.csproj
index 3e74bf7..2ab809f 100644
--- a/TIASshot.Tests/TIASshot.Tests.csproj
+++ b/TIASshot.Tests/TIASshot.Tests.csproj
@@ -4,7 +4,7 @@
-
+
Debug
@@ -18,6 +18,8 @@
512
+ true
+ true
true
@@ -46,8 +48,8 @@
..\packages\NUnit.4.6.1\lib\net462\nunit.framework.dll
-
- ..\packages\OpenCvSharp4.4.10.0.20241108\lib\net48\OpenCvSharp.dll
+
+ ..\packages\OpenCvSharp4.4.13.0.20260627\lib\netstandard2.0\OpenCvSharp.dll
@@ -93,6 +95,7 @@
+
@@ -109,7 +112,8 @@
このプロジェクトは、このコンピューター上にない NuGet パッケージを参照しています。それらのパッケージをダウンロードするには、[NuGet パッケージの復元] を使用します。詳細については、http://go.microsoft.com/fwlink/?LinkID=322105 を参照してください。見つからないファイルは {0} です。
-
+
+
@@ -121,4 +125,5 @@
+
\ No newline at end of file
diff --git a/TIASshot.Tests/packages.config b/TIASshot.Tests/packages.config
index e00ab8d..9527e71 100644
--- a/TIASshot.Tests/packages.config
+++ b/TIASshot.Tests/packages.config
@@ -4,8 +4,8 @@
-
-
+
+
diff --git a/TIASshot/App.config b/TIASshot/App.config
index c9521b1..f08cfd6 100644
--- a/TIASshot/App.config
+++ b/TIASshot/App.config
@@ -6,6 +6,10 @@
+
+
+
+
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/SelfTest/SelfTestChart.cs b/TIASshot/SelfTest/SelfTestChart.cs
index e450f6a..18721ea 100644
--- a/TIASshot/SelfTest/SelfTestChart.cs
+++ b/TIASshot/SelfTest/SelfTestChart.cs
@@ -83,7 +83,7 @@
}
private static void DrawMarker(Mat frame, int id, Point chartPos) {
- using (var dict = CvAruco.GetPredefinedDictionary(PredefinedDictionaryName.Dict4X4_50))
+ using (var dict = CvAruco.GetPredefinedDictionary(PredefinedDictionaryType.Dict4X4_50))
using (var marker = new Mat())
using (var markerBgr = new Mat()) {
dict.GenerateImageMarker(id, MarkerSize, marker, 1);
diff --git a/TIASshot/SelfTest/SelfTestRunner.cs b/TIASshot/SelfTest/SelfTestRunner.cs
index 22dc475..39dbc15 100644
--- a/TIASshot/SelfTest/SelfTestRunner.cs
+++ b/TIASshot/SelfTest/SelfTestRunner.cs
@@ -518,7 +518,7 @@
}
}
- using (var dict = CvAruco.GetPredefinedDictionary(PredefinedDictionaryName.Dict4X4_50)) {
+ using (var dict = CvAruco.GetPredefinedDictionary(PredefinedDictionaryType.Dict4X4_50)) {
c.Expect(dict.MarkerSize == 4, $"ArUco 辞書のマーカーサイズが不正: {dict.MarkerSize}");
}
c.Info("Mat 演算・行列演算・Span アクセス・ArUco 辞書: OK");
diff --git a/TIASshot/TIASshot.csproj b/TIASshot/TIASshot.csproj
index 35f36d7..0ebe8b3 100644
--- a/TIASshot/TIASshot.csproj
+++ b/TIASshot/TIASshot.csproj
@@ -1,6 +1,6 @@
-
+
Debug
@@ -66,14 +66,14 @@
.\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
@@ -211,7 +211,8 @@
このプロジェクトは、このコンピューター上にない NuGet パッケージを参照しています。それらのパッケージをダウンロードするには、[NuGet パッケージの復元] を使用します。詳細については、http://go.microsoft.com/fwlink/?LinkID=322105 を参照してください。見つからないファイルは {0} です。
-
+
+
@@ -224,4 +225,5 @@
+
\ No newline at end of file
diff --git a/TIASshot/packages.config b/TIASshot/packages.config
index 6666815..88b1e4e 100644
--- a/TIASshot/packages.config
+++ b/TIASshot/packages.config
@@ -1,10 +1,10 @@
-
-
-
-
-
+
+
+
+
+
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 69b1fb7..baedb6c 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,11 +11,13 @@
**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 補完ライブラリ**
@@ -26,6 +28,8 @@
- `System.Runtime.CompilerServices.Unsafe` (6.1.2)
- `System.ValueTuple` (4.6.2)
+※ `System.Drawing.Common` と `System.ValueTuple` を除く 4 件は OpenCvSharp 4.13 の推移的依存であり,バージョンは相互に不可分(`System.Memory` 4.6.3 が他 3 件の下限を指定している).個別に上げ下げしないこと.
+
## テストプロジェクト TIASshot.Tests (Test Project)
テストプロジェクトも `packages.config` で管理する(本体と同じレガシー形式・x64).
@@ -34,7 +38,8 @@
- net48 向けの依存として `System.Threading.Tasks.Extensions` (4.6.3)・`System.ValueTuple` (4.6.2) も参照する
- `NUnit3TestAdapter` (4.6.0): テストランナー連携(Visual Studio / vstest)
- `Microsoft.NET.Test.Sdk` (17.14.1): テスト実行基盤(依存として `Microsoft.CodeCoverage` 18.10.1 が入る)
-- ※ `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 206f15c..f43d319 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"
@@ -63,6 +63,8 @@
保存済みの実画像を入力として,チャートマスクの位置を比較する.
+※ 2026-08-29 に自動テスト `ChartDetectorTests` を追加した(合成した AR マーカー画像を入力し,早期 return・上下逆判定・固定判定・24 枚のマスク生成を検証する).**本節の実機による目視比較を置き換えるものではない**.自動テストが担保するのは検出ロジックの分岐と幾何であり,実写のノイズ・照明ムラ・レンズ歪み・実際のマーカー印刷品質に対する検出性能は実機でしか確認できない.OpenCvSharp のネイティブ側更新(例: 4.10 → 4.13)を行ったときは,自動テストが緑でも本節の実機比較を必ず実施すること.
+
### 基準データの作成(リファクタリング前に実施)
1. キャリブレーション時に保存される `TCC_ROIs.jpg` を基準データとして保管する
diff --git a/docs/PROGRESS.md b/docs/PROGRESS.md
index 2c8fa80..acabf36 100644
--- a/docs/PROGRESS.md
+++ b/docs/PROGRESS.md
@@ -158,3 +158,7 @@
- 失敗注入で検出力を確認済み(System.Memory.dll 除去・exe.config のリダイレクト改ざんで FAIL).カメラ SDK の欠落は WARN 扱いで判定しない.
- Phase 1b(verifier)を本プロジェクトで初めて実行し,ランナーが人間の Release 成果物まで消す・packages 退避失敗で終了コードが 2 にならない,の 2 点を人間に回る前に捕捉して直した.
- 後段: セルフテストが代替しない範囲(実カメラ SDK との結合・実プレビュー表示・実チャートの検出性能・画像の見た目・長時間のメモリ/GDI)は引き続き実機で人間が確認する.`Form1.ShowImage` 等が新しいコントロールを参照するようになった場合はヘッドレス用コンストラクタの追従が要る.
+
+- [x] **OpenCvSharp 4.13 の ArUco API 移行を main に取り込み(#31・PR #36)** (2026-09-29): 実機確認待ちで保留していた PR #36 に main(NUnit 4.6.1 移行・セルフテスト導入など 20 コミット)を取り込んで競合を解消し,実機確認を経てマージした.競合した依存設定は main 側を正とし,OpenCvSharp の 4.13 化(`lib/netstandard2.0` 参照・runtime の `.targets` 取り込み・`OpenCvSharp` のバインディングリダイレクト)だけを当て直した.
+ - 失敗パターン: PR の保留中に main へ入ったセルフテスト(`SelfTestChart`・`SelfTestRunner`・`SelfTestChartTests`)が旧 API(`PredefinedDictionaryName`・`CvAruco.DetectMarkers`)で書かれており,競合解消だけではビルドが壊れる状態だった.4.10 と 4.13 の両方で動く書き方が無い API 移行を長く保留すると,その間に増えた旧 API の利用箇所も移行対象になる.取り込み時は `grep` で旧 API の残存を確認すること.
+ - 後段: System.Drawing.Common 10 系への追従(#45)が可能になった.