diff --git a/TIASshot.Tests/IoUtilTests.cs b/TIASshot.Tests/IoUtilTests.cs
index 5ff291e..d770ef5 100644
--- a/TIASshot.Tests/IoUtilTests.cs
+++ b/TIASshot.Tests/IoUtilTests.cs
@@ -70,6 +70,114 @@
}
///
+ /// 再現テスト: 小数点にカンマを使うロケール(de-DE)でもピリオド小数の CSV を正しく解釈する
+ /// (同梱 tcc_srgb.csv 等はピリオド小数で記述される仕様のため,パースはロケール非依存であること.
+ /// カルチャ依存だと "96.32301186" が桁区切り扱いで 9632301186 に化け,例外にならず校正が黙って壊れる)
+ ///
+ [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(0, 0), 1e-6);
+ Assert.AreEqual(112.325025, loaded.At(0, 1), 1e-6);
+ Assert.AreEqual(186.5234201, loaded.At(0, 2), 1e-6);
+ }
+ } finally {
+ System.Threading.Thread.CurrentThread.CurrentCulture = original;
+ }
+ }
+
+ ///
+ /// 再現テスト: 小数点にカンマを使うロケール(de-DE)でもピリオド小数で CSV を出力する
+ /// (カルチャ依存だと小数点がカンマになり,列区切りのカンマと衝突して CSV が壊れる)
+ ///
+ [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(0, 0) = 1.2345678;
+ src.At(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;
+ }
+ }
+
+ ///
+ /// 特性化テスト: ドット小数ロケール(ja-JP)での出力バイト列を固定する
+ /// (InvariantCulture 固定化の修正で従来ロケールの出力が 1 バイトも変わらない保護)
+ ///
+ [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;
+ }
+ }
+
+ ///
+ /// 特性化テスト: リポジトリ同梱の tcc_srgb.csv / tcc_xyz.csv が従来どおり読めること
+ /// (CameraBase の TCC 参照値読み込み経路の保護.先頭要素の値と行列サイズを固定する)
+ ///
+ [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(0, 0), 1e-6,
+ "tcc_srgb.csv の先頭要素が従来値で読めること");
+ }
+ }
+
+ ///
/// 存在しないファイルをロードしたとき null を返す
///
[Test]