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.IsNotNull(loaded, "ロード結果が null であってはならない");
Assert.AreEqual(src.Rows, loaded.Rows, "行数が一致すること");
Assert.AreEqual(src.Cols, loaded.Cols, "列数が一致すること");
for (int row = 0; row < src.Rows; row++) {
for (int col = 0; col < src.Cols; col++) {
Assert.AreEqual(
src.At<double>(row, col),
loaded.At<double>(row, col),
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.IsNotNull(loaded);
Assert.AreEqual(Pi, loaded.At<double>(0, 0), 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.IsNotNull(loaded);
Assert.AreEqual(96.32301186, loaded.At<double>(0, 0), 1e-6);
Assert.AreEqual(112.325025, loaded.At<double>(0, 1), 1e-6);
Assert.AreEqual(186.5234201, loaded.At<double>(0, 2), 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.AreEqual("1.2345678,-2.5000000", line,
"ロケールによらずピリオド小数・カンマ列区切りで出力すること");
}
} 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.AreEqual("1.2345670,-2.3456780,0.0000010", lines[0]);
Assert.AreEqual("100.0000000,-0.5000000,999.9999990", lines[1]);
}
} 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.IsTrue(File.Exists(srgbPath), $"同梱 tcc_srgb.csv が見つからない: {srgbPath}");
Assert.IsTrue(File.Exists(xyzPath), $"同梱 tcc_xyz.csv が見つからない: {xyzPath}");
using (var srgb = IoUtil.LoadMatFromCsv(srgbPath))
using (var xyz = IoUtil.LoadMatFromCsv(xyzPath)) {
Assert.IsNotNull(srgb, "tcc_srgb.csv が読めること");
Assert.IsNotNull(xyz, "tcc_xyz.csv が読めること");
Assert.AreEqual(3, srgb.Cols, "tcc_srgb.csv は 3 列であること");
Assert.AreEqual(3, xyz.Cols, "tcc_xyz.csv は 3 列であること");
Assert.AreEqual(96.32301186, srgb.At<double>(0, 0), 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.IsNull(result, "存在しないファイルのロードは 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;
}
}
}