using System.IO;
using NUnit.Framework;
using OpenCvSharp;
namespace TIASshot.Tests {
/// <summary>
/// IoUtil のテスト(CSV 読み書き往復でネイティブ DLL ロードも検証)
/// </summary>
[TestFixture]
public class IoUtilTests {
private string _csvPath;
[SetUp]
public void SetUp() {
_csvPath = Path.GetTempFileName();
}
[TearDown]
public void TearDown() {
if (File.Exists(_csvPath)) File.Delete(_csvPath);
}
/// <summary>
/// 2x3 の CV_64FC1 Mat を CSV に保存して再読込したとき、各要素が ±1e-6 以内で一致する
/// </summary>
[Test]
public void SaveAndLoad_2x3Mat_ValuesMatch() {
// Arrange
using (var src = CreateMat2x3()) {
// Act
IoUtil.SaveMatToCsv(_csvPath, src);
using (var loaded = IoUtil.LoadMatFromCsv(_csvPath)) {
// Assert
Assert.That(loaded, Is.Not.Null, "ロード結果が null であってはならない");
Assert.That(loaded.Rows, Is.EqualTo(src.Rows), "行数が一致すること");
Assert.That(loaded.Cols, Is.EqualTo(src.Cols), "列数が一致すること");
for (int row = 0; row < src.Rows; row++) {
for (int col = 0; col < src.Cols; col++) {
Assert.That(
loaded.At<double>(row, col),
Is.EqualTo(src.At<double>(row, col)).Within(1e-6),
$"[{row},{col}] の値が ±1e-6 以内で一致すること");
}
}
}
}
}
/// <summary>
/// 1x1 の Mat(スカラー)を CSV に保存して再読込しても値が保たれる
/// </summary>
[Test]
public void SaveAndLoad_1x1Mat_ValuePreserved() {
// Arrange
const double Pi = 3.14159265358979;
using (var src = new Mat(1, 1, MatType.CV_64FC1)) {
src.At<double>(0, 0) = Pi;
// Act
IoUtil.SaveMatToCsv(_csvPath, src);
using (var loaded = IoUtil.LoadMatFromCsv(_csvPath)) {
// Assert
Assert.That(loaded, Is.Not.Null);
Assert.That(loaded.At<double>(0, 0), Is.EqualTo(Pi).Within(1e-6));
}
}
}
/// <summary>
/// 再現テスト: 小数点にカンマを使うロケール(de-DE)でもピリオド小数の CSV を正しく解釈する
/// (同梱 tcc_srgb.csv 等はピリオド小数で記述される仕様のため,パースはロケール非依存であること.
/// カルチャ依存だと "96.32301186" が桁区切り扱いで 9632301186 に化け,例外にならず校正が黙って壊れる)
/// </summary>
[Test]
[NonParallelizable]
public void LoadMatFromCsv_カンマ小数ロケール_ピリオド小数を正しく解釈する() {
// Arrange
File.WriteAllText(_csvPath, "96.32301186,112.325025,186.5234201\r\n");
var original = System.Threading.Thread.CurrentThread.CurrentCulture;
try {
System.Threading.Thread.CurrentThread.CurrentCulture =
new System.Globalization.CultureInfo("de-DE");
// Act
using (var loaded = IoUtil.LoadMatFromCsv(_csvPath)) {
// Assert
Assert.That(loaded, Is.Not.Null);
Assert.That(loaded.At<double>(0, 0), Is.EqualTo(96.32301186).Within(1e-6));
Assert.That(loaded.At<double>(0, 1), Is.EqualTo(112.325025).Within(1e-6));
Assert.That(loaded.At<double>(0, 2), Is.EqualTo(186.5234201).Within(1e-6));
}
} finally {
System.Threading.Thread.CurrentThread.CurrentCulture = original;
}
}
/// <summary>
/// 再現テスト: 小数点にカンマを使うロケール(de-DE)でもピリオド小数で CSV を出力する
/// (カルチャ依存だと小数点がカンマになり,列区切りのカンマと衝突して CSV が壊れる)
/// </summary>
[Test]
[NonParallelizable]
public void SaveMatToCsv_カンマ小数ロケール_ピリオド小数で出力する() {
// Arrange
var original = System.Threading.Thread.CurrentThread.CurrentCulture;
try {
System.Threading.Thread.CurrentThread.CurrentCulture =
new System.Globalization.CultureInfo("de-DE");
using (var src = new Mat(1, 2, MatType.CV_64FC1)) {
src.At<double>(0, 0) = 1.2345678;
src.At<double>(0, 1) = -2.5;
// Act
IoUtil.SaveMatToCsv(_csvPath, src);
// Assert
var line = File.ReadAllLines(_csvPath)[0];
Assert.That(line, Is.EqualTo("1.2345678,-2.5000000"),
"ロケールによらずピリオド小数・カンマ列区切りで出力すること");
}
} finally {
System.Threading.Thread.CurrentThread.CurrentCulture = original;
}
}
/// <summary>
/// 特性化テスト: ドット小数ロケール(ja-JP)での出力バイト列を固定する
/// (InvariantCulture 固定化の修正で従来ロケールの出力が 1 バイトも変わらない保護)
/// </summary>
[Test]
[NonParallelizable]
public void SaveMatToCsv_ドット小数ロケール_出力が従来と同一() {
// Arrange
var original = System.Threading.Thread.CurrentThread.CurrentCulture;
try {
System.Threading.Thread.CurrentThread.CurrentCulture =
new System.Globalization.CultureInfo("ja-JP");
using (var src = CreateMat2x3()) {
// Act
IoUtil.SaveMatToCsv(_csvPath, src);
// Assert
var lines = File.ReadAllLines(_csvPath);
Assert.That(lines[0], Is.EqualTo("1.2345670,-2.3456780,0.0000010"));
Assert.That(lines[1], Is.EqualTo("100.0000000,-0.5000000,999.9999990"));
}
} finally {
System.Threading.Thread.CurrentThread.CurrentCulture = original;
}
}
/// <summary>
/// 特性化テスト: リポジトリ同梱の tcc_srgb.csv / tcc_xyz.csv が従来どおり読めること
/// (CameraBase の TCC 参照値読み込み経路の保護.先頭要素の値と行列サイズを固定する)
/// </summary>
[Test]
public void LoadMatFromCsv_同梱tccCsv_従来どおり読める() {
var repoDir = Path.GetFullPath(Path.Combine(
TestContext.CurrentContext.TestDirectory,
"..", "..", "..", "..", "TIASshot"));
var srgbPath = Path.Combine(repoDir, "tcc_srgb.csv");
var xyzPath = Path.Combine(repoDir, "tcc_xyz.csv");
Assert.That(File.Exists(srgbPath), Is.True, $"同梱 tcc_srgb.csv が見つからない: {srgbPath}");
Assert.That(File.Exists(xyzPath), Is.True, $"同梱 tcc_xyz.csv が見つからない: {xyzPath}");
using (var srgb = IoUtil.LoadMatFromCsv(srgbPath))
using (var xyz = IoUtil.LoadMatFromCsv(xyzPath)) {
Assert.That(srgb, Is.Not.Null, "tcc_srgb.csv が読めること");
Assert.That(xyz, Is.Not.Null, "tcc_xyz.csv が読めること");
Assert.That(srgb.Cols, Is.EqualTo(3), "tcc_srgb.csv は 3 列であること");
Assert.That(xyz.Cols, Is.EqualTo(3), "tcc_xyz.csv は 3 列であること");
Assert.That(srgb.At<double>(0, 0), Is.EqualTo(96.32301186).Within(1e-6),
"tcc_srgb.csv の先頭要素が従来値で読めること");
}
}
/// <summary>
/// 存在しないファイルをロードしたとき null を返す
/// </summary>
[Test]
public void LoadMatFromCsv_NonExistentFile_ReturnsNull() {
// Act
var result = IoUtil.LoadMatFromCsv("does_not_exist_xyz.csv");
// Assert
Assert.That(result, Is.Null, "存在しないファイルのロードは null を返すこと");
}
// ---------------------------------------------------------------
// ヘルパー
// ---------------------------------------------------------------
/// <summary>
/// 代表的な 2x3 CV_64FC1 Mat を生成する
/// </summary>
private static Mat CreateMat2x3() {
var m = new Mat(2, 3, MatType.CV_64FC1);
m.At<double>(0, 0) = 1.234567;
m.At<double>(0, 1) = -2.345678;
m.At<double>(0, 2) = 0.000001;
m.At<double>(1, 0) = 100.0;
m.At<double>(1, 1) = -0.5;
m.At<double>(1, 2) = 999.999999;
return m;
}
}
}