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