Merge pull request #26688 from sturkmen72:gif-png-webp-avif

Animated GIF APNG WEBP AVIF revisions
This commit is contained in:
Alexander Smorkalov 2025-01-04 16:44:14 +03:00 committed by GitHub
commit ff18c9cc79
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
10 changed files with 210 additions and 109 deletions

View File

@ -303,21 +303,6 @@ bool AvifEncoder::write(const Mat &img, const std::vector<int> &params) {
return writemulti(img_vec, params);
}
bool AvifEncoder::writemulti(const std::vector<Mat> &img_vec,
const std::vector<int> &params) {
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 AvifEncoder::writeanimation(const Animation& animation,
const std::vector<int> &params) {
int bit_depth = 8;

View File

@ -41,11 +41,7 @@ class AvifEncoder CV_FINAL : public BaseImageEncoder {
~AvifEncoder() CV_OVERRIDE;
bool isFormatSupported(int depth) const CV_OVERRIDE;
bool write(const Mat& img, const std::vector<int>& params) CV_OVERRIDE;
bool writemulti(const std::vector<Mat>& img_vec,
const std::vector<int>& params) CV_OVERRIDE;
bool writeanimation(const Animation& animation, const std::vector<int>& params) CV_OVERRIDE;
ImageEncoder newEncoder() const CV_OVERRIDE;

View File

@ -43,6 +43,7 @@
#include "grfmt_base.hpp"
#include "bitstrm.hpp"
#include <opencv2/core/utils/logger.hpp>
namespace cv
{
@ -139,9 +140,19 @@ bool BaseImageEncoder::setDestination( std::vector<uchar>& buf )
return true;
}
bool BaseImageEncoder::writemulti(const std::vector<Mat>&, const std::vector<int>& )
bool BaseImageEncoder::writemulti(const std::vector<Mat>& img_vec, const std::vector<int>& params)
{
return false;
if(img_vec.size() > 1)
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 BaseImageEncoder::writeanimation(const Animation&, const std::vector<int>& )

View File

@ -219,7 +219,7 @@ void GifDecoder::readExtensions() {
len = (uchar)m_strm.getByte();
CV_Assert(len == 4);
auto flags = (uchar)m_strm.getByte();
m_strm.getWord(); // delay time, not used
m_animation.durations.push_back(m_strm.getWord() * 10); // delay time
opMode = (GifOpMode)((flags & 0x1C) >> 2);
hasTransparentColor = flags & 0x01;
transparentColor = (uchar)m_strm.getByte();
@ -407,6 +407,10 @@ bool GifDecoder::getFrameCount_() {
while (len) {
m_strm.skip(len);
len = m_strm.getByte();
if (len == 3 && m_strm.getByte() == 1)
{
m_animation.loop_count = m_strm.getWord();
}
}
} else if (!(type ^ 0x2C)) {
// skip image data
@ -490,16 +494,11 @@ bool GifEncoder::isFormatSupported(int depth) const {
bool GifEncoder::write(const Mat &img, const std::vector<int> &params) {
std::vector<Mat> img_vec(1, img);
return writeFrames(img_vec, params);
return writemulti(img_vec, params);
}
bool GifEncoder::writemulti(const std::vector<Mat> &img_vec, const std::vector<int> &params) {
return writeFrames(img_vec, params);
}
bool GifEncoder::writeFrames(const std::vector<Mat>& img_vec,
const std::vector<int>& params) {
if (img_vec.empty()) {
bool GifEncoder::writeanimation(const Animation& animation, const std::vector<int>& params) {
if (animation.frames.empty()) {
return false;
}
@ -511,6 +510,8 @@ bool GifEncoder::writeFrames(const std::vector<Mat>& img_vec,
return false;
}
loopCount = animation.loop_count;
// confirm the params
for (size_t i = 0; i < params.size(); i += 2) {
switch (params[i]) {
@ -561,13 +562,13 @@ bool GifEncoder::writeFrames(const std::vector<Mat>& img_vec,
if (fast) {
const uchar transparent = 0x92; // 1001_0010: the middle of the color table
if (dithering == GRFMT_GIF_None) {
img_vec_ = img_vec;
img_vec_ = animation.frames;
transparentColor = transparent;
} else {
localColorTableSize = 0;
int transRGB;
const int depth = 3 << 8 | 3 << 4 | 2; // r:g:b = 3:3:2
for (auto &img: img_vec) {
for (auto &img: animation.frames) {
Mat img_(img.size(), img.type());
transRGB = ditheringKernel(img, img_, depth, criticalTransparency);
if (transRGB >= 0) {
@ -583,13 +584,13 @@ bool GifEncoder::writeFrames(const std::vector<Mat>& img_vec,
} else if (dithering != GRFMT_GIF_None) {
int depth = (int)floor(log2(colorNum) / 3) + dithering;
depth = depth << 8 | depth << 4 | depth;
for (auto &img : img_vec) {
for (auto &img : animation.frames) {
Mat img_(img.size(), img.type());
ditheringKernel(img, img_, depth, criticalTransparency);
img_vec_.push_back(img_);
}
} else {
img_vec_ = img_vec;
img_vec_ = animation.frames;
}
bool result = writeHeader(img_vec_);
if (!result) {
@ -597,8 +598,9 @@ bool GifEncoder::writeFrames(const std::vector<Mat>& img_vec,
return false;
}
for (const auto &img : img_vec_) {
result = writeFrame(img);
for (size_t i = 0; i < img_vec_.size(); i++) {
frameDelay = cvRound(animation.durations[i] / 10);
result = writeFrame(img_vec_[i]);
}
strm.putByte(0x3B); // trailer

View File

@ -86,9 +86,7 @@ public:
bool isFormatSupported(int depth) const CV_OVERRIDE;
bool write(const Mat& img, const std::vector<int>& params) CV_OVERRIDE;
bool writemulti(const std::vector<Mat>& img_vec,
const std::vector<int>& params) CV_OVERRIDE;
bool writeanimation(const Animation& animation, const std::vector<int>& params) CV_OVERRIDE;
ImageEncoder newEncoder() const CV_OVERRIDE;

View File

@ -152,8 +152,7 @@ bool APNGFrame::setMat(const cv::Mat& src, unsigned delayNum, unsigned delayDen)
if (!src.empty())
{
png_uint_32 rowbytes = src.cols * src.channels();
png_uint_32 rowbytes = src.depth() == CV_16U ? src.cols * src.channels() * 2 : 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;
@ -389,7 +388,7 @@ bool PngDecoder::readData( Mat& img )
{
if (m_frame_count > 1)
{
Mat mat_cur = Mat(img.rows, img.cols, m_type);
Mat mat_cur = Mat::zeros(img.rows, img.cols, m_type);
uint32_t id = 0;
uint32_t j = 0;
uint32_t imagesize = m_width * m_height * mat_cur.channels();
@ -437,7 +436,7 @@ bool PngDecoder::readData( Mat& img )
if (dop == 2)
memcpy(frameNext.getPixels(), frameCur.getPixels(), imagesize);
compose_frame(frameCur.getRows(), frameRaw.getRows(), bop, x0, y0, w0, h0, mat_cur.channels());
compose_frame(frameCur.getRows(), frameRaw.getRows(), bop, x0, y0, w0, h0, mat_cur);
if (delay_den < 1000)
delay_num = cvRound(1000.0 / delay_den);
m_animation.durations.push_back(delay_num);
@ -495,7 +494,7 @@ bool PngDecoder::readData( Mat& img )
{
if (processing_finish())
{
compose_frame(frameCur.getRows(), frameRaw.getRows(), bop, x0, y0, w0, h0, mat_cur.channels());
compose_frame(frameCur.getRows(), frameRaw.getRows(), bop, x0, y0, w0, h0, mat_cur);
if (delay_den < 1000)
delay_num = cvRound(1000.0 / delay_den);
m_animation.durations.push_back(delay_num);
@ -606,41 +605,81 @@ bool PngDecoder::nextPage() {
return ++m_frame_no < (int)m_frame_count;
}
void PngDecoder::compose_frame(std::vector<png_bytep>& rows_dst, const std::vector<png_bytep>& rows_src, unsigned char _bop, uint32_t x, uint32_t y, uint32_t w, uint32_t h, int channels)
void PngDecoder::compose_frame(std::vector<png_bytep>& rows_dst, const std::vector<png_bytep>& rows_src, unsigned char _bop, uint32_t x, uint32_t y, uint32_t w, uint32_t h, Mat& img)
{
uint32_t i, j;
int u, v, al;
int channels = img.channels();
if (img.depth() == CV_16U)
cv::parallel_for_(cv::Range(0, h), [&](const cv::Range& range) {
for (int j = range.start; j < range.end; j++) {
uint16_t* sp = reinterpret_cast<uint16_t*>(rows_src[j]);
uint16_t* dp = reinterpret_cast<uint16_t*>(rows_dst[j + y]) + x * channels;
for (j = 0; j < h; j++)
{
if (_bop == 0) {
// Overwrite mode: copy source row directly to destination
memcpy(dp, sp, w * channels * sizeof(uint16_t));
}
else {
// Blending mode
for (unsigned int i = 0; i < w; i++, sp += channels, dp += channels) {
if (sp[3] == 65535) { // Fully opaque in 16-bit (max value)
memcpy(dp, sp, channels * sizeof(uint16_t));
}
else if (sp[3] != 0) { // Partially transparent
if (dp[3] != 0) { // Both source and destination have alpha
uint32_t u = sp[3] * 65535; // 16-bit max
uint32_t v = (65535 - sp[3]) * dp[3];
uint32_t al = u + v;
dp[0] = static_cast<uint16_t>((sp[0] * u + dp[0] * v) / al); // Red
dp[1] = static_cast<uint16_t>((sp[1] * u + dp[1] * v) / al); // Green
dp[2] = static_cast<uint16_t>((sp[2] * u + dp[2] * v) / al); // Blue
dp[3] = static_cast<uint16_t>(al / 65535); // Alpha
}
else {
// If destination alpha is 0, copy source pixel
memcpy(dp, sp, channels * sizeof(uint16_t));
}
}
}
}
}
});
else
cv::parallel_for_(cv::Range(0, h), [&](const cv::Range& range) {
for (int j = range.start; j < range.end; j++) {
unsigned char* sp = rows_src[j];
unsigned char* dp = rows_dst[j + y] + x * channels;
if (_bop == 0)
if (_bop == 0) {
// Overwrite mode: copy source row directly to destination
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);
else {
// Blending mode
for (unsigned int i = 0; i < w; i++, sp += channels, dp += channels) {
if (sp[3] == 255) {
// Fully opaque: copy source pixel directly
memcpy(dp, sp, channels);
}
else if (sp[3] != 0) {
// Alpha blending
if (dp[3] != 0) {
int u = sp[3] * 255;
int v = (255 - sp[3]) * dp[3];
int al = u + v;
dp[0] = (sp[0] * u + dp[0] * v) / al; // Red
dp[1] = (sp[1] * u + dp[1] * v) / al; // Green
dp[2] = (sp[2] * u + dp[2] * v) / al; // Blue
dp[3] = al / 255; // Alpha
}
else {
// If destination alpha is 0, copy source pixel
memcpy(dp, sp, channels);
}
}
}
}
}
});
}
size_t PngDecoder::read_from_io(void* _Buffer, size_t _ElementSize, size_t _ElementCount)
@ -742,7 +781,6 @@ bool PngDecoder::processing_finish()
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);
}
@ -1352,21 +1390,6 @@ void PngEncoder::deflateRectFin(unsigned char* zbuf, uint32_t* zsize, int bpp, i
deflateEnd(&fin_zstream);
}
bool PngEncoder::writemulti(const std::vector<Mat>& img_vec, const std::vector<int>& 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<int>& params)
{
int compression_level = 6;
@ -1433,6 +1456,8 @@ bool PngEncoder::writeanimation(const Animation& animation, const std::vector<in
if (animation.frames[i].channels() == 3)
cvtColor(animation.frames[i], tmpframes[i], COLOR_BGR2RGB);
if (tmpframes[i].depth() != CV_8U)
tmpframes[i].convertTo(tmpframes[i], CV_8U, 1.0 / 255);
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))

View File

@ -136,7 +136,7 @@ protected:
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(std::vector<png_bytep>& rows_dst, const std::vector<png_bytep>& rows_src, unsigned char bop, uint32_t x, uint32_t y, uint32_t w, uint32_t h, int channels);
void compose_frame(std::vector<png_bytep>& rows_dst, const std::vector<png_bytep>& rows_src, unsigned char bop, uint32_t x, uint32_t y, uint32_t w, uint32_t h, Mat& img);
size_t read_from_io(void* _Buffer, size_t _ElementSize, size_t _ElementCount);
uint32_t read_chunk(Chunk& chunk);
@ -176,7 +176,6 @@ public:
bool isFormatSupported( int depth ) const CV_OVERRIDE;
bool write( const Mat& img, const std::vector<int>& params ) CV_OVERRIDE;
bool writemulti(const std::vector<Mat>& img_vec, const std::vector<int>& params) CV_OVERRIDE;
bool writeanimation(const Animation& animinfo, const std::vector<int>& params) CV_OVERRIDE;
ImageEncoder newEncoder() const CV_OVERRIDE;

View File

@ -392,20 +392,6 @@ bool WebPEncoder::write(const Mat& img, const std::vector<int>& params)
return (size > 0) && (bytes_written == size);
}
bool WebPEncoder::writemulti(const std::vector<Mat>& img_vec, const std::vector<int>& params)
{
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 WebPEncoder::writeanimation(const Animation& animation, const std::vector<int>& params)
{
CV_CheckDepthEQ(animation.frames[0].depth(), CV_8U, "WebP codec supports only 8-bit unsigned images");

View File

@ -90,7 +90,6 @@ public:
~WebPEncoder() CV_OVERRIDE;
bool write(const Mat& img, const std::vector<int>& params) CV_OVERRIDE;
bool writemulti(const std::vector<Mat>& img_vec, const std::vector<int>& params) CV_OVERRIDE;
bool writeanimation(const Animation& animation, const std::vector<int>& params) CV_OVERRIDE;
ImageEncoder newEncoder() const CV_OVERRIDE;

View File

@ -99,6 +99,61 @@ static bool fillFrames(Animation& animation, bool hasAlpha, int n = 14)
return true;
}
#ifdef HAVE_IMGCODEC_GIF
TEST(Imgcodecs_Gif, imwriteanimation_rgba)
{
Animation s_animation, l_animation;
EXPECT_TRUE(fillFrames(s_animation, true));
s_animation.bgcolor = Scalar(0, 0, 0, 0); // TO DO not implemented yet.
// Create a temporary output filename for saving the animation.
string output = cv::tempfile(".gif");
// 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));
size_t expected_frame_count = s_animation.frames.size();
// 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(cvRound(s_animation.durations[i] / 10), cvRound(l_animation.durations[i] / 10));
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[16], NORM_INF));
EXPECT_EQ(0, cvtest::norm(l_animation.frames[6], l_animation.frames[17], NORM_INF));
EXPECT_EQ(0, cvtest::norm(l_animation.frames[7], l_animation.frames[18], 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<uchar> buf;
readFileBytes(output, buf);
vector<Mat> 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()));
}
#endif // HAVE_IMGCODEC_GIF
#ifdef HAVE_WEBP
TEST(Imgcodecs_WebP, imwriteanimation_rgba)
@ -305,6 +360,51 @@ TEST(Imgcodecs_APNG, imwriteanimation_rgba)
EXPECT_EQ(0, remove(output.c_str()));
}
TEST(Imgcodecs_APNG, imwriteanimation_rgba16u)
{
Animation s_animation, l_animation;
EXPECT_TRUE(fillFrames(s_animation, true));
for (size_t i = 0; i < s_animation.frames.size(); i++)
{
s_animation.frames[i].convertTo(s_animation.frames[i], CV_16U, 255);
}
// 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());
std::vector<uchar> buf;
readFileBytes(output, buf);
vector<Mat> 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;