From 802cf95aa3d42816995695bf68a86b3532f12760 Mon Sep 17 00:00:00 2001 From: Pierre Chatelier Date: Sat, 10 Oct 2026 11:31:47 +0200 Subject: [PATCH] Merge pull request #30165 from chacha21:tiff_compression_level Adding IMWRITE_TIFF_COMPRESSION_LEVEL - #30165 For some compression schemes (DEFLATE, ZSTD), libtiff supports a TIFFTAG_ZIPQUALITY. This PR adds an IMWRITE_TIFF_COMPRESSION_LEVEL param to set it. - [x] I agree to contribute to the project under Apache 2 License. - [x] To the best of my knowledge, the proposed patch is not based on a code under GPL or another license that is incompatible with OpenCV - [X] The PR is proposed to the proper branch - [ ] There is a reference to the original bug report and related work - [ ] There is accuracy test, performance test and test data in opencv_extra repository, if applicable Patch to opencv_extra has the same branch name. - [ ] The feature is well documented and sample code can be built with the project CMake I don't know what kind of automatic tests could be relevant. I just checked that 1) the param is honoured 2) for some basic mats, level 1 gives lower compression than level 9 3) level -1 (default) gives a compression ratio between levels 1 and 9 (actually seems to be 6, as expected) But I am not sure that 2) and 3) are generally guaranteed by the compression scheme for any data, so is it relevant to make a test assert on that ? --- .../imgcodecs/include/opencv2/imgcodecs.hpp | 1 + modules/imgcodecs/src/grfmt_tiff.cpp | 77 ++++++++++++++++++- modules/imgcodecs/test/test_tiff.cpp | 57 ++++++++++++++ 3 files changed, 134 insertions(+), 1 deletion(-) diff --git a/modules/imgcodecs/include/opencv2/imgcodecs.hpp b/modules/imgcodecs/include/opencv2/imgcodecs.hpp index 53777ed9a3..0cbd5a8d6b 100644 --- a/modules/imgcodecs/include/opencv2/imgcodecs.hpp +++ b/modules/imgcodecs/include/opencv2/imgcodecs.hpp @@ -110,6 +110,7 @@ enum ImwriteFlags { IMWRITE_TIFF_XDPI = 257,//!< For TIFF, use to specify the X direction DPI IMWRITE_TIFF_YDPI = 258,//!< For TIFF, use to specify the Y direction DPI IMWRITE_TIFF_COMPRESSION = 259,//!< For TIFF, use to specify the image compression scheme. See cv::ImwriteTiffCompressionFlags. The compression scheme can be specified by this flag; the default is LZW compression, except for 32F depth where it is NONE + IMWRITE_TIFF_COMPRESSION_LEVEL = 260,//!< For TIFF, if IMWRITE_TIFF_COMPRESSION was used and supports it, sets a compression level. Valid range and meaning depends on the compression scheme. It is currently 1-9 for DEFLATE, 0-9 for LZMA, 1-22 for ZSTD, 1-100 for WEBP. -1 can be used as default. IMWRITE_TIFF_ROWSPERSTRIP = 278,//!< For TIFF, use to specify the number of rows per strip. IMWRITE_TIFF_PREDICTOR = 317,//!< For TIFF, use to specify predictor. See cv::ImwriteTiffPredictorFlags. Default is IMWRITE_TIFF_PREDICTOR_HORIZONTAL . IMWRITE_JPEG2000_COMPRESSION_X1000 = 272,//!< For JPEG2000, use to specify the target compression rate (multiplied by 1000). The value can be from 0 to 1000. Default is 1000. diff --git a/modules/imgcodecs/src/grfmt_tiff.cpp b/modules/imgcodecs/src/grfmt_tiff.cpp index de3bc0951e..5a63985a72 100644 --- a/modules/imgcodecs/src/grfmt_tiff.cpp +++ b/modules/imgcodecs/src/grfmt_tiff.cpp @@ -1336,7 +1336,7 @@ TiffEncoder::TiffEncoder() { m_description = "TIFF Files (*.tiff;*.tif)"; m_buf_supported = true; - m_supported_encode_key = {IMWRITE_TIFF_RESUNIT, IMWRITE_TIFF_XDPI, IMWRITE_TIFF_YDPI, IMWRITE_TIFF_COMPRESSION, IMWRITE_TIFF_ROWSPERSTRIP, IMWRITE_TIFF_PREDICTOR}; + m_supported_encode_key = {IMWRITE_TIFF_RESUNIT, IMWRITE_TIFF_XDPI, IMWRITE_TIFF_YDPI, IMWRITE_TIFF_COMPRESSION, IMWRITE_TIFF_ROWSPERSTRIP, IMWRITE_TIFF_PREDICTOR, IMWRITE_TIFF_COMPRESSION_LEVEL}; } TiffEncoder::~TiffEncoder() @@ -1477,6 +1477,8 @@ bool TiffEncoder::writeLibTiff( const std::vector& img_vec, const std::vect const int predictor_default_32F = IMWRITE_TIFF_PREDICTOR_FLOATINGPOINT; const int predictor_default = IMWRITE_TIFF_PREDICTOR_HORIZONTAL; int compression = -1; + int compressionLevel = -1; + std::pair compressionLevelValidRange = {-1, -1}; int predictor = -1; int resUnit = -1, dpiX = -1, dpiY = -1; @@ -1521,6 +1523,45 @@ bool TiffEncoder::writeLibTiff( const std::vector& img_vec, const std::vect break; } } + switch (compression) { + case IMWRITE_TIFF_COMPRESSION_DEFLATE: + case IMWRITE_TIFF_COMPRESSION_ADOBE_DEFLATE: + compressionLevelValidRange = std::make_pair(1, 9); + break; + case IMWRITE_TIFF_COMPRESSION_LZMA: + compressionLevelValidRange = std::make_pair(0, 9); + break; + case IMWRITE_TIFF_COMPRESSION_ZSTD: + compressionLevelValidRange = std::make_pair(1, 22); + break; + case IMWRITE_TIFF_COMPRESSION_WEBP: + compressionLevelValidRange = std::make_pair(1, 100); + break; + default: + break; + } + + if (readParam(params, IMWRITE_TIFF_COMPRESSION_LEVEL, compressionLevel)) + { + const bool isValidCompression = + (compression == IMWRITE_TIFF_COMPRESSION_DEFLATE) || + (compression == IMWRITE_TIFF_COMPRESSION_ADOBE_DEFLATE) || + (compression == IMWRITE_TIFF_COMPRESSION_LZMA) || + (compression == IMWRITE_TIFF_COMPRESSION_ZSTD) || + (compression == IMWRITE_TIFF_COMPRESSION_WEBP); + const bool isValidValue = (compressionLevel == -1) || + ((compressionLevel >= compressionLevelValidRange.first) && (compressionLevel <= compressionLevelValidRange.second)); + if (!isValidCompression) { + CV_LOG_WARNING(nullptr, cv::format("IMWRITE_TIFF_COMPRESSION_LEVEL can only be used for DEFLATE, LZMA, ZSTD or WEBP compression. It is ignored.")); + compressionLevel = -1; + } + else if (!isValidValue) { + CV_LOG_WARNING(nullptr, cv::format("The value(%d) for IMWRITE_TIFF_COMPRESSION_LEVEL=%d must be in the range %d-%d (or -1 for default). It is set to default.", + compressionLevel, compression, + compressionLevelValidRange.first, compressionLevelValidRange.second)); + compressionLevel = -1; + } + } if(readParam(params, IMWRITE_TIFF_PREDICTOR, predictor)) { switch(predictor) { @@ -1675,6 +1716,40 @@ bool TiffEncoder::writeLibTiff( const std::vector& img_vec, const std::vect CV_TIFF_CHECK_CALL(TIFFSetField(tif, TIFFTAG_BITSPERSAMPLE, bitsPerChannel)); CV_TIFF_CHECK_CALL(TIFFSetField(tif, TIFFTAG_COMPRESSION, page_compression)); + if (compressionLevel >= 0) + { + switch(page_compression) + { + case IMWRITE_TIFF_COMPRESSION_DEFLATE: + case IMWRITE_TIFF_COMPRESSION_ADOBE_DEFLATE: + CV_TIFF_CHECK_CALL(TIFFSetField(tif, TIFFTAG_ZIPQUALITY, compressionLevel)); + break; + case IMWRITE_TIFF_COMPRESSION_LZMA: + #ifdef TIFFTAG_LZMAPRESET // libtiff 4.0.0+ + CV_TIFF_CHECK_CALL(TIFFSetField(tif, TIFFTAG_LZMAPRESET, compressionLevel)); + #else + CV_LOG_WARNING(nullptr, cv::format("The LZMA compression(%d) is not supported in that build of the TIFF library. It is ignored.", page_compression)); + #endif + break; + case IMWRITE_TIFF_COMPRESSION_ZSTD: + #ifdef TIFFTAG_ZSTD_LEVEL // libtiff 4.0.10+ + CV_TIFF_CHECK_CALL(TIFFSetField(tif, TIFFTAG_ZSTD_LEVEL, compressionLevel)); + #else + CV_LOG_WARNING(nullptr, cv::format("The ZSTD compression(%d) is not supported in that build of the TIFF library. It is ignored.", page_compression)); + #endif + break; + case IMWRITE_TIFF_COMPRESSION_WEBP: + #ifdef TIFFTAG_WEBP_LEVEL // libtiff 4.0.10+ + CV_TIFF_CHECK_CALL(TIFFSetField(tif, TIFFTAG_WEBP_LEVEL, compressionLevel)); + #else + CV_LOG_WARNING(nullptr, cv::format("The WEBP compression(%d) is not supported in that build of the TIFF library. It is ignored.", page_compression)); + #endif + break; + default: + CV_LOG_WARNING(nullptr, cv::format("Unexpected page_compression %d", page_compression)); + break; + }//end switch(page_compression) + }//end if (compressionLevel >= 0) CV_TIFF_CHECK_CALL(TIFFSetField(tif, TIFFTAG_PHOTOMETRIC, colorspace)); CV_TIFF_CHECK_CALL(TIFFSetField(tif, TIFFTAG_SAMPLESPERPIXEL, channels)); CV_TIFF_CHECK_CALL(TIFFSetField(tif, TIFFTAG_PLANARCONFIG, PLANARCONFIG_CONTIG)); diff --git a/modules/imgcodecs/test/test_tiff.cpp b/modules/imgcodecs/test/test_tiff.cpp index 508d0f4841..66232cf6ff 100644 --- a/modules/imgcodecs/test/test_tiff.cpp +++ b/modules/imgcodecs/test/test_tiff.cpp @@ -1830,6 +1830,63 @@ TEST(Imgcodecs_Tiff, decode_planar_separate_multipage) EXPECT_PRED_FORMAT2(cvtest::MatComparator(0, 0), page1, pages[1]); } +// https://github.com/opencv/opencv/pull/30165 + +typedef tuple< int, int, int > Method_minLevel_maxLevel; +typedef testing::TestWithParam Imgcodecs_Tiff_compress_level; + +TEST_P(Imgcodecs_Tiff_compress_level, normal) +{ + // Get test parameters + const int compressMethod = get<0>(GetParam()); + const int minLevel = get<1>(GetParam()); + const int maxLevel = get<2>(GetParam()); + + //build a dummy image that can be heavily compressed + cv::Mat src = cv::Mat(cv::Size(640, 480), CV_8UC3); + src.forEach([&](cv::Vec3b& pixel, const int* pos) { + const int y = pos[0]; + const int x = pos[1]; + const uchar value = static_cast(y*src.cols+x); + pixel = cv::Vec3b(value, value, value); + }); + + std::vector bufNone; + std::vector paramsNone = { IMWRITE_TIFF_COMPRESSION, IMWRITE_TIFF_COMPRESSION_NONE}; + ASSERT_NO_THROW(ASSERT_TRUE(imencode(".tiff", src, bufNone, paramsNone))); + + std::vector bufDef; + std::vector paramsDef = {IMWRITE_TIFF_COMPRESSION, compressMethod}; + ASSERT_NO_THROW(ASSERT_TRUE(imencode(".tiff", src, bufDef, paramsDef))); + + std::vector bufMin; + std::vector paramsMin = {IMWRITE_TIFF_COMPRESSION, compressMethod, IMWRITE_TIFF_COMPRESSION_LEVEL, minLevel}; + ASSERT_NO_THROW(ASSERT_TRUE(imencode(".tiff", src, bufMin, paramsMin))); + + std::vector bufMax; + std::vector paramsMax = {IMWRITE_TIFF_COMPRESSION, compressMethod, IMWRITE_TIFF_COMPRESSION_LEVEL, maxLevel}; + ASSERT_NO_THROW(ASSERT_TRUE(imencode(".tiff", src, bufMax, paramsMax))); + + ASSERT_LT(bufDef.size(), bufNone.size()); + ASSERT_LE(bufDef.size(), bufMin.size()); + ASSERT_LE(bufMax.size(), bufDef.size()); + ASSERT_NE(bufMax.size(), bufMin.size()); +} + +const Method_minLevel_maxLevel supportedMethod_minLevel_maxLevel[] = +{ + make_tuple(IMWRITE_TIFF_COMPRESSION_DEFLATE, 1, 9), + make_tuple(IMWRITE_TIFF_COMPRESSION_ADOBE_DEFLATE, 1, 9), +// make_tuple(IMWRITE_TIFF_COMPRESSION_LZMA, 0, 9), +// make_tuple(IMWRITE_TIFF_COMPRESSION_ZSTD, 1, 22), +// make_tuple(IMWRITE_TIFF_COMPRESSION_WEBP, 1, 100), +}; + +INSTANTIATE_TEST_CASE_P(Imgcodecs_Tiff, Imgcodecs_Tiff_compress_level, + testing::ValuesIn(supportedMethod_minLevel_maxLevel) +); + + #endif }} // namespace