From ce7426aeddb3867d9178ba54711a3fd5251eed23 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?C=C3=A9dric=20Traizet?= <cedric.traizet@c-s.fr>
Date: Mon, 12 Oct 2020 14:47:58 +0200
Subject: [PATCH] ENH: compare ImageMetadata dictionaries in the
 --compare-metadata regression function

---
 .../Metadata/include/otbGeometryMetadata.h    |   3 +
 .../Core/Metadata/include/otbMetaDataKey.h    |  51 ++++++
 .../Core/Metadata/src/otbGeometryMetadata.cxx |  18 +++
 Modules/Core/Metadata/src/otbMetaDataKey.cxx  |  67 ++++++++
 Modules/IO/ImageIO/test/CMakeLists.txt        |  11 ++
 Modules/IO/TestKernel/src/otbTestHelper.cxx   | 150 +++++++++++++++++-
 Modules/IO/TestKernel/test/CMakeLists.txt     |  18 ++-
 Modules/IO/TestKernel/test/otbDummyTest.cxx   |  33 ++++
 .../test/otbTestKernelTestDriver.cxx          |   1 +
 9 files changed, 350 insertions(+), 2 deletions(-)
 create mode 100644 Modules/IO/TestKernel/test/otbDummyTest.cxx

diff --git a/Modules/Core/Metadata/include/otbGeometryMetadata.h b/Modules/Core/Metadata/include/otbGeometryMetadata.h
index ccd902ecce..c370625026 100644
--- a/Modules/Core/Metadata/include/otbGeometryMetadata.h
+++ b/Modules/Core/Metadata/include/otbGeometryMetadata.h
@@ -164,6 +164,9 @@ struct OTBMetadata_EXPORT RPCParam
 
 };
 
+OTBMetadata_EXPORT bool operator==(const RPCParam & lhs, const RPCParam & rhs);
+
+
 } // end namespace Projection
 
 } // end namespace otb
diff --git a/Modules/Core/Metadata/include/otbMetaDataKey.h b/Modules/Core/Metadata/include/otbMetaDataKey.h
index 88656b521e..2fc0030789 100644
--- a/Modules/Core/Metadata/include/otbMetaDataKey.h
+++ b/Modules/Core/Metadata/include/otbMetaDataKey.h
@@ -240,6 +240,8 @@ struct OTBMetadata_EXPORT Time : tm
 
 };
 
+OTBMetadata_EXPORT bool operator==(const Time & lhs, const Time & rhs);
+
 struct LUTAxis
 {
   /** number of measurements on this axis */
@@ -254,6 +256,9 @@ struct LUTAxis
   std::string ToJSON(bool multiline=false) const;
 };
 
+OTBMetadata_EXPORT bool operator==(const LUTAxis & lhs, const LUTAxis & rhs);
+
+
 template <unsigned int VDim> class LUT
 {
 public:
@@ -268,6 +273,26 @@ public:
   void OTBMetadata_EXPORT FromString(std::string);
 };
 
+template <unsigned int VDim>
+OTBMetadata_EXPORT bool operator==(const LUT<VDim> & lhs, const LUT<VDim> & rhs)
+{
+  bool axisComparison = true;
+  for (unsigned int i = 0; i < VDim; i++)
+  {
+    axisComparison = axisComparison && lhs.Axis[i] == rhs.Axis[i];
+  }
+  return axisComparison && lhs.Array == rhs.Array;
+}
+
+
+template <unsigned int VDim>
+std::ostream& operator<<(std::ostream& os, const LUT<VDim>& val)
+{
+  os << val.ToString();
+  return os;
+}
+
+
 typedef LUT<1> LUT1D;
 
 typedef LUT<2> LUT2D;
@@ -300,6 +325,32 @@ extern OTBMetadata_EXPORT MDL1DBmType MDL1DNames;
 typedef boost::bimap<MDL2D, std::string> MDL2DBmType;
 extern OTBMetadata_EXPORT MDL2DBmType MDL2DNames;
 
