From 1ae5a8586b3beb28864c79c6ea6ebc6e06ecfe94 Mon Sep 17 00:00:00 2001 From: Kumataro Date: Mon, 20 Apr 2026 02:11:33 +0900 Subject: [PATCH] Merge pull request #28615 from Kumataro:fix28606 imgcodecs(png): support cICP metadata for imreadWithMetadata() #28615 Close https://github.com/opencv/opencv/issues/28606 Related https://github.com/opencv/opencv/pull/27741 (support for imwriteWithMetadata) ### Pull Request Readiness Checklist See details at https://github.com/opencv/opencv/wiki/How_to_contribute#making-a-good-pull-request - [x] I agree to contribute to the project under Apache 2 License. - [x] To the best of my knowledge, the proposed patch is not based on a code under GPL or another license that is incompatible with OpenCV - [x] The PR is proposed to the proper branch - [x] There is a reference to the original bug report and related work - [x] There is accuracy test, performance test and test data in opencv_extra repository, if applicable Patch to opencv_extra has the same branch name. - [ ] The feature is well documented and sample code can be built with the project CMake --- CMakeLists.txt | 10 +++++++ modules/imgcodecs/CMakeLists.txt | 9 +++++++ modules/imgcodecs/src/grfmt_png.cpp | 14 ++++++++++ modules/imgcodecs/test/test_exif.cpp | 39 ++++++++++++++++++++++++++++ modules/imgcodecs/test/test_png.cpp | 11 +++++++- 5 files changed, 82 insertions(+), 1 deletion(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 8e30518445..4138c81098 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1537,6 +1537,8 @@ if(WITH_SPNG) elseif(HAVE_SPNG) status(" PNG:" "${SPNG_LIBRARY} (ver ${SPNG_VERSION})") endif() + + status(" Metadata Support:" "EXIF XMP ICC") # SPNG does not support cICP chunk. elseif(WITH_PNG OR HAVE_PNG) status(" PNG:" PNG_FOUND THEN "${PNG_LIBRARY} (ver ${PNG_VERSION_STRING})" ELSE "build (ver ${PNG_VERSION_STRING})") if(BUILD_PNG AND PNG_HARDWARE_OPTIMIZATIONS) @@ -1565,6 +1567,14 @@ elseif(WITH_PNG OR HAVE_PNG) elseif(BUILD_PNG) status(" SIMD Support Request:" "NO") endif() + + if(NOT (PNG_VERSION_STRING VERSION_LESS "1.6.45")) + status(" Metadata Support:" "EXIF XMP ICC cICP") + elseif(NOT (PNG_VERSION_STRING VERSION_LESS "1.6.31")) + status(" Metadata Support:" "EXIF XMP ICC") + else() + status(" Metadata Support:" "XMP ICC") + endif() endif() if(WITH_TIFF OR HAVE_TIFF) diff --git a/modules/imgcodecs/CMakeLists.txt b/modules/imgcodecs/CMakeLists.txt index c322645b20..d11b26b9ca 100644 --- a/modules/imgcodecs/CMakeLists.txt +++ b/modules/imgcodecs/CMakeLists.txt @@ -204,4 +204,13 @@ if(TARGET opencv_test_imgcodecs AND ((HAVE_PNG AND NOT (PNG_VERSION_STRING VERSI # details: https://github.com/glennrp/libpng/commit/68cb0aaee3de6371b81a4613476d9b33e43e95b1 ocv_target_compile_definitions(opencv_test_imgcodecs PRIVATE OPENCV_IMGCODECS_PNG_WITH_EXIF=1) endif() +if(TARGET opencv_test_imgcodecs AND ((HAVE_PNG AND NOT (PNG_VERSION_STRING VERSION_LESS "1.6.45")) OR HAVE_SPNG)) + # cICP does not support in spng + # cICP support added in libpng 1.6.45 + ocv_target_compile_definitions(opencv_test_imgcodecs PRIVATE OPENCV_IMGCODECS_PNG_WITH_cICP=1) +endif() +if(TARGET opencv_test_imgcodecs AND HAVE_PNG AND PNG_VERSION_STRING VERSION_LESS "1.6.0") + # Old libpng (< 1.6.0) is known to have lower precision in internal RGB-to-Gray calculation for 16-bit images. + ocv_target_compile_definitions(opencv_test_imgcodecs PRIVATE OPENCV_IMGCODECS_PNG_EPS_16BIT_GRAY=9) +endif() ocv_add_perf_tests() diff --git a/modules/imgcodecs/src/grfmt_png.cpp b/modules/imgcodecs/src/grfmt_png.cpp index e1e5d9a710..824cf961f9 100644 --- a/modules/imgcodecs/src/grfmt_png.cpp +++ b/modules/imgcodecs/src/grfmt_png.cpp @@ -684,6 +684,20 @@ bool PngDecoder::readData( Mat& img ) m_exif.parseExif(exif, num_exif); } #endif +#ifdef PNG_cICP_SUPPORTED + png_byte prim_id, tran_id, matrix_id, video_full_range_flag; + if (png_get_cICP(m_png_ptr, m_info_ptr, &prim_id, &tran_id, &matrix_id, &video_full_range_flag)) + { + uint8_t cicp_data[4] = { + static_cast(prim_id), + static_cast(tran_id), + static_cast(matrix_id), + static_cast(video_full_range_flag) + }; + auto& out = m_metadata[IMAGE_METADATA_CICP]; + out.insert(out.end(), cicp_data, cicp_data + 4); + } +#endif result = true; } diff --git a/modules/imgcodecs/test/test_exif.cpp b/modules/imgcodecs/test/test_exif.cpp index bd89728ce0..ebc849e9e6 100644 --- a/modules/imgcodecs/test/test_exif.cpp +++ b/modules/imgcodecs/test/test_exif.cpp @@ -144,6 +144,17 @@ namespace opencv_test { namespace { return iccp_data; } +#ifdef OPENCV_IMGCODECS_PNG_WITH_cICP + static std::vector getSampleCicpData() { + return { + 9, // BT.2020 / BT.2100 + 16, // SMPTE ST 2084 (PQ) + 0, // Identity (RGB) + 1, // Full Range + }; + } +#endif + /** * Test to check whether the EXIF orientation tag was processed successfully or not. * The test uses a set of 8 images named testExifOrientation_{1 to 8}.(extension). @@ -457,18 +468,27 @@ TEST(Imgcodecs_Png, Read_Write_With_Exif) EXPECT_EQ(img2.rows, img.rows); EXPECT_EQ(img2.type(), imgtype); EXPECT_EQ(read_metadata_types, read_metadata_types2); + +#ifdef OPENCV_IMGCODECS_PNG_WITH_EXIF ASSERT_GE(read_metadata_types.size(), 1u); EXPECT_EQ(read_metadata, read_metadata2); EXPECT_EQ(read_metadata_types[0], IMAGE_METADATA_EXIF); EXPECT_EQ(read_metadata_types.size(), read_metadata.size()); EXPECT_EQ(read_metadata[0], metadata[0]); +#else + ASSERT_GE(read_metadata_types.size(), 0u); +#endif EXPECT_EQ(cv::norm(img2, img3, NORM_INF), 0.); double mse = cv::norm(img, img2, NORM_L2SQR)/(img.rows*img.cols); EXPECT_EQ(mse, 0); // png is lossless remove(outputname.c_str()); } +#ifdef OPENCV_IMGCODECS_PNG_WITH_cICP +TEST(Imgcodecs_Png, Read_Write_With_Exif_Xmp_Iccp_cICP) +#else TEST(Imgcodecs_Png, Read_Write_With_Exif_Xmp_Iccp) +#endif { int png_compression = 3; int imgtype = CV_MAKETYPE(CV_8U, 3); @@ -482,6 +502,11 @@ TEST(Imgcodecs_Png, Read_Write_With_Exif_Xmp_Iccp) getSampleIccpData(), }; +#ifdef OPENCV_IMGCODECS_PNG_WITH_cICP + metadata_types.push_back(IMAGE_METADATA_CICP); + metadata.push_back(getSampleCicpData()); +#endif + std::vector write_params = { IMWRITE_PNG_COMPRESSION, png_compression }; @@ -498,9 +523,23 @@ TEST(Imgcodecs_Png, Read_Write_With_Exif_Xmp_Iccp) EXPECT_EQ(img2.rows, img.rows); EXPECT_EQ(img2.type(), imgtype); +#ifdef OPENCV_IMGCODECS_PNG_WITH_EXIF EXPECT_EQ(metadata_types, read_metadata_types); EXPECT_EQ(read_metadata_types, read_metadata_types2); EXPECT_EQ(metadata, read_metadata); +#else + ASSERT_GE(read_metadata_types.size(), 2u); + EXPECT_EQ(read_metadata_types[0], IMAGE_METADATA_XMP); + EXPECT_EQ(read_metadata_types[1], IMAGE_METADATA_ICCP); + + ASSERT_GE(read_metadata_types2.size(), 2u); + EXPECT_EQ(read_metadata_types2[0], IMAGE_METADATA_XMP); + EXPECT_EQ(read_metadata_types2[1], IMAGE_METADATA_ICCP); + + ASSERT_GE(read_metadata.size(), 2u); + EXPECT_EQ(metadata[1], read_metadata[0]); + EXPECT_EQ(metadata[2], read_metadata[1]); +#endif remove(outputname.c_str()); } diff --git a/modules/imgcodecs/test/test_png.cpp b/modules/imgcodecs/test/test_png.cpp index 1c8eae233c..2110bcd795 100644 --- a/modules/imgcodecs/test/test_png.cpp +++ b/modules/imgcodecs/test/test_png.cpp @@ -8,6 +8,13 @@ namespace opencv_test { namespace { #if defined(HAVE_PNG) || defined(HAVE_SPNG) +// See https://github.com/opencv/opencv/pull/28615 +// Precision differences in 16-bit grayscale conversion between old and modern libpng versions +#define OPENCV_IMGCODECS_PNG_EPS_DEFAULT (4) +#ifndef OPENCV_IMGCODECS_PNG_EPS_16BIT_GRAY +#define OPENCV_IMGCODECS_PNG_EPS_16BIT_GRAY (OPENCV_IMGCODECS_PNG_EPS_DEFAULT) +#endif + TEST(Imgcodecs_Png, write_big) { const string root = cvtest::TS::ptr()->get_data_path(); @@ -276,10 +283,12 @@ TEST_P(Imgcodecs_Png_PngSuite, decode) cvtColor(gt_3, gt_258, COLOR_BGR2RGB); } + const double epsGrayAnydepth = ((gt.depth() == CV_16U) && (gt.channels() > 1)) ? OPENCV_IMGCODECS_PNG_EPS_16BIT_GRAY: OPENCV_IMGCODECS_PNG_EPS_DEFAULT; + // Perform comparisons with different imread flags EXPECT_PRED_FORMAT2(cvtest::MatComparator(1, 0), imread(filename, IMREAD_GRAYSCALE), gt_0); EXPECT_PRED_FORMAT2(cvtest::MatComparator(1, 0), imread(filename, IMREAD_COLOR), gt_1); - EXPECT_PRED_FORMAT2(cvtest::MatComparator(4, 0), imread(filename, IMREAD_ANYDEPTH), gt_2); + EXPECT_PRED_FORMAT2(cvtest::MatComparator(epsGrayAnydepth, 0), imread(filename, IMREAD_ANYDEPTH), gt_2); // IMREAD_GRAYSCALE is used. EXPECT_PRED_FORMAT2(cvtest::MatComparator(0, 0), imread(filename, IMREAD_COLOR | IMREAD_ANYDEPTH), gt_3); EXPECT_PRED_FORMAT2(cvtest::MatComparator(1, 0), imread(filename, IMREAD_COLOR_RGB), gt_256); EXPECT_PRED_FORMAT2(cvtest::MatComparator(0, 0), imread(filename, IMREAD_COLOR_RGB | IMREAD_ANYDEPTH), gt_258);