From f54286b672d346514a346ad062d774e41f52b93e Mon Sep 17 00:00:00 2001 From: Yan Wen Date: Fri, 18 Jul 2025 09:19:58 +0100 Subject: [PATCH 1/9] Merge pull request #27453 from MELSunny:4.x Add Raspberry Pi 4 and 5 V4L2 Stateless HEVC Hardware Acceleration with FFmpeg #27453 This PR enables V4L2 stateless HEVC hardware acceleration for Raspberry Pi 5 within OpenCV's videoio module. It leverages FFmpeg's drm acceleration ([FFmpeg API changes](https://github.com/FFmpeg/FFmpeg/blob/ee1f79b0fa4c82da9c19328b049b593c71611402/doc/APIchanges#L1529)), significantly improving HEVC decoding performance on RPi5 for robotics and embedded vision applications. I have a working proof-of-concept with local benchmarks showing clear gains. Checklist Status: Ready: License, branch (4.x), FFmpeg reference, and (linked) related issue (#27452). Seeking Guidance: Need help with formal C++ performance/accuracy tests, opencv_extra integration, and full documentation/examples. As a Python developer, I welcome C++ best practice feedback and assistance with testing setup. ### 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 - [ ] 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 --- modules/videoio/include/opencv2/videoio.hpp | 1 + modules/videoio/src/cap_ffmpeg_hw.hpp | 20 ++++++++++++++++++++ modules/videoio/src/cap_interface.hpp | 1 + modules/videoio/test/test_precomp.hpp | 1 + modules/videoio/test/test_video_io.cpp | 1 + samples/tapi/video_acceleration.cpp | 1 + 6 files changed, 25 insertions(+) diff --git a/modules/videoio/include/opencv2/videoio.hpp b/modules/videoio/include/opencv2/videoio.hpp index bdda38e5e2..d618874c4a 100644 --- a/modules/videoio/include/opencv2/videoio.hpp +++ b/modules/videoio/include/opencv2/videoio.hpp @@ -268,6 +268,7 @@ enum VideoAccelerationType VIDEO_ACCELERATION_D3D11 = 2, //!< DirectX 11 VIDEO_ACCELERATION_VAAPI = 3, //!< VAAPI VIDEO_ACCELERATION_MFX = 4, //!< libmfx (Intel MediaSDK/oneVPL) + VIDEO_ACCELERATION_DRM = 5, //!< Raspberry Pi V4 }; //! @} Hardware acceleration support diff --git a/modules/videoio/src/cap_ffmpeg_hw.hpp b/modules/videoio/src/cap_ffmpeg_hw.hpp index 0998ee9f48..65045a2bc7 100644 --- a/modules/videoio/src/cap_ffmpeg_hw.hpp +++ b/modules/videoio/src/cap_ffmpeg_hw.hpp @@ -57,6 +57,9 @@ extern "C" { #ifdef HAVE_MFX // dependency only on MFX header files, no linkage dependency #include #endif +#ifdef HAVE_DRM +#include +#endif } #define HW_DEFAULT_POOL_SIZE 32 @@ -81,6 +84,7 @@ const char* getVideoAccelerationName(VideoAccelerationType va_type) case VIDEO_ACCELERATION_D3D11: return "d3d11"; case VIDEO_ACCELERATION_VAAPI: return "vaapi"; case VIDEO_ACCELERATION_MFX: return "mfx"; + case VIDEO_ACCELERATION_DRM : return "drm"; } return "unknown"; } @@ -119,6 +123,7 @@ std::string getDecoderConfiguration(VideoAccelerationType va_type, AVDictionary case VIDEO_ACCELERATION_D3D11: return ""; case VIDEO_ACCELERATION_VAAPI: return "vaapi.iHD"; case VIDEO_ACCELERATION_MFX: return "qsv.iHD"; + case VIDEO_ACCELERATION_DRM: return "drm"; } return ""; #endif @@ -158,6 +163,8 @@ std::string getEncoderConfiguration(VideoAccelerationType va_type, AVDictionary case VIDEO_ACCELERATION_D3D11: return ""; case VIDEO_ACCELERATION_VAAPI: return "vaapi.iHD"; case VIDEO_ACCELERATION_MFX: return "qsv.iHD"; + case VIDEO_ACCELERATION_DRM: return ""; + // Raspberry Pi 5 has no encoders, so we don't support it } return "unknown"; #endif @@ -236,6 +243,18 @@ bool hw_check_device(AVBufferRef* ctx, AVHWDeviceType hw_type, const std::string dxgiDevice->Release(); } } +#endif +#if defined(HAVE_DRM) + if (hw_device_ctx->type == AV_HWDEVICE_TYPE_DRM) { + AVDRMDeviceContext *drm_ctx = (AVDRMDeviceContext *)hw_device_ctx->hwctx; + if (drm_ctx->fd >= 0) { + drmVersionPtr drm_version = drmGetVersion(drm_ctx->fd); + if (drm_version) { + device_name = drm_version->name; + drmFreeVersion(drm_version); + } + } + } #endif if (hw_device_ctx->type == AV_HWDEVICE_TYPE_VAAPI) { #if defined(HAVE_VA) && (VA_MAJOR_VERSION >= 1) @@ -908,6 +927,7 @@ VideoAccelerationType hw_type_to_va_type(AVHWDeviceType hw_type) { { AV_HWDEVICE_TYPE_VAAPI, VIDEO_ACCELERATION_VAAPI }, { AV_HWDEVICE_TYPE_QSV, VIDEO_ACCELERATION_MFX }, { AV_HWDEVICE_TYPE_CUDA, (VideoAccelerationType)(1 << 11) }, + { AV_HWDEVICE_TYPE_DRM, VIDEO_ACCELERATION_DRM } }; for (const HWTypeFFMPEG& hw : known_hw_types) { if (hw_type == hw.hw_type) diff --git a/modules/videoio/src/cap_interface.hpp b/modules/videoio/src/cap_interface.hpp index a1924f5682..58cada06d3 100644 --- a/modules/videoio/src/cap_interface.hpp +++ b/modules/videoio/src/cap_interface.hpp @@ -417,6 +417,7 @@ std::ostream& operator<<(std::ostream& out, const VideoAccelerationType& va_type case VIDEO_ACCELERATION_D3D11: out << "D3D11"; return out; case VIDEO_ACCELERATION_VAAPI: out << "VAAPI"; return out; case VIDEO_ACCELERATION_MFX: out << "MFX"; return out; + case VIDEO_ACCELERATION_DRM: out << "DRM"; return out; } out << cv::format("UNKNOWN(0x%ux)", static_cast(va_type)); return out; diff --git a/modules/videoio/test/test_precomp.hpp b/modules/videoio/test/test_precomp.hpp index 205d8f92bd..323f407c68 100644 --- a/modules/videoio/test/test_precomp.hpp +++ b/modules/videoio/test/test_precomp.hpp @@ -35,6 +35,7 @@ std::ostream& operator<<(std::ostream& out, const VideoAccelerationType& va_type {VIDEO_ACCELERATION_D3D11, "D3D11"}, {VIDEO_ACCELERATION_VAAPI, "VAAPI"}, {VIDEO_ACCELERATION_MFX, "MFX"}, + {VIDEO_ACCELERATION_DRM, "DRM"}, }; for (const auto& va : va_types) { if (va_type == va.va_type) { diff --git a/modules/videoio/test/test_video_io.cpp b/modules/videoio/test/test_video_io.cpp index 03d69dfe5b..0a631d8a04 100644 --- a/modules/videoio/test/test_video_io.cpp +++ b/modules/videoio/test/test_video_io.cpp @@ -871,6 +871,7 @@ static const VideoAccelerationType hw_types[] = { VIDEO_ACCELERATION_D3D11, #else VIDEO_ACCELERATION_VAAPI, + VIDEO_ACCELERATION_DRM, #endif }; diff --git a/samples/tapi/video_acceleration.cpp b/samples/tapi/video_acceleration.cpp index 5169236c29..b0d0e5be5d 100644 --- a/samples/tapi/video_acceleration.cpp +++ b/samples/tapi/video_acceleration.cpp @@ -42,6 +42,7 @@ struct { { VIDEO_ACCELERATION_D3D11, "d3d11" }, { VIDEO_ACCELERATION_VAAPI, "vaapi" }, { VIDEO_ACCELERATION_MFX, "mfx" }, + { VIDEO_ACCELERATION_DRM, "drm" }, }; class FPSCounter { From e2d87defd10d415c29120c3b1c07ac7364ba12f2 Mon Sep 17 00:00:00 2001 From: Pierre Chatelier Date: Fri, 18 Jul 2025 10:24:00 +0200 Subject: [PATCH 2/9] Merge pull request #27522 from chacha21:fix_cuda129_msvc Fix compilation problems with MSVC+Cuda 12.9 #27522 fix for #27521 Actually, when ENABLE_CUDA_FIRST_CLASS_LANGUAGE is enabled, the fix it not necessary. However, even when ENABLE_CUDA_FIRST_CLASS_LANGUAGE is enabled, I have checked that the fix is harmless So I propose to keep it simple for now and enable the fix whatever the state of ENABLE_CUDA_FIRST_CLASS_LANGUAGE ### 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 - [ ] 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 --- cmake/OpenCVDetectCUDAUtils.cmake | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/cmake/OpenCVDetectCUDAUtils.cmake b/cmake/OpenCVDetectCUDAUtils.cmake index 040273d6c5..7f0f923380 100644 --- a/cmake/OpenCVDetectCUDAUtils.cmake +++ b/cmake/OpenCVDetectCUDAUtils.cmake @@ -388,8 +388,13 @@ macro(ocv_nvcc_flags) set(CUDA_NVCC_FLAGS ${CUDA_NVCC_FLAGS} -Xcompiler=-fno-finite-math-only) endif() - if(WIN32 AND NOT (CUDA_VERSION VERSION_LESS "11.2")) - set(CUDA_NVCC_FLAGS ${CUDA_NVCC_FLAGS} -Xcudafe --display_error_number --diag-suppress 1394,1388) + if(WIN32) + if (NOT (CUDA_VERSION VERSION_LESS "11.2")) + set(CUDA_NVCC_FLAGS ${CUDA_NVCC_FLAGS} -Xcudafe --display_error_number --diag-suppress 1394,1388) + endif() + if(CUDA_VERSION GREATER "12.8") + set(CUDA_NVCC_FLAGS ${CUDA_NVCC_FLAGS} -Xcompiler=/Zc:preprocessor) + endif() endif() if(CMAKE_CROSSCOMPILING AND (ARM OR AARCH64)) From ab8a1fa2801bcaa197c2629534c046ec7b1166c5 Mon Sep 17 00:00:00 2001 From: Suleyman TURKMEN Date: Fri, 18 Jul 2025 20:41:12 +0300 Subject: [PATCH 3/9] Merge pull request #27551 from sturkmen72:png-improvements Add enum IMWRITE_PNG_ZLIBBUFFER_SIZE #27551 ### Pull Request Readiness Checklist This patch enables users to set the internal zlib compression buffer size for PNG encoding 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 - [ ] 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 --- .../imgcodecs/include/opencv2/imgcodecs.hpp | 7 +++-- modules/imgcodecs/src/grfmt_png.cpp | 30 ++++++++++++------- modules/imgcodecs/test/test_png.cpp | 7 +++-- 3 files changed, 28 insertions(+), 16 deletions(-) diff --git a/modules/imgcodecs/include/opencv2/imgcodecs.hpp b/modules/imgcodecs/include/opencv2/imgcodecs.hpp index 14d562c823..d102c5c9c7 100644 --- a/modules/imgcodecs/include/opencv2/imgcodecs.hpp +++ b/modules/imgcodecs/include/opencv2/imgcodecs.hpp @@ -94,9 +94,10 @@ enum ImwriteFlags { IMWRITE_JPEG_CHROMA_QUALITY = 6, //!< Separate chroma quality level, 0 - 100, default is -1 - don't use. If JPEG_LIB_VERSION < 70, Not supported. IMWRITE_JPEG_SAMPLING_FACTOR = 7, //!< For JPEG, set sampling factor. See cv::ImwriteJPEGSamplingFactorParams. IMWRITE_PNG_COMPRESSION = 16, //!< For PNG, it can be the compression level from 0 to 9. A higher value means a smaller size and longer compression time. If specified, strategy is changed to IMWRITE_PNG_STRATEGY_DEFAULT (Z_DEFAULT_STRATEGY). Default value is 1 (best speed setting). - IMWRITE_PNG_STRATEGY = 17, //!< One of cv::ImwritePNGFlags, default is IMWRITE_PNG_STRATEGY_RLE. - IMWRITE_PNG_BILEVEL = 18, //!< Binary level PNG, 0 or 1, default is 0. - IMWRITE_PNG_FILTER = 19, //!< One of cv::ImwritePNGFilterFlags, default is IMWRITE_PNG_FILTER_SUB. + IMWRITE_PNG_STRATEGY = 17, //!< For PNG, One of cv::ImwritePNGFlags, default is IMWRITE_PNG_STRATEGY_RLE. + IMWRITE_PNG_BILEVEL = 18, //!< For PNG, Binary level PNG, 0 or 1, default is 0. + IMWRITE_PNG_FILTER = 19, //!< For PNG, One of cv::ImwritePNGFilterFlags, default is IMWRITE_PNG_FILTER_SUB. + IMWRITE_PNG_ZLIBBUFFER_SIZE = 20, //!< For PNG, sets the size of the internal zlib compression buffer in bytes. IMWRITE_PXM_BINARY = 32, //!< For PPM, PGM, or PBM, it can be a binary format flag, 0 or 1. Default value is 1. IMWRITE_EXR_TYPE = (3 << 4) + 0 /* 48 */, //!< override EXR storage type (FLOAT (FP32) is default) IMWRITE_EXR_COMPRESSION = (3 << 4) + 1 /* 49 */, //!< override EXR compression type (ZIP_COMPRESSION = 3 is default) diff --git a/modules/imgcodecs/src/grfmt_png.cpp b/modules/imgcodecs/src/grfmt_png.cpp index c78c0efc55..903ae2475b 100644 --- a/modules/imgcodecs/src/grfmt_png.cpp +++ b/modules/imgcodecs/src/grfmt_png.cpp @@ -947,26 +947,36 @@ bool PngEncoder::write( const Mat& img, const std::vector& params ) for( size_t i = 0; i < params.size(); i += 2 ) { - if( params[i] == IMWRITE_PNG_COMPRESSION ) + switch (params[i]) { + case IMWRITE_PNG_COMPRESSION: m_compression_strategy = IMWRITE_PNG_STRATEGY_DEFAULT; // Default strategy m_compression_level = params[i+1]; m_compression_level = MIN(MAX(m_compression_level, 0), Z_BEST_COMPRESSION); set_compression_level = true; - } - if( params[i] == IMWRITE_PNG_STRATEGY ) - { + break; + + case IMWRITE_PNG_STRATEGY: m_compression_strategy = params[i+1]; m_compression_strategy = MIN(MAX(m_compression_strategy, 0), Z_FIXED); - } - if( params[i] == IMWRITE_PNG_BILEVEL ) - { + break; + + case IMWRITE_PNG_BILEVEL: m_isBilevel = params[i+1] != 0; - } - if( params[i] == IMWRITE_PNG_FILTER ) - { + break; + + case IMWRITE_PNG_FILTER: m_filter = params[i+1]; set_filter = true; + break; + + case IMWRITE_PNG_ZLIBBUFFER_SIZE: + png_set_compression_buffer_size(png_ptr, params[i+1]); + break; + + default: + CV_LOG_WARNING(NULL, "An unknown or unsupported ImwriteFlags value was specified and has been ignored."); + break; } } diff --git a/modules/imgcodecs/test/test_png.cpp b/modules/imgcodecs/test/test_png.cpp index 583039202a..a1a743ee0f 100644 --- a/modules/imgcodecs/test/test_png.cpp +++ b/modules/imgcodecs/test/test_png.cpp @@ -12,14 +12,15 @@ TEST(Imgcodecs_Png, write_big) { const string root = cvtest::TS::ptr()->get_data_path(); const string filename = root + "readwrite/read.png"; - const string dst_file = cv::tempfile(".png"); Mat img; ASSERT_NO_THROW(img = imread(filename)); ASSERT_FALSE(img.empty()); EXPECT_EQ(13043, img.cols); EXPECT_EQ(13917, img.rows); - ASSERT_NO_THROW(imwrite(dst_file, img)); - EXPECT_EQ(0, remove(dst_file.c_str())); + + vector buff; + ASSERT_NO_THROW(imencode(".png", img, buff, { IMWRITE_PNG_ZLIBBUFFER_SIZE, INT_MAX })); + EXPECT_EQ((size_t)816219, buff.size()); } TEST(Imgcodecs_Png, encode) From 15e3cf3cc5eb05a2aaf64ebde451be6870538fa9 Mon Sep 17 00:00:00 2001 From: Andrei Tamas Date: Tue, 22 Jul 2025 17:09:39 +0300 Subject: [PATCH 4/9] Merge pull request #27549 from atamas19:tensor_naming_functionality G-API: Implement cfgEnsureNamedTensors option to OpenVINO Params #27549 Added the option cfgEnsureNamedTensors to be applied on OpenVINO models with nameless tensors. If a tensor doesn't have a name, it sets a default one. The default name is created using OpenVINO's standard `make_default_tensor_name` . `make_default_tensor_name` had to be rewritten because it is only available in OpenVINO's dev_api. ### 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 - [ ] There is a reference to the original bug report and related work - [ ] There is accuracy test, performance test and test data in opencv_extra repository, if applicable Patch to opencv_extra has the same branch name. - [x] The feature is well documented and sample code can be built with the project CMake --- .../gapi/include/opencv2/gapi/infer/ov.hpp | 26 +++++++++++++++++++ modules/gapi/src/backends/ov/govbackend.cpp | 25 ++++++++++++++++++ 2 files changed, 51 insertions(+) diff --git a/modules/gapi/include/opencv2/gapi/infer/ov.hpp b/modules/gapi/include/opencv2/gapi/infer/ov.hpp index 782792489b..9515744cac 100644 --- a/modules/gapi/include/opencv2/gapi/infer/ov.hpp +++ b/modules/gapi/include/opencv2/gapi/infer/ov.hpp @@ -99,6 +99,8 @@ struct ParamDesc { PluginConfigT config; size_t nireq = 1; + + bool ensure_named_tensors = false; }; // NB: Just helper to avoid code duplication. @@ -205,6 +207,24 @@ public: return *this; } + /** @brief Ensures the model has named tensors. + + This function is used to ensure that all tensors in the model have names. + It goes through all input and output nodes of the model and sets the names + if they are not set. This is neccessary for models with nameless tensors. + + If a tensor does not have a name, it will be assigned a default name + based on the producer node's friendly name. If the producer node has multiple + outputs, the name will be in the form "node_name:N", where N is the output index. + + @param flag If true, then it guarantees that all tensors will have names. + @return reference to this parameter structure. + */ + Params& cfgEnsureNamedTensors(bool flag = true) { + m_desc.ensure_named_tensors = flag; + return *this; + } + /** @brief Specifies tensor layout for an input layer. The function is used to set tensor layout for an input layer. @@ -524,6 +544,12 @@ public: return *this; } + /** @see ov::Params::cfgEnsureNamedTensors. */ + Params& cfgEnsureNamedTensors(bool flag = true) { + m_desc.ensure_named_tensors = flag; + return *this; + } + /** @see ov::Params::cfgInputTensorLayout. */ Params& cfgInputTensorLayout(std::string layout) { detail::getModelToSetAttrOrThrow(m_desc.kind, "input tensor layout") diff --git a/modules/gapi/src/backends/ov/govbackend.cpp b/modules/gapi/src/backends/ov/govbackend.cpp index ed2b6fd94e..dbaba382db 100644 --- a/modules/gapi/src/backends/ov/govbackend.cpp +++ b/modules/gapi/src/backends/ov/govbackend.cpp @@ -69,6 +69,27 @@ ov::Core cv::gapi::ov::wrap::getCore() { ? create_OV_Core_pointer() : create_OV_Core_instance(); } +static std::string make_default_tensor_name(const ov::Output& output) { + auto default_name = output.get_node()->get_friendly_name(); + if (output.get_node()->get_output_size() > 1) { + default_name += ':' + std::to_string(output.get_index()); + } + return default_name; +} + +static void ensureNamedTensors(std::shared_ptr model) { + for (auto& input : model->inputs()) { + if (input.get_names().empty()) { + input.set_names({make_default_tensor_name(input)}); + } + } + for (auto& output : model->outputs()) { + if (output.get_names().empty()) { + output.set_names({make_default_tensor_name(output)}); + } + } +} + static ov::AnyMap toOV(const ParamDesc::PluginConfigT &config) { return {config.begin(), config.end()}; } @@ -236,6 +257,10 @@ struct OVUnit { .read_model(desc.model_path, desc.bin_path); GAPI_Assert(model); + if (params.ensure_named_tensors) { + ensureNamedTensors(model); + } + if (params.num_in == 1u && params.input_names.empty()) { params.input_names = { model->inputs().begin()->get_any_name() }; } From 32bd8c9632ffb4ff678fe4a3c70c9aa19a62b0dc Mon Sep 17 00:00:00 2001 From: Suleyman TURKMEN Date: Wed, 23 Jul 2025 14:59:34 +0300 Subject: [PATCH 5/9] Merge pull request #27503 from sturkmen72:metadata [GSOC 2025] PNG&WebP Metadata Reading Writing Improvements #27503 Merge with https://github.com/opencv/opencv_extra/pull/1271 ### 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 - [ ] There is a reference to the original bug report and related work - [ ] 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 --- .../imgcodecs/include/opencv2/imgcodecs.hpp | 50 +-- modules/imgcodecs/src/exif.cpp | 68 ++++ modules/imgcodecs/src/exif.hpp | 1 + modules/imgcodecs/src/grfmt_base.cpp | 30 +- modules/imgcodecs/src/grfmt_base.hpp | 21 +- modules/imgcodecs/src/grfmt_png.cpp | 96 ++++- modules/imgcodecs/src/grfmt_png.hpp | 1 - modules/imgcodecs/src/grfmt_spng.cpp | 77 ++++ modules/imgcodecs/src/grfmt_webp.cpp | 89 ++++- modules/imgcodecs/src/loadsave.cpp | 18 +- modules/imgcodecs/test/test_exif.cpp | 330 ++++++++++++++---- 11 files changed, 655 insertions(+), 126 deletions(-) diff --git a/modules/imgcodecs/include/opencv2/imgcodecs.hpp b/modules/imgcodecs/include/opencv2/imgcodecs.hpp index d102c5c9c7..039482017f 100644 --- a/modules/imgcodecs/include/opencv2/imgcodecs.hpp +++ b/modules/imgcodecs/include/opencv2/imgcodecs.hpp @@ -254,11 +254,13 @@ enum ImwriteGIFCompressionFlags { enum ImageMetadataType { - IMAGE_METADATA_UNKNOWN = -1, - IMAGE_METADATA_EXIF = 0, - IMAGE_METADATA_XMP = 1, - IMAGE_METADATA_ICCP = 2, - IMAGE_METADATA_MAX = 2 + IMAGE_METADATA_UNKNOWN = -1, // Used when metadata type is unrecognized or not set + + IMAGE_METADATA_EXIF = 0, // EXIF metadata (e.g., camera info, GPS, orientation) + IMAGE_METADATA_XMP = 1, // XMP metadata (eXtensible Metadata Platform - Adobe format) + IMAGE_METADATA_ICCP = 2, // ICC Profile (color profile for color management) + + IMAGE_METADATA_MAX = 2 // Highest valid index (usually used for bounds checking) }; //! @} imgcodecs_flags @@ -355,7 +357,7 @@ Currently, the following file formats are supported: the environment variable `OPENCV_IO_MAX_IMAGE_PIXELS`. See @ref tutorial_env_reference. @param filename Name of the file to be loaded. -@param flags Flag that can take values of `cv::ImreadModes`. +@param flags Flag that can take values of cv::ImreadModes, default with cv::IMREAD_COLOR_BGR. */ CV_EXPORTS_W Mat imread( const String& filename, int flags = IMREAD_COLOR_BGR ); @@ -364,19 +366,25 @@ CV_EXPORTS_W Mat imread( const String& filename, int flags = IMREAD_COLOR_BGR ); This is an overloaded member function, provided for convenience. It differs from the above function only in what argument(s) it accepts and the return value. @param filename Name of file to be loaded. @param dst object in which the image will be loaded. -@param flags Flag that can take values of cv::ImreadModes +@param flags Flag that can take values of cv::ImreadModes, default with cv::IMREAD_COLOR_BGR. @note The image passing through the img parameter can be pre-allocated. The memory is reused if the shape and the type match with the load image. */ CV_EXPORTS_W void imread( const String& filename, OutputArray dst, int flags = IMREAD_COLOR_BGR ); -/** @brief Reads an image from a file together with associated metadata. +/** @brief Reads an image from a file along with associated metadata. -The function imreadWithMetadata reads image from the specified file. It does the same thing as imread, but additionally reads metadata if the corresponding file contains any. +This function behaves similarly to cv::imread(), loading an image from the specified file. +In addition to the image pixel data, it also attempts to extract any available metadata +embedded in the file (such as EXIF, XMP, etc.), depending on file format support. + +@note In the case of color images, the decoded images will have the channels stored in **B G R** order. @param filename Name of the file to be loaded. -@param metadataTypes Output vector with types of metadata chucks returned in metadata, see ImageMetadataType. -@param metadata Output vector of vectors or vector of matrices to store the retrieved metadata -@param flags Flag that can take values of cv::ImreadModes +@param metadataTypes Output vector with types of metadata chunks returned in metadata, see ImageMetadataType. +@param metadata Output vector of vectors or vector of matrices to store the retrieved metadata. +@param flags Flag that can take values of cv::ImreadModes, default with cv::IMREAD_ANYCOLOR. + +@return The loaded image as a cv::Mat object. If the image cannot be read, the function returns an empty matrix. */ CV_EXPORTS_W Mat imreadWithMetadata( const String& filename, CV_OUT std::vector& metadataTypes, OutputArrayOfArrays metadata, int flags = IMREAD_ANYCOLOR); @@ -560,29 +568,31 @@ See cv::imread for the list of supported formats and flags description. @note In the case of color images, the decoded images will have the channels stored in **B G R** order. @param buf Input array or vector of bytes. -@param flags The same flags as in cv::imread, see cv::ImreadModes. +@param flags Flag that can take values of cv::ImreadModes. */ CV_EXPORTS_W Mat imdecode( InputArray buf, int flags ); -/** @brief Reads an image from a buffer in memory together with associated metadata. +/** @brief Reads an image from a memory buffer and extracts associated metadata. -The function imdecode reads an image from the specified buffer in the memory. If the buffer is too short or +This function decodes an image from the specified memory buffer. If the buffer is too short or contains invalid data, the function returns an empty matrix ( Mat::data==NULL ). See cv::imread for the list of supported formats and flags description. @note In the case of color images, the decoded images will have the channels stored in **B G R** order. -@param buf Input array or vector of bytes. -@param metadataTypes Output vector with types of metadata chucks returned in metadata, see ImageMetadataType. +@param buf Input array or vector of bytes containing the encoded image data. +@param metadataTypes Output vector with types of metadata chucks returned in metadata, see cv::ImageMetadataType @param metadata Output vector of vectors or vector of matrices to store the retrieved metadata -@param flags The same flags as in cv::imread, see cv::ImreadModes. +@param flags Flag that can take values of cv::ImreadModes, default with cv::IMREAD_ANYCOLOR. + +@return The decoded image as a cv::Mat object. If decoding fails, the function returns an empty matrix. */ CV_EXPORTS_W Mat imdecodeWithMetadata( InputArray buf, CV_OUT std::vector& metadataTypes, OutputArrayOfArrays metadata, int flags = IMREAD_ANYCOLOR ); /** @overload @param buf Input array or vector of bytes. -@param flags The same flags as in cv::imread, see cv::ImreadModes. +@param flags Flag that can take values of cv::ImreadModes, default with cv::IMREAD_ANYCOLOR. @param dst The optional output placeholder for the decoded matrix. It can save the image reallocations when the function is called repeatedly for images of the same size. In case of decoder failure the function returns empty cv::Mat object, but does not release user-provided dst buffer. @@ -598,7 +608,7 @@ See cv::imreadmulti for the list of supported formats and flags description. @note In the case of color images, the decoded images will have the channels stored in **B G R** order. @param buf Input array or vector of bytes. -@param flags The same flags as in cv::imread, see cv::ImreadModes. +@param flags Flag that can take values of cv::ImreadModes. @param mats A vector of Mat objects holding each page, if more than one. @param range A continuous selection of pages. */ diff --git a/modules/imgcodecs/src/exif.cpp b/modules/imgcodecs/src/exif.cpp index 3f1bbdbe18..5484031c50 100644 --- a/modules/imgcodecs/src/exif.cpp +++ b/modules/imgcodecs/src/exif.cpp @@ -42,6 +42,7 @@ #include "precomp.hpp" #include "exif.hpp" +#include "opencv2/core/utils/logger.hpp" namespace { @@ -53,6 +54,43 @@ namespace { namespace cv { +static std::string HexStringToBytes(const char* hexstring, size_t expected_length); + +// Converts the NULL terminated 'hexstring' which contains 2-byte character +// representations of hex values to raw data. +// 'hexstring' may contain values consisting of [A-F][a-f][0-9] in pairs, +// e.g., 7af2..., separated by any number of newlines. +// 'expected_length' is the anticipated processed size. +// On success the raw buffer is returned with its length equivalent to +// 'expected_length'. NULL is returned if the processed length is less than +// 'expected_length' or any character aside from those above is encountered. +// The returned buffer must be freed by the caller. +static std::string HexStringToBytes(const char* hexstring, + size_t expected_length) { + const char* src = hexstring; + size_t actual_length = 0; + std::string raw_data; + raw_data.resize(expected_length); + char* dst = const_cast(raw_data.data()); + + for (; actual_length < expected_length && *src != '\0'; ++src) { + char* end; + char val[3]; + if (*src == '\n') continue; + val[0] = *src++; + val[1] = *src; + val[2] = '\0'; + *dst++ = static_cast(strtol(val, &end, 16)); + if (end != val + 2) break; + ++actual_length; + } + + if (actual_length != expected_length) { + raw_data.clear(); + } + return raw_data; +} + ExifEntry_t::ExifEntry_t() : field_float(0), field_double(0), field_u32(0), field_s32(0), tag(INVALID_TAG), field_u16(0), field_s16(0), field_u8(0), field_s8(0) @@ -99,6 +137,36 @@ const std::vector& ExifReader::getData() const return m_data; } +bool ExifReader::processRawProfile(const char* profile, size_t profile_len) { + const char* src = profile; + char* end; + int expected_length; + + if (profile == nullptr || profile_len == 0) return false; + + // ImageMagick formats 'raw profiles' as + // '\n\n(%8lu)\n\n'. + if (*src != '\n') { + CV_LOG_WARNING(NULL, cv::format("Malformed raw profile, expected '\\n' got '\\x%.2X'", *src)); + return false; + } + ++src; + // skip the profile name and extract the length. + while (*src != '\0' && *src++ != '\n') {} + expected_length = static_cast(strtol(src, &end, 10)); + if (*end != '\n') { + CV_LOG_WARNING(NULL, cv::format("Malformed raw profile, expected '\\n' got '\\x%.2X'", *src)); + return false; + } + ++end; + + // 'end' now points to the profile payload. + std::string payload = HexStringToBytes(end, expected_length); + if (payload.size() == 0) return false; + + return parseExif((unsigned char*)payload.c_str() + 6, expected_length - 6); +} + /** * @brief Parsing the exif data buffer and prepare (internal) exif directory * diff --git a/modules/imgcodecs/src/exif.hpp b/modules/imgcodecs/src/exif.hpp index 3c5fbc7fe8..137d383ee2 100644 --- a/modules/imgcodecs/src/exif.hpp +++ b/modules/imgcodecs/src/exif.hpp @@ -154,6 +154,7 @@ public: ExifReader(); ~ExifReader(); + bool processRawProfile(const char* profile, size_t profile_len); /** * @brief Parse the file with exif info diff --git a/modules/imgcodecs/src/grfmt_base.cpp b/modules/imgcodecs/src/grfmt_base.cpp index 1241edb077..9c9dead740 100644 --- a/modules/imgcodecs/src/grfmt_base.cpp +++ b/modules/imgcodecs/src/grfmt_base.cpp @@ -56,6 +56,8 @@ BaseImageDecoder::BaseImageDecoder() m_scale_denom = 1; m_use_rgb = false; m_frame_count = 1; + m_read_options = 0; + m_metadata.resize(IMAGE_METADATA_MAX + 1); } bool BaseImageDecoder::haveMetadata(ImageMetadataType type) const @@ -67,12 +69,21 @@ bool BaseImageDecoder::haveMetadata(ImageMetadataType type) const Mat BaseImageDecoder::getMetadata(ImageMetadataType type) const { - if (type == IMAGE_METADATA_EXIF) { - const std::vector& exif = m_exif.getData(); - if (!exif.empty()) { - Mat exifmat(1, (int)exif.size(), CV_8U, (void*)exif.data()); - return exifmat; - } + auto makeMat = [](const std::vector& data) -> Mat { + return data.empty() ? Mat() : Mat(1, (int)data.size(), CV_8U, (void*)data.data()); + }; + + switch (type) { + case IMAGE_METADATA_EXIF: + return makeMat(m_exif.getData()); + + case IMAGE_METADATA_XMP: + case IMAGE_METADATA_ICCP: + return makeMat(m_metadata[type]); + + default: + CV_LOG_WARNING(NULL, "Unknown metadata type requested: " << static_cast(type)); + break; } return Mat(); } @@ -116,6 +127,13 @@ int BaseImageDecoder::setScale( const int& scale_denom ) return temp; } +int BaseImageDecoder::setReadOptions(int read_options) +{ + int temp = m_read_options; + m_read_options = read_options; + return temp; +} + void BaseImageDecoder::setRGB(bool useRGB) { m_use_rgb = useRGB; diff --git a/modules/imgcodecs/src/grfmt_base.hpp b/modules/imgcodecs/src/grfmt_base.hpp index 2eeb2fc130..889fdd0a1d 100644 --- a/modules/imgcodecs/src/grfmt_base.hpp +++ b/modules/imgcodecs/src/grfmt_base.hpp @@ -109,7 +109,24 @@ public: * @param scale_denom The denominator of the scale factor (image is scaled down by 1/scale_denom). * @return The scale factor that was set. */ - virtual int setScale(const int& scale_denom); + int setScale(const int& scale_denom); + + /** + * @brief Set read options for image decoding. + * + * This function sets internal flags that control various read-time behaviors + * such as metadata extraction (e.g., XMP, ICC profiles, textual data) + * during image decoding. The flags can be combined using bitwise OR to enable + * multiple options simultaneously. + * + * @param options Bitwise OR of read option flags to enable. + * + * @return The previous value of the read options flags. + * + * @note Setting this has no effect unless the image format and decoder support + * the selected options. Unknown flags will be ignored. + */ + int setReadOptions(int read_options); /** * @brief Read the image header to extract basic properties (width, height, type). @@ -173,6 +190,8 @@ protected: ExifReader m_exif; ///< Object for reading EXIF metadata from the image. size_t m_frame_count; ///< Number of frames in the image (for animations and multi-page images). Animation m_animation; + int m_read_options; + std::vector > m_metadata; }; diff --git a/modules/imgcodecs/src/grfmt_png.cpp b/modules/imgcodecs/src/grfmt_png.cpp index 903ae2475b..5adf71323e 100644 --- a/modules/imgcodecs/src/grfmt_png.cpp +++ b/modules/imgcodecs/src/grfmt_png.cpp @@ -218,16 +218,14 @@ bool PngDecoder::InitPngPtr() { return false; m_info_ptr = png_create_info_struct(m_png_ptr); - m_end_info = png_create_info_struct(m_png_ptr); - return (m_info_ptr && m_end_info); + return (m_info_ptr != nullptr); } void PngDecoder::ClearPngPtr() { if (m_png_ptr) - png_destroy_read_struct(&m_png_ptr, &m_info_ptr, &m_end_info); + png_destroy_read_struct(&m_png_ptr, &m_info_ptr, nullptr); m_png_ptr = nullptr; m_info_ptr = nullptr; - m_end_info = nullptr; } ImageDecoder PngDecoder::newDecoder() const @@ -573,7 +571,7 @@ bool PngDecoder::readData( Mat& img ) volatile bool result = false; bool color = img.channels() > 1; - if( m_png_ptr && m_info_ptr && m_end_info && m_width && m_height ) + if( m_png_ptr && m_info_ptr && m_width && m_height ) { if( setjmp( png_jmpbuf ( m_png_ptr ) ) == 0 ) { @@ -623,17 +621,49 @@ bool PngDecoder::readData( Mat& img ) buffer[y] = img.data + y*img.step; png_read_image( m_png_ptr, buffer ); - png_read_end( m_png_ptr, m_end_info ); + png_read_end( m_png_ptr, m_info_ptr); + if (m_read_options) { + // Get tEXt chunks + png_textp text_ptr; + int num_text = 0; + + png_get_text(m_png_ptr, m_info_ptr, &text_ptr, &num_text); + + for (size_t i = 0; i < static_cast(num_text); ++i) { + const char* key = text_ptr[i].key; + const char* value = text_ptr[i].text; + size_t len = text_ptr[i].text_length; + + if (key && (!std::strcmp(key, "Raw profile type exif") || !std::strcmp(key, "Raw profile type APP1"))) { + m_exif.processRawProfile(value, len); + } + else if (key && !std::strcmp(key, "XML:com.adobe.xmp")) { + auto& out = m_metadata[IMAGE_METADATA_XMP]; + out.insert(out.end(), + reinterpret_cast(value), + reinterpret_cast(value) + std::strlen(value) + 1); + } + } + + png_charp icc_name; + int compression_type; + png_bytep icc_profile; + png_uint_32 icc_length; + + if (png_get_iCCP(m_png_ptr, m_info_ptr, &icc_name, &compression_type, &icc_profile, &icc_length)) { + auto& out = m_metadata[IMAGE_METADATA_ICCP]; + out.insert(out.end(), + reinterpret_cast(icc_profile), + reinterpret_cast(icc_profile) + icc_length); + } + } #ifdef PNG_eXIf_SUPPORTED png_uint_32 num_exif = 0; png_bytep exif = 0; - // Exif info could be in info_ptr (intro_info) or end_info per specification if( png_get_valid(m_png_ptr, m_info_ptr, PNG_INFO_eXIf) ) png_get_eXIf_1(m_png_ptr, m_info_ptr, &num_exif, &exif); - else if( png_get_valid(m_png_ptr, m_end_info, PNG_INFO_eXIf) ) - png_get_eXIf_1(m_png_ptr, m_end_info, &num_exif, &exif); if( exif && num_exif > 0 ) { @@ -865,6 +895,8 @@ PngEncoder::PngEncoder() m_buf_supported = true; m_support_metadata.assign((size_t)IMAGE_METADATA_MAX+1, false); m_support_metadata[IMAGE_METADATA_EXIF] = true; + m_support_metadata[IMAGE_METADATA_XMP] = true; + m_support_metadata[IMAGE_METADATA_ICCP] = true; op_zstream1.zalloc = NULL; op_zstream2.zalloc = NULL; next_seq_num = 0; @@ -993,6 +1025,42 @@ bool PngEncoder::write( const Mat& img, const std::vector& params ) PNG_INTERLACE_NONE, PNG_COMPRESSION_TYPE_DEFAULT, PNG_FILTER_TYPE_DEFAULT ); + if (!m_metadata.empty()) { + std::vector& exif = m_metadata[IMAGE_METADATA_EXIF]; + if (!exif.empty()) { + png_set_eXIf_1(png_ptr, info_ptr, static_cast(exif.size()), exif.data()); + } + + std::vector& xmp = m_metadata[IMAGE_METADATA_XMP]; + if (!xmp.empty()) { + png_text text_chunk; + text_chunk.compression = PNG_TEXT_COMPRESSION_NONE; + text_chunk.key = const_cast("XML:com.adobe.xmp"); + text_chunk.text = reinterpret_cast(xmp.data()); + text_chunk.text_length = static_cast(xmp.size()); + + png_set_text(png_ptr, info_ptr, &text_chunk, 1); + } + + std::vector iccp = m_metadata[IMAGE_METADATA_ICCP]; + if (!iccp.empty()) { + // PNG standard requires a profile name (null-terminated, max 79 characters, printable Latin-1) + const char* iccp_profile_name = "ICC Profile"; + + // Compression type must be 0 (deflate) as per libpng docs + int compression_type = PNG_COMPRESSION_TYPE_BASE; + + // Some ICC profiles are already compressed (e.g., if saved from Photoshop), + // but png_set_iCCP still expects uncompressed input, and it compresses it internally. + + png_set_iCCP(png_ptr, info_ptr, + iccp_profile_name, + compression_type, + reinterpret_cast(iccp.data()), + static_cast(iccp.size())); + } + } + png_write_info( png_ptr, info_ptr ); if (m_isBilevel) @@ -1006,16 +1074,6 @@ bool PngEncoder::write( const Mat& img, const std::vector& params ) for( y = 0; y < height; y++ ) buffer[y] = img.data + y*img.step; - if (!m_metadata.empty()) { - std::vector& exif = m_metadata[IMAGE_METADATA_EXIF]; - if (!exif.empty()) { - writeChunk(f, "eXIf", exif.data(), (uint32_t)exif.size()); - } - // [TODO] add xmp and icc. They need special handling, - // see https://dev.exiv2.org/projects/exiv2/wiki/The_Metadata_in_PNG_files and - // https://www.libpng.org/pub/png/spec/1.2/PNG-Chunks.html. - } - png_write_image( png_ptr, buffer.data() ); png_write_end( png_ptr, info_ptr ); diff --git a/modules/imgcodecs/src/grfmt_png.hpp b/modules/imgcodecs/src/grfmt_png.hpp index 3a94dd57a3..6094139baf 100644 --- a/modules/imgcodecs/src/grfmt_png.hpp +++ b/modules/imgcodecs/src/grfmt_png.hpp @@ -150,7 +150,6 @@ private: png_structp m_png_ptr = nullptr; // pointer to decompression structure png_infop m_info_ptr = nullptr; // pointer to image information structure - png_infop m_end_info = nullptr; // pointer to one more image information structure int m_bit_depth; FILE* m_f; int m_color_type; diff --git a/modules/imgcodecs/src/grfmt_spng.cpp b/modules/imgcodecs/src/grfmt_spng.cpp index acf2f0d55d..8057af1222 100644 --- a/modules/imgcodecs/src/grfmt_spng.cpp +++ b/modules/imgcodecs/src/grfmt_spng.cpp @@ -422,6 +422,48 @@ bool SPngDecoder::readData(Mat &img) result = m_exif.parseExif((unsigned char *)exif_s.data, exif_s.length); } } + + if (m_read_options) + { + uint32_t text_count; + + // Retrieve all text chunks + if (spng_get_text(png_ptr, NULL, &text_count) == SPNG_OK) + { + std::vector texts(text_count); + spng_get_text(png_ptr, texts.data(), &text_count); + + for (size_t i = 0; i < text_count; ++i) + { + char* key = texts[i].keyword; + char* value = texts[i].text; + size_t len = texts[i].length; + + if (key && (!std::strcmp(key, "Raw profile type exif") || !std::strcmp(key, "Raw profile type APP1"))) + { + m_exif.processRawProfile(value, len); + } + else if (key && !std::strcmp(key, "XML:com.adobe.xmp")) + { + auto& out = m_metadata[IMAGE_METADATA_XMP]; + out.insert(out.end(), + value, + value + len + 1); // include null terminator + } + } + } + + // ICC Profile + spng_iccp iccp_data; + + if (spng_get_iccp(png_ptr, &iccp_data) == SPNG_OK && iccp_data.profile_len > 0) + { + auto& out = m_metadata[IMAGE_METADATA_ICCP]; + out.insert(out.end(), + iccp_data.profile, + iccp_data.profile + iccp_data.profile_len); + } + } } } } @@ -435,6 +477,10 @@ SPngEncoder::SPngEncoder() { m_description = "Portable Network Graphics files (*.png)"; m_buf_supported = true; + m_support_metadata.assign((size_t)IMAGE_METADATA_MAX + 1, false); + m_support_metadata[IMAGE_METADATA_EXIF] = true; + m_support_metadata[IMAGE_METADATA_XMP] = true; + m_support_metadata[IMAGE_METADATA_ICCP] = true; } SPngEncoder::~SPngEncoder() @@ -536,6 +582,37 @@ bool SPngEncoder::write(const Mat &img, const std::vector ¶ms) spng_set_option(ctx, SPNG_IMG_COMPRESSION_LEVEL, compression_level); spng_set_option(ctx, SPNG_IMG_COMPRESSION_STRATEGY, compression_strategy); + if (!m_metadata.empty()) { + std::vector& exif = m_metadata[IMAGE_METADATA_EXIF]; + if (!exif.empty()) { + spng_exif s_exif; + s_exif.data = reinterpret_cast(exif.data()); + s_exif.length = exif.size(); + spng_set_exif(ctx, &s_exif); + } + + std::vector& xmp = m_metadata[IMAGE_METADATA_XMP]; + if (!xmp.empty()) { + spng_text text_chunk; + strncpy(text_chunk.keyword, "XML:com.adobe.xmp", sizeof(text_chunk.keyword) - 1); + text_chunk.keyword[sizeof(text_chunk.keyword) - 1] = '\0'; + text_chunk.type = SPNG_TEXT; + text_chunk.text = reinterpret_cast(xmp.data()); + text_chunk.length = xmp.size(); + spng_set_text(ctx, &text_chunk, 1); + } + + std::vector& iccp = m_metadata[IMAGE_METADATA_ICCP]; + if (!iccp.empty()) { + spng_iccp s_iccp; + strncpy(s_iccp.profile_name, "ICC Profile", sizeof(s_iccp.profile_name) - 1); + s_iccp.profile_name[sizeof(s_iccp.profile_name) - 1] = '\0'; + s_iccp.profile_len = iccp.size(); + s_iccp.profile = reinterpret_cast(iccp.data()); + spng_set_iccp(ctx, &s_iccp); + } + } + int ret; spng_encode_chunks(ctx); ret = spng_encode_image(ctx, nullptr, 0, SPNG_FMT_PNG, SPNG_ENCODE_PROGRESSIVE); diff --git a/modules/imgcodecs/src/grfmt_webp.cpp b/modules/imgcodecs/src/grfmt_webp.cpp index 3e63dd7acb..c82750020a 100644 --- a/modules/imgcodecs/src/grfmt_webp.cpp +++ b/modules/imgcodecs/src/grfmt_webp.cpp @@ -68,6 +68,7 @@ WebPDecoder::WebPDecoder() fs_size = 0; m_has_animation = false; m_previous_timestamp = 0; + m_read_options = 1; } WebPDecoder::~WebPDecoder() {} @@ -197,6 +198,47 @@ bool WebPDecoder::readData(Mat &img) } CV_Assert(data.type() == CV_8UC1); CV_Assert(data.rows == 1); + if (m_read_options) { + WebPData webp_data; + webp_data.bytes = (const uint8_t*)data.ptr(); + webp_data.size = data.total(); + + std::vector metadata; + WebPDemuxer* demux = WebPDemux(&webp_data); + + if (demux) + { + WebPChunkIterator chunk_iter; + + if (WebPDemuxGetChunk(demux, "EXIF", 1, &chunk_iter)) + { + metadata = std::vector(chunk_iter.chunk.bytes, + chunk_iter.chunk.bytes + chunk_iter.chunk.size); + WebPDemuxReleaseChunkIterator(&chunk_iter); + m_exif.parseExif(metadata.data(), metadata.size()); + } + + if (WebPDemuxGetChunk(demux, "ICCP", 1, &chunk_iter)) + { + metadata = std::vector(chunk_iter.chunk.bytes, + chunk_iter.chunk.bytes + chunk_iter.chunk.size); + WebPDemuxReleaseChunkIterator(&chunk_iter); + m_metadata[IMAGE_METADATA_ICCP] = metadata; + } + + if (WebPDemuxGetChunk(demux, "XMP ", 1, &chunk_iter)) // note the space in "XMP " + { + metadata = std::vector(chunk_iter.chunk.bytes, + chunk_iter.chunk.bytes + chunk_iter.chunk.size); + WebPDemuxReleaseChunkIterator(&chunk_iter); + m_metadata[IMAGE_METADATA_XMP] = metadata; + } + + WebPDemuxDelete(demux); + m_read_options = 0; + } + } + Mat read_img; CV_CheckType(img.type(), img.type() == CV_8UC1 || img.type() == CV_8UC3 || img.type() == CV_8UC4, ""); if (img.type() != m_type || img.cols != m_width || img.rows != m_height) @@ -292,6 +334,10 @@ WebPEncoder::WebPEncoder() { m_description = "WebP files (*.webp)"; m_buf_supported = true; + m_support_metadata.assign((size_t)IMAGE_METADATA_MAX + 1, false); + m_support_metadata[IMAGE_METADATA_EXIF] = true; + m_support_metadata[IMAGE_METADATA_XMP] = true; + m_support_metadata[IMAGE_METADATA_ICCP] = true; } WebPEncoder::~WebPEncoder() { } @@ -364,6 +410,45 @@ bool WebPEncoder::write(const Mat& img, const std::vector& params) size = WebPEncodeBGRA(image->ptr(), width, height, (int)image->step, quality, &out); } } + + WebPData finalData = { out, size }; + if (!m_metadata.empty()) { + + WebPMux* mux = WebPMuxNew(); + WebPData imageData = { out, size }; + WebPMuxSetImage(mux, &imageData, 0); + + WebPData metadata; + if (m_metadata[IMAGE_METADATA_EXIF].size() > 0) + { + metadata.bytes = m_metadata[IMAGE_METADATA_EXIF].data(); + metadata.size = m_metadata[IMAGE_METADATA_EXIF].size(); + WebPMuxSetChunk(mux, "EXIF", &metadata, 1); + } + if (m_metadata[IMAGE_METADATA_XMP].size() > 0) + { + metadata.bytes = m_metadata[IMAGE_METADATA_XMP].data(); + metadata.size = m_metadata[IMAGE_METADATA_XMP].size(); + WebPMuxSetChunk(mux, "XMP ", &metadata, 1); + } + + if (m_metadata[IMAGE_METADATA_ICCP].size() > 0) + { + metadata.bytes = m_metadata[IMAGE_METADATA_ICCP].data(); + metadata.size = m_metadata[IMAGE_METADATA_ICCP].size(); + WebPMuxSetChunk(mux, "ICCP", &metadata, 1); + } + + if (WebPMuxAssemble(mux, &finalData) == WEBP_MUX_OK) { + size = finalData.size; + WebPMuxDelete(mux); + } + else { + WebPMuxDelete(mux); + CV_Error(Error::StsError, "Failed to assemble WebP with EXIF"); + } + } + #if WEBP_DECODER_ABI_VERSION >= 0x0206 Ptr out_cleaner(out, WebPFree); #else @@ -375,7 +460,7 @@ bool WebPEncoder::write(const Mat& img, const std::vector& params) if (m_buf) { m_buf->resize(size); - memcpy(&(*m_buf)[0], out, size); + memcpy(&(*m_buf)[0], finalData.bytes, size); bytes_written = size; } else @@ -383,7 +468,7 @@ bool WebPEncoder::write(const Mat& img, const std::vector& params) FILE *fd = fopen(m_filename.c_str(), "wb"); if (fd != NULL) { - bytes_written = fwrite(out, sizeof(uint8_t), size, fd); + bytes_written = fwrite(finalData.bytes, sizeof(uint8_t), size, fd); if (size != bytes_written) { CV_LOG_ERROR(NULL, cv::format("Only %zu or %zu bytes are written\n",bytes_written, size)); diff --git a/modules/imgcodecs/src/loadsave.cpp b/modules/imgcodecs/src/loadsave.cpp index d1e3bc93cf..8e354c4dc9 100644 --- a/modules/imgcodecs/src/loadsave.cpp +++ b/modules/imgcodecs/src/loadsave.cpp @@ -498,9 +498,6 @@ imread_( const String& filename, int flags, OutputArray mat, /// Search for the relevant decoder to handle the imagery ImageDecoder decoder; - if (metadata_types) - metadata_types->clear(); - #ifdef HAVE_GDAL if(flags != IMREAD_UNCHANGED && (flags & IMREAD_LOAD_GDAL) == IMREAD_LOAD_GDAL ){ decoder = GdalDecoder().newDecoder(); @@ -539,6 +536,12 @@ imread_( const String& filename, int flags, OutputArray mat, /// set the filename in the driver decoder->setSource( filename ); + if (metadata_types) + { + metadata_types->clear(); + decoder->setReadOptions(1); + } + try { // read the header to make sure it succeeds @@ -1268,9 +1271,6 @@ imdecode_( const Mat& buf, int flags, Mat& mat, std::vector* metadata_types, OutputArrayOfArrays metadata ) { - if (metadata_types) - metadata_types->clear(); - CV_Assert(!buf.empty()); CV_Assert(buf.isContinuous()); CV_Assert(buf.checkVector(1, CV_8U) > 0); @@ -1321,6 +1321,12 @@ imdecode_( const Mat& buf, int flags, Mat& mat, decoder->setSource(filename); } + if (metadata_types) + { + metadata_types->clear(); + decoder->setReadOptions(1); + } + bool success = false; try { diff --git a/modules/imgcodecs/test/test_exif.cpp b/modules/imgcodecs/test/test_exif.cpp index 792c38514f..09e70b3046 100644 --- a/modules/imgcodecs/test/test_exif.cpp +++ b/modules/imgcodecs/test/test_exif.cpp @@ -9,7 +9,142 @@ namespace opencv_test { namespace { -/** + static Mat makeCirclesImage(Size size, int type, int nbits) + { + Mat img(size, type); + img.setTo(Scalar::all(0)); + RNG& rng = theRNG(); + int maxval = (int)(1 << nbits); + for (int i = 0; i < 100; i++) { + int x = rng.uniform(0, img.cols); + int y = rng.uniform(0, img.rows); + int radius = rng.uniform(5, std::min(img.cols, img.rows) / 5); + int b = rng.uniform(0, maxval); + int g = rng.uniform(0, maxval); + int r = rng.uniform(0, maxval); + circle(img, Point(x, y), radius, Scalar(b, g, r), -1, LINE_AA); + } + return img; + } + + static std::vector getSampleExifData() { + return { + 'M', 'M', 0, '*', 0, 0, 0, 8, 0, 10, 1, 0, 0, 4, 0, 0, 0, 1, 0, 0, 5, + 0, 1, 1, 0, 4, 0, 0, 0, 1, 0, 0, 2, 208, 1, 2, 0, 3, 0, 0, 0, 1, + 0, 10, 0, 0, 1, 18, 0, 3, 0, 0, 0, 1, 0, 1, 0, 0, 1, 14, 0, 2, 0, 0, + 0, '"', 0, 0, 0, 176, 1, '1', 0, 2, 0, 0, 0, 7, 0, 0, 0, 210, 1, 26, + 0, 5, 0, 0, 0, 1, 0, 0, 0, 218, 1, 27, 0, 5, 0, 0, 0, 1, 0, 0, 0, + 226, 1, '(', 0, 3, 0, 0, 0, 1, 0, 2, 0, 0, 135, 'i', 0, 4, 0, 0, 0, + 1, 0, 0, 0, 134, 0, 0, 0, 0, 0, 3, 144, 0, 0, 7, 0, 0, 0, 4, '0', '2', + '2', '1', 160, 2, 0, 4, 0, 0, 0, 1, 0, 0, 5, 0, 160, 3, 0, 4, 0, 0, + 0, 1, 0, 0, 2, 208, 0, 0, 0, 0, 'S', 'a', 'm', 'p', 'l', 'e', ' ', '1', '0', + '-', 'b', 'i', 't', ' ', 'i', 'm', 'a', 'g', 'e', ' ', 'w', 'i', 't', 'h', ' ', + 'm', 'e', 't', 'a', 'd', 'a', 't', 'a', 0, 'O', 'p', 'e', 'n', 'C', 'V', 0, 0, + 0, 0, 0, 'H', 0, 0, 0, 1, 0, 0, 0, 'H', 0, 0, 0, 1 + }; + } + + static std::vector getSampleXmpData() { + return { + '<','x',':','x','m','p','m','e','t','a',' ','x','m','l','n','s',':','x','=', + '"','a','d','o','b','e',':','x','m','p','"','>', + '<','x','m','p',':','C','r','e','a','t','o','r','T','o','o','l','>', + 'O','p','e','n','C','V', + '<','/','x','m','p',':','C','r','e','a','t','o','r','T','o','o','l','>', + '<','/','x',':','x','m','p','m','e','t','a','>',0 + }; + } + + // Returns a Minimal ICC profile data (Generated with help from ChatGPT) + static std::vector getSampleIccpData() { + std::vector iccp_data(192, 0); + + iccp_data[3] = 192; // Profile size: 192 bytes + + iccp_data[12] = 'm'; + iccp_data[13] = 'n'; + iccp_data[14] = 't'; + iccp_data[15] = 'r'; + + iccp_data[16] = 'R'; + iccp_data[17] = 'G'; + iccp_data[18] = 'B'; + iccp_data[19] = ' '; + + iccp_data[20] = 'X'; + iccp_data[21] = 'Y'; + iccp_data[22] = 'Z'; + iccp_data[23] = ' '; + + // File signature 'acsp' at offset 36 (0x24) + iccp_data[36] = 'a'; + iccp_data[37] = 'c'; + iccp_data[38] = 's'; + iccp_data[39] = 'p'; + + // Illuminant D50 at offset 68 (0x44), example values: + iccp_data[68] = 0x00; + iccp_data[69] = 0x00; + iccp_data[70] = 0xF6; + iccp_data[71] = 0xD6; // 0.9642 + iccp_data[72] = 0x00; + iccp_data[73] = 0x01; + iccp_data[74] = 0x00; + iccp_data[75] = 0x00; // 1.0 + iccp_data[76] = 0x00; + iccp_data[77] = 0x00; + iccp_data[78] = 0xD3; + iccp_data[79] = 0x2D; // 0.8249 + + // Tag count at offset 128 (0x80) = 1 + iccp_data[131] = 1; + + // Tag record at offset 132 (0x84): signature 'desc', offset 128, size 64 + iccp_data[132] = 'd'; + iccp_data[133] = 'e'; + iccp_data[134] = 's'; + iccp_data[135] = 'c'; + + iccp_data[139] = 128; // offset + + iccp_data[143] = 64; // size + + // Tag data 'desc' at offset 128 (start of tag data) + // Set type 'desc' etc. here, for simplicity fill zeros + + iccp_data[144] = 'd'; + iccp_data[145] = 'e'; + iccp_data[146] = 's'; + iccp_data[147] = 'c'; + + // ASCII string length at offset 156 + iccp_data[156] = 20; // length + + // ASCII string "Minimal ICC Profile" starting at offset 160 + iccp_data[160] = 'M'; + iccp_data[161] = 'i'; + iccp_data[162] = 'n'; + iccp_data[163] = 'i'; + iccp_data[164] = 'm'; + iccp_data[165] = 'a'; + iccp_data[166] = 'l'; + iccp_data[167] = ' '; + iccp_data[168] = 'I'; + iccp_data[169] = 'C'; + iccp_data[170] = 'C'; + iccp_data[171] = ' '; + iccp_data[172] = 'P'; + iccp_data[173] = 'r'; + iccp_data[174] = 'o'; + iccp_data[175] = 'f'; + iccp_data[176] = 'i'; + iccp_data[177] = 'l'; + iccp_data[178] = 'e'; + + return iccp_data; + } + + /** * 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). * Each test image is a 10x10 square, divided into four smaller sub-squares: @@ -145,47 +280,24 @@ const std::vector exif_files "readwrite/testExifOrientation_7.avif", "readwrite/testExifOrientation_8.avif", #endif +#ifdef HAVE_WEBP + "readwrite/testExifOrientation_1.webp", + "readwrite/testExifOrientation_2.webp", + "readwrite/testExifOrientation_3.webp", + "readwrite/testExifOrientation_4.webp", + "readwrite/testExifOrientation_5.webp", + "readwrite/testExifOrientation_6.webp", + "readwrite/testExifOrientation_7.webp", + "readwrite/testExifOrientation_8.webp", +#endif }; INSTANTIATE_TEST_CASE_P(Imgcodecs, Exif, testing::ValuesIn(exif_files)); -static Mat makeCirclesImage(Size size, int type, int nbits) -{ - Mat img(size, type); - img.setTo(Scalar::all(0)); - RNG& rng = theRNG(); - int maxval = (int)(1 << nbits); - for (int i = 0; i < 100; i++) { - int x = rng.uniform(0, img.cols); - int y = rng.uniform(0, img.rows); - int radius = rng.uniform(5, std::min(img.cols, img.rows)/5); - int b = rng.uniform(0, maxval); - int g = rng.uniform(0, maxval); - int r = rng.uniform(0, maxval); - circle(img, Point(x, y), radius, Scalar(b, g, r), -1, LINE_AA); - } - return img; -} - #ifdef HAVE_AVIF TEST(Imgcodecs_Avif, ReadWriteWithExif) { - static const uchar exif_data[] = { - 'M', 'M', 0, '*', 0, 0, 0, 8, 0, 10, 1, 0, 0, 4, 0, 0, 0, 1, 0, 0, 5, - 0, 1, 1, 0, 4, 0, 0, 0, 1, 0, 0, 2, 208, 1, 2, 0, 3, 0, 0, 0, 1, - 0, 10, 0, 0, 1, 18, 0, 3, 0, 0, 0, 1, 0, 1, 0, 0, 1, 14, 0, 2, 0, 0, - 0, '"', 0, 0, 0, 176, 1, '1', 0, 2, 0, 0, 0, 7, 0, 0, 0, 210, 1, 26, - 0, 5, 0, 0, 0, 1, 0, 0, 0, 218, 1, 27, 0, 5, 0, 0, 0, 1, 0, 0, 0, - 226, 1, '(', 0, 3, 0, 0, 0, 1, 0, 2, 0, 0, 135, 'i', 0, 4, 0, 0, 0, - 1, 0, 0, 0, 134, 0, 0, 0, 0, 0, 3, 144, 0, 0, 7, 0, 0, 0, 4, '0', '2', - '2', '1', 160, 2, 0, 4, 0, 0, 0, 1, 0, 0, 5, 0, 160, 3, 0, 4, 0, 0, - 0, 1, 0, 0, 2, 208, 0, 0, 0, 0, 'S', 'a', 'm', 'p', 'l', 'e', ' ', '1', '0', - '-', 'b', 'i', 't', ' ', 'i', 'm', 'a', 'g', 'e', ' ', 'w', 'i', 't', 'h', ' ', - 'm', 'e', 't', 'a', 'd', 'a', 't', 'a', 0, 'O', 'p', 'e', 'n', 'C', 'V', 0, 0, - 0, 0, 0, 'H', 0, 0, 0, 1, 0, 0, 0, 'H', 0, 0, 0, 1 - }; - int avif_nbits = 10; int avif_speed = 10; int avif_quality = 85; @@ -195,8 +307,8 @@ TEST(Imgcodecs_Avif, ReadWriteWithExif) Mat img = makeCirclesImage(Size(1280, 720), imgtype, avif_nbits); std::vector metadata_types = {IMAGE_METADATA_EXIF}; - std::vector > metadata(1); - metadata[0].assign(exif_data, exif_data + sizeof(exif_data)); + std::vector> metadata = { + getSampleExifData() }; std::vector write_params = { IMWRITE_AVIF_DEPTH, avif_nbits, @@ -228,31 +340,63 @@ TEST(Imgcodecs_Avif, ReadWriteWithExif) } #endif // HAVE_AVIF -TEST(Imgcodecs_Jpeg, ReadWriteWithExif) +#ifdef HAVE_WEBP +TEST(Imgcodecs_WebP, Read_Write_With_Exif_Xmp_Iccp) { - static const uchar exif_data[] = { - 'M', 'M', 0, '*', 0, 0, 0, 8, 0, 10, 1, 0, 0, 4, 0, 0, 0, 1, 0, 0, 5, - 0, 1, 1, 0, 4, 0, 0, 0, 1, 0, 0, 2, 208, 1, 2, 0, 3, 0, 0, 0, 1, - 0, 8, 0, 0, 1, 18, 0, 3, 0, 0, 0, 1, 0, 1, 0, 0, 1, 14, 0, 2, 0, 0, - 0, '!', 0, 0, 0, 176, 1, '1', 0, 2, 0, 0, 0, 7, 0, 0, 0, 210, 1, 26, - 0, 5, 0, 0, 0, 1, 0, 0, 0, 218, 1, 27, 0, 5, 0, 0, 0, 1, 0, 0, 0, - 226, 1, '(', 0, 3, 0, 0, 0, 1, 0, 2, 0, 0, 135, 'i', 0, 4, 0, 0, 0, - 1, 0, 0, 0, 134, 0, 0, 0, 0, 0, 3, 144, 0, 0, 7, 0, 0, 0, 4, '0', '2', - '2', '1', 160, 2, 0, 4, 0, 0, 0, 1, 0, 0, 5, 0, 160, 3, 0, 4, 0, 0, - 0, 1, 0, 0, 2, 208, 0, 0, 0, 0, 'S', 'a', 'm', 'p', 'l', 'e', ' ', '8', '-', - 'b', 'i', 't', ' ', 'i', 'm', 'a', 'g', 'e', ' ', 'w', 'i', 't', 'h', ' ', 'm', - 'e', 't', 'a', 'd', 'a', 't', 'a', 0, 0, 'O', 'p', 'e', 'n', 'C', 'V', 0, 0, - 0, 0, 0, 'H', 0, 0, 0, 1, 0, 0, 0, 'H', 0, 0, 0, 1 + int imgtype = CV_MAKETYPE(CV_8U, 3); + const std::string outputname = cv::tempfile(".webp"); + cv::Mat img = makeCirclesImage(cv::Size(160, 120), imgtype, 8); + + std::vector metadata_types = {IMAGE_METADATA_EXIF, IMAGE_METADATA_XMP, IMAGE_METADATA_ICCP}; + std::vector> metadata = { + getSampleExifData(), + getSampleXmpData(), + getSampleIccpData() }; + int webp_quality = 101; // 101 is lossless compression + std::vector write_params = {IMWRITE_WEBP_QUALITY, webp_quality}; + + imwriteWithMetadata(outputname, img, metadata_types, metadata, write_params); + + std::vector compressed; + imencodeWithMetadata(outputname, img, metadata_types, metadata, compressed, write_params); + + std::vector read_metadata_types, read_metadata_types2; + std::vector> read_metadata, read_metadata2; + + cv::Mat img2 = imreadWithMetadata(outputname, read_metadata_types, read_metadata, cv::IMREAD_UNCHANGED); + cv::Mat img3 = imdecodeWithMetadata(compressed, read_metadata_types2, read_metadata2, cv::IMREAD_UNCHANGED); + + EXPECT_EQ(img2.cols, img.cols); + EXPECT_EQ(img2.rows, img.rows); + EXPECT_EQ(img2.type(), imgtype); + + EXPECT_EQ(read_metadata_types, read_metadata_types2); + EXPECT_EQ(read_metadata_types.size(), 3u); + + EXPECT_EQ(read_metadata, read_metadata2); + EXPECT_EQ(read_metadata, metadata); + + EXPECT_EQ(cv::norm(img2, img3, cv::NORM_INF), 0.0); + + double mse = cv::norm(img, img2, cv::NORM_L2SQR) / (img.rows * img.cols); + EXPECT_EQ(mse, 0); + + remove(outputname.c_str()); +} +#endif // HAVE_WEBP + +TEST(Imgcodecs_Jpeg, Read_Write_With_Exif) +{ int jpeg_quality = 95; int imgtype = CV_MAKETYPE(CV_8U, 3); const string outputname = cv::tempfile(".jpeg"); Mat img = makeCirclesImage(Size(1280, 720), imgtype, 8); std::vector metadata_types = {IMAGE_METADATA_EXIF}; - std::vector > metadata(1); - metadata[0].assign(exif_data, exif_data + sizeof(exif_data)); + std::vector> metadata = { + getSampleExifData() }; std::vector write_params = { IMWRITE_JPEG_QUALITY, jpeg_quality @@ -281,31 +425,16 @@ TEST(Imgcodecs_Jpeg, ReadWriteWithExif) remove(outputname.c_str()); } -TEST(Imgcodecs_Png, ReadWriteWithExif) +TEST(Imgcodecs_Png, Read_Write_With_Exif) { - static const uchar exif_data[] = { - 'M', 'M', 0, '*', 0, 0, 0, 8, 0, 10, 1, 0, 0, 4, 0, 0, 0, 1, 0, 0, 5, - 0, 1, 1, 0, 4, 0, 0, 0, 1, 0, 0, 2, 208, 1, 2, 0, 3, 0, 0, 0, 1, - 0, 8, 0, 0, 1, 18, 0, 3, 0, 0, 0, 1, 0, 1, 0, 0, 1, 14, 0, 2, 0, 0, - 0, '!', 0, 0, 0, 176, 1, '1', 0, 2, 0, 0, 0, 7, 0, 0, 0, 210, 1, 26, - 0, 5, 0, 0, 0, 1, 0, 0, 0, 218, 1, 27, 0, 5, 0, 0, 0, 1, 0, 0, 0, - 226, 1, '(', 0, 3, 0, 0, 0, 1, 0, 2, 0, 0, 135, 'i', 0, 4, 0, 0, 0, - 1, 0, 0, 0, 134, 0, 0, 0, 0, 0, 3, 144, 0, 0, 7, 0, 0, 0, 4, '0', '2', - '2', '1', 160, 2, 0, 4, 0, 0, 0, 1, 0, 0, 5, 0, 160, 3, 0, 4, 0, 0, - 0, 1, 0, 0, 2, 208, 0, 0, 0, 0, 'S', 'a', 'm', 'p', 'l', 'e', ' ', '8', '-', - 'b', 'i', 't', ' ', 'i', 'm', 'a', 'g', 'e', ' ', 'w', 'i', 't', 'h', ' ', 'm', - 'e', 't', 'a', 'd', 'a', 't', 'a', 0, 0, 'O', 'p', 'e', 'n', 'C', 'V', 0, 0, - 0, 0, 0, 'H', 0, 0, 0, 1, 0, 0, 0, 'H', 0, 0, 0, 1 - }; - int png_compression = 3; int imgtype = CV_MAKETYPE(CV_8U, 3); const string outputname = cv::tempfile(".png"); - Mat img = makeCirclesImage(Size(1280, 720), imgtype, 8); + Mat img = makeCirclesImage(Size(160, 120), imgtype, 8); std::vector metadata_types = {IMAGE_METADATA_EXIF}; - std::vector > metadata(1); - metadata[0].assign(exif_data, exif_data + sizeof(exif_data)); + std::vector> metadata = { + getSampleExifData() }; std::vector write_params = { IMWRITE_PNG_COMPRESSION, png_compression @@ -334,6 +463,65 @@ TEST(Imgcodecs_Png, ReadWriteWithExif) remove(outputname.c_str()); } +TEST(Imgcodecs_Png, Read_Write_With_Exif_Xmp_Iccp) +{ + int png_compression = 3; + int imgtype = CV_MAKETYPE(CV_8U, 3); + const string outputname = cv::tempfile(".png"); + Mat img = makeCirclesImage(Size(160, 120), imgtype, 8); + + std::vector metadata_types = { IMAGE_METADATA_EXIF, IMAGE_METADATA_XMP, IMAGE_METADATA_ICCP }; + std::vector> metadata = { + getSampleExifData(), + getSampleXmpData(), + getSampleIccpData(), + }; + + std::vector write_params = { + IMWRITE_PNG_COMPRESSION, png_compression + }; + + imwriteWithMetadata(outputname, img, metadata_types, metadata, write_params); + std::vector compressed; + imencodeWithMetadata(outputname, img, metadata_types, metadata, compressed, write_params); + + std::vector read_metadata_types, read_metadata_types2; + std::vector > read_metadata, read_metadata2; + Mat img2 = imreadWithMetadata(outputname, read_metadata_types, read_metadata, IMREAD_UNCHANGED); + Mat img3 = imdecodeWithMetadata(compressed, read_metadata_types2, read_metadata2, IMREAD_UNCHANGED); + EXPECT_EQ(img2.cols, img.cols); + EXPECT_EQ(img2.rows, img.rows); + EXPECT_EQ(img2.type(), imgtype); + + EXPECT_EQ(metadata_types, read_metadata_types); + EXPECT_EQ(read_metadata_types, read_metadata_types2); + EXPECT_EQ(metadata, read_metadata); + remove(outputname.c_str()); +} + +TEST(Imgcodecs_Png, Read_Exif_From_Text) +{ + const string root = cvtest::TS::ptr()->get_data_path(); + const string filename = root + "../perf/320x260.png"; + const string dst_file = cv::tempfile(".png"); + + std::vector exif_data = + { 'M' , 'M' , 0, '*' , 0, 0, 0, 8, 0, 4, 1, + 26, 0, 5, 0, 0, 0, 1, 0, 0, 0, 62, 1, 27, 0, 5, 0, 0, 0, 1, 0, 0, 0, + 70, 1, 40, 0, 3, 0, 0, 0, 1, 0, 2, 0, 0, 1, 49, 0, 2, 0, 0, 0, 18, 0, + 0, 0, 78, 0, 0, 0, 0, 0, 0, 0, 96, 0, 0, 0, 1, 0, 0, 0, 96, 0, 0, 0, + 1, 80, 97, 105, 110, 116, 46, 78, 69, 84, 32, 118, 51, 46, 53, 46, 49, 48, 0 + }; + + std::vector read_metadata_types; + std::vector > read_metadata; + Mat img = imreadWithMetadata(filename, read_metadata_types, read_metadata, IMREAD_GRAYSCALE); + + std::vector metadata_types = { IMAGE_METADATA_EXIF }; + EXPECT_EQ(read_metadata_types, metadata_types); + EXPECT_EQ(read_metadata[0], exif_data); +} + static size_t locateString(const uchar* exif, size_t exif_size, const std::string& pattern) { size_t plen = pattern.size(); From ec9b8de3fae0dc93d45b231e58a30764f6d6f06f Mon Sep 17 00:00:00 2001 From: Alexander Smorkalov <2536374+asmorkalov@users.noreply.github.com> Date: Thu, 24 Jul 2025 11:23:21 +0300 Subject: [PATCH 6/9] Merge pull request #27569 from asmorkalov:as/counters_build_fix Fixed build issue with some old GCC versions #27569 Fixes build for GCC 4.8.5 at least. Looks like the method default signature differs cross GCC versions or newer version has softer checker. ### 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 - [ ] The PR is proposed to the proper branch - [ ] There is a reference to the original bug report and related work - [ ] 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 --- modules/imgcodecs/test/test_exif.cpp | 6 +++--- modules/imgproc/src/contours_blockstorage.hpp | 4 ++-- modules/imgproc/src/contours_common.hpp | 4 ++-- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/modules/imgcodecs/test/test_exif.cpp b/modules/imgcodecs/test/test_exif.cpp index 09e70b3046..8576c4f1e4 100644 --- a/modules/imgcodecs/test/test_exif.cpp +++ b/modules/imgcodecs/test/test_exif.cpp @@ -565,13 +565,13 @@ TEST_P(ReadExif_Sanity, Check) static const std::vector exif_sanity_params { #ifdef HAVE_JPEG - {"readwrite/testExifOrientation_3.jpg", 916, "Photoshop", 120}, + ReadExif_Sanity_Params("readwrite/testExifOrientation_3.jpg", 916, "Photoshop", 120), #endif #ifdef OPENCV_IMGCODECS_PNG_WITH_EXIF - {"readwrite/testExifOrientation_5.png", 112, "ExifTool", 102}, + ReadExif_Sanity_Params("readwrite/testExifOrientation_5.png", 112, "ExifTool", 102), #endif #ifdef HAVE_AVIF - {"readwrite/testExifOrientation_7.avif", 913, "Photoshop", 120}, + ReadExif_Sanity_Params("readwrite/testExifOrientation_7.avif", 913, "Photoshop", 120), #endif }; diff --git a/modules/imgproc/src/contours_blockstorage.hpp b/modules/imgproc/src/contours_blockstorage.hpp index 7b7c55a72d..a30cba96d4 100644 --- a/modules/imgproc/src/contours_blockstorage.hpp +++ b/modules/imgproc/src/contours_blockstorage.hpp @@ -26,14 +26,14 @@ class BlockStorage { dynamicBlocks.push_back(new block_type); } BlockStorage(const BlockStorage&) = delete; - BlockStorage(BlockStorage&&) noexcept = default; + BlockStorage(BlockStorage&&) = default; ~BlockStorage() { for(const auto & block : dynamicBlocks) { delete block; } } BlockStorage& operator=(const BlockStorage&) = delete; - BlockStorage& operator=(BlockStorage&&) noexcept = default; + BlockStorage& operator=(BlockStorage&&) = default; void clear(void) { const size_t minDynamicBlocks = !staticBlocksCount ? 1 : 0; diff --git a/modules/imgproc/src/contours_common.hpp b/modules/imgproc/src/contours_common.hpp index e02945fd01..08f3dc1b3d 100644 --- a/modules/imgproc/src/contours_common.hpp +++ b/modules/imgproc/src/contours_common.hpp @@ -190,7 +190,7 @@ public: ContourDataStorage(ContourDataStorage&&) noexcept = default; ~ContourDataStorage() = default; ContourDataStorage& operator=(const ContourDataStorage&) = delete; - ContourDataStorage& operator=(ContourDataStorage&&) noexcept = default; + ContourDataStorage& operator=(ContourDataStorage&&) = default; public: typename storage_t::RangeIterator getRangeIterator(void) const {return storage->getRangeIterator(first, last);} public: @@ -243,7 +243,7 @@ public: Contour(const Contour&) = delete; Contour(Contour&& other) noexcept = default; Contour& operator=(const Contour&) = delete; - Contour& operator=(Contour&& other) noexcept = default; + Contour& operator=(Contour&& other) = default; ~Contour() = default; void updateBoundingRect() {} bool isEmpty() const From ac9d0e0d0dc0af77785fc7c445c25340243cf08a Mon Sep 17 00:00:00 2001 From: Alexander Smorkalov Date: Wed, 23 Jul 2025 17:24:44 +0300 Subject: [PATCH 7/9] Removed hack for calibration boards in samples and tutorials. --- samples/cpp/calibration.cpp | 15 +++++++++++++-- .../camera_calibration/camera_calibration.cpp | 9 +++++++-- 2 files changed, 20 insertions(+), 4 deletions(-) diff --git a/samples/cpp/calibration.cpp b/samples/cpp/calibration.cpp index f8134368b2..ac80a06b59 100644 --- a/samples/cpp/calibration.cpp +++ b/samples/cpp/calibration.cpp @@ -178,8 +178,19 @@ static bool runCalibration( vector > imagePoints, vector > objectPoints(1); calcChessboardCorners(boardSize, squareSize, objectPoints[0], patternType); - int offset = patternType != CHARUCOBOARD ? boardSize.width - 1: boardSize.width - 2; - objectPoints[0][offset].x = objectPoints[0][0].x + grid_width; + // Board imperfectness correction introduced in PR #12772 + // The correction does not make sense for asymmetric and assymetric circles grids + if (patternType == CHESSBOARD) + { + int offset = boardSize.width - 1; + objectPoints[0][offset].x = objectPoints[0][0].x + grid_width; + } + else if (patternType == CHARUCOBOARD) + { + int offset = boardSize.width - 2; + objectPoints[0][offset].x = objectPoints[0][0].x + grid_width; + } + newObjPoints = objectPoints[0]; objectPoints.resize(imagePoints.size(),objectPoints[0]); diff --git a/samples/cpp/tutorial_code/calib3d/camera_calibration/camera_calibration.cpp b/samples/cpp/tutorial_code/calib3d/camera_calibration/camera_calibration.cpp index a80962b4c3..c051d60a7d 100644 --- a/samples/cpp/tutorial_code/calib3d/camera_calibration/camera_calibration.cpp +++ b/samples/cpp/tutorial_code/calib3d/camera_calibration/camera_calibration.cpp @@ -640,10 +640,15 @@ static bool runCalibration( Settings& s, Size& imageSize, Mat& cameraMatrix, Mat vector > objectPoints(1); calcBoardCornerPositions(s.boardSize, s.squareSize, objectPoints[0], s.calibrationPattern); - if (s.calibrationPattern == Settings::Pattern::CHARUCOBOARD) { + + // Board imperfectness correction introduced in PR #12772 + // The correction does not make sense for asymmetric and assymetric circles grids + if (s.calibrationPattern == Settings::Pattern::CHARUCOBOARD) + { objectPoints[0][s.boardSize.width - 2].x = objectPoints[0][0].x + grid_width; } - else { + else if (s.calibrationPattern != Settings::Pattern::CHESSBOARD) + { objectPoints[0][s.boardSize.width - 1].x = objectPoints[0][0].x + grid_width; } newObjPoints = objectPoints[0]; From e4fabe393cf86618ebb474f8e81bb05a30b4fc8f Mon Sep 17 00:00:00 2001 From: Alexander Smorkalov Date: Thu, 24 Jul 2025 14:29:09 +0300 Subject: [PATCH 8/9] More intrinsic calibration flags in integractive calibration tool. --- apps/interactive-calibration/calibCommon.hpp | 6 ++++++ apps/interactive-calibration/main.cpp | 4 ++++ apps/interactive-calibration/parametersController.cpp | 3 +++ 3 files changed, 13 insertions(+) diff --git a/apps/interactive-calibration/calibCommon.hpp b/apps/interactive-calibration/calibCommon.hpp index ed6c3613d1..967982300b 100644 --- a/apps/interactive-calibration/calibCommon.hpp +++ b/apps/interactive-calibration/calibCommon.hpp @@ -118,6 +118,9 @@ namespace calib double solverEps; int solverMaxIters; bool fastSolving; + bool rationalModel; + bool thinPrismModel; + bool tiltedModel; double filterAlpha; internalParameters() @@ -125,6 +128,9 @@ namespace calib solverEps = 1e-7; solverMaxIters = 30; fastSolving = false; + rationalModel = false; + thinPrismModel = false; + tiltedModel = false; filterAlpha = 0.1; } }; diff --git a/apps/interactive-calibration/main.cpp b/apps/interactive-calibration/main.cpp index b9ec7e8805..889a536a69 100644 --- a/apps/interactive-calibration/main.cpp +++ b/apps/interactive-calibration/main.cpp @@ -134,6 +134,10 @@ int main(int argc, char** argv) int calibrationFlags = 0; if(intParams.fastSolving) calibrationFlags |= cv::CALIB_USE_QR; + if(intParams.rationalModel) calibrationFlags |= cv::CALIB_RATIONAL_MODEL; + if(intParams.thinPrismModel) calibrationFlags |= cv::CALIB_THIN_PRISM_MODEL; + if(intParams.tiltedModel) calibrationFlags |= cv::CALIB_TILTED_MODEL; + cv::Ptr controller(new calibController(globalData, calibrationFlags, parser.get("ft"), capParams.minFramesNum)); cv::Ptr dataController(new calibDataController(globalData, capParams.maxFramesNum, diff --git a/apps/interactive-calibration/parametersController.cpp b/apps/interactive-calibration/parametersController.cpp index 90d24b04c9..48639a2bdb 100644 --- a/apps/interactive-calibration/parametersController.cpp +++ b/apps/interactive-calibration/parametersController.cpp @@ -49,6 +49,9 @@ bool calib::parametersController::loadFromFile(const std::string &inputFileName) readFromNode(reader["solver_eps"], mInternalParameters.solverEps); readFromNode(reader["solver_max_iters"], mInternalParameters.solverMaxIters); readFromNode(reader["fast_solver"], mInternalParameters.fastSolving); + readFromNode(reader["rational_model"], mInternalParameters.rationalModel); + readFromNode(reader["thin_prism_model"], mInternalParameters.thinPrismModel); + readFromNode(reader["tiltedModel"], mInternalParameters.tiltedModel); readFromNode(reader["frame_filter_conv_param"], mInternalParameters.filterAlpha); bool retValue = From 603d7ded88a0cbf5df237277e988d1db99122f68 Mon Sep 17 00:00:00 2001 From: Yuantao Feng Date: Mon, 28 Jul 2025 13:59:46 +0800 Subject: [PATCH 9/9] Merge pull request #27579 from fengyuentau:4x/core/filestorage core: support parsing null in json parser in FileStorage #27579 Fixes https://github.com/opencv/opencv/issues/27578 ### 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 --- modules/core/src/persistence_json.cpp | 4 +--- modules/core/test/test_io.cpp | 24 ++++++++++++++++++++++++ 2 files changed, 25 insertions(+), 3 deletions(-) diff --git a/modules/core/src/persistence_json.cpp b/modules/core/src/persistence_json.cpp index c5326c674a..9d05650814 100644 --- a/modules/core/src/persistence_json.cpp +++ b/modules/core/src/persistence_json.cpp @@ -623,9 +623,7 @@ public: } if( len == 4 && memcmp( beg, "null", 4 ) == 0 ) - { - CV_PARSE_ERROR_CPP( "Value 'null' is not supported by this parser" ); - } + ; else if( (len == 4 && memcmp( beg, "true", 4 ) == 0) || (len == 5 && memcmp( beg, "false", 5 ) == 0) ) { diff --git a/modules/core/test/test_io.cpp b/modules/core/test/test_io.cpp index 6cf4bc8625..67b0ab5803 100644 --- a/modules/core/test/test_io.cpp +++ b/modules/core/test/test_io.cpp @@ -1545,6 +1545,30 @@ TEST(Core_InputOutput, FileStorage_format_yml_gz) EXPECT_EQ(FileStorage::FORMAT_YAML, fs.getFormat()); } +TEST(Core_InputOutput, FileStorage_json_null_object) +{ + std::string test = + "{ " + "\"padding\": null," + "\"truncation\": null," + "\"version\": \"1.0\"" + "}"; + FileStorage fs(test, FileStorage::READ | FileStorage::MEMORY); + + ASSERT_TRUE(fs["padding"].isNone()); + ASSERT_TRUE(fs["truncation"].isNone()); + ASSERT_TRUE(fs["version"].isString()); + + ASSERT_EQ(fs["padding"].name(), "padding"); + ASSERT_EQ(fs["truncation"].name(), "truncation"); + ASSERT_EQ(fs["version"].name(), "version"); + + ASSERT_EQ(fs["padding"].string(), ""); + ASSERT_EQ(fs["truncation"].string(), ""); + ASSERT_EQ(fs["version"].string(), "1.0"); + fs.release(); +} + TEST(Core_InputOutput, FileStorage_json_named_nodes) { std::string test =