+template<class T>
+std::string EnumToString(T t);
+
+template<>
+std::string EnumToString(MDGeom value);
+
+template<>
+std::string EnumToString(MDNum value);
+
+template<>
+std::string EnumToString(MDStr value);
+
+template<>
+std::string EnumToString(MDL1D value);
+
+template<>
+std::string EnumToString(MDL2D value);
+
+template<>
+std::string EnumToString(MDTime value);
+
+// Specialization for extra keys
+template<>
+std::string EnumToString(std::string value);
+
+
 } // end namespace MetaData
 
 namespace Utils
diff --git a/Modules/Core/Metadata/src/otbGeometryMetadata.cxx b/Modules/Core/Metadata/src/otbGeometryMetadata.cxx
index 0dadc355c4..fea2819667 100644
--- a/Modules/Core/Metadata/src/otbGeometryMetadata.cxx
+++ b/Modules/Core/Metadata/src/otbGeometryMetadata.cxx
@@ -107,5 +107,23 @@ std::string RPCParam::ToJSON(bool multiline) const
   return oss.str();
 }
 
+bool operator==(const RPCParam & lhs, const RPCParam & rhs)
+{
+  return lhs.LineOffset == rhs.LineOffset
+      && lhs.SampleOffset == rhs.SampleOffset
+      && lhs.LatOffset == rhs.LatOffset
+      && lhs.LonOffset == rhs.LonOffset
+      && lhs.HeightOffset == rhs.HeightOffset
+      && lhs.LineScale == rhs.LineScale
+      && lhs.SampleScale == rhs.SampleScale
+      && lhs.LatScale == rhs.LatScale
+      && lhs.LonScale == rhs.LonScale
+      && lhs.HeightScale == rhs.HeightScale
+      && std::equal(lhs.LineNum, lhs.LineNum+20, rhs.LineNum )
+      && std::equal(lhs.LineDen, lhs.LineDen+20, rhs.LineDen )
+      && std::equal(lhs.SampleNum, lhs.SampleNum+20, rhs.SampleNum )
+      && std::equal(lhs.SampleDen, lhs.SampleDen+20, rhs.SampleDen );
+}
+
 } // end namespace Projection
 } // end namespace otb
diff --git a/Modules/Core/Metadata/src/otbMetaDataKey.cxx b/Modules/Core/Metadata/src/otbMetaDataKey.cxx
index db6ec2ac91..9808d0fec7 100644
--- a/Modules/Core/Metadata/src/otbMetaDataKey.cxx
+++ b/Modules/Core/Metadata/src/otbMetaDataKey.cxx
@@ -187,6 +187,16 @@ std::istream& operator>>(std::istream& is, Time& val)
 
 #undef _OTB_ISTREAM_EXPECT
 
+
+
+bool operator==(const Time & lhs, const Time & rhs)
+{
+  tm tmLhs = lhs;
+  tm tmRhs = rhs;
+  return mktime(&tmLhs) + lhs.frac_sec == mktime(&tmRhs) + rhs.frac_sec;
+}
+
+
 std::string LUTAxis::ToJSON(bool multiline) const
 {
   std::ostringstream oss;
@@ -394,6 +404,63 @@ MDGeomBmType MDGeomNames = bimapGenerator<MDGeom>(std::map<MDGeom, std::string>
   {MDGeom::Adjustment,     "Adjustment"}
 });
 
+
+OTBMetadata_EXPORT bool operator==(const LUTAxis & lhs, const LUTAxis & rhs)
+{
+  return lhs.Size == rhs.Size
+      && lhs.Origin == rhs.Origin
+      && lhs.Spacing == rhs.Spacing
+      && lhs.Values == rhs.Values;
+}
+
+template<>
+std::string EnumToString(MDGeom value)
+{
+  return MetaData::MDGeomNames.left.at(value);
+}
+
+template<>
+std::string EnumToString(MDNum value)
+{
+  return MetaData::MDNumNames.left.at(value);
+}
+
+template<>
+std::string EnumToString(MDStr value)
+{
+  return MetaData::MDStrNames.left.at(value);
+}
+
+template<>
+std::string EnumToString(MDL1D value)
+{
+  return MetaData::MDL1DNames.left.at(value);
+}
+
+template<>
+std::string EnumToString(MDL2D value)
+{
+  return MetaData::MDL2DNames.left.at(value);
+}
+
+template<>
+std::string EnumToString(MDTime value)
+{
+  return MetaData::MDTimeNames.left.at(value);
+}
+
+// Specialization for extra keys
+template<>
+std::string EnumToString(std::string value)
+{
+  return value;
+}
+
+
+
+
+
+
 } // end namespace MetaData
 
 } // end namespace otb
diff --git a/Modules/IO/ImageIO/test/CMakeLists.txt b/Modules/IO/ImageIO/test/CMakeLists.txt
index 3a8be81881..3bc5cbfe53 100644
--- a/Modules/IO/ImageIO/test/CMakeLists.txt
+++ b/Modules/IO/ImageIO/test/CMakeLists.txt
@@ -1306,3 +1306,14 @@ otb_add_test(NAME ioTvCompoundMetadataReaderTest
   otbWriteGeomFile
   ${INPUTDATA}/QB_Toulouse_combo.vrt
   ${TEMP}/ioTvCompoundMetadataReaderTest.tif)
+
+
+
+
+otb_add_test(NAME ioTvCompareMetadataTest COMMAND otbImageIOTestDriver
+  --compare-metadata ${EPSILON_9}
+  LARGEINPUT{SENTINEL1/S1A_S6_SLC__1SSV_20150619T195043}
+  LARGEINPUT{SENTINEL1/S1A_S6_SLC__1SSV_20150619T195043}
+  otbImageFileReaderRGBTest
+  ${INPUTDATA}/couleurs_extrait.png
+  ${TEMP}/ioImageFileReaderRGB_PNG2PNG.png )
diff --git a/Modules/IO/TestKernel/src/otbTestHelper.cxx b/Modules/IO/TestKernel/src/otbTestHelper.cxx
index 426db29ec5..b6875c0156 100644
--- a/Modules/IO/TestKernel/src/otbTestHelper.cxx
+++ b/Modules/IO/TestKernel/src/otbTestHelper.cxx
@@ -1418,7 +1418,78 @@ int TestHelper::RegressionTestImage(int cpt, const char* testImageFilename, cons
   return ret;
 }
 
-int TestHelper::RegressionTestMetaData(const char* testImageFilename, const char* baselineImageFilename, const double /*toleranceDiffPixelImage*/) const
+namespace
+{
+
+template <class InputIt1, class InputIt2, class BinaryPredicate >
+int CompareMetadataDict( InputIt1 first1, InputIt1 last1,
+                          InputIt2 first2, InputIt2 last2,
+                          bool reportErrors,
+                          const BinaryPredicate & p)
+{
+  if (std::distance(first1, last1) != std::distance(first2, last2))
+  {
+    if (reportErrors)
+    {
+      std::cerr << "Input metadata dictionaries have different sizes" << std::endl;
+    }
+
+    return 1;
+  }
+
+  int errorCount = 0;
+
+  while (first1 != last1)
+  {
+    if (first1->first != first2->first)
+    {
+      errorCount++;
+      if (reportErrors)
+      {
+        std::cerr << "Metadata key " << otb::MetaData::EnumToString(first1->first) 
+                    << " does not match between test and baseline images: "
+                    << std::endl;
+      }
+      return errorCount;
+    }
+
+
+    if (!p(first1->second, first2->second))
+    {
+      errorCount++;
+      if (reportErrors)
+        std::cerr << "Metadata " << otb::MetaData::EnumToString(first1->first) 
+                  << " does not match between test and baseline images: "
+                  << std::endl
+                  << "Baseline image: " 
+                  << first1->second
+                  << std::endl
+                  << "Test image: " 
+                  << first2->second 
+                  << std::endl;
+    }
+
+    ++first1;
+    ++first2;
+  }
+
+  return errorCount;
+}
+
+
+template <class InputIt1, class InputIt2 >
+int CompareMetadataDict( InputIt1 first1, InputIt1 last1,
+                          InputIt2 first2, InputIt2 last2,
+                          bool reportErrors)
+{
+  auto p = []( const decltype(first1->second) & rhs, 
+               const decltype(first2->second) & lhs) 
+          {return rhs == lhs;};
+  return CompareMetadataDict(first1, last1, first2, last2, reportErrors, p);
+}
+}
+
+int TestHelper::RegressionTestMetaData(const char* testImageFilename, const char* baselineImageFilename, const double tolerance) const
 {
   // Use the factory mechanism to read the test and baseline files and convert them to double
   typedef otb::Image<double, ITK_TEST_DIMENSION_MAX> ImageType;
@@ -1449,6 +1520,7 @@ int TestHelper::RegressionTestMetaData(const char* testImageFilename, const char
   }
 
   unsigned int errcount = 0;
+
   // The sizes of the baseline and test image must match
   ImageType::SizeType baselineSize;
   baselineSize = baselineReader->GetOutput()->GetLargestPossibleRegion().GetSize();
@@ -1583,12 +1655,88 @@ int TestHelper::RegressionTestMetaData(const char* testImageFilename, const char
       }
     }
   }
+  
+  const auto & baselineImageMetadata = blImPtr->GetImageMetadata();
+  const auto & testImageMetadata = testImPtr->GetImageMetadata();
+
+  // Compare string keys (strict equality)
+  errcount += CompareMetadataDict(baselineImageMetadata.StringKeys.begin(),
+                                    baselineImageMetadata.StringKeys.end(), 
+                                    testImageMetadata.StringKeys.begin(),
+                                    testImageMetadata.StringKeys.end(),
+                                    m_ReportErrors);
+
+  // Compare numeric keys
+  auto compareDouble = [tolerance](double lhs, double rhs)
+        {return fabs(lhs - rhs) 
+                <= ( (fabs(lhs) < fabs(rhs) ? fabs(rhs) : fabs(lhs)) * tolerance);};
+
+  errcount += CompareMetadataDict(baselineImageMetadata.NumericKeys.begin(),
+                                    baselineImageMetadata.NumericKeys.end(), 
+                                    testImageMetadata.NumericKeys.begin(),
+                                    testImageMetadata.NumericKeys.end(),
+                                    m_ReportErrors,
+                                    compareDouble);
+
+  // Compare time keys (strict equality)
+  errcount += CompareMetadataDict(baselineImageMetadata.TimeKeys.begin(),
+                                    baselineImageMetadata.TimeKeys.end(), 
+                                    testImageMetadata.TimeKeys.begin(),
+                                    testImageMetadata.TimeKeys.end(),
+                                    m_ReportErrors);
+
+
+  // Compare LUTs (strict equality)
+  errcount += CompareMetadataDict(baselineImageMetadata.LUT1DKeys.begin(),
+                                    baselineImageMetadata.LUT1DKeys.end(), 
+                                    testImageMetadata.LUT1DKeys.begin(),
+                                    testImageMetadata.LUT1DKeys.end(),
+                                    m_ReportErrors);
+
+  errcount += CompareMetadataDict(baselineImageMetadata.LUT2DKeys.begin(),
+                                    baselineImageMetadata.LUT2DKeys.end(), 
+                                    testImageMetadata.LUT2DKeys.begin(),
+                                    testImageMetadata.LUT2DKeys.end(),
+                                    m_ReportErrors);
+
+
+  // Compare extra keys (strict equality)
+  errcount += CompareMetadataDict(baselineImageMetadata.ExtraKeys.begin(),
+                                    baselineImageMetadata.ExtraKeys.end(), 
+                                    testImageMetadata.ExtraKeys.begin(),
+                                    testImageMetadata.ExtraKeys.end(),
+                                    m_ReportErrors);
+
+
+  if (baselineImageMetadata.Has(MDGeom::RPC))
+  {
+    if (!testImageMetadata.Has(MDGeom::RPC))
+    {
+      errcount++;
+      if (m_ReportErrors)
+      {
+        std::cerr << "Test image does not have RPC coefficients" << std::endl;
+      }
+
+    }
+    if (!(boost::any_cast<Projection::RPCParam>(baselineImageMetadata[MDGeom::RPC]) 
+          == boost::any_cast<Projection::RPCParam>(testImageMetadata[MDGeom::RPC])))
+    {
+      errcount++;
+      if (m_ReportErrors)
+      {
+        std::cerr << "RPC parameters mismatch between baseline and test images" << std::endl;
+      }
+    }
+  }
+  
   if (errcount > 0)
   {
     std::cout << "<DartMeasurement name=\"MetadataError\" type=\"numeric/int\">";
     std::cout << errcount;
     std::cout << "</DartMeasurement>" << std::endl;
   }
+
   return errcount;
 }
 
diff --git a/Modules/IO/TestKernel/test/CMakeLists.txt b/Modules/IO/TestKernel/test/CMakeLists.txt
index 2800ab8599..2320848aa4 100644
--- a/Modules/IO/TestKernel/test/CMakeLists.txt
+++ b/Modules/IO/TestKernel/test/CMakeLists.txt
@@ -29,7 +29,8 @@ set(OTBTestKernelTests
   otbCompareAsciiTests.cxx
   otbCopyTest.cxx
   otbCompareAsciiTestsEpsilon3_WhiteSpace.cxx
-  otbTestKernelTestDriver.cxx )
+  otbTestKernelTestDriver.cxx 
+  otbDummyTest.cxx)
 
 add_executable(otbTestKernelTestDriver ${OTBTestKernelTests})
 target_link_libraries(otbTestKernelTestDriver ${OTBTestKernel-Test_LIBRARIES})
@@ -205,3 +206,18 @@ otb_add_test(NAME tsTvCompareImages_DifferentSizes COMMAND otbTestKernelTestDriv
   ${TEMP}/tsTvCompareImages_DifferentSizes.tif
   )
 set_property(TEST tsTvCompareImages_DifferentSizes PROPERTY WILL_FAIL true)
+
+otb_add_test(NAME tsTvCompareMetadata1 COMMAND otbTestKernelTestDriver
+  --compare-metadata 0
+  LARGEINPUT{SENTINEL1/S1A_S6_SLC__1SSV_20150619T195043}
+  LARGEINPUT{SENTINEL1/S1A_S6_SLC__1SSV_20150619T195043}
+  otbDummyTest
+  )
+
+otb_add_test(NAME tsTvCompareMetadata2 COMMAND otbTestKernelTestDriver
+  --compare-metadata 0
+  ${OTB_DATA_LARGEINPUT_ROOT}/PLEIADES-PRE/TLSE_JP2_ORTHO_DIMAPv2_PMS-N_lossy_12bits/IMG_PHR1Z_PMS_N_001/IMG_PHR1A_PMS-N_201006181052297_ORT_IPU_20111011_0619-001_R1C1.JP2
+  ${OTB_DATA_LARGEINPUT_ROOT}/PLEIADES-PRE/TLSE_TIFF_ORTHO_DIMAPv2_MS_lossless_8bits/IMG_PHR1A_MS_002/IMG_PHR1A_MS_201006181052297_ORT_IPU_20111109_7807-004_R1C1.TIF
+  otbDummyTest
+  )
+set_property(TEST tsTvCompareMetadata2 PROPERTY WILL_FAIL true)
\ No newline at end of file
diff --git a/Modules/IO/TestKernel/test/otbDummyTest.cxx b/Modules/IO/TestKernel/test/otbDummyTest.cxx
new file mode 100644
index 0000000000..19a8d604cd
--- /dev/null
+++ b/Modules/IO/TestKernel/test/otbDummyTest.cxx
@@ -0,0 +1,33 @@
+/*
+ * Copyright (C) 2005-2020 Centre National d'Etudes Spatiales (CNES)
+ *
+ * This file is part of Orfeo Toolbox
+ *
+ *     https://www.orfeo-toolbox.org/
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include <iostream>
+
+// This test does nothing. It is used to test the TestKernel module
+int otbDummyTest(int argc, char* argv[])
+{ 
+  if (argc != 1)
+  {
+    std::cerr << argv[0] << "does not take any additional parameter" << std::endl;
+    return EXIT_FAILURE;
+  }
+
+  return EXIT_SUCCESS;
+}
diff --git a/Modules/IO/TestKernel/test/otbTestKernelTestDriver.cxx b/Modules/IO/TestKernel/test/otbTestKernelTestDriver.cxx
index 62863ae248..356e3c73d1 100644
--- a/Modules/IO/TestKernel/test/otbTestKernelTestDriver.cxx
+++ b/Modules/IO/TestKernel/test/otbTestKernelTestDriver.cxx
@@ -31,4 +31,5 @@ void RegisterTests()
   REGISTER_TEST(otbCompareAsciiTests);
   REGISTER_TEST(otbCompareAsciiTestsEpsilon3_WhiteSpace);
   REGISTER_TEST(otbCopyTest);
+  REGISTER_TEST(otbDummyTest);
 }
-- 
GitLab