diff --git a/modules/imgcodecs/include/opencv2/imgcodecs.hpp b/modules/imgcodecs/include/opencv2/imgcodecs.hpp index 160d9744ae..458e68483e 100644 --- a/modules/imgcodecs/include/opencv2/imgcodecs.hpp +++ b/modules/imgcodecs/include/opencv2/imgcodecs.hpp @@ -96,6 +96,7 @@ enum ImwriteFlags { IMWRITE_PNG_COMPRESSION = 16, //!< For PNG, it can be the compression level from 0 to 9. A higher value means a smaller size and longer compression time. If specified, strategy is changed to IMWRITE_PNG_STRATEGY_DEFAULT (Z_DEFAULT_STRATEGY). Default value is 1 (best speed setting). IMWRITE_PNG_STRATEGY = 17, //!< One of cv::ImwritePNGFlags, default is IMWRITE_PNG_STRATEGY_RLE. IMWRITE_PNG_BILEVEL = 18, //!< Binary level PNG, 0 or 1, default is 0. + IMWRITE_PNG_FILTER = 19, //!< One of cv::ImwritePNGFilterFlags, default is IMWRITE_PNG_FILTER_SUB. IMWRITE_PXM_BINARY = 32, //!< For PPM, PGM, or PBM, it can be a binary format flag, 0 or 1. Default value is 1. IMWRITE_EXR_TYPE = (3 << 4) + 0 /* 48 */, //!< override EXR storage type (FLOAT (FP32) is default) IMWRITE_EXR_COMPRESSION = (3 << 4) + 1 /* 49 */, //!< override EXR compression type (ZIP_COMPRESSION = 3 is default) @@ -211,6 +212,17 @@ enum ImwritePNGFlags { IMWRITE_PNG_STRATEGY_FIXED = 4 //!< Using this value prevents the use of dynamic Huffman codes, allowing for a simpler decoder for special applications. }; +//! Imwrite PNG specific values for IMWRITE_PNG_FILTER parameter key +enum ImwritePNGFilterFlags { + IMWRITE_PNG_FILTER_NONE = 8, //!< Applies no filter to the PNG image (useful when you want to save the raw pixel data without any compression filter). + IMWRITE_PNG_FILTER_SUB = 16, //!< Applies the "sub" filter, which calculates the difference between the current byte and the previous byte in the row. + IMWRITE_PNG_FILTER_UP = 32, //!< applies the "up" filter, which calculates the difference between the current byte and the corresponding byte directly above it. + IMWRITE_PNG_FILTER_AVG = 64, //!< applies the "average" filter, which calculates the average of the byte to the left and the byte above. + IMWRITE_PNG_FILTER_PAETH = 128, //!< applies the "Paeth" filter, a more complex filter that predicts the next pixel value based on neighboring pixels. + IMWRITE_PNG_FAST_FILTERS = (IMWRITE_PNG_FILTER_NONE | IMWRITE_PNG_FILTER_SUB | IMWRITE_PNG_FILTER_UP), //!< This is a combination of IMWRITE_PNG_FILTER_NONE, IMWRITE_PNG_FILTER_SUB, and IMWRITE_PNG_FILTER_UP, typically used for faster compression. + IMWRITE_PNG_ALL_FILTERS = (IMWRITE_PNG_FAST_FILTERS | IMWRITE_PNG_FILTER_AVG | IMWRITE_PNG_FILTER_PAETH) //!< This combines all available filters (NONE, SUB, UP, AVG, and PAETH), which will attempt to apply all of them for the best possible compression. + }; + //! Imwrite PAM specific tupletype flags used to define the 'TUPLETYPE' field of a PAM file. enum ImwritePAMFlags { IMWRITE_PAM_FORMAT_NULL = 0, diff --git a/modules/imgcodecs/perf/perf_decode_encode.cpp b/modules/imgcodecs/perf/perf_decode_encode.cpp index ce693cb878..5f675f7a44 100644 --- a/modules/imgcodecs/perf/perf_decode_encode.cpp +++ b/modules/imgcodecs/perf/perf_decode_encode.cpp @@ -101,7 +101,7 @@ PERF_TEST_P(Encode, bgr, testing::ValuesIn(exts)) TEST_CYCLE() imencode(GetParam(), src, buf); - std::cout << "Encoded buffer size: " << buf.size() + std::cout << " Encoded buffer size: " << buf.size() << " bytes, Compression ratio: " << std::fixed << std::setprecision(2) << (static_cast(buf.size()) / (src.total() * src.channels())) * 100.0 << "%" << std::endl; @@ -120,7 +120,7 @@ PERF_TEST_P(Encode, multi, testing::ValuesIn(exts_multi)) TEST_CYCLE() imencode(GetParam(), vec, buf); - std::cout << "Encoded buffer size: " << buf.size() + std::cout << " Encoded buffer size: " << buf.size() << " bytes, Compression ratio: " << std::fixed << std::setprecision(2) << (static_cast(buf.size()) / (vec[0].total() * vec[0].channels())) * 100.0 << "%" << std::endl; diff --git a/modules/imgcodecs/perf/perf_png.cpp b/modules/imgcodecs/perf/perf_png.cpp index 5bbef590bd..65e4c3fcca 100644 --- a/modules/imgcodecs/perf/perf_png.cpp +++ b/modules/imgcodecs/perf/perf_png.cpp @@ -11,7 +11,10 @@ namespace opencv_test using namespace perf; -typedef perf::TestBaseWithParam PNG; +CV_ENUM(PNGStrategy, IMWRITE_PNG_STRATEGY_DEFAULT, IMWRITE_PNG_STRATEGY_FILTERED, IMWRITE_PNG_STRATEGY_HUFFMAN_ONLY, IMWRITE_PNG_STRATEGY_RLE, IMWRITE_PNG_STRATEGY_FIXED); +CV_ENUM(PNGFilters, IMWRITE_PNG_FILTER_NONE, IMWRITE_PNG_FILTER_SUB, IMWRITE_PNG_FILTER_UP, IMWRITE_PNG_FILTER_AVG, IMWRITE_PNG_FILTER_PAETH, IMWRITE_PNG_FAST_FILTERS, IMWRITE_PNG_ALL_FILTERS); + +typedef perf::TestBaseWithParam> PNG; PERF_TEST(PNG, decode) { @@ -58,6 +61,30 @@ PERF_TEST(PNG, encode) SANITY_CHECK_NOTHING(); } +PERF_TEST_P(PNG, params, + testing::Combine( + testing::Values(IMWRITE_PNG_STRATEGY_DEFAULT, IMWRITE_PNG_STRATEGY_FILTERED, IMWRITE_PNG_STRATEGY_HUFFMAN_ONLY, IMWRITE_PNG_STRATEGY_RLE, IMWRITE_PNG_STRATEGY_FIXED), + testing::Values(IMWRITE_PNG_FILTER_NONE, IMWRITE_PNG_FILTER_SUB, IMWRITE_PNG_FILTER_UP, IMWRITE_PNG_FILTER_AVG, IMWRITE_PNG_FILTER_PAETH, IMWRITE_PNG_FAST_FILTERS, IMWRITE_PNG_ALL_FILTERS), + testing::Values(1, 6))) +{ + String filename = getDataPath("perf/1920x1080.png"); + const int strategy = get<0>(GetParam()); + const int filter = get<1>(GetParam()); + const int level = get<2>(GetParam()); + + Mat src = imread(filename); + EXPECT_FALSE(src.empty()) << "Cannot open test image perf/1920x1080.png"; + vector buf; + + TEST_CYCLE() imencode(".png", src, buf, { IMWRITE_PNG_COMPRESSION, level, IMWRITE_PNG_STRATEGY, strategy, IMWRITE_PNG_FILTER, filter }); + + std::cout << " Encoded buffer size: " << buf.size() + << " bytes, Compression ratio: " << std::fixed << std::setprecision(2) + << (static_cast(buf.size()) / (src.total() * src.channels())) * 100.0 << "%" << std::endl; + + SANITY_CHECK_NOTHING(); +} + #endif // HAVE_PNG } // namespace diff --git a/modules/imgcodecs/src/grfmt_png.cpp b/modules/imgcodecs/src/grfmt_png.cpp index 84df975471..5eb4a379ae 100644 --- a/modules/imgcodecs/src/grfmt_png.cpp +++ b/modules/imgcodecs/src/grfmt_png.cpp @@ -814,6 +814,10 @@ PngEncoder::PngEncoder() next_seq_num = 0; trnssize = 0; palsize = 0; + m_compression_level = Z_BEST_SPEED; + m_compression_strategy = IMWRITE_PNG_STRATEGY_RLE; // Default strategy + m_filter = IMWRITE_PNG_FILTER_SUB; // Default filter + m_isBilevel = false; memset(palette, 0, sizeof(palette)); memset(trns, 0, sizeof(trns)); memset(op, 0, sizeof(op)); @@ -870,6 +874,9 @@ bool PngEncoder::write( const Mat& img, const std::vector& params ) { if( setjmp( png_jmpbuf ( png_ptr ) ) == 0 ) { + bool set_compression_level = false; + bool set_filter = false; + if( m_buf ) { png_set_write_fn(png_ptr, this, @@ -882,45 +889,39 @@ bool PngEncoder::write( const Mat& img, const std::vector& params ) png_init_io( png_ptr, (png_FILE_p)f ); } - int compression_level = -1; // Invalid value to allow setting 0-9 as valid - 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); + m_compression_strategy = IMWRITE_PNG_STRATEGY_DEFAULT; // Default strategy + m_compression_level = params[i+1]; + m_compression_level = MIN(MAX(m_compression_level, 0), Z_BEST_COMPRESSION); + set_compression_level = true; } if( params[i] == IMWRITE_PNG_STRATEGY ) { - compression_strategy = params[i+1]; - compression_strategy = MIN(MAX(compression_strategy, 0), Z_FIXED); + m_compression_strategy = params[i+1]; + m_compression_strategy = MIN(MAX(m_compression_strategy, 0), Z_FIXED); } if( params[i] == IMWRITE_PNG_BILEVEL ) { - isBilevel = params[i+1] != 0; + m_isBilevel = params[i+1] != 0; + } + if( params[i] == IMWRITE_PNG_FILTER ) + { + m_filter = params[i+1]; + set_filter = true; } } if( m_buf || f ) { - if( compression_level >= 0 ) - { - png_set_compression_level( png_ptr, compression_level ); - } - else - { - // tune parameters for speed - // (see http://wiki.linuxquestions.org/wiki/Libpng) - png_set_filter(png_ptr, PNG_FILTER_TYPE_BASE, PNG_FILTER_SUB); - png_set_compression_level(png_ptr, Z_BEST_SPEED); - } - png_set_compression_strategy(png_ptr, compression_strategy); + if (!set_compression_level || set_filter) + png_set_filter(png_ptr, PNG_FILTER_TYPE_BASE, m_filter); + png_set_compression_level(png_ptr, m_compression_level); + png_set_compression_strategy(png_ptr, m_compression_strategy); - png_set_IHDR( png_ptr, info_ptr, width, height, depth == CV_8U ? isBilevel?1:8 : 16, + png_set_IHDR( png_ptr, info_ptr, width, height, depth == CV_8U ? m_isBilevel ? 1 : 8 : 16, channels == 1 ? PNG_COLOR_TYPE_GRAY : channels == 3 ? PNG_COLOR_TYPE_RGB : PNG_COLOR_TYPE_RGBA, PNG_INTERLACE_NONE, PNG_COMPRESSION_TYPE_DEFAULT, @@ -928,7 +929,7 @@ bool PngEncoder::write( const Mat& img, const std::vector& params ) png_write_info( png_ptr, info_ptr ); - if (isBilevel) + if (m_isBilevel) png_set_packing(png_ptr); png_set_bgr( png_ptr ); @@ -1399,7 +1400,7 @@ void PngEncoder::deflateRectFin(unsigned char* zbuf, uint32_t* zsize, int bpp, i 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); + deflateInit2(&fin_zstream, m_compression_level, 8, 15, 8, op[n].filters ? Z_FILTERED : m_compression_strategy); fin_zstream.next_out = zbuf; fin_zstream.avail_out = zbuf_size; @@ -1415,30 +1416,27 @@ bool PngEncoder::writeanimation(const Animation& animation, const std::vector> 3) + ((idat_size + 63) >> 6) + 11; diff --git a/modules/imgcodecs/src/grfmt_png.hpp b/modules/imgcodecs/src/grfmt_png.hpp index 6e1a06473d..3a94dd57a3 100644 --- a/modules/imgcodecs/src/grfmt_png.hpp +++ b/modules/imgcodecs/src/grfmt_png.hpp @@ -232,6 +232,10 @@ private: unsigned char trns[256]; uint32_t palsize, trnssize; uint32_t next_seq_num; + int m_compression_level; + int m_compression_strategy; + int m_filter; + bool m_isBilevel; }; } diff --git a/modules/imgcodecs/src/grfmt_spng.cpp b/modules/imgcodecs/src/grfmt_spng.cpp index 59b2decc6e..9804c7a8ae 100644 --- a/modules/imgcodecs/src/grfmt_spng.cpp +++ b/modules/imgcodecs/src/grfmt_spng.cpp @@ -535,9 +535,12 @@ bool SPngEncoder::write(const Mat &img, const std::vector ¶ms) struct spng_ihdr ihdr = {}; ihdr.height = height; ihdr.width = width; - int compression_level = -1; // Invalid value to allow setting 0-9 as valid + int compression_level = Z_BEST_SPEED; int compression_strategy = IMWRITE_PNG_STRATEGY_RLE; // Default strategy + int filter = IMWRITE_PNG_FILTER_SUB; // Default filter bool isBilevel = false; + bool set_compression_level = false; + bool set_filter = false; for (size_t i = 0; i < params.size(); i += 2) { @@ -546,6 +549,7 @@ bool SPngEncoder::write(const Mat &img, const std::vector ¶ms) compression_strategy = IMWRITE_PNG_STRATEGY_DEFAULT; // Default strategy compression_level = params[i + 1]; compression_level = MIN(MAX(compression_level, 0), Z_BEST_COMPRESSION); + set_compression_level = true; } if (params[i] == IMWRITE_PNG_STRATEGY) { @@ -556,6 +560,11 @@ bool SPngEncoder::write(const Mat &img, const std::vector ¶ms) { isBilevel = params[i + 1] != 0; } + if( params[i] == IMWRITE_PNG_FILTER ) + { + filter = params[i+1]; + set_filter = true; + } } ihdr.bit_depth = depth == CV_8U ? isBilevel ? 1 : 8 : 16; @@ -579,15 +588,9 @@ bool SPngEncoder::write(const Mat &img, const std::vector ¶ms) if (m_buf || f) { - if (compression_level >= 0) - { - spng_set_option(ctx, SPNG_IMG_COMPRESSION_LEVEL, compression_level); - } - else - { - spng_set_option(ctx, SPNG_FILTER_CHOICE, SPNG_FILTER_CHOICE_SUB); - spng_set_option(ctx, SPNG_IMG_COMPRESSION_LEVEL, Z_BEST_SPEED); - } + if (!set_compression_level || set_filter) + spng_set_option(ctx, SPNG_FILTER_CHOICE, filter); + spng_set_option(ctx, SPNG_IMG_COMPRESSION_LEVEL, compression_level); spng_set_option(ctx, SPNG_IMG_COMPRESSION_STRATEGY, compression_strategy); int ret; diff --git a/modules/imgcodecs/test/test_animation.cpp b/modules/imgcodecs/test/test_animation.cpp index df0a00a8b1..4e3f95013c 100644 --- a/modules/imgcodecs/test/test_animation.cpp +++ b/modules/imgcodecs/test/test_animation.cpp @@ -3,31 +3,10 @@ // of this distribution and at http://opencv.org/license.html. #include "test_precomp.hpp" +#include "test_common.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. diff --git a/modules/imgcodecs/test/test_common.cpp b/modules/imgcodecs/test/test_common.cpp index a0addafa80..73ca694d11 100644 --- a/modules/imgcodecs/test/test_common.cpp +++ b/modules/imgcodecs/test/test_common.cpp @@ -48,4 +48,21 @@ Mat generateTestImageGrayscale() return image; } +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); + fclose(wfile); + + EXPECT_EQ(data_size, wfile_size); + } +} + } // namespace diff --git a/modules/imgcodecs/test/test_common.hpp b/modules/imgcodecs/test/test_common.hpp index ec9a5d87fa..31387110ca 100644 --- a/modules/imgcodecs/test/test_common.hpp +++ b/modules/imgcodecs/test/test_common.hpp @@ -9,6 +9,7 @@ namespace opencv_test { Mat generateTestImageBGR(); Mat generateTestImageGrayscale(); +void readFileBytes(const std::string& fname, std::vector& buf); } // namespace diff --git a/modules/imgcodecs/test/test_png.cpp b/modules/imgcodecs/test/test_png.cpp index 2eb8a82a96..95b0bc0793 100644 --- a/modules/imgcodecs/test/test_png.cpp +++ b/modules/imgcodecs/test/test_png.cpp @@ -2,6 +2,7 @@ // 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" +#include "test_common.hpp" namespace opencv_test { namespace { @@ -327,6 +328,47 @@ const string pngsuite_files_corrupted[] = { INSTANTIATE_TEST_CASE_P(/*nothing*/, Imgcodecs_Png_PngSuite_Corrupted, testing::ValuesIn(pngsuite_files_corrupted)); +CV_ENUM(PNGStrategy, IMWRITE_PNG_STRATEGY_DEFAULT, IMWRITE_PNG_STRATEGY_FILTERED, IMWRITE_PNG_STRATEGY_HUFFMAN_ONLY, IMWRITE_PNG_STRATEGY_RLE, IMWRITE_PNG_STRATEGY_FIXED); +CV_ENUM(PNGFilters, IMWRITE_PNG_FILTER_NONE, IMWRITE_PNG_FILTER_SUB, IMWRITE_PNG_FILTER_UP, IMWRITE_PNG_FILTER_AVG, IMWRITE_PNG_FILTER_PAETH, IMWRITE_PNG_FAST_FILTERS, IMWRITE_PNG_ALL_FILTERS); + +typedef testing::TestWithParam> Imgcodecs_Png_Encode; + +TEST_P(Imgcodecs_Png_Encode, params) +{ + const string root = cvtest::TS::ptr()->get_data_path(); + const string filename = root + "pngsuite/" + get<0>(GetParam()); + + const int strategy = get<1>(GetParam()); + const int filter = get<2>(GetParam()); + const int compression_level = get<3>(GetParam()); + + std::vector file_buf; + readFileBytes(filename, file_buf); + Mat src = imdecode(file_buf, IMREAD_UNCHANGED); + EXPECT_FALSE(src.empty()) << "Cannot decode test image " << filename; + + vector buf; + imencode(".png", src, buf, { IMWRITE_PNG_COMPRESSION, compression_level, IMWRITE_PNG_STRATEGY, strategy, IMWRITE_PNG_FILTER, filter }); + EXPECT_EQ(buf.size(), file_buf.size()); +} + +INSTANTIATE_TEST_CASE_P(/**/, + Imgcodecs_Png_Encode, + testing::Values( + make_tuple("f00n0g08.png", IMWRITE_PNG_STRATEGY_DEFAULT, IMWRITE_PNG_FILTER_NONE, 6), + make_tuple("f00n2c08.png", IMWRITE_PNG_STRATEGY_DEFAULT, IMWRITE_PNG_FILTER_NONE, 6), + make_tuple("f01n0g08.png", IMWRITE_PNG_STRATEGY_FILTERED, IMWRITE_PNG_FILTER_SUB, 6), + make_tuple("f01n2c08.png", IMWRITE_PNG_STRATEGY_FILTERED, IMWRITE_PNG_FILTER_SUB, 6), + make_tuple("f02n0g08.png", IMWRITE_PNG_STRATEGY_FILTERED, IMWRITE_PNG_FILTER_UP, 6), + make_tuple("f02n2c08.png", IMWRITE_PNG_STRATEGY_FILTERED, IMWRITE_PNG_FILTER_UP, 6), + make_tuple("f03n0g08.png", IMWRITE_PNG_STRATEGY_FILTERED, IMWRITE_PNG_FILTER_AVG, 6), + make_tuple("f03n2c08.png", IMWRITE_PNG_STRATEGY_FILTERED, IMWRITE_PNG_FILTER_AVG, 6), + make_tuple("f04n0g08.png", IMWRITE_PNG_STRATEGY_FILTERED, IMWRITE_PNG_FILTER_PAETH, 6), + make_tuple("f04n2c08.png", IMWRITE_PNG_STRATEGY_FILTERED, IMWRITE_PNG_FILTER_PAETH, 6), + make_tuple("z03n2c08.png", IMWRITE_PNG_STRATEGY_FILTERED, IMWRITE_PNG_ALL_FILTERS, 3), + make_tuple("z06n2c08.png", IMWRITE_PNG_STRATEGY_FILTERED, IMWRITE_PNG_ALL_FILTERS, 6), + make_tuple("z09n2c08.png", IMWRITE_PNG_STRATEGY_FILTERED, IMWRITE_PNG_ALL_FILTERS, 9))); + typedef testing::TestWithParam> Imgcodecs_Png_ImwriteFlags; TEST_P(Imgcodecs_Png_ImwriteFlags, compression_level)