From 8bc65a1d136c2ac4c31910ee271b5f3c9d14317e Mon Sep 17 00:00:00 2001 From: Suleyman TURKMEN Date: Mon, 30 Dec 2024 11:32:31 +0300 Subject: [PATCH] Merge pull request #25715 from sturkmen72:apng_support Animated PNG Support #25715 Continues https://github.com/opencv/opencv/pull/25608 ### 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. - [x] The feature is well documented and sample code can be built with the project CMake --- modules/imgcodecs/src/grfmt_avif.cpp | 2 +- modules/imgcodecs/src/grfmt_png.cpp | 1346 ++++++++++++++++++++- modules/imgcodecs/src/grfmt_png.hpp | 128 +- modules/imgcodecs/src/loadsave.cpp | 3 +- modules/imgcodecs/test/test_animation.cpp | 436 +++++++ modules/imgcodecs/test/test_webp.cpp | 231 +--- 6 files changed, 1847 insertions(+), 299 deletions(-) create mode 100644 modules/imgcodecs/test/test_animation.cpp diff --git a/modules/imgcodecs/src/grfmt_avif.cpp b/modules/imgcodecs/src/grfmt_avif.cpp index 23cdbb1baa..58f6ce91c1 100644 --- a/modules/imgcodecs/src/grfmt_avif.cpp +++ b/modules/imgcodecs/src/grfmt_avif.cpp @@ -243,7 +243,7 @@ bool AvifDecoder::readData(Mat &img) { return false; } - m_animation.durations.push_back(decoder_->imageTiming.durationInTimescales); + m_animation.durations.push_back(decoder_->imageTiming.duration * 1000); if (decoder_->image->exif.size > 0) { m_exif.parseExif(decoder_->image->exif.data, decoder_->image->exif.size); diff --git a/modules/imgcodecs/src/grfmt_png.cpp b/modules/imgcodecs/src/grfmt_png.cpp index 726c8b90b7..32620fdbc0 100644 --- a/modules/imgcodecs/src/grfmt_png.cpp +++ b/modules/imgcodecs/src/grfmt_png.cpp @@ -51,6 +51,52 @@ and png2bmp sample from libpng distribution (Copyright (C) 1999-2001 MIYASAKA Masaru) \****************************************************************************************/ +/****************************************************************************\ + * + * this file includes some modified part of apngasm and APNG Optimizer 1.4 + * both have zlib license. + * + ****************************************************************************/ + + + /* apngasm + * + * The next generation of apngasm, the APNG Assembler. + * The apngasm CLI tool and library can assemble and disassemble APNG image files. + * + * https://github.com/apngasm/apngasm + + + /* APNG Optimizer 1.4 + * + * Makes APNG files smaller. + * + * http://sourceforge.net/projects/apng/files + * + * Copyright (c) 2011-2015 Max Stepin + * maxst at users.sourceforge.net + * + * zlib license + * ------------ + * + * This software is provided 'as-is', without any express or implied + * warranty. In no event will the authors be held liable for any damages + * arising from the use of this software. + * + * Permission is granted to anyone to use this software for any purpose, + * including commercial applications, and to alter it and redistribute it + * freely, subject to the following restrictions: + * + * 1. The origin of this software must not be misrepresented; you must not + * claim that you wrote the original software. If you use this software + * in a product, an acknowledgment in the product documentation would be + * appreciated but is not required. + * 2. Altered source versions must be plainly marked as such, and must not be + * misrepresented as being the original software. + * 3. This notice may not be removed or altered from any source distribution. + * + */ + #ifndef _LFS64_LARGEFILE # define _LFS64_LARGEFILE 0 #endif @@ -58,14 +104,13 @@ # define _FILE_OFFSET_BITS 0 #endif -#include -#include - #include "grfmt_png.hpp" +#include #if defined _MSC_VER && _MSC_VER >= 1200 // interaction between '_setjmp' and C++ object destruction is non-portable #pragma warning( disable: 4611 ) + #pragma warning( disable: 4244 ) #endif // the following defines are a hack to avoid multiple problems with frame pointer handling and setjmp @@ -76,7 +121,64 @@ namespace cv { -/////////////////////// PngDecoder /////////////////// +const uint32_t id_IHDR = 0x52444849; // PNG header +const uint32_t id_acTL = 0x4C546361; // Animation control chunk +const uint32_t id_fcTL = 0x4C546366; // Frame control chunk +const uint32_t id_IDAT = 0x54414449; // first frame and/or default image +const uint32_t id_fdAT = 0x54416466; // Frame data chunk +const uint32_t id_PLTE = 0x45544C50; +const uint32_t id_bKGD = 0x44474B62; +const uint32_t id_tRNS = 0x534E5274; +const uint32_t id_IEND = 0x444E4549; // end/footer chunk + +APNGFrame::APNGFrame() +{ + _pixels = NULL; + _width = 0; + _height = 0; + _colorType = 0; + _paletteSize = 0; + _transparencySize = 0; + _delayNum = 1; + _delayDen = 1000; + _rows = NULL; +} + +APNGFrame::~APNGFrame() {} + +bool APNGFrame::setMat(const cv::Mat& src, unsigned delayNum, unsigned delayDen) +{ + _delayNum = delayNum; + _delayDen = delayDen; + + if (!src.empty()) + { + png_uint_32 rowbytes = src.cols * src.channels(); + + _width = src.cols; + _height = src.rows; + _colorType = src.channels() == 1 ? PNG_COLOR_TYPE_GRAY : src.channels() == 3 ? PNG_COLOR_TYPE_RGB : PNG_COLOR_TYPE_RGB_ALPHA; + _pixels = src.data; + _rows = new png_bytep[_height * sizeof(png_bytep)]; + + for (unsigned int i = 0; i < _height; ++i) + _rows[i] = _pixels + i * rowbytes; + return true; + } + return false; +} + +void APNGFrame::setWidth(unsigned int width) { _width = width; } +void APNGFrame::setHeight(unsigned int height) { _height = height;} +void APNGFrame::setColorType(unsigned char colorType) { _colorType = colorType; } +void APNGFrame::setPalette(const rgb* palette) { std::copy(palette, palette + 256, _palette); } +void APNGFrame::setTransparency(const unsigned char* transparency) { std::copy(transparency, transparency + 256, _transparency); } +void APNGFrame::setPaletteSize(int paletteSize) { _paletteSize = paletteSize; } +void APNGFrame::setTransparencySize(int transparencySize) { _transparencySize = transparencySize; } +void APNGFrame::setDelayNum(unsigned int delayNum) { _delayNum = delayNum; } +void APNGFrame::setDelayDen(unsigned int delayDen) { _delayDen = delayDen; } +void APNGFrame::setPixels(unsigned char* pixels) { _pixels = pixels; } +void APNGFrame::setRows(unsigned char** rows) { _rows = rows; } PngDecoder::PngDecoder() { @@ -88,20 +190,18 @@ PngDecoder::PngDecoder() m_buf_supported = true; m_buf_pos = 0; m_bit_depth = 0; + m_frame_no = 0; + w0 = 0; + h0 = 0; + x0 = 0; + y0 = 0; + delay_num = 0; + delay_den = 0; + dop = 0; + bop = 0; } - PngDecoder::~PngDecoder() -{ - close(); -} - -ImageDecoder PngDecoder::newDecoder() const -{ - return makePtr(); -} - -void PngDecoder::close() { if( m_f ) { @@ -119,8 +219,12 @@ void PngDecoder::close() } } +ImageDecoder PngDecoder::newDecoder() const +{ + return makePtr(); +} -void PngDecoder::readDataFromBuf( void* _png_ptr, uchar* dst, size_t size ) +void PngDecoder::readDataFromBuf( void* _png_ptr, unsigned char* dst, size_t size ) { png_structp png_ptr = (png_structp)_png_ptr; PngDecoder* decoder = (PngDecoder*)(png_get_io_ptr(png_ptr)); @@ -138,7 +242,6 @@ void PngDecoder::readDataFromBuf( void* _png_ptr, uchar* dst, size_t size ) bool PngDecoder::readHeader() { volatile bool result = false; - close(); png_structp png_ptr = png_create_read_struct( PNG_LIBPNG_VER_STRING, 0, 0, 0 ); @@ -156,55 +259,126 @@ bool PngDecoder::readHeader() { if( setjmp( png_jmpbuf( png_ptr ) ) == 0 ) { + unsigned char sig[8]; + uint32_t id = 0; + Chunk chunk; + if( !m_buf.empty() ) png_set_read_fn(png_ptr, this, (png_rw_ptr)readDataFromBuf ); else { - m_f = fopen( m_filename.c_str(), "rb" ); - if( m_f ) - png_init_io( png_ptr, m_f ); + m_f = fopen(m_filename.c_str(), "rb"); + if (!m_f) + return false; + png_init_io(png_ptr, m_f); + + if (fread(sig, 1, 8, m_f)) + id = read_chunk(m_chunkIHDR); } - if( !m_buf.empty() || m_f ) + if (id != id_IHDR) { - png_uint_32 wdth, hght; - int bit_depth, color_type, num_trans=0; - png_bytep trans; - png_color_16p trans_values; + read_from_io(&sig, 8, 1); + id = read_chunk(m_chunkIHDR); + } - png_read_info( png_ptr, info_ptr ); + if (!(id == id_IHDR && m_chunkIHDR.size == 25)) + return false; - png_get_IHDR( png_ptr, info_ptr, &wdth, &hght, - &bit_depth, &color_type, 0, 0, 0 ); + while (true) + { + m_is_fcTL_loaded = false; + id = read_chunk(chunk); - m_width = (int)wdth; - m_height = (int)hght; - m_color_type = color_type; - m_bit_depth = bit_depth; + if ((m_f && feof(m_f)) || (!m_buf.empty() && m_buf_pos > m_buf.total())) + return false; - if( bit_depth <= 8 || bit_depth == 16 ) + if (id == id_IDAT) { - switch(color_type) - { - case PNG_COLOR_TYPE_RGB: - case PNG_COLOR_TYPE_PALETTE: - png_get_tRNS(png_ptr, info_ptr, &trans, &num_trans, &trans_values); - if( num_trans > 0 ) - m_type = CV_8UC4; - else - m_type = CV_8UC3; - break; - case PNG_COLOR_TYPE_GRAY_ALPHA: - case PNG_COLOR_TYPE_RGB_ALPHA: - m_type = CV_8UC4; - break; - default: - m_type = CV_8UC1; - } - if( bit_depth == 16 ) - m_type = CV_MAKETYPE(CV_16U, CV_MAT_CN(m_type)); - result = true; + if (m_f) + fseek(m_f, 0, SEEK_SET); + else + m_buf_pos = 0; + break; } + + if (id == id_acTL && chunk.size == 20) + { + m_animation.loop_count = png_get_uint_32(chunk.p + 12); + + if (chunk.p[8] > 0) + { + chunk.p[8] = 0; + chunk.p[9] = 0; + m_frame_count = png_get_uint_32(chunk.p + 8); + m_frame_count++; + } + else + m_frame_count = png_get_uint_32(chunk.p + 8); + } + + if (id == id_fcTL) + { + m_is_fcTL_loaded = true; + w0 = png_get_uint_32(chunk.p + 12); + h0 = png_get_uint_32(chunk.p + 16); + x0 = png_get_uint_32(chunk.p + 20); + y0 = png_get_uint_32(chunk.p + 24); + delay_num = png_get_uint_16(chunk.p + 28); + delay_den = png_get_uint_16(chunk.p + 30); + dop = chunk.p[32]; + bop = chunk.p[33]; + } + + if (id == id_bKGD) + { + int bgcolor = png_get_uint_32(chunk.p + 8); + m_animation.bgcolor[3] = (bgcolor >> 24) & 0xFF; + m_animation.bgcolor[2] = (bgcolor >> 16) & 0xFF; + m_animation.bgcolor[1] = (bgcolor >> 8) & 0xFF; + m_animation.bgcolor[0] = bgcolor & 0xFF; + } + + if (id == id_PLTE || id == id_tRNS) + m_chunksInfo.push_back(chunk); + } + + png_uint_32 wdth, hght; + int bit_depth, color_type, num_trans=0; + png_bytep trans; + png_color_16p trans_values; + + png_read_info( png_ptr, info_ptr ); + png_get_IHDR(png_ptr, info_ptr, &wdth, &hght, + &bit_depth, &color_type, 0, 0, 0); + + m_width = (int)wdth; + m_height = (int)hght; + m_color_type = color_type; + m_bit_depth = bit_depth; + + if (bit_depth <= 8 || bit_depth == 16) + { + switch (color_type) + { + case PNG_COLOR_TYPE_RGB: + case PNG_COLOR_TYPE_PALETTE: + png_get_tRNS(png_ptr, info_ptr, &trans, &num_trans, &trans_values); + if (num_trans > 0) + m_type = CV_8UC4; + else + m_type = CV_8UC3; + break; + case PNG_COLOR_TYPE_GRAY_ALPHA: + case PNG_COLOR_TYPE_RGB_ALPHA: + m_type = CV_8UC4; + break; + default: + m_type = CV_8UC1; + } + if (bit_depth == 16) + m_type = CV_MAKETYPE(CV_16U, CV_MAT_CN(m_type)); + result = true; } } } @@ -213,12 +387,147 @@ bool PngDecoder::readHeader() return result; } - bool PngDecoder::readData( Mat& img ) { + if (m_frame_count > 1) + { + Mat mat_cur = Mat(img.rows, img.cols, m_type); + uint32_t id = 0; + uint32_t j = 0; + uint32_t imagesize = m_width * m_height * mat_cur.channels(); + m_is_IDAT_loaded = false; + + if (m_frame_no == 0) + { + m_mat_raw = Mat(img.rows, img.cols, m_type); + m_mat_next = Mat(img.rows, img.cols, m_type); + frameRaw.setMat(m_mat_raw); + frameNext.setMat(m_mat_next); + if (m_f) + fseek(m_f, -8, SEEK_CUR); + else + m_buf_pos -= 8; + + } + else + m_mat_next.copyTo(mat_cur); + + frameCur.setMat(mat_cur); + + processing_start((void*)&frameRaw, mat_cur); + png_structp png_ptr = (png_structp)m_png_ptr; + png_infop info_ptr = (png_infop)m_info_ptr; + + while (true) + { + Chunk chunk; + id = read_chunk(chunk); + if (!id) + return false; + + if (id == id_fcTL && m_is_IDAT_loaded) + { + if (!m_is_fcTL_loaded) + { + m_is_fcTL_loaded = true; + w0 = m_width; + h0 = m_height; + } + + if (processing_finish()) + { + if (dop == 2) + memcpy(frameNext.getPixels(), frameCur.getPixels(), imagesize); + + compose_frame(frameCur.getRows(), frameRaw.getRows(), bop, x0, y0, w0, h0, mat_cur.channels()); + if (delay_den < 1000) + delay_num = cvRound(1000.0 / delay_den); + m_animation.durations.push_back(delay_num); + + if (mat_cur.channels() == img.channels()) + mat_cur.copyTo(img); + else if (img.channels() == 1) + cvtColor(mat_cur, img, COLOR_BGRA2GRAY); + else if (img.channels() == 3) + cvtColor(mat_cur, img, COLOR_BGRA2BGR); + + if (dop != 2) + { + memcpy(frameNext.getPixels(), frameCur.getPixels(), imagesize); + if (dop == 1) + for (j = 0; j < h0; j++) + memset(frameNext.getRows()[y0 + j] + x0 * img.channels(), 0, w0 * img.channels()); + } + } + else + { + delete[] chunk.p; + return false; + } + + w0 = png_get_uint_32(chunk.p + 12); + h0 = png_get_uint_32(chunk.p + 16); + x0 = png_get_uint_32(chunk.p + 20); + y0 = png_get_uint_32(chunk.p + 24); + delay_num = png_get_uint_16(chunk.p + 28); + delay_den = png_get_uint_16(chunk.p + 30); + dop = chunk.p[32]; + bop = chunk.p[33]; + + if (int(x0 + w0) > img.cols || int(y0 + h0) > img.rows || dop > 2 || bop > 1) + { + delete[] chunk.p; + return false; + } + + memcpy(m_chunkIHDR.p + 8, chunk.p + 12, 8); + return true; + } + else if (id == id_IDAT) + { + m_is_IDAT_loaded = true; + png_process_data(png_ptr, info_ptr, chunk.p, chunk.size); + } + else if (id == id_fdAT && m_is_fcTL_loaded) + { + m_is_IDAT_loaded = true; + png_save_uint_32(chunk.p + 4, chunk.size - 16); + memcpy(chunk.p + 8, "IDAT", 4); + png_process_data(png_ptr, info_ptr, chunk.p + 4, chunk.size - 4); + } + else if (id == id_IEND) + { + if (processing_finish()) + { + compose_frame(frameCur.getRows(), frameRaw.getRows(), bop, x0, y0, w0, h0, mat_cur.channels()); + if (delay_den < 1000) + delay_num = cvRound(1000.0 / delay_den); + m_animation.durations.push_back(delay_num); + + if (mat_cur.channels() == img.channels()) + mat_cur.copyTo(img); + else if (img.channels() == 1) + cvtColor(mat_cur, img, COLOR_BGRA2GRAY); + else if (img.channels() == 3) + cvtColor(mat_cur, img, COLOR_BGRA2BGR); + } + else + return false; + + delete[] chunk.p; + return true; + } + else + png_process_data(png_ptr, info_ptr, chunk.p, chunk.size); + + delete[] chunk.p; + } + return false; + } + volatile bool result = false; - AutoBuffer _buffer(m_height); - uchar** buffer = _buffer.data(); + AutoBuffer _buffer(m_height); + unsigned char** buffer = _buffer.data(); bool color = img.channels() > 1; png_structp png_ptr = (png_structp)m_png_ptr; @@ -300,22 +609,178 @@ bool PngDecoder::readData( Mat& img ) return result; } +bool PngDecoder::nextPage() { + return ++m_frame_no < (int)m_frame_count; +} + +void PngDecoder::compose_frame(unsigned char** rows_dst, unsigned char** rows_src, unsigned char _bop, uint32_t x, uint32_t y, uint32_t w, uint32_t h, int channels) +{ + uint32_t i, j; + int u, v, al; + + for (j = 0; j < h; j++) + { + unsigned char* sp = rows_src[j]; + unsigned char* dp = rows_dst[j + y] + x * channels; + + if (_bop == 0) + memcpy(dp, sp, w * channels); + else + for (i = 0; i < w; i++, sp += 4, dp += 4) + { + if (sp[3] == 255) + memcpy(dp, sp, 4); + else + if (sp[3] != 0) + { + if (dp[3] != 0) + { + u = sp[3] * 255; + v = (255 - sp[3]) * dp[3]; + al = u + v; + dp[0] = (sp[0] * u + dp[0] * v) / al; + dp[1] = (sp[1] * u + dp[1] * v) / al; + dp[2] = (sp[2] * u + dp[2] * v) / al; + dp[3] = al / 255; + } + else + memcpy(dp, sp, 4); + } + } + } +} + +size_t PngDecoder::read_from_io(void* _Buffer, size_t _ElementSize, size_t _ElementCount) +{ + if (m_f) + return fread(_Buffer, _ElementSize, _ElementCount, m_f); + + if (m_buf_pos > m_buf.cols * m_buf.rows * m_buf.elemSize()) + CV_Error(Error::StsInternal, "PNG input buffer is incomplete"); + + memcpy( _Buffer, m_buf.ptr() + m_buf_pos, _ElementSize ); + m_buf_pos += _ElementSize; + return 1; +} + +uint32_t PngDecoder::read_chunk(Chunk& chunk) +{ + unsigned char len[4]; + if (read_from_io(&len, 4, 1) == 1) + { + chunk.size = png_get_uint_32(len) + 12; + if (chunk.size > PNG_USER_CHUNK_MALLOC_MAX) + { + CV_LOG_WARNING(NULL, "chunk data is too large"); + } + chunk.p = new unsigned char[chunk.size]; + memcpy(chunk.p, len, 4); + if (read_from_io(chunk.p + 4, chunk.size - 4, 1) == 1) + return *(uint32_t*)(chunk.p + 4); + } + return 0; +} + +bool PngDecoder::processing_start(void* frame_ptr, const Mat& img) +{ + static uint8_t header[8] = { 137, 80, 78, 71, 13, 10, 26, 10 }; + + png_structp png_ptr = png_create_read_struct(PNG_LIBPNG_VER_STRING, NULL, NULL, NULL); + png_infop info_ptr = png_create_info_struct(png_ptr); + + m_png_ptr = png_ptr; + m_info_ptr = info_ptr; + + if (!png_ptr || !info_ptr) + return false; + + if (setjmp(png_jmpbuf(png_ptr))) + { + png_destroy_read_struct(&png_ptr, &info_ptr, 0); + return false; + } + + png_set_crc_action(png_ptr, PNG_CRC_QUIET_USE, PNG_CRC_QUIET_USE); + png_set_progressive_read_fn(png_ptr, frame_ptr, (png_progressive_info_ptr)info_fn, row_fn, NULL); + + if (img.channels() < 4) + png_set_strip_alpha(png_ptr); + else + png_set_tRNS_to_alpha(png_ptr); + + png_process_data(png_ptr, info_ptr, header, 8); + png_process_data(png_ptr, info_ptr, m_chunkIHDR.p, m_chunkIHDR.size); + + if ((m_color_type & PNG_COLOR_MASK_COLOR) && img.channels() > 1 && !m_use_rgb) + png_set_bgr(png_ptr); // convert RGB to BGR + else if (img.channels() > 1) + png_set_gray_to_rgb(png_ptr); // Gray->RGB + else + png_set_rgb_to_gray(png_ptr, 1, 0.299, 0.587); // RGB->Gray + + for (size_t i = 0; i < m_chunksInfo.size(); i++) + png_process_data(png_ptr, info_ptr, m_chunksInfo[i].p, m_chunksInfo[i].size); + + return true; +} + +bool PngDecoder::processing_finish() +{ + static uint8_t footer[12] = { 0, 0, 0, 0, 73, 69, 78, 68, 174, 66, 96, 130 }; + + png_structp png_ptr = (png_structp)m_png_ptr; + png_infop info_ptr = (png_infop)m_info_ptr; + + if (!png_ptr || !info_ptr) + return false; + + if (setjmp(png_jmpbuf(png_ptr))) + { + png_destroy_read_struct(&png_ptr, &info_ptr, 0); + return false; + } + + png_process_data(png_ptr, info_ptr, footer, 12); + png_destroy_read_struct(&png_ptr, &info_ptr, 0); + m_png_ptr = 0; + return true; +} + +void PngDecoder::info_fn(png_structp png_ptr, png_infop info_ptr) +{ + png_set_expand(png_ptr); + png_set_strip_16(png_ptr); + (void)png_set_interlace_handling(png_ptr); + png_read_update_info(png_ptr, info_ptr); +} + +void PngDecoder::row_fn(png_structp png_ptr, png_bytep new_row, png_uint_32 row_num, int pass) +{ + CV_UNUSED(pass); + APNGFrame* frame = (APNGFrame*)png_get_progressive_ptr(png_ptr); + png_progressive_combine_row(png_ptr, frame->getRows()[row_num], new_row); +} /////////////////////// PngEncoder /////////////////// - PngEncoder::PngEncoder() { m_description = "Portable Network Graphics files (*.png)"; m_buf_supported = true; + op_zstream1.zalloc = NULL; + op_zstream2.zalloc = NULL; + next_seq_num = 0; + trnssize = 0; + palsize = 0; + memset(palette, 0, sizeof(palette)); + memset(trns, 0, sizeof(trns)); + memset(op, 0, sizeof(op)); } - PngEncoder::~PngEncoder() { } - bool PngEncoder::isFormatSupported( int depth ) const { return depth == CV_8U || depth == CV_16U; @@ -326,8 +791,7 @@ ImageEncoder PngEncoder::newEncoder() const return makePtr(); } - -void PngEncoder::writeDataToBuf(void* _png_ptr, uchar* src, size_t size) +void PngEncoder::writeDataToBuf(void* _png_ptr, unsigned char* src, size_t size) { if( size == 0 ) return; @@ -339,7 +803,6 @@ void PngEncoder::writeDataToBuf(void* _png_ptr, uchar* src, size_t size) memcpy( &(*encoder->m_buf)[cursz], src, size ); } - void PngEncoder::flushBuf(void*) { } @@ -449,6 +912,765 @@ bool PngEncoder::write( const Mat& img, const std::vector& params ) return result; } +size_t PngEncoder::write_to_io(void const* _Buffer, size_t _ElementSize, size_t _ElementCount, FILE * _Stream) +{ + if (_Stream) + return fwrite(_Buffer, _ElementSize, _ElementCount, _Stream); + + size_t cursz = m_buf->size(); + m_buf->resize(cursz + _ElementCount); + memcpy( &(*m_buf)[cursz], _Buffer, _ElementCount ); + return _ElementCount; +} + +void PngEncoder::writeChunk(FILE* f, const char* name, unsigned char* data, uint32_t length) +{ + unsigned char buf[4]; + uint32_t crc = crc32(0, Z_NULL, 0); + + png_save_uint_32(buf, length); + write_to_io(buf, 1, 4, f); + write_to_io(name, 1, 4, f); + crc = crc32(crc, (const Bytef*)name, 4); + + if (memcmp(name, "fdAT", 4) == 0) + { + png_save_uint_32(buf, next_seq_num++); + write_to_io(buf, 1, 4, f); + crc = crc32(crc, buf, 4); + length -= 4; + } + + if (data != NULL && length > 0) + { + write_to_io(data, 1, length, f); + crc = crc32(crc, data, length); + } + + png_save_uint_32(buf, crc); + write_to_io(buf, 1, 4, f); +} + +void PngEncoder::writeIDATs(FILE* f, int frame, unsigned char* data, uint32_t length, uint32_t idat_size) +{ + uint32_t z_cmf = data[0]; + if ((z_cmf & 0x0f) == 8 && (z_cmf & 0xf0) <= 0x70) + { + if (length >= 2) + { + uint32_t z_cinfo = z_cmf >> 4; + uint32_t half_z_window_size = 1 << (z_cinfo + 7); + while (idat_size <= half_z_window_size && half_z_window_size >= 256) + { + z_cinfo--; + half_z_window_size >>= 1; + } + z_cmf = (z_cmf & 0x0f) | (z_cinfo << 4); + if (data[0] != (unsigned char)z_cmf) + { + data[0] = (unsigned char)z_cmf; + data[1] &= 0xe0; + data[1] += (unsigned char)(0x1f - ((z_cmf << 8) + data[1]) % 0x1f); + } + } + } + + while (length > 0) + { + uint32_t ds = length; + if (ds > 32768) + ds = 32768; + + if (frame == 0) + writeChunk(f, "IDAT", data, ds); + else + writeChunk(f, "fdAT", data, ds + 4); + + data += ds; + length -= ds; + } +} + +void PngEncoder::processRect(unsigned char* row, int rowbytes, int bpp, int stride, int h, unsigned char* rows) +{ + int i, j, v; + int a, b, c, pa, pb, pc, p; + unsigned char* prev = NULL; + unsigned char* dp = rows; + unsigned char* out; + + for (j = 0; j < h; j++) + { + uint32_t sum = 0; + unsigned char* best_row = row_buf.data(); + uint32_t mins = ((uint32_t)(-1)) >> 1; + + out = row_buf.data() + 1; + for (i = 0; i < rowbytes; i++) + { + v = out[i] = row[i]; + sum += (v < 128) ? v : 256 - v; + } + mins = sum; + + sum = 0; + out = sub_row.data() + 1; + for (i = 0; i < bpp; i++) + { + v = out[i] = row[i]; + sum += (v < 128) ? v : 256 - v; + } + for (i = bpp; i < rowbytes; i++) + { + v = out[i] = row[i] - row[i - bpp]; + sum += (v < 128) ? v : 256 - v; + if (sum > mins) + break; + } + if (sum < mins) + { + mins = sum; + best_row = sub_row.data(); + } + + if (prev) + { + sum = 0; + out = up_row.data() + 1; + for (i = 0; i < rowbytes; i++) + { + v = out[i] = row[i] - prev[i]; + sum += (v < 128) ? v : 256 - v; + if (sum > mins) + break; + } + if (sum < mins) + { + mins = sum; + best_row = up_row.data(); + } + + sum = 0; + out = avg_row.data() + 1; + for (i = 0; i < bpp; i++) + { + v = out[i] = row[i] - prev[i] / 2; + sum += (v < 128) ? v : 256 - v; + } + for (i = bpp; i < rowbytes; i++) + { + v = out[i] = row[i] - (prev[i] + row[i - bpp]) / 2; + sum += (v < 128) ? v : 256 - v; + if (sum > mins) + break; + } + if (sum < mins) + { + mins = sum; + best_row = avg_row.data(); + } + + sum = 0; + out = paeth_row.data() + 1; + for (i = 0; i < bpp; i++) + { + v = out[i] = row[i] - prev[i]; + sum += (v < 128) ? v : 256 - v; + } + for (i = bpp; i < rowbytes; i++) + { + a = row[i - bpp]; + b = prev[i]; + c = prev[i - bpp]; + p = b - c; + pc = a - c; + pa = abs(p); + pb = abs(pc); + pc = abs(p + pc); + p = (pa <= pb && pa <= pc) ? a : (pb <= pc) ? b + : c; + v = out[i] = row[i] - p; + sum += (v < 128) ? v : 256 - v; + if (sum > mins) + break; + } + if (sum < mins) + { + best_row = paeth_row.data(); + } + } + + if (rows == NULL) + { + // deflate_rect_op() + op_zstream1.next_in = row_buf.data(); + op_zstream1.avail_in = rowbytes + 1; + deflate(&op_zstream1, Z_NO_FLUSH); + + op_zstream2.next_in = best_row; + op_zstream2.avail_in = rowbytes + 1; + deflate(&op_zstream2, Z_NO_FLUSH); + } + else + { + // deflate_rect_fin() + memcpy(dp, best_row, rowbytes + 1); + dp += rowbytes + 1; + } + + prev = row; + row += stride; + } +} + +void PngEncoder::deflateRectOp(unsigned char* pdata, int x, int y, int w, int h, int bpp, int stride, int zbuf_size, int n) +{ + unsigned char* row = pdata + y * stride + x * bpp; + int rowbytes = w * bpp; + + op_zstream1.data_type = Z_BINARY; + op_zstream1.next_out = op_zbuf1.data(); + op_zstream1.avail_out = zbuf_size; + + op_zstream2.data_type = Z_BINARY; + op_zstream2.next_out = op_zbuf2.data(); + op_zstream2.avail_out = zbuf_size; + + processRect(row, rowbytes, bpp, stride, h, NULL); + + deflate(&op_zstream1, Z_FINISH); + deflate(&op_zstream2, Z_FINISH); + op[n].p = pdata; + + if (op_zstream1.total_out < op_zstream2.total_out) + { + op[n].size = op_zstream1.total_out; + op[n].filters = 0; + } + else + { + op[n].size = op_zstream2.total_out; + op[n].filters = 1; + } + op[n].x = x; + op[n].y = y; + op[n].w = w; + op[n].h = h; + op[n].valid = 1; + deflateReset(&op_zstream1); + deflateReset(&op_zstream2); +} + +bool PngEncoder::getRect(uint32_t w, uint32_t h, unsigned char* pimage1, unsigned char* pimage2, unsigned char* ptemp, uint32_t bpp, uint32_t stride, int zbuf_size, uint32_t has_tcolor, uint32_t tcolor, int n) +{ + uint32_t i, j, x0, y0, w0, h0; + uint32_t x_min = w - 1; + uint32_t y_min = h - 1; + uint32_t x_max = 0; + uint32_t y_max = 0; + uint32_t diffnum = 0; + uint32_t over_is_possible = 1; + + if (!has_tcolor) + over_is_possible = 0; + + if (bpp == 1) + { + unsigned char* pa = pimage1; + unsigned char* pb = pimage2; + unsigned char* pc = ptemp; + + for (j = 0; j < h; j++) + for (i = 0; i < w; i++) + { + unsigned char c = *pb++; + if (*pa++ != c) + { + diffnum++; + if (has_tcolor && c == tcolor) + over_is_possible = 0; + if (i < x_min) + x_min = i; + if (i > x_max) + x_max = i; + if (j < y_min) + y_min = j; + if (j > y_max) + y_max = j; + } + else + c = tcolor; + + *pc++ = c; + } + } + else if (bpp == 2) + { + unsigned short* pa = (unsigned short*)pimage1; + unsigned short* pb = (unsigned short*)pimage2; + unsigned short* pc = (unsigned short*)ptemp; + + for (j = 0; j < h; j++) + for (i = 0; i < w; i++) + { + uint32_t c1 = *pa++; + uint32_t c2 = *pb++; + if ((c1 != c2) && ((c1 >> 8) || (c2 >> 8))) + { + diffnum++; + if ((c2 >> 8) != 0xFF) + over_is_possible = 0; + if (i < x_min) + x_min = i; + if (i > x_max) + x_max = i; + if (j < y_min) + y_min = j; + if (j > y_max) + y_max = j; + } + else + c2 = 0; + + *pc++ = c2; + } + } + else if (bpp == 3) + { + unsigned char* pa = pimage1; + unsigned char* pb = pimage2; + unsigned char* pc = ptemp; + + for (j = 0; j < h; j++) + for (i = 0; i < w; i++) + { + uint32_t c1 = (pa[2] << 16) + (pa[1] << 8) + pa[0]; + uint32_t c2 = (pb[2] << 16) + (pb[1] << 8) + pb[0]; + if (c1 != c2) + { + diffnum++; + if (has_tcolor && c2 == tcolor) + over_is_possible = 0; + if (i < x_min) + x_min = i; + if (i > x_max) + x_max = i; + if (j < y_min) + y_min = j; + if (j > y_max) + y_max = j; + } + else + c2 = tcolor; + + memcpy(pc, &c2, 3); + pa += 3; + pb += 3; + pc += 3; + } + } + else if (bpp == 4) + { + uint32_t* pa = (uint32_t*)pimage1; + uint32_t* pb = (uint32_t*)pimage2; + uint32_t* pc = (uint32_t*)ptemp; + + for (j = 0; j < h; j++) + for (i = 0; i < w; i++) + { + uint32_t c1 = *pa++; + uint32_t c2 = *pb++; + if ((c1 != c2) && ((c1 >> 24) || (c2 >> 24))) + { + diffnum++; + if ((c2 >> 24) != 0xFF) + over_is_possible = 0; + if (i < x_min) + x_min = i; + if (i > x_max) + x_max = i; + if (j < y_min) + y_min = j; + if (j > y_max) + y_max = j; + } + else + c2 = 0; + + *pc++ = c2; + } + } + + if (diffnum == 0) + { + return false; + } + else + { + x0 = x_min; + y0 = y_min; + w0 = x_max - x_min + 1; + h0 = y_max - y_min + 1; + } + + if (n < 3) + { + deflateRectOp(pimage2, x0, y0, w0, h0, bpp, stride, zbuf_size, n * 2); + + if (over_is_possible) + deflateRectOp(ptemp, x0, y0, w0, h0, bpp, stride, zbuf_size, n * 2 + 1); + } + + return true; +} + +void PngEncoder::deflateRectFin(unsigned char* zbuf, uint32_t* zsize, int bpp, int stride, unsigned char* rows, int zbuf_size, int n) +{ + unsigned char* row = op[n].p + op[n].y * stride + op[n].x * bpp; + int rowbytes = op[n].w * bpp; + + if (op[n].filters == 0) + { + unsigned char* dp = rows; + for (int j = 0; j < op[n].h; j++) + { + *dp++ = 0; + memcpy(dp, row, rowbytes); + dp += rowbytes; + row += stride; + } + } + else + processRect(row, rowbytes, bpp, stride, op[n].h, rows); + + z_stream fin_zstream; + fin_zstream.data_type = Z_BINARY; + fin_zstream.zalloc = Z_NULL; + fin_zstream.zfree = Z_NULL; + fin_zstream.opaque = Z_NULL; + deflateInit2(&fin_zstream, Z_BEST_COMPRESSION, 8, 15, 8, op[n].filters ? Z_FILTERED : Z_DEFAULT_STRATEGY); + + fin_zstream.next_out = zbuf; + fin_zstream.avail_out = zbuf_size; + fin_zstream.next_in = rows; + fin_zstream.avail_in = op[n].h * (rowbytes + 1); + deflate(&fin_zstream, Z_FINISH); + *zsize = fin_zstream.total_out; + deflateEnd(&fin_zstream); +} + +bool PngEncoder::writemulti(const std::vector& img_vec, const std::vector& params) +{ + CV_Assert(img_vec[0].depth() == CV_8U); + CV_LOG_INFO(NULL, "Multi page image will be written as animation with 1 second frame duration."); + + Animation animation; + animation.frames = img_vec; + + for (size_t i = 0; i < animation.frames.size(); i++) + { + animation.durations.push_back(1000); + } + return writeanimation(animation, params); +} + +bool PngEncoder::writeanimation(const Animation& animation, const std::vector& params) +{ + int compression_level = 6; + int compression_strategy = IMWRITE_PNG_STRATEGY_RLE; // Default strategy + bool isBilevel = false; + + for (size_t i = 0; i < params.size(); i += 2) + { + if (params[i] == IMWRITE_PNG_COMPRESSION) + { + compression_strategy = IMWRITE_PNG_STRATEGY_DEFAULT; // Default strategy + compression_level = params[i + 1]; + compression_level = MIN(MAX(compression_level, 0), Z_BEST_COMPRESSION); + } + if (params[i] == IMWRITE_PNG_STRATEGY) + { + compression_strategy = params[i + 1]; + compression_strategy = MIN(MAX(compression_strategy, 0), Z_FIXED); + } + if (params[i] == IMWRITE_PNG_BILEVEL) + { + isBilevel = params[i + 1] != 0; + } + } + + CV_UNUSED(isBilevel); + uint32_t first =0; + uint32_t loops= animation.loop_count; + uint32_t coltype= animation.frames[0].channels() == 1 ? PNG_COLOR_TYPE_GRAY : animation.frames[0].channels() == 3 ? PNG_COLOR_TYPE_RGB : PNG_COLOR_TYPE_RGB_ALPHA; + + FILE* m_f = NULL; + uint32_t i, j, k; + uint32_t x0, y0, w0, h0, dop, bop; + uint32_t idat_size, zbuf_size, zsize; + unsigned char header[8] = { 137, 80, 78, 71, 13, 10, 26, 10 }; + uint32_t num_frames = (int)animation.frames.size(); + uint32_t width = animation.frames[0].cols; + uint32_t height = animation.frames[0].rows; + uint32_t bpp = (coltype == 6) ? 4 : (coltype == 2) ? 3 + : (coltype == 4) ? 2 + : 1; + uint32_t has_tcolor = (coltype >= 4 || (coltype <= 2 && trnssize)) ? 1 : 0; + uint32_t tcolor = 0; + uint32_t rowbytes = width * bpp; + uint32_t imagesize = rowbytes * height; + + AutoBuffer temp(imagesize); + AutoBuffer over1(imagesize); + AutoBuffer over2(imagesize); + AutoBuffer over3(imagesize); + AutoBuffer rest(imagesize); + AutoBuffer rows((rowbytes + 1) * height); + + std::vector frames; + std::vector tmpframes; + + for (i = 0; i < (uint32_t)animation.frames.size(); i++) + { + APNGFrame apngFrame; + tmpframes.push_back(animation.frames[i].clone()); + // TO DO optimize BGR RGB conversations + if (animation.frames[i].channels() == 4) + cvtColor(animation.frames[i], tmpframes[i], COLOR_BGRA2RGBA); + if (animation.frames[i].channels() == 3) + cvtColor(animation.frames[i], tmpframes[i], COLOR_BGR2RGB); + + apngFrame.setMat(tmpframes[i], animation.durations[i]); + + if (i > 0 && !getRect(width, height, frames.back().getPixels(), apngFrame.getPixels(), over1.data(), bpp, rowbytes, 0, 0, 0, 3)) + { + frames[i - 1].setDelayNum(frames.back().getDelayNum() + apngFrame.getDelayNum()); + num_frames--; + } + else + frames.push_back(apngFrame); + } + + if (trnssize) + { + if (coltype == 0) + tcolor = trns[1]; + else if (coltype == 2) + tcolor = (((trns[5] << 8) + trns[3]) << 8) + trns[1]; + else if (coltype == 3) + { + for (i = 0; i < trnssize; i++) + if (trns[i] == 0) + { + has_tcolor = 1; + tcolor = i; + break; + } + } + } + + if (m_buf || (m_f = fopen(m_filename.c_str(), "wb")) != 0) + { + unsigned char buf_IHDR[13]; + unsigned char buf_acTL[8]; + unsigned char buf_fcTL[26]; + + png_save_uint_32(buf_IHDR, width); + png_save_uint_32(buf_IHDR + 4, height); + buf_IHDR[8] = 8; + buf_IHDR[9] = coltype; + buf_IHDR[10] = 0; + buf_IHDR[11] = 0; + buf_IHDR[12] = 0; + + png_save_uint_32(buf_acTL, num_frames - first); + png_save_uint_32(buf_acTL + 4, loops); + + write_to_io(header, 1, 8, m_f); + + writeChunk(m_f, "IHDR", buf_IHDR, 13); + + if (num_frames > 1) + writeChunk(m_f, "acTL", buf_acTL, 8); + else + first = 0; + + if (palsize > 0) + writeChunk(m_f, "PLTE", (unsigned char*)(&palette), palsize * 3); + + if ((animation.bgcolor != Scalar()) && (animation.frames.size() > 1)) + { + uint64_t bgvalue = (static_cast(animation.bgcolor[0]) & 0xFF) << 24 | + (static_cast(animation.bgcolor[1]) & 0xFF) << 16 | + (static_cast(animation.bgcolor[2]) & 0xFF) << 8 | + (static_cast(animation.bgcolor[3]) & 0xFF); + writeChunk(m_f, "bKGD", (unsigned char*)(&bgvalue), 6); //the bKGD chunk must precede the first IDAT chunk, and must follow the PLTE chunk. + } + + if (trnssize > 0) + writeChunk(m_f, "tRNS", trns, trnssize); + + op_zstream1.data_type = Z_BINARY; + op_zstream1.zalloc = Z_NULL; + op_zstream1.zfree = Z_NULL; + op_zstream1.opaque = Z_NULL; + deflateInit2(&op_zstream1, compression_level, 8, 15, 8, compression_strategy); + + op_zstream2.data_type = Z_BINARY; + op_zstream2.zalloc = Z_NULL; + op_zstream2.zfree = Z_NULL; + op_zstream2.opaque = Z_NULL; + deflateInit2(&op_zstream2, compression_level, 8, 15, 8, Z_FILTERED); + + idat_size = (rowbytes + 1) * height; + zbuf_size = idat_size + ((idat_size + 7) >> 3) + ((idat_size + 63) >> 6) + 11; + + AutoBuffer zbuf(zbuf_size); + op_zbuf1.allocate(zbuf_size); + op_zbuf2.allocate(zbuf_size); + row_buf.allocate(rowbytes + 1); + sub_row.allocate(rowbytes + 1); + up_row.allocate(rowbytes + 1); + avg_row.allocate(rowbytes + 1); + paeth_row.allocate(rowbytes + 1); + + row_buf[0] = 0; + sub_row[0] = 1; + up_row[0] = 2; + avg_row[0] = 3; + paeth_row[0] = 4; + + x0 = 0; + y0 = 0; + w0 = width; + h0 = height; + bop = 0; + next_seq_num = 0; + + for (j = 0; j < 6; j++) + op[j].valid = 0; + deflateRectOp(frames[0].getPixels(), x0, y0, w0, h0, bpp, rowbytes, zbuf_size, 0); + deflateRectFin(zbuf.data(), &zsize, bpp, rowbytes, rows.data(), zbuf_size, 0); + + if (first) + { + writeIDATs(m_f, 0, zbuf.data(), zsize, idat_size); + for (j = 0; j < 6; j++) + op[j].valid = 0; + deflateRectOp(frames[1].getPixels(), x0, y0, w0, h0, bpp, rowbytes, zbuf_size, 0); + deflateRectFin(zbuf.data(), &zsize, bpp, rowbytes, rows.data(), zbuf_size, 0); + } + + for (i = first; i < num_frames - 1; i++) + { + uint32_t op_min; + int op_best; + + for (j = 0; j < 6; j++) + op[j].valid = 0; + + /* dispose = none */ + getRect(width, height, frames[i].getPixels(), frames[i + 1].getPixels(), over1.data(), bpp, rowbytes, zbuf_size, has_tcolor, tcolor, 0); + + /* dispose = background */ + if (has_tcolor) + { + memcpy(temp.data(), frames[i].getPixels(), imagesize); + if (coltype == 2) + for (j = 0; j < h0; j++) + for (k = 0; k < w0; k++) + memcpy(temp.data() + ((j + y0) * width + (k + x0)) * 3, &tcolor, 3); + else + for (j = 0; j < h0; j++) + memset(temp.data() + ((j + y0) * width + x0) * bpp, tcolor, w0 * bpp); + + getRect(width, height, temp.data(), frames[i + 1].getPixels(), over2.data(), bpp, rowbytes, zbuf_size, has_tcolor, tcolor, 1); + } + + /* dispose = previous */ + if (i > first) + getRect(width, height, rest.data(), frames[i + 1].getPixels(), over3.data(), bpp, rowbytes, zbuf_size, has_tcolor, tcolor, 2); + + op_min = op[0].size; + op_best = 0; + for (j = 1; j < 6; j++) + if (op[j].valid) + { + if (op[j].size < op_min) + { + op_min = op[j].size; + op_best = j; + } + } + + dop = op_best >> 1; + + png_save_uint_32(buf_fcTL, next_seq_num++); + png_save_uint_32(buf_fcTL + 4, w0); + png_save_uint_32(buf_fcTL + 8, h0); + png_save_uint_32(buf_fcTL + 12, x0); + png_save_uint_32(buf_fcTL + 16, y0); + png_save_uint_16(buf_fcTL + 20, frames[i].getDelayNum()); + png_save_uint_16(buf_fcTL + 22, frames[i].getDelayDen()); + buf_fcTL[24] = dop; + buf_fcTL[25] = bop; + writeChunk(m_f, "fcTL", buf_fcTL, 26); + + writeIDATs(m_f, i, zbuf.data(), zsize, idat_size); + + /* process apng dispose - begin */ + if (dop != 2) + memcpy(rest.data(), frames[i].getPixels(), imagesize); + + if (dop == 1) + { + if (coltype == 2) + for (j = 0; j < h0; j++) + for (k = 0; k < w0; k++) + memcpy(rest.data() + ((j + y0) * width + (k + x0)) * 3, &tcolor, 3); + else + for (j = 0; j < h0; j++) + memset(rest.data() + ((j + y0) * width + x0) * bpp, tcolor, w0 * bpp); + } + /* process apng dispose - end */ + + x0 = op[op_best].x; + y0 = op[op_best].y; + w0 = op[op_best].w; + h0 = op[op_best].h; + bop = op_best & 1; + + deflateRectFin(zbuf.data(), &zsize, bpp, rowbytes, rows.data(), zbuf_size, op_best); + } + + if (num_frames > 1) + { + png_save_uint_32(buf_fcTL, next_seq_num++); + png_save_uint_32(buf_fcTL + 4, w0); + png_save_uint_32(buf_fcTL + 8, h0); + png_save_uint_32(buf_fcTL + 12, x0); + png_save_uint_32(buf_fcTL + 16, y0); + png_save_uint_16(buf_fcTL + 20, frames[i].getDelayNum()); + png_save_uint_16(buf_fcTL + 22, frames[i].getDelayDen()); + buf_fcTL[24] = 0; + buf_fcTL[25] = bop; + writeChunk(m_f, "fcTL", buf_fcTL, 26); + } + + writeIDATs(m_f, num_frames - 1, zbuf.data(), zsize, idat_size); + + writeChunk(m_f, "IEND", 0, 0); + + if (m_f) + fclose(m_f); + + deflateEnd(&op_zstream1); + deflateEnd(&op_zstream2); + } + + return true; +} + } #endif diff --git a/modules/imgcodecs/src/grfmt_png.hpp b/modules/imgcodecs/src/grfmt_png.hpp index 3d8d1a764a..c23fb8ab54 100644 --- a/modules/imgcodecs/src/grfmt_png.hpp +++ b/modules/imgcodecs/src/grfmt_png.hpp @@ -47,26 +47,98 @@ #include "grfmt_base.hpp" #include "bitstrm.hpp" +#include +#include namespace cv { +struct Chunk { unsigned char* p; uint32_t size; }; +struct OP { unsigned char* p; uint32_t size; int x, y, w, h, valid, filters; }; + +typedef struct { + unsigned char r, g, b; +} rgb; + +class APNGFrame { +public: + + APNGFrame(); + + // Destructor + ~APNGFrame(); + + bool setMat(const cv::Mat& src, unsigned delayNum = 1, unsigned delayDen = 1000); + + // Getters and Setters + unsigned char* getPixels() const { return _pixels; } + void setPixels(unsigned char* pixels); + + unsigned int getWidth() const { return _width; } + void setWidth(unsigned int width); + + unsigned int getHeight() const { return _height; } + void setHeight(unsigned int height); + + unsigned char getColorType() const { return _colorType; } + void setColorType(unsigned char colorType); + + rgb* getPalette() { return _palette; } + void setPalette(const rgb* palette); + + unsigned char* getTransparency() { return _transparency; } + void setTransparency(const unsigned char* transparency); + + int getPaletteSize() const { return _paletteSize; } + void setPaletteSize(int paletteSize); + + int getTransparencySize() const { return _transparencySize; } + void setTransparencySize(int transparencySize); + + unsigned int getDelayNum() const { return _delayNum; } + void setDelayNum(unsigned int delayNum); + + unsigned int getDelayDen() const { return _delayDen; } + void setDelayDen(unsigned int delayDen); + + unsigned char** getRows() const { return _rows; } + void setRows(unsigned char** rows); + +private: + unsigned char* _pixels; + unsigned int _width; + unsigned int _height; + unsigned char _colorType; + rgb _palette[256]; + unsigned char _transparency[256]; + int _paletteSize; + int _transparencySize; + unsigned int _delayNum; + unsigned int _delayDen; + unsigned char** _rows; +}; + class PngDecoder CV_FINAL : public BaseImageDecoder { public: - PngDecoder(); virtual ~PngDecoder(); bool readData( Mat& img ) CV_OVERRIDE; bool readHeader() CV_OVERRIDE; - void close(); + bool nextPage() CV_OVERRIDE; ImageDecoder newDecoder() const CV_OVERRIDE; protected: - static void readDataFromBuf(void* png_ptr, uchar* dst, size_t size); + static void info_fn(png_structp png_ptr, png_infop info_ptr); + static void row_fn(png_structp png_ptr, png_bytep new_row, png_uint_32 row_num, int pass); + bool processing_start(void* frame_ptr, const Mat& img); + bool processing_finish(); + void compose_frame(unsigned char** rows_dst, unsigned char** rows_src, unsigned char bop, uint32_t x, uint32_t y, uint32_t w, uint32_t h, int channels); + size_t read_from_io(void* _Buffer, size_t _ElementSize, size_t _ElementCount); + uint32_t read_chunk(Chunk& chunk); int m_bit_depth; void* m_png_ptr; // pointer to decompression structure @@ -74,7 +146,25 @@ protected: void* m_end_info; // pointer to one more image information structure FILE* m_f; int m_color_type; + Chunk m_chunkIHDR; + int m_frame_no; size_t m_buf_pos; + std::vector m_chunksInfo; + APNGFrame frameRaw; + APNGFrame frameNext; + APNGFrame frameCur; + Mat m_mat_raw; + Mat m_mat_next; + uint32_t w0; + uint32_t h0; + uint32_t x0; + uint32_t y0; + uint32_t delay_num; + uint32_t delay_den; + uint32_t dop; + uint32_t bop; + bool m_is_fcTL_loaded; + bool m_is_IDAT_loaded; }; @@ -84,14 +174,40 @@ public: PngEncoder(); virtual ~PngEncoder(); - bool isFormatSupported( int depth ) const CV_OVERRIDE; - bool write( const Mat& img, const std::vector& params ) CV_OVERRIDE; + bool isFormatSupported( int depth ) const CV_OVERRIDE; + bool write( const Mat& img, const std::vector& params ) CV_OVERRIDE; + bool writemulti(const std::vector& img_vec, const std::vector& params) CV_OVERRIDE; + bool writeanimation(const Animation& animinfo, const std::vector& params) CV_OVERRIDE; ImageEncoder newEncoder() const CV_OVERRIDE; protected: - static void writeDataToBuf(void* png_ptr, uchar* src, size_t size); + static void writeDataToBuf(void* png_ptr, unsigned char* src, size_t size); static void flushBuf(void* png_ptr); + size_t write_to_io(void const* _Buffer, size_t _ElementSize, size_t _ElementCount, FILE* _Stream); + +private: + void writeChunk(FILE* f, const char* name, unsigned char* data, uint32_t length); + void writeIDATs(FILE* f, int frame, unsigned char* data, uint32_t length, uint32_t idat_size); + void processRect(unsigned char* row, int rowbytes, int bpp, int stride, int h, unsigned char* rows); + void deflateRectFin(unsigned char* zbuf, uint32_t* zsize, int bpp, int stride, unsigned char* rows, int zbuf_size, int n); + void deflateRectOp(unsigned char* pdata, int x, int y, int w, int h, int bpp, int stride, int zbuf_size, int n); + bool getRect(uint32_t w, uint32_t h, unsigned char* pimage1, unsigned char* pimage2, unsigned char* ptemp, uint32_t bpp, uint32_t stride, int zbuf_size, uint32_t has_tcolor, uint32_t tcolor, int n); + + AutoBuffer op_zbuf1; + AutoBuffer op_zbuf2; + AutoBuffer row_buf; + AutoBuffer sub_row; + AutoBuffer up_row; + AutoBuffer avg_row; + AutoBuffer paeth_row; + z_stream op_zstream1; + z_stream op_zstream2; + OP op[6]; + rgb palette[256]; + unsigned char trns[256]; + uint32_t palsize, trnssize; + uint32_t next_seq_num; }; } diff --git a/modules/imgcodecs/src/loadsave.cpp b/modules/imgcodecs/src/loadsave.cpp index 14c8b2c81a..e044554350 100644 --- a/modules/imgcodecs/src/loadsave.cpp +++ b/modules/imgcodecs/src/loadsave.cpp @@ -776,7 +776,8 @@ imreadanimation_(const String& filename, int flags, int start, int count, Animat if (current >= start) { - animation.durations.push_back(decoder->animation().durations[decoder->animation().durations.size() - 1]); + int duration = decoder->animation().durations.size() > 0 ? decoder->animation().durations.back() : 1000; + animation.durations.push_back(duration); animation.frames.push_back(mat); } diff --git a/modules/imgcodecs/test/test_animation.cpp b/modules/imgcodecs/test/test_animation.cpp new file mode 100644 index 0000000000..fcfce4b6ab --- /dev/null +++ b/modules/imgcodecs/test/test_animation.cpp @@ -0,0 +1,436 @@ +// This file is part of OpenCV project. +// It is subject to the license terms in the LICENSE file found in the top-level directory +// of this distribution and at http://opencv.org/license.html. + +#include "test_precomp.hpp" + +namespace opencv_test { namespace { + +static void readFileBytes(const std::string& fname, std::vector& buf) +{ + FILE * wfile = fopen(fname.c_str(), "rb"); + if (wfile != NULL) + { + fseek(wfile, 0, SEEK_END); + size_t wfile_size = ftell(wfile); + fseek(wfile, 0, SEEK_SET); + + buf.resize(wfile_size); + + size_t data_size = fread(&buf[0], 1, wfile_size, wfile); + + if(wfile) + { + fclose(wfile); + } + + EXPECT_EQ(data_size, wfile_size); + } +} + +static bool fillFrames(Animation& animation, bool hasAlpha, int n = 14) +{ + // Set the path to the test image directory and filename for loading. + const string root = cvtest::TS::ptr()->get_data_path(); + const string filename = root + "pngsuite/tp1n3p08.png"; + + EXPECT_TRUE(imreadanimation(filename, animation)); + EXPECT_EQ(1000, animation.durations.back()); + + if (!hasAlpha) + cvtColor(animation.frames[0], animation.frames[0], COLOR_BGRA2BGR); + + animation.loop_count = 0xffff; // 0xffff is the maximum value to set. + + // Add the first frame with a duration value of 400 milliseconds. + int duration = 80; + animation.durations[0] = duration * 5; + Mat image = animation.frames[0].clone(); + putText(animation.frames[0], "0", Point(5, 28), FONT_HERSHEY_SIMPLEX, .5, Scalar(100, 255, 0, 255), 2); + + // Define a region of interest (ROI) + Rect roi(2, 16, 26, 16); + + // Modify the ROI in n iterations to simulate slight changes in animation frames. + for (int i = 1; i < n; i++) + { + roi.x++; + roi.width -= 2; + RNG rng = theRNG(); + for (int x = roi.x; x < roi.x + roi.width; x++) + for (int y = roi.y; y < roi.y + roi.height; y++) + { + if (hasAlpha) + { + Vec4b& pixel = image.at(y, x); + if (pixel[3] > 0) + { + if (pixel[0] > 10) pixel[0] -= (uchar)rng.uniform(2, 5); + if (pixel[1] > 10) pixel[1] -= (uchar)rng.uniform(2, 5); + if (pixel[2] > 10) pixel[2] -= (uchar)rng.uniform(2, 5); + pixel[3] -= (uchar)rng.uniform(2, 5); + } + } + else + { + Vec3b& pixel = image.at(y, x); + if (pixel[0] > 50) pixel[0] -= (uchar)rng.uniform(2, 5); + if (pixel[1] > 50) pixel[1] -= (uchar)rng.uniform(2, 5); + if (pixel[2] > 50) pixel[2] -= (uchar)rng.uniform(2, 5); + } + } + + // Update the duration and add the modified frame to the animation. + duration += rng.uniform(2, 10); // Increase duration with random value (to be sure different duration values saved correctly). + animation.frames.push_back(image.clone()); + putText(animation.frames[i], format("%d", i), Point(5, 28), FONT_HERSHEY_SIMPLEX, .5, Scalar(100, 255, 0, 255), 2); + animation.durations.push_back(duration); + } + + // Add two identical frames with the same duration. + if (animation.frames.size() > 1 && animation.frames.size() < 20) + { + animation.durations.push_back(++duration); + animation.frames.push_back(animation.frames.back()); + animation.durations.push_back(++duration); + animation.frames.push_back(animation.frames.back()); + } + + return true; +} + +#ifdef HAVE_WEBP + +TEST(Imgcodecs_WebP, imwriteanimation_rgba) +{ + Animation s_animation, l_animation; + EXPECT_TRUE(fillFrames(s_animation, true)); + s_animation.bgcolor = Scalar(50, 100, 150, 128); // different values for test purpose. + + // Create a temporary output filename for saving the animation. + string output = cv::tempfile(".webp"); + + // Write the animation to a .webp file and verify success. + EXPECT_TRUE(imwriteanimation(output, s_animation)); + + // Read the animation back and compare with the original. + EXPECT_TRUE(imreadanimation(output, l_animation)); + + // Since the last frames are identical, WebP optimizes by storing only one of them, + // and the duration value for the last frame is handled by libwebp. + size_t expected_frame_count = s_animation.frames.size() - 2; + + // Verify that the number of frames matches the expected count. + EXPECT_EQ(expected_frame_count, imcount(output)); + EXPECT_EQ(expected_frame_count, l_animation.frames.size()); + + // Check that the background color and loop count match between saved and loaded animations. + EXPECT_EQ(l_animation.bgcolor, s_animation.bgcolor); // written as BGRA order + EXPECT_EQ(l_animation.loop_count, s_animation.loop_count); + + // Verify that the durations of frames match. + for (size_t i = 0; i < l_animation.frames.size() - 1; i++) + EXPECT_EQ(s_animation.durations[i], l_animation.durations[i]); + + EXPECT_TRUE(imreadanimation(output, l_animation, 5, 3)); + EXPECT_EQ(expected_frame_count + 3, l_animation.frames.size()); + EXPECT_EQ(l_animation.frames.size(), l_animation.durations.size()); + EXPECT_EQ(0, cvtest::norm(l_animation.frames[5], l_animation.frames[14], NORM_INF)); + EXPECT_EQ(0, cvtest::norm(l_animation.frames[6], l_animation.frames[15], NORM_INF)); + EXPECT_EQ(0, cvtest::norm(l_animation.frames[7], l_animation.frames[16], NORM_INF)); + + // Verify whether the imread function successfully loads the first frame + Mat frame = imread(output, IMREAD_UNCHANGED); + EXPECT_EQ(0, cvtest::norm(l_animation.frames[0], frame, NORM_INF)); + + std::vector buf; + readFileBytes(output, buf); + vector webp_frames; + + EXPECT_TRUE(imdecodemulti(buf, IMREAD_UNCHANGED, webp_frames)); + EXPECT_EQ(expected_frame_count, webp_frames.size()); + + // Clean up by removing the temporary file. + EXPECT_EQ(0, remove(output.c_str())); +} + +TEST(Imgcodecs_WebP, imwriteanimation_rgb) +{ + Animation s_animation, l_animation; + EXPECT_TRUE(fillFrames(s_animation, false)); + + // Create a temporary output filename for saving the animation. + string output = cv::tempfile(".webp"); + + // Write the animation to a .webp file and verify success. + EXPECT_TRUE(imwriteanimation(output, s_animation)); + + // Read the animation back and compare with the original. + EXPECT_TRUE(imreadanimation(output, l_animation)); + + // Since the last frames are identical, WebP optimizes by storing only one of them, + // and the duration value for the last frame is handled by libwebp. + size_t expected_frame_count = s_animation.frames.size() - 2; + + // Verify that the number of frames matches the expected count. + EXPECT_EQ(expected_frame_count, imcount(output)); + EXPECT_EQ(expected_frame_count, l_animation.frames.size()); + + // Verify that the durations of frames match. + for (size_t i = 0; i < l_animation.frames.size() - 1; i++) + EXPECT_EQ(s_animation.durations[i], l_animation.durations[i]); + + EXPECT_TRUE(imreadanimation(output, l_animation, 5, 3)); + EXPECT_EQ(expected_frame_count + 3, l_animation.frames.size()); + EXPECT_EQ(l_animation.frames.size(), l_animation.durations.size()); + EXPECT_TRUE(cvtest::norm(l_animation.frames[5], l_animation.frames[14], NORM_INF) == 0); + EXPECT_TRUE(cvtest::norm(l_animation.frames[6], l_animation.frames[15], NORM_INF) == 0); + EXPECT_TRUE(cvtest::norm(l_animation.frames[7], l_animation.frames[16], NORM_INF) == 0); + + // Verify whether the imread function successfully loads the first frame + Mat frame = imread(output, IMREAD_COLOR); + EXPECT_TRUE(cvtest::norm(l_animation.frames[0], frame, NORM_INF) == 0); + + std::vector buf; + readFileBytes(output, buf); + + vector webp_frames; + EXPECT_TRUE(imdecodemulti(buf, IMREAD_UNCHANGED, webp_frames)); + EXPECT_EQ(expected_frame_count,webp_frames.size()); + + // Clean up by removing the temporary file. + EXPECT_EQ(0, remove(output.c_str())); +} + +TEST(Imgcodecs_WebP, imwritemulti_rgba) +{ + Animation s_animation; + EXPECT_TRUE(fillFrames(s_animation, true)); + + string output = cv::tempfile(".webp"); + ASSERT_TRUE(imwrite(output, s_animation.frames)); + vector read_frames; + ASSERT_TRUE(imreadmulti(output, read_frames, IMREAD_UNCHANGED)); + EXPECT_EQ(s_animation.frames.size() - 2, read_frames.size()); + EXPECT_EQ(4, s_animation.frames[0].channels()); + EXPECT_EQ(0, remove(output.c_str())); +} + +TEST(Imgcodecs_WebP, imwritemulti_rgb) +{ + Animation s_animation; + EXPECT_TRUE(fillFrames(s_animation, false)); + + string output = cv::tempfile(".webp"); + ASSERT_TRUE(imwrite(output, s_animation.frames)); + vector read_frames; + ASSERT_TRUE(imreadmulti(output, read_frames)); + EXPECT_EQ(s_animation.frames.size() - 2, read_frames.size()); + EXPECT_EQ(0, remove(output.c_str())); +} + +TEST(Imgcodecs_WebP, imencode_rgba) +{ + Animation s_animation; + EXPECT_TRUE(fillFrames(s_animation, true, 3)); + + std::vector buf; + vector apng_frames; + + // Test encoding and decoding the images in memory (without saving to disk). + EXPECT_TRUE(imencode(".webp", s_animation.frames, buf)); + EXPECT_TRUE(imdecodemulti(buf, IMREAD_UNCHANGED, apng_frames)); + EXPECT_EQ(s_animation.frames.size() - 2, apng_frames.size()); +} + +#endif // HAVE_WEBP + +#ifdef HAVE_PNG + +TEST(Imgcodecs_APNG, imwriteanimation_rgba) +{ + Animation s_animation, l_animation; + EXPECT_TRUE(fillFrames(s_animation, true)); + + // Create a temporary output filename for saving the animation. + string output = cv::tempfile(".png"); + + // Write the animation to a .png file and verify success. + EXPECT_TRUE(imwriteanimation(output, s_animation)); + + // Read the animation back and compare with the original. + EXPECT_TRUE(imreadanimation(output, l_animation)); + + size_t expected_frame_count = s_animation.frames.size() - 2; + + // Verify that the number of frames matches the expected count. + EXPECT_EQ(expected_frame_count, imcount(output)); + EXPECT_EQ(expected_frame_count, l_animation.frames.size()); + + for (size_t i = 0; i < l_animation.frames.size() - 1; i++) + { + EXPECT_EQ(s_animation.durations[i], l_animation.durations[i]); + EXPECT_EQ(0, cvtest::norm(s_animation.frames[i], l_animation.frames[i], NORM_INF)); + } + + EXPECT_TRUE(imreadanimation(output, l_animation, 5, 3)); + EXPECT_EQ(expected_frame_count + 3, l_animation.frames.size()); + EXPECT_EQ(l_animation.frames.size(), l_animation.durations.size()); + EXPECT_EQ(0, cvtest::norm(l_animation.frames[5], l_animation.frames[14], NORM_INF)); + EXPECT_EQ(0, cvtest::norm(l_animation.frames[6], l_animation.frames[15], NORM_INF)); + EXPECT_EQ(0, cvtest::norm(l_animation.frames[7], l_animation.frames[16], NORM_INF)); + + // Verify whether the imread function successfully loads the first frame + Mat frame = imread(output, IMREAD_UNCHANGED); + EXPECT_EQ(0, cvtest::norm(l_animation.frames[0], frame, NORM_INF)); + + std::vector buf; + readFileBytes(output, buf); + vector apng_frames; + + EXPECT_TRUE(imdecodemulti(buf, IMREAD_UNCHANGED, apng_frames)); + EXPECT_EQ(expected_frame_count, apng_frames.size()); + + apng_frames.clear(); + // Test saving the animation frames as individual still images. + EXPECT_TRUE(imwrite(output, s_animation.frames)); + + // Read back the still images into a vector of Mats. + EXPECT_TRUE(imreadmulti(output, apng_frames)); + + // Expect all frames written as multi-page image + EXPECT_EQ(expected_frame_count, apng_frames.size()); + + // Clean up by removing the temporary file. + EXPECT_EQ(0, remove(output.c_str())); +} + +TEST(Imgcodecs_APNG, imwriteanimation_rgb) +{ + Animation s_animation, l_animation; + EXPECT_TRUE(fillFrames(s_animation, false)); + + string output = cv::tempfile(".png"); + + // Write the animation to a .png file and verify success. + EXPECT_TRUE(imwriteanimation(output, s_animation)); + + // Read the animation back and compare with the original. + EXPECT_TRUE(imreadanimation(output, l_animation)); + EXPECT_EQ(l_animation.frames.size(), s_animation.frames.size() - 2); + for (size_t i = 0; i < l_animation.frames.size() - 1; i++) + { + EXPECT_EQ(0, cvtest::norm(s_animation.frames[i], l_animation.frames[i], NORM_INF)); + } + EXPECT_EQ(0, remove(output.c_str())); +} + +TEST(Imgcodecs_APNG, imwritemulti_rgba) +{ + Animation s_animation; + EXPECT_TRUE(fillFrames(s_animation, true)); + + string output = cv::tempfile(".png"); + EXPECT_EQ(true, imwrite(output, s_animation.frames)); + vector read_frames; + EXPECT_EQ(true, imreadmulti(output, read_frames, IMREAD_UNCHANGED)); + EXPECT_EQ(read_frames.size(), s_animation.frames.size() - 2); + EXPECT_EQ(imcount(output), read_frames.size()); + EXPECT_EQ(0, remove(output.c_str())); +} + +TEST(Imgcodecs_APNG, imwritemulti_rgb) +{ + Animation s_animation; + EXPECT_TRUE(fillFrames(s_animation, false)); + + string output = cv::tempfile(".png"); + ASSERT_TRUE(imwrite(output, s_animation.frames)); + vector read_frames; + ASSERT_TRUE(imreadmulti(output, read_frames)); + EXPECT_EQ(read_frames.size(), s_animation.frames.size() - 2); + EXPECT_EQ(0, remove(output.c_str())); + + for (size_t i = 0; i < read_frames.size(); i++) + { + EXPECT_EQ(0, cvtest::norm(s_animation.frames[i], read_frames[i], NORM_INF)); + } +} + +TEST(Imgcodecs_APNG, imwritemulti_gray) +{ + Animation s_animation; + EXPECT_TRUE(fillFrames(s_animation, false)); + + for (size_t i = 0; i < s_animation.frames.size(); i++) + { + cvtColor(s_animation.frames[i], s_animation.frames[i], COLOR_BGR2GRAY); + } + + string output = cv::tempfile(".png"); + EXPECT_TRUE(imwrite(output, s_animation.frames)); + vector read_frames; + EXPECT_TRUE(imreadmulti(output, read_frames)); + EXPECT_EQ(1, read_frames[0].channels()); + read_frames.clear(); + EXPECT_TRUE(imreadmulti(output, read_frames, IMREAD_UNCHANGED)); + EXPECT_EQ(1, read_frames[0].channels()); + read_frames.clear(); + EXPECT_TRUE(imreadmulti(output, read_frames, IMREAD_COLOR)); + EXPECT_EQ(3, read_frames[0].channels()); + read_frames.clear(); + EXPECT_TRUE(imreadmulti(output, read_frames, IMREAD_GRAYSCALE)); + EXPECT_EQ(0, remove(output.c_str())); + + for (size_t i = 0; i < read_frames.size(); i++) + { + EXPECT_EQ(0, cvtest::norm(s_animation.frames[i], read_frames[i], NORM_INF)); + } +} + +TEST(Imgcodecs_APNG, imwriteanimation_bgcolor) +{ + Animation s_animation, l_animation; + EXPECT_TRUE(fillFrames(s_animation, true, 2)); + s_animation.bgcolor = Scalar(50, 100, 150, 128); // different values for test purpose. + + // Create a temporary output filename for saving the animation. + string output = cv::tempfile(".png"); + + // Write the animation to a .png file and verify success. + EXPECT_TRUE(imwriteanimation(output, s_animation)); + + // Read the animation back and compare with the original. + EXPECT_TRUE(imreadanimation(output, l_animation)); + + // Check that the background color match between saved and loaded animations. + EXPECT_EQ(l_animation.bgcolor, s_animation.bgcolor); + EXPECT_EQ(0, remove(output.c_str())); + + EXPECT_TRUE(fillFrames(s_animation, true, 2)); + s_animation.bgcolor = Scalar(); + + output = cv::tempfile(".png"); + EXPECT_TRUE(imwriteanimation(output, s_animation)); + EXPECT_TRUE(imreadanimation(output, l_animation)); + EXPECT_EQ(l_animation.bgcolor, s_animation.bgcolor); + + EXPECT_EQ(0, remove(output.c_str())); +} + +TEST(Imgcodecs_APNG, imencode_rgba) +{ + Animation s_animation; + EXPECT_TRUE(fillFrames(s_animation, true, 3)); + + std::vector buf; + vector read_frames; + // Test encoding and decoding the images in memory (without saving to disk). + EXPECT_TRUE(imencode(".png", s_animation.frames, buf)); + EXPECT_TRUE(imdecodemulti(buf, IMREAD_UNCHANGED, read_frames)); + EXPECT_EQ(read_frames.size(), s_animation.frames.size() - 2); +} + +#endif // HAVE_PNG + +}} // namespace diff --git a/modules/imgcodecs/test/test_webp.cpp b/modules/imgcodecs/test/test_webp.cpp index 0e8280c73b..6414e60469 100644 --- a/modules/imgcodecs/test/test_webp.cpp +++ b/modules/imgcodecs/test/test_webp.cpp @@ -1,6 +1,7 @@ // This file is part of OpenCV project. // It is subject to the license terms in the LICENSE file found in the top-level directory -// of this distribution and at http://opencv.org/license.html +// of this distribution and at http://opencv.org/license.html. + #include "test_precomp.hpp" namespace opencv_test { namespace { @@ -113,234 +114,6 @@ TEST(Imgcodecs_WebP, encode_decode_with_alpha_webp) EXPECT_EQ(512, img_webp_bgr.rows); } -TEST(Imgcodecs_WebP, load_save_animation_rgba) -{ - RNG rng = theRNG(); - - // Set the path to the test image directory and filename for loading. - const string root = cvtest::TS::ptr()->get_data_path(); - const string filename = root + "pngsuite/tp1n3p08.png"; - - // Create an Animation object using the default constructor. - // This initializes the loop count to 0 (infinite looping), background color to 0 (transparent) - Animation l_animation; - - // Create an Animation object with custom parameters. - int loop_count = 0xffff; // 0xffff is the maximum value to set. - Scalar bgcolor(125, 126, 127, 128); // different values for test purpose. - Animation s_animation(loop_count, bgcolor); - - // Load the image file with alpha channel (IMREAD_UNCHANGED). - Mat image = imread(filename, IMREAD_UNCHANGED); - ASSERT_FALSE(image.empty()) << "Failed to load image: " << filename; - - // Add the first frame with a duration value of 500 milliseconds. - int duration = 100; - s_animation.durations.push_back(duration * 5); - s_animation.frames.push_back(image.clone()); // Store the first frame. - putText(s_animation.frames[0], "0", Point(5, 28), FONT_HERSHEY_SIMPLEX, .5, Scalar(100, 255, 0, 255), 2); - - // Define a region of interest (ROI) in the loaded image for manipulation. - Mat roi = image(Rect(0, 16, 32, 16)); // Select a subregion of the image. - - // Modify the ROI in 13 iterations to simulate slight changes in animation frames. - for (int i = 1; i < 14; i++) - { - for (int x = 0; x < roi.rows; x++) - for (int y = 0; y < roi.cols; y++) - { - // Apply random changes to pixel values to create animation variations. - Vec4b& pixel = roi.at(x, y); - if (pixel[3] > 0) - { - if (pixel[0] > 10) pixel[0] -= (uchar)rng.uniform(3, 10); // Reduce blue channel. - if (pixel[1] > 10) pixel[1] -= (uchar)rng.uniform(3, 10); // Reduce green channel. - if (pixel[2] > 10) pixel[2] -= (uchar)rng.uniform(3, 10); // Reduce red channel. - pixel[3] -= (uchar)rng.uniform(2, 5); // Reduce alpha channel. - } - } - - // Update the duration and add the modified frame to the animation. - duration += rng.uniform(2, 10); // Increase duration with random value (to be sure different duration values saved correctly). - s_animation.frames.push_back(image.clone()); - putText(s_animation.frames[i], format("%d", i), Point(5, 28), FONT_HERSHEY_SIMPLEX, .5, Scalar(100, 255, 0, 255), 2); - s_animation.durations.push_back(duration); - } - - // Add two identical frames with the same duration. - s_animation.durations.push_back(duration); - s_animation.frames.push_back(s_animation.frames[13].clone()); - s_animation.durations.push_back(duration); - s_animation.frames.push_back(s_animation.frames[13].clone()); - - // Create a temporary output filename for saving the animation. - string output = cv::tempfile(".webp"); - - // Write the animation to a .webp file and verify success. - EXPECT_TRUE(imwriteanimation(output, s_animation)); - imwriteanimation("output.webp", s_animation); - - // Read the animation back and compare with the original. - EXPECT_TRUE(imreadanimation(output, l_animation)); - - // Since the last frames are identical, WebP optimizes by storing only one of them, - // and the duration value for the last frame is handled by libwebp. - size_t expected_frame_count = s_animation.frames.size() - 2; - - // Verify that the number of frames matches the expected count. - EXPECT_EQ(imcount(output), expected_frame_count); - EXPECT_EQ(l_animation.frames.size(), expected_frame_count); - - // Check that the background color and loop count match between saved and loaded animations. - EXPECT_EQ(l_animation.bgcolor, s_animation.bgcolor); // written as BGRA order - EXPECT_EQ(l_animation.loop_count, s_animation.loop_count); - - // Verify that the durations of frames match. - for (size_t i = 0; i < l_animation.frames.size() - 1; i++) - EXPECT_EQ(s_animation.durations[i], l_animation.durations[i]); - - EXPECT_TRUE(imreadanimation(output, l_animation, 5, 3)); - EXPECT_EQ(l_animation.frames.size(), expected_frame_count + 3); - EXPECT_EQ(l_animation.frames.size(), l_animation.durations.size()); - EXPECT_EQ(0, cvtest::norm(l_animation.frames[5], l_animation.frames[14], NORM_INF)); - EXPECT_EQ(0, cvtest::norm(l_animation.frames[6], l_animation.frames[15], NORM_INF)); - EXPECT_EQ(0, cvtest::norm(l_animation.frames[7], l_animation.frames[16], NORM_INF)); - - // Verify whether the imread function successfully loads the first frame - Mat frame = imread(output, IMREAD_UNCHANGED); - EXPECT_EQ(0, cvtest::norm(l_animation.frames[0], frame, NORM_INF)); - - std::vector buf; - readFileBytes(output, buf); - vector webp_frames; - - EXPECT_TRUE(imdecodemulti(buf, IMREAD_UNCHANGED, webp_frames)); - EXPECT_EQ(expected_frame_count, webp_frames.size()); - - webp_frames.clear(); - // Test saving the animation frames as individual still images. - EXPECT_TRUE(imwrite(output, s_animation.frames)); - - // Read back the still images into a vector of Mats. - EXPECT_TRUE(imreadmulti(output, webp_frames)); - - // Expect all frames written as multi-page image - expected_frame_count = 14; - EXPECT_EQ(expected_frame_count, webp_frames.size()); - - // Test encoding and decoding the images in memory (without saving to disk). - webp_frames.clear(); - EXPECT_TRUE(imencode(".webp", s_animation.frames, buf)); - EXPECT_TRUE(imdecodemulti(buf, IMREAD_UNCHANGED, webp_frames)); - EXPECT_EQ(expected_frame_count, webp_frames.size()); - - // Clean up by removing the temporary file. - EXPECT_EQ(0, remove(output.c_str())); -} - -TEST(Imgcodecs_WebP, load_save_animation_rgb) -{ - RNG rng = theRNG(); - - // Set the path to the test image directory and filename for loading. - const string root = cvtest::TS::ptr()->get_data_path(); - const string filename = root + "pngsuite/tp1n3p08.png"; - - // Create an Animation object using the default constructor. - // This initializes the loop count to 0 (infinite looping), background color to 0 (transparent) - Animation l_animation; - - // Create an Animation object with custom parameters. - int loop_count = 0xffff; // 0xffff is the maximum value to set. - Scalar bgcolor(125, 126, 127, 128); // different values for test purpose. - Animation s_animation(loop_count, bgcolor); - - // Load the image file without alpha channel - Mat image = imread(filename); - ASSERT_FALSE(image.empty()) << "Failed to load image: " << filename; - - // Add the first frame with a duration value of 500 milliseconds. - int duration = 100; - s_animation.durations.push_back(duration * 5); - s_animation.frames.push_back(image.clone()); // Store the first frame. - putText(s_animation.frames[0], "0", Point(5, 28), FONT_HERSHEY_SIMPLEX, .5, Scalar(100, 255, 0, 255), 2); - - // Define a region of interest (ROI) in the loaded image for manipulation. - Mat roi = image(Rect(0, 16, 32, 16)); // Select a subregion of the image. - - // Modify the ROI in 13 iterations to simulate slight changes in animation frames. - for (int i = 1; i < 14; i++) - { - for (int x = 0; x < roi.rows; x++) - for (int y = 0; y < roi.cols; y++) - { - // Apply random changes to pixel values to create animation variations. - Vec3b& pixel = roi.at(x, y); - if (pixel[0] > 50) pixel[0] -= (uchar)rng.uniform(3, 10); // Reduce blue channel. - if (pixel[1] > 50) pixel[1] -= (uchar)rng.uniform(3, 10); // Reduce green channel. - if (pixel[2] > 50) pixel[2] -= (uchar)rng.uniform(3, 10); // Reduce red channel. - } - - // Update the duration and add the modified frame to the animation. - duration += rng.uniform(2, 10); // Increase duration with random value (to be sure different duration values saved correctly). - s_animation.frames.push_back(image.clone()); - putText(s_animation.frames[i], format("%d", i), Point(5, 28), FONT_HERSHEY_SIMPLEX, .5, Scalar(100, 255, 0, 255), 2); - s_animation.durations.push_back(duration); - } - - // Add two identical frames with the same duration. - s_animation.durations.push_back(duration); - s_animation.frames.push_back(s_animation.frames[13].clone()); - s_animation.durations.push_back(duration); - s_animation.frames.push_back(s_animation.frames[13].clone()); - - // Create a temporary output filename for saving the animation. - string output = cv::tempfile(".webp"); - - // Write the animation to a .webp file and verify success. - EXPECT_EQ(true, imwriteanimation(output, s_animation)); - - // Read the animation back and compare with the original. - EXPECT_EQ(true, imreadanimation(output, l_animation)); - - // Since the last frames are identical, WebP optimizes by storing only one of them, - // and the duration value for the last frame is handled by libwebp. - size_t expected_frame_count = s_animation.frames.size() - 2; - - // Verify that the number of frames matches the expected count. - EXPECT_EQ(imcount(output), expected_frame_count); - EXPECT_EQ(l_animation.frames.size(), expected_frame_count); - - // Check that the background color and loop count match between saved and loaded animations. - EXPECT_EQ(l_animation.bgcolor, s_animation.bgcolor); // written as BGRA order - EXPECT_EQ(l_animation.loop_count, s_animation.loop_count); - - // Verify that the durations of frames match. - for (size_t i = 0; i < l_animation.frames.size() - 1; i++) - EXPECT_EQ(s_animation.durations[i], l_animation.durations[i]); - - EXPECT_EQ(true, imreadanimation(output, l_animation, 5, 3)); - EXPECT_EQ(l_animation.frames.size(), expected_frame_count + 3); - EXPECT_EQ(l_animation.frames.size(), l_animation.durations.size()); - EXPECT_TRUE(cvtest::norm(l_animation.frames[5], l_animation.frames[14], NORM_INF) == 0); - EXPECT_TRUE(cvtest::norm(l_animation.frames[6], l_animation.frames[15], NORM_INF) == 0); - EXPECT_TRUE(cvtest::norm(l_animation.frames[7], l_animation.frames[16], NORM_INF) == 0); - - // Verify whether the imread function successfully loads the first frame - Mat frame = imread(output, IMREAD_COLOR); - EXPECT_TRUE(cvtest::norm(l_animation.frames[0], frame, NORM_INF) == 0); - - std::vector buf; - readFileBytes(output, buf); - - vector webp_frames; - EXPECT_TRUE(imdecodemulti(buf, IMREAD_UNCHANGED, webp_frames)); - EXPECT_EQ(webp_frames.size(), expected_frame_count); - - // Clean up by removing the temporary file. - EXPECT_EQ(0, remove(output.c_str())); -} - #endif // HAVE_WEBP }} // namespace