diff --git a/modules/imgcodecs/include/opencv2/imgcodecs.hpp b/modules/imgcodecs/include/opencv2/imgcodecs.hpp index c610802b10..d0f6ee61d6 100644 --- a/modules/imgcodecs/include/opencv2/imgcodecs.hpp +++ b/modules/imgcodecs/include/opencv2/imgcodecs.hpp @@ -251,6 +251,15 @@ enum ImwriteGIFCompressionFlags { IMWRITE_GIF_COLORTABLE_SIZE_256 = 8 }; +enum ImageMetadataType +{ + IMAGE_METADATA_UNKNOWN = -1, + IMAGE_METADATA_EXIF = 0, + IMAGE_METADATA_XMP = 1, + IMAGE_METADATA_ICCP = 2, + IMAGE_METADATA_MAX = 2 +}; + //! @} imgcodecs_flags /** @brief Represents an animation with multiple frames. @@ -360,6 +369,17 @@ The image passing through the img parameter can be pre-allocated. The memory is */ 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. + +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. +@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 +*/ +CV_EXPORTS_W Mat imreadWithMetadata( const String& filename, CV_OUT std::vector& metadataTypes, + OutputArrayOfArrays metadata, int flags = IMREAD_ANYCOLOR); + /** @brief Loads a multi-page image from a file. The function imreadmulti loads a multi-page image from the specified file into a vector of Mat objects. @@ -508,6 +528,20 @@ It also demonstrates how to save multiple images in a TIFF file: CV_EXPORTS_W bool imwrite( const String& filename, InputArray img, const std::vector& params = std::vector()); +/** @brief Saves an image to a specified file with metadata + +The function imwriteWithMetadata saves the image to the specified file. It does the same thing as imwrite, but additionally writes metadata if the corresponding format supports it. +@param filename Name of the file. As with imwrite, image format is determined by the file extension. +@param img (Mat or vector of Mat) Image or Images to be saved. +@param metadataTypes Vector with types of metadata chucks stored in metadata to write, see ImageMetadataType. +@param metadata Vector of vectors or vector of matrices with chunks of metadata to store into the file +@param params Format-specific parameters encoded as pairs (paramId_1, paramValue_1, paramId_2, paramValue_2, ... .) see cv::ImwriteFlags +*/ +CV_EXPORTS_W bool imwriteWithMetadata( const String& filename, InputArray img, + const std::vector& metadataTypes, + InputArrayOfArrays& metadata, + const std::vector& params = std::vector()); + //! @brief multi-image overload for bindings CV_WRAP static inline bool imwritemulti(const String& filename, InputArrayOfArrays img, @@ -529,6 +563,22 @@ See cv::imread for the list of supported formats and flags description. */ CV_EXPORTS_W Mat imdecode( InputArray buf, int flags ); +/** @brief Reads an image from a buffer in memory together with associated metadata. + +The function imdecode reads an image from the specified buffer in the memory. 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 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. +*/ +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. @@ -567,6 +617,24 @@ CV_EXPORTS_W bool imencode( const String& ext, InputArray img, CV_OUT std::vector& buf, const std::vector& params = std::vector()); +/** @brief Encodes an image into a memory buffer. + +The function imencode compresses the image and stores it in the memory buffer that is resized to fit the +result. See cv::imwrite for the list of supported formats and flags description. + +@param ext File extension that defines the output format. Must include a leading period. +@param img Image to be compressed. +@param metadataTypes Vector with types of metadata chucks stored in metadata to write, see ImageMetadataType. +@param metadata Vector of vectors or vector of matrices with chunks of metadata to store into the file +@param buf Output buffer resized to fit the compressed image. +@param params Format-specific parameters. See cv::imwrite and cv::ImwriteFlags. +*/ +CV_EXPORTS_W bool imencodeWithMetadata( const String& ext, InputArray img, + const std::vector& metadataTypes, + InputArrayOfArrays metadata, + CV_OUT std::vector& buf, + const std::vector& params = std::vector()); + /** @brief Encodes array of images into a memory buffer. The function is analog to cv::imencode for in-memory multi-page image compression. diff --git a/modules/imgcodecs/src/exif.cpp b/modules/imgcodecs/src/exif.cpp index 8ed9760556..3f1bbdbe18 100644 --- a/modules/imgcodecs/src/exif.cpp +++ b/modules/imgcodecs/src/exif.cpp @@ -94,6 +94,10 @@ ExifEntry_t ExifReader::getTag(const ExifTagName tag) const return entry; } +const std::vector& ExifReader::getData() const +{ + return m_data; +} /** * @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 a8914bec03..3c5fbc7fe8 100644 --- a/modules/imgcodecs/src/exif.hpp +++ b/modules/imgcodecs/src/exif.hpp @@ -175,6 +175,10 @@ public: */ ExifEntry_t getTag( const ExifTagName tag ) const; + /** + * @brief Get the whole exif buffer + */ + const std::vector& getData() const; private: std::vector m_data; diff --git a/modules/imgcodecs/src/grfmt_avif.cpp b/modules/imgcodecs/src/grfmt_avif.cpp index c35eb50306..600f673fb4 100644 --- a/modules/imgcodecs/src/grfmt_avif.cpp +++ b/modules/imgcodecs/src/grfmt_avif.cpp @@ -68,8 +68,8 @@ avifResult CopyToMat(const avifImage *image, int channels, bool useRGB , Mat *ma return avifImageYUVToRGB(image, &rgba); } -AvifImageUniquePtr ConvertToAvif(const cv::Mat &img, bool lossless, - int bit_depth) { +AvifImageUniquePtr ConvertToAvif(const cv::Mat &img, bool lossless, int bit_depth, + const std::vector >& metadata) { CV_Assert(img.depth() == CV_8U || img.depth() == CV_16U); const int width = img.cols; @@ -112,6 +112,18 @@ AvifImageUniquePtr ConvertToAvif(const cv::Mat &img, bool lossless, result->yuvRange = AVIF_RANGE_FULL; } + if (!metadata.empty()) { + const std::vector& metadata_exif = metadata[IMAGE_METADATA_EXIF]; + const std::vector& metadata_xmp = metadata[IMAGE_METADATA_XMP]; + const std::vector& metadata_iccp = metadata[IMAGE_METADATA_ICCP]; + if (!metadata_exif.empty()) + avifImageSetMetadataExif(result, (const uint8_t*)metadata_exif.data(), metadata_exif.size()); + if (!metadata_exif.empty()) + avifImageSetMetadataXMP(result, (const uint8_t*)metadata_xmp.data(), metadata_xmp.size()); + if (!metadata_iccp.empty()) + avifImageSetProfileICC(result, (const uint8_t*)metadata_iccp.data(), metadata_iccp.size()); + } + avifRGBImage rgba; avifRGBImageSetDefaults(&rgba, result); if (img.channels() == 3) { @@ -120,7 +132,7 @@ AvifImageUniquePtr ConvertToAvif(const cv::Mat &img, bool lossless, CV_Assert(img.channels() == 4); rgba.format = AVIF_RGB_FORMAT_BGRA; } - rgba.rowBytes = img.step[0]; + rgba.rowBytes = (uint32_t)img.step[0]; rgba.depth = bit_depth; rgba.pixels = const_cast(reinterpret_cast(img.data)); @@ -287,6 +299,10 @@ bool AvifDecoder::nextPage() { AvifEncoder::AvifEncoder() { m_description = "AVIF files (*.avif)"; m_buf_supported = true; + m_support_metadata.assign((size_t)IMAGE_METADATA_MAX + 1, false); + m_support_metadata[(size_t)IMAGE_METADATA_EXIF] = true; + m_support_metadata[(size_t)IMAGE_METADATA_XMP] = true; + m_support_metadata[(size_t)IMAGE_METADATA_ICCP] = true; encoder_ = avifEncoderCreate(); } @@ -349,7 +365,7 @@ bool AvifEncoder::writeanimation(const Animation& animation, img.channels() == 1 || img.channels() == 3 || img.channels() == 4, "AVIF only supports 1, 3, 4 channels"); - images.emplace_back(ConvertToAvif(img, do_lossless, bit_depth)); + images.emplace_back(ConvertToAvif(img, do_lossless, bit_depth, m_metadata)); } for (size_t i = 0; i < images.size(); i++) diff --git a/modules/imgcodecs/src/grfmt_base.cpp b/modules/imgcodecs/src/grfmt_base.cpp index dc3d07ab78..1241edb077 100644 --- a/modules/imgcodecs/src/grfmt_base.cpp +++ b/modules/imgcodecs/src/grfmt_base.cpp @@ -58,11 +58,30 @@ BaseImageDecoder::BaseImageDecoder() m_frame_count = 1; } +bool BaseImageDecoder::haveMetadata(ImageMetadataType type) const +{ + if (type == IMAGE_METADATA_EXIF) + return !m_exif.getData().empty(); + return false; +} + +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; + } + } + return Mat(); +} ExifEntry_t BaseImageDecoder::getExifTag(const ExifTagName tag) const { return m_exif.getTag(tag); } + bool BaseImageDecoder::setSource( const String& filename ) { m_filename = filename; @@ -140,6 +159,23 @@ bool BaseImageEncoder::setDestination( std::vector& buf ) return true; } +bool BaseImageEncoder::addMetadata(ImageMetadataType type, const Mat& metadata) +{ + CV_Assert_N(type >= IMAGE_METADATA_EXIF, type <= IMAGE_METADATA_MAX); + if (metadata.empty()) + return true; + size_t itype = (size_t)type; + if (itype >= m_support_metadata.size() || !m_support_metadata[itype]) + return false; + if (m_metadata.empty()) + m_metadata.resize((size_t)IMAGE_METADATA_MAX+1); + CV_Assert(metadata.elemSize() == 1); + CV_Assert(metadata.isContinuous()); + const unsigned char* data = metadata.ptr(); + m_metadata[itype].assign(data, data + metadata.total()); + return true; +} + bool BaseImageEncoder::write(const Mat &img, const std::vector ¶ms) { std::vector img_vec(1, img); return writemulti(img_vec, params); diff --git a/modules/imgcodecs/src/grfmt_base.hpp b/modules/imgcodecs/src/grfmt_base.hpp index 6d98bd3735..2eeb2fc130 100644 --- a/modules/imgcodecs/src/grfmt_base.hpp +++ b/modules/imgcodecs/src/grfmt_base.hpp @@ -69,6 +69,20 @@ public: */ virtual int type() const { return m_type; } + /** + * @brief Checks whether file contains metadata of the certain type. + * @param type The type of metadata to look for + */ + virtual bool haveMetadata(ImageMetadataType type) const; + + /** + * @brief Retrieves metadata (if any) of the certain kind. + * If there is no such metadata, the method returns empty array. + * + * @param type The type of metadata to look for + */ + virtual Mat getMetadata(ImageMetadataType type) const; + /** * @brief Fetch a specific EXIF tag from the image's metadata. * @param tag The EXIF tag to retrieve. @@ -205,6 +219,13 @@ public: */ virtual bool setDestination(std::vector& buf); + /** + * @brief Sets the metadata to write together with the image data + * @param type The type of metadata to add + * @param metadata The packed metadata (Exif, XMP, ...) + */ + virtual bool addMetadata(ImageMetadataType type, const Mat& metadata); + /** * @brief Encode and write the image data. * @param img The Mat object containing the image data to be encoded. @@ -243,6 +264,8 @@ public: virtual void throwOnError() const; protected: + std::vector > m_metadata; // see IMAGE_METADATA_... + std::vector m_support_metadata; String m_description; ///< Description of the encoder (e.g., format name, capabilities). String m_filename; ///< Destination file name for encoded data. std::vector* m_buf; ///< Pointer to the buffer for encoded data if using memory-based destination. diff --git a/modules/imgcodecs/src/grfmt_jpeg.cpp b/modules/imgcodecs/src/grfmt_jpeg.cpp index a3a7f70c3c..9b2ab59b2b 100644 --- a/modules/imgcodecs/src/grfmt_jpeg.cpp +++ b/modules/imgcodecs/src/grfmt_jpeg.cpp @@ -600,6 +600,8 @@ JpegEncoder::JpegEncoder() { m_description = "JPEG files (*.jpeg;*.jpg;*.jpe)"; m_buf_supported = true; + m_support_metadata.assign((size_t)IMAGE_METADATA_MAX + 1, false); + m_support_metadata[(size_t)IMAGE_METADATA_EXIF] = true; } @@ -815,6 +817,22 @@ bool JpegEncoder::write( const Mat& img, const std::vector& params ) jpeg_start_compress( &cinfo, TRUE ); + if (!m_metadata.empty()) { + const std::vector& metadata_exif = m_metadata[IMAGE_METADATA_EXIF]; + size_t exif_size = metadata_exif.size(); + if (exif_size > 0u) { + const char app1_exif_prefix[] = {'E', 'x', 'i', 'f', '\0', '\0'}; + size_t app1_exif_prefix_size = sizeof(app1_exif_prefix); + size_t data_size = exif_size + app1_exif_prefix_size; + + std::vector metadata_app1(data_size); + uchar* data = metadata_app1.data(); + memcpy(data, app1_exif_prefix, app1_exif_prefix_size); + memcpy(data + app1_exif_prefix_size, metadata_exif.data(), exif_size); + jpeg_write_marker(&cinfo, JPEG_APP0 + 1, data, (unsigned)data_size); + } + } + if( doDirectWrite ) { for( int y = 0; y < height; y++ ) diff --git a/modules/imgcodecs/src/grfmt_png.cpp b/modules/imgcodecs/src/grfmt_png.cpp index a47db5aa2a..f0f656bd25 100644 --- a/modules/imgcodecs/src/grfmt_png.cpp +++ b/modules/imgcodecs/src/grfmt_png.cpp @@ -858,6 +858,8 @@ PngEncoder::PngEncoder() { m_description = "Portable Network Graphics files (*.png;*.apng)"; m_buf_supported = true; + m_support_metadata.assign((size_t)IMAGE_METADATA_MAX+1, false); + m_support_metadata[IMAGE_METADATA_EXIF] = true; op_zstream1.zalloc = NULL; op_zstream2.zalloc = NULL; next_seq_num = 0; @@ -989,6 +991,16 @@ 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/loadsave.cpp b/modules/imgcodecs/src/loadsave.cpp index dfbf118fb9..8f811f9085 100644 --- a/modules/imgcodecs/src/loadsave.cpp +++ b/modules/imgcodecs/src/loadsave.cpp @@ -410,6 +410,76 @@ static void ApplyExifOrientation(ExifEntry_t orientationTag, OutputArray img) } } +static void readMetadata(ImageDecoder& decoder, + std::vector* metadata_types, + OutputArrayOfArrays metadata) +{ + if (!metadata_types) + return; + int kind = metadata.kind(); + void* obj = metadata.getObj(); + std::vector* matvector = nullptr; + std::vector >* vecvector = nullptr; + if (kind == _InputArray::STD_VECTOR_MAT) { + matvector = (std::vector*)obj; + } else if (kind == _InputArray::STD_VECTOR_VECTOR) { + int elemtype = metadata.type(0); + CV_Assert(elemtype == CV_8UC1 || elemtype == CV_8SC1); + vecvector = (std::vector >*)obj; + } else { + CV_Error(Error::StsBadArg, + "unsupported metadata type, should be a vector of matrices or vector of byte vectors"); + } + std::vector src_metadata; + for (int m = (int)IMAGE_METADATA_EXIF; m <= (int)IMAGE_METADATA_MAX; m++) { + Mat mm = decoder->getMetadata((ImageMetadataType)m); + if (!mm.empty()) { + CV_Assert(mm.isContinuous()); + CV_Assert(mm.elemSize() == 1u); + metadata_types->push_back(m); + src_metadata.push_back(mm); + } + } + size_t nmetadata = metadata_types->size(); + if (matvector) { + matvector->resize(nmetadata); + for (size_t m = 0; m < nmetadata; m++) + src_metadata[m].copyTo(matvector->at(m)); + } else { + vecvector->resize(nmetadata); + for (size_t m = 0; m < nmetadata; m++) { + const Mat& mm = src_metadata[m]; + const uchar* data = (uchar*)mm.data; + vecvector->at(m).assign(data, data + mm.total()); + } + } +} + +static const char* metadataTypeToString(ImageMetadataType type) +{ + return type == IMAGE_METADATA_EXIF ? "Exif" : + type == IMAGE_METADATA_XMP ? "XMP" : + type == IMAGE_METADATA_ICCP ? "ICC Profile" : "???"; +} + +static void addMetadata(ImageEncoder& encoder, + const std::vector& metadata_types, + InputArrayOfArrays metadata) +{ + size_t nmetadata_chunks = metadata_types.size(); + for (size_t i = 0; i < nmetadata_chunks; i++) { + ImageMetadataType metadata_type = (ImageMetadataType)metadata_types[i]; + bool ok = encoder->addMetadata(metadata_type, metadata.getMat((int)i)); + if (!ok) { + std::string desc = encoder->getDescription(); + CV_LOG_WARNING(NULL, "Imgcodecs: metadata of type '" + << metadataTypeToString(metadata_type) + << "' is not supported when encoding '" + << desc << "'"); + } + } +} + /** * Read an image into memory and return the information * @@ -419,11 +489,15 @@ static void ApplyExifOrientation(ExifEntry_t orientationTag, OutputArray img) * */ static bool -imread_( const String& filename, int flags, OutputArray mat ) +imread_( const String& filename, int flags, OutputArray mat, + std::vector* metadata_types, OutputArrayOfArrays metadata) { /// 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(); @@ -509,6 +583,8 @@ imread_( const String& filename, int flags, OutputArray mat ) CV_CheckTrue(original_ptr == real_mat.data, "Internal imread issue"); success = true; } + + readMetadata(decoder, metadata_types, metadata); } catch (const cv::Exception& e) { @@ -662,7 +738,24 @@ Mat imread( const String& filename, int flags ) Mat img; /// load the data - imread_( filename, flags, img ); + imread_( filename, flags, img, nullptr, noArray() ); + + /// return a reference to the data + return img; +} + +Mat imreadWithMetadata( const String& filename, + std::vector& metadata_types, + OutputArrayOfArrays metadata, + int flags ) +{ + CV_TRACE_FUNCTION(); + + /// create the basic container + Mat img; + + /// load the data + imread_( filename, flags, img, &metadata_types, metadata ); /// return a reference to the data return img; @@ -673,7 +766,7 @@ void imread( const String& filename, OutputArray dst, int flags ) CV_TRACE_FUNCTION(); /// load the data - imread_(filename, flags, dst); + imread_(filename, flags, dst, nullptr, noArray()); } /** @@ -946,6 +1039,8 @@ size_t imcount(const String& filename, int flags) static bool imwrite_( const String& filename, const std::vector& img_vec, + const std::vector& metadata_types, + InputArrayOfArrays metadata, const std::vector& params_, bool flipv ) { bool isMultiImg = img_vec.size() > 1; @@ -981,6 +1076,8 @@ static bool imwrite_( const String& filename, const std::vector& img_vec, } encoder->setDestination( filename ); + addMetadata(encoder, metadata_types, metadata); + #if CV_VERSION_MAJOR < 5 && defined(HAVE_IMGCODEC_HDR) bool fixed = false; std::vector params_pair(2); @@ -1055,7 +1152,26 @@ bool imwrite( const String& filename, InputArray _img, img_vec.push_back(_img.getMat()); CV_Assert(!img_vec.empty()); - return imwrite_(filename, img_vec, params, false); + return imwrite_(filename, img_vec, {}, noArray(), params, false); +} + +bool imwriteWithMetadata( const String& filename, InputArray _img, + const std::vector& metadata_types, + InputArrayOfArrays metadata, + const std::vector& params ) +{ + CV_TRACE_FUNCTION(); + + CV_Assert(!_img.empty()); + + std::vector img_vec; + if (_img.isMatVector() || _img.isUMatVector()) + _img.getMatVector(img_vec); + else + img_vec.push_back(_img.getMat()); + + CV_Assert(!img_vec.empty()); + return imwrite_(filename, img_vec, metadata_types, metadata, params, false); } static bool imwriteanimation_(const String& filename, const Animation& animation, const std::vector& params) @@ -1140,8 +1256,13 @@ bool imencodeanimation(const String& ext, const Animation& animation, std::vecto } static bool -imdecode_( const Mat& buf, int flags, Mat& mat ) +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); @@ -1231,6 +1352,7 @@ imdecode_( const Mat& buf, int flags, Mat& mat ) { if (decoder->readData(mat)) success = true; + readMetadata(decoder, metadata_types, metadata); } catch (const cv::Exception& e) { @@ -1274,7 +1396,7 @@ Mat imdecode( InputArray _buf, int flags ) CV_TRACE_FUNCTION(); Mat buf = _buf.getMat(), img; - if (!imdecode_(buf, flags, img)) + if (!imdecode_(buf, flags, img, nullptr, noArray())) img.release(); return img; @@ -1286,12 +1408,24 @@ Mat imdecode( InputArray _buf, int flags, Mat* dst ) Mat buf = _buf.getMat(), img; dst = dst ? dst : &img; - if (imdecode_(buf, flags, *dst)) + if (imdecode_(buf, flags, *dst, nullptr, noArray())) return *dst; else return cv::Mat(); } +Mat imdecodeWithMetadata( InputArray _buf, std::vector& metadata_types, + OutputArrayOfArrays metadata, int flags ) +{ + CV_TRACE_FUNCTION(); + + Mat buf = _buf.getMat(), img; + if (!imdecode_(buf, flags, img, &metadata_types, metadata)) + img.release(); + + return img; +} + static bool imdecodemulti_(const Mat& buf, int flags, std::vector& mats, int start, int count) { @@ -1447,8 +1581,10 @@ bool imdecodemulti(InputArray _buf, int flags, CV_OUT std::vector& mats, co } } -bool imencode( const String& ext, InputArray _img, - std::vector& buf, const std::vector& params_ ) +bool imencodeWithMetadata( const String& ext, InputArray _img, + const std::vector& metadata_types, + InputArrayOfArrays metadata, + std::vector& buf, const std::vector& params_ ) { CV_TRACE_FUNCTION(); @@ -1517,6 +1653,7 @@ bool imencode( const String& ext, InputArray _img, code = encoder->setDestination(filename); CV_Assert( code ); } + addMetadata(encoder, metadata_types, metadata); try { if (!isMultiImg) @@ -1553,6 +1690,12 @@ bool imencode( const String& ext, InputArray _img, return code; } +bool imencode( const String& ext, InputArray img, + std::vector& buf, const std::vector& params_ ) +{ + return imencodeWithMetadata(ext, img, {}, noArray(), buf, params_); +} + bool imencodemulti( const String& ext, InputArrayOfArrays imgs, std::vector& buf, const std::vector& params) { diff --git a/modules/imgcodecs/test/test_exif.cpp b/modules/imgcodecs/test/test_exif.cpp index d1a9e720a9..792c38514f 100644 --- a/modules/imgcodecs/test/test_exif.cpp +++ b/modules/imgcodecs/test/test_exif.cpp @@ -148,7 +148,246 @@ const std::vector exif_files }; INSTANTIATE_TEST_CASE_P(Imgcodecs, Exif, - testing::ValuesIn(exif_files)); + 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; + int imgdepth = avif_nbits > 8 ? CV_16U : CV_8U; + int imgtype = CV_MAKETYPE(imgdepth, 3); + const string outputname = cv::tempfile(".avif"); + 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 write_params = { + IMWRITE_AVIF_DEPTH, avif_nbits, + IMWRITE_AVIF_SPEED, avif_speed, + IMWRITE_AVIF_QUALITY, avif_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; + 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(read_metadata_types, read_metadata_types2); + EXPECT_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]); + EXPECT_EQ(cv::norm(img2, img3, NORM_INF), 0.); + double mse = cv::norm(img, img2, NORM_L2SQR)/(img.rows*img.cols); + EXPECT_LT(mse, 1500); + remove(outputname.c_str()); } +#endif // HAVE_AVIF + +TEST(Imgcodecs_Jpeg, 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, 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 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 write_params = { + IMWRITE_JPEG_QUALITY, jpeg_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; + 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(read_metadata_types, read_metadata_types2); + EXPECT_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]); + EXPECT_EQ(cv::norm(img2, img3, NORM_INF), 0.); + double mse = cv::norm(img, img2, NORM_L2SQR)/(img.rows*img.cols); + EXPECT_LT(mse, 80); + remove(outputname.c_str()); +} + +TEST(Imgcodecs_Png, 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, 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); + + std::vector metadata_types = {IMAGE_METADATA_EXIF}; + std::vector > metadata(1); + metadata[0].assign(exif_data, exif_data + sizeof(exif_data)); + + 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(read_metadata_types, read_metadata_types2); + EXPECT_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]); + 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()); +} + +static size_t locateString(const uchar* exif, size_t exif_size, const std::string& pattern) +{ + size_t plen = pattern.size(); + for (size_t i = 0; i + plen <= exif_size; i++) { + if (exif[i] == pattern[0] && memcmp(&exif[i], pattern.c_str(), plen) == 0) + return i; + } + return 0xFFFFFFFFu; +} + +typedef std::tuple ReadExif_Sanity_Params; +typedef testing::TestWithParam ReadExif_Sanity; + +TEST_P(ReadExif_Sanity, Check) +{ + std::string filename = get<0>(GetParam()); + size_t exif_size = get<1>(GetParam()); + std::string pattern = get<2>(GetParam()); + size_t ploc = get<3>(GetParam()); + + const string root = cvtest::TS::ptr()->get_data_path(); + filename = root + filename; + + std::vector metadata_types; + std::vector metadata; + Mat img = imreadWithMetadata(filename, metadata_types, metadata, 1); + + EXPECT_EQ(img.type(), CV_8UC3); + ASSERT_GE(metadata_types.size(), 1u); + EXPECT_EQ(metadata_types.size(), metadata.size()); + const Mat& exif = metadata[IMAGE_METADATA_EXIF]; + EXPECT_EQ(exif.type(), CV_8U); + EXPECT_EQ(exif.total(), exif_size); + ASSERT_GE(exif_size, 26u); // minimal exif should take at least 26 bytes + // (the header + IDF0 with at least 1 entry). + EXPECT_TRUE(exif.data[0] == 'I' || exif.data[0] == 'M'); + EXPECT_EQ(exif.data[0], exif.data[1]); + EXPECT_EQ(locateString(exif.data, exif_size, pattern), ploc); +} + +static const std::vector exif_sanity_params +{ +#ifdef HAVE_JPEG + {"readwrite/testExifOrientation_3.jpg", 916, "Photoshop", 120}, +#endif +#ifdef OPENCV_IMGCODECS_PNG_WITH_EXIF + {"readwrite/testExifOrientation_5.png", 112, "ExifTool", 102}, +#endif +#ifdef HAVE_AVIF + {"readwrite/testExifOrientation_7.avif", 913, "Photoshop", 120}, +#endif +}; + +INSTANTIATE_TEST_CASE_P(Imgcodecs, ReadExif_Sanity, + testing::ValuesIn(exif_sanity_params)); + +}} diff --git a/modules/python/test/test_imread.py b/modules/python/test/test_imread.py index b5f286d426..471c786acc 100644 --- a/modules/python/test/test_imread.py +++ b/modules/python/test/test_imread.py @@ -22,6 +22,18 @@ class imread_test(NewOpenCVTests): cv.imread(path, img) self.assertEqual(cv.norm(ref, img, cv.NORM_INF), 0.0) + def test_imread_with_meta(self): + path = self.extraTestDataPath + '/highgui/readwrite/testExifOrientation_1.jpg' + img, meta_types, meta_data = cv.imreadWithMetadata(path) + self.assertTrue(img is not None) + self.assertTrue(meta_types is not None) + self.assertTrue(meta_data is not None) + + path = self.extraTestDataPath + '/highgui/readwrite/testExifOrientation_1.png' + img, meta_types, meta_data = cv.imreadWithMetadata(path) + self.assertTrue(img is not None) + self.assertTrue(meta_types is not None) + self.assertTrue(meta_data is not None) if __name__ == '__main__': NewOpenCVTests.bootstrap()