Files
Satya Mallick 8d126c4a67 Merge pull request #29872 from spmallick:perf/png-idat-header
imgcodecs: avoid copying PNG IDAT data during header parsing 🤖🤖🤖 - #29872

`PngDecoder::readHeader()` copies the first IDAT chunk into a temporary vector, discards it, then rewinds the input so libpng can read it again. A PNG stored in one large IDAT chunk therefore incurs an avoidable allocation and payload copy before decoding.

Skip materializing IDAT data during header discovery. For memory input, validate the remaining length and advance the cursor. For file input, seek to and read the final CRC byte to retain early rejection of truncated chunks. Keep buffered reads for small file chunks and when the seek offset cannot fit in `long`; libpng still validates IDAT CRC while decoding static PNGs. APNG frame decoding retains its existing chunk-reading path.

Validation against `52377dee533dbba170d6437a7cf6d42a749a3f8f`, with only this PNG patch applied:

- macOS 26.6.2 / Apple M5 Pro / Apple Clang 21: 579 image-codec cases, four default skips, zero failures; all four new perf smoke cases pass.
- Linux (Ubuntu 24.04.4) / Intel Core i7-6850K / GCC 13.3: the same 579 cases, four default skips, zero failures; all four new perf smoke cases pass.
- All 19 new regression cases also pass against the unchanged baseline libraries on both hosts.
- Matching Release settings within each host, bundled libpng 1.6.57 and zlib 1.3.2.

The revised, committed `PNGDecode` performance cases measure `imdecode(..., IMREAD_UNCHANGED)` on generated random, uncompressed RGB PNGs in memory. Seven alternating baseline/candidate pairs use 30 samples per case and one OpenCV thread; Linux runs are pinned to one CPU. The same revised performance executable is used with each library set, and actual loaded library paths are verified. These measurements are separate from the file-loading results below.

| Memory-decoding workload | Mac speedup | Linux speedup |
| --- | ---: | ---: |
| 512×512, ordinary chunks | 1.001× (0.999–1.003) | 0.999× (0.998–1.002) |
| 512×512, single IDAT | 1.024× (1.022–1.030) | 1.069× (1.067–1.070) |
| 3840×2160, ordinary chunks | 0.999× (0.995–1.011) | 1.001× (1.000–1.001) |
| 3840×2160, single IDAT | 1.022× (1.000–1.024) | 1.383× (1.356–1.419) |

Values are paired median speedups with descriptive bootstrap 95% intervals. Every process passed all four cases with exact pixel comparisons.

Separate C++ `imread(..., IMREAD_COLOR)` measurements use the earlier structured 3840×2160 RGB PNG fixtures, warm filesystem cache, one OpenCV thread, and seven alternating baseline/candidate pairs (minimum 0.15 seconds per batch). Linux runs are pinned to one CPU. Each host checks 100 image/mode cases; decoded dimensions, types, and pixel hashes match across every run and between hosts. The values below are paired median speedups, with bootstrap 95% intervals.

| Workload | Mac speedup | Linux speedup |
| --- | ---: | ---: |
| Uncompressed stream in one ~24.9 MB IDAT | 1.077× (1.046–1.110) | 1.171× (1.170–1.171) |
| Same uncompressed stream in ordinary IDAT chunks | 1.014× (0.945–1.025) | 1.000× (0.997–1.001) |
| Compressed stream in one IDAT | 1.014× (1.007–1.020) | 1.024× (1.021–1.025) |

The large-IDAT case has median latency 16.08→14.83 ms on the Mac and 37.23→31.90 ms on Linux. A separate 15-pair Mac check puts the ordinary chunked control at 1.002× (0.994–1.015). The gain depends on first-IDAT size and decoding cost; ordinary chunked PNGs are approximately neutral.

The new performance tests use the test framework’s `theRNG()` and time `imdecode(encoded, IMREAD_UNCHANGED)` entirely in memory. They generate identical uncompressed RGB images with ordinary 8 KiB IDAT chunks or a single full-image IDAT, at 512×512 and 3840×2160. Encoding and repacking happen outside the timed loop; the performance cases create no temporary files. Run the included cases with the following filters. For an A/B comparison, copy only the test/performance changes to the baseline tree:

```sh
opencv_test_imgcodecs --gtest_filter='*Png_ReadIDAT*'
opencv_perf_imgcodecs --gtest_filter='PNGDecode_idat_layout.idat_layout/*'
```

The regression tests exercise file and memory input, 8/16-bit grayscale/RGB/RGBA, single/multiple/empty-first IDAT layouts, incomplete headers/payloads/CRCs, and corrupt CRCs. They check `imread`, `imdecode`, and `imcount`. All fixtures are generated; no `opencv_extra` patch is needed. Windows was not tested locally.

### Pull Request Readiness Checklist

- [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 code under GPL or another license incompatible with OpenCV.
- [x] The PR is proposed to the proper branch (`5.x`, optimization).
- [x] Accuracy and performance tests are included; test data is generated.
- Original bug report: none; self-contained performance improvement.
- Documentation/sample changes: not applicable; no public API change.
2026-09-19 14:33:43 +03:00

746 lines
25 KiB
C++

// 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"
#include "test_common.hpp"
#include <cstdio>
#include <cstring>
namespace opencv_test { namespace {
#if defined(HAVE_PNG) || defined(HAVE_SPNG)
// See https://github.com/opencv/opencv/pull/28615
// Precision differences in 16-bit grayscale conversion between old and modern libpng versions
#define OPENCV_IMGCODECS_PNG_EPS_DEFAULT (4)
#ifndef OPENCV_IMGCODECS_PNG_EPS_16BIT_GRAY
#define OPENCV_IMGCODECS_PNG_EPS_16BIT_GRAY (OPENCV_IMGCODECS_PNG_EPS_DEFAULT)
#endif
TEST(Imgcodecs_Png, write_big)
{
const string root = cvtest::TS::ptr()->get_data_path();
const string filename = root + "readwrite/read.png";
Mat img;
ASSERT_NO_THROW(img = imread(filename));
ASSERT_FALSE(img.empty());
EXPECT_EQ(13043, img.cols);
EXPECT_EQ(13917, img.rows);
vector<uchar> buff;
bool status = false;
ASSERT_NO_THROW(status = imencode(".png", img, buff, { IMWRITE_PNG_ZLIBBUFFER_SIZE, 1024*1024 }));
ASSERT_TRUE(status);
#ifdef HAVE_PNG
EXPECT_EQ((size_t)816219, buff.size());
#else
EXPECT_EQ((size_t)817407, buff.size());
#endif
}
TEST(Imgcodecs_Png, encode)
{
vector<uchar> buff;
Mat img_gt = Mat::zeros(1000, 1000, CV_8U);
vector<int> param;
param.push_back(IMWRITE_PNG_COMPRESSION);
param.push_back(3); //default(3) 0-9.
bool status = false;
EXPECT_NO_THROW(status = imencode(".png", img_gt, buff, param));
ASSERT_TRUE(status);
Mat img;
EXPECT_NO_THROW(img = imdecode(buff, IMREAD_ANYDEPTH)); // hang
EXPECT_FALSE(img.empty());
EXPECT_PRED_FORMAT2(cvtest::MatComparator(0, 0), img, img_gt);
}
#ifdef HAVE_PNG
static size_t pngChunkLength(const vector<uchar>& buffer, size_t offset)
{
return (static_cast<size_t>(buffer[offset]) << 24) |
(static_cast<size_t>(buffer[offset + 1]) << 16) |
(static_cast<size_t>(buffer[offset + 2]) << 8) |
static_cast<size_t>(buffer[offset + 3]);
}
static vector<size_t> pngIDATOffsets(const vector<uchar>& buffer)
{
vector<size_t> offsets;
for (size_t offset = 8; offset + 12 <= buffer.size();)
{
const size_t length = pngChunkLength(buffer, offset);
CV_Assert(length <= buffer.size() - offset - 12);
if (std::memcmp(&buffer[offset + 4], "IDAT", 4) == 0)
offsets.push_back(offset);
offset += length + 12;
}
return offsets;
}
typedef testing::TestWithParam<testing::tuple<int, int>> Imgcodecs_Png_ReadIDAT;
TEST_P(Imgcodecs_Png_ReadIDAT, decode)
{
const int layout = get<1>(GetParam()); // Single, multiple, or empty first IDAT.
Mat source(256, 256, get<0>(GetParam()));
theRNG().fill(source, RNG::UNIFORM, 0, source.depth() == CV_8U ? 256 : 65536);
vector<uchar> buffer;
ASSERT_TRUE(imencode(".png", source, buffer,
{ IMWRITE_PNG_COMPRESSION, 0, IMWRITE_PNG_ZLIBBUFFER_SIZE,
layout == 1 ? 8192 : 1024 * 1024 }));
const vector<size_t> offsets = pngIDATOffsets(buffer);
ASSERT_FALSE(offsets.empty());
if (layout == 1)
ASSERT_GT(offsets.size(), static_cast<size_t>(1));
else
{
ASSERT_EQ(static_cast<size_t>(1), offsets.size());
ASSERT_GT(pngChunkLength(buffer, offsets[0]), static_cast<size_t>(8192));
}
if (layout == 2)
{
// A zero-length IDAT is valid before the first compressed byte.
const uchar emptyIDAT[] = { 0, 0, 0, 0, 'I', 'D', 'A', 'T', 0x35, 0xaf, 0x06, 0x1e };
buffer.insert(buffer.begin() + offsets[0], emptyIDAT, emptyIDAT + sizeof(emptyIDAT));
}
Mat decoded;
ASSERT_NO_THROW(decoded = imdecode(buffer, IMREAD_UNCHANGED));
EXPECT_PRED_FORMAT2(cvtest::MatComparator(0, 0), decoded, source);
}
TEST_P(Imgcodecs_Png_ReadIDAT, damaged_first_IDAT)
{
const Mat source(256, 256, CV_8UC3, Scalar(17, 81, 203));
Mat decoded;
vector<uchar> buffer;
ASSERT_TRUE(imencode(".png", source, buffer,
{ IMWRITE_PNG_COMPRESSION, 0, IMWRITE_PNG_ZLIBBUFFER_SIZE, 1024 * 1024 }));
const vector<size_t> offsets = pngIDATOffsets(buffer);
ASSERT_EQ(static_cast<size_t>(1), offsets.size());
const size_t offset = offsets[0];
const size_t length = pngChunkLength(buffer, offset);
ASSERT_NO_THROW(decoded = imdecode(buffer, IMREAD_UNCHANGED));
EXPECT_PRED_FORMAT2(cvtest::MatComparator(0, 0), decoded, source);
// Incomplete header, absent/partial payload, and absent/partial CRC.
const size_t truncatedSizes[] = { offset + 4, offset + 7, offset + 8,
offset + 8 + length / 2, offset + 8 + length, offset + 8 + length + 3 };
for (size_t i = 0; i < sizeof(truncatedSizes) / sizeof(truncatedSizes[0]); ++i)
{
SCOPED_TRACE(format("truncated size: %zu", truncatedSizes[i]));
const vector<uchar> truncated(buffer.begin(), buffer.begin() + truncatedSizes[i]);
ASSERT_NO_THROW(decoded = imdecode(truncated, IMREAD_UNCHANGED));
EXPECT_TRUE(decoded.empty());
}
// Header-only imcount does not validate IDAT CRC, but decoding must do so.
buffer[offset + 8 + length] ^= 1;
ASSERT_NO_THROW(decoded = imdecode(buffer, IMREAD_UNCHANGED));
EXPECT_TRUE(decoded.empty());
}
INSTANTIATE_TEST_CASE_P(/*nothing*/, Imgcodecs_Png_ReadIDAT,
testing::Combine(testing::Values(CV_8UC1, CV_8UC3, CV_16UC1),
testing::Values(0, 1, 2)));
#endif
TEST(Imgcodecs_Png, regression_ImreadVSCvtColor)
{
const string root = cvtest::TS::ptr()->get_data_path();
const string imgName = root + "../cv/shared/lena.png";
Mat original_image = imread(imgName);
Mat gray_by_codec = imread(imgName, IMREAD_GRAYSCALE);
Mat gray_by_cvt;
cvtColor(original_image, gray_by_cvt, COLOR_BGR2GRAY);
Mat diff;
absdiff(gray_by_codec, gray_by_cvt, diff);
EXPECT_LT(cvtest::mean(diff)[0], 1.);
EXPECT_PRED_FORMAT2(cvtest::MatComparator(10, 0), gray_by_codec, gray_by_cvt);
}
// Test OpenCV issue 3075 is solved
TEST(Imgcodecs_Png, read_color_palette_with_alpha)
{
const string root = cvtest::TS::ptr()->get_data_path();
Mat img;
// First Test : Read PNG with alpha, imread flag -1
img = imread(root + "readwrite/color_palette_alpha.png", IMREAD_UNCHANGED);
ASSERT_FALSE(img.empty());
ASSERT_TRUE(img.channels() == 4);
// pixel is red in BGRA
EXPECT_EQ(img.at<Vec4b>(0, 0), Vec4b(0, 0, 255, 255));
EXPECT_EQ(img.at<Vec4b>(0, 1), Vec4b(0, 0, 255, 255));
// Second Test : Read PNG without alpha, imread flag -1
img = imread(root + "readwrite/color_palette_no_alpha.png", IMREAD_UNCHANGED);
ASSERT_FALSE(img.empty());
ASSERT_TRUE(img.channels() == 3);
// pixel is red in BGR
EXPECT_EQ(img.at<Vec3b>(0, 0), Vec3b(0, 0, 255));
EXPECT_EQ(img.at<Vec3b>(0, 1), Vec3b(0, 0, 255));
// Third Test : Read PNG with alpha, imread flag 1
img = imread(root + "readwrite/color_palette_alpha.png", IMREAD_COLOR);
ASSERT_FALSE(img.empty());
ASSERT_TRUE(img.channels() == 3);
// pixel is red in BGR
EXPECT_EQ(img.at<Vec3b>(0, 0), Vec3b(0, 0, 255));
EXPECT_EQ(img.at<Vec3b>(0, 1), Vec3b(0, 0, 255));
img = imread(root + "readwrite/color_palette_alpha.png", IMREAD_COLOR_RGB);
ASSERT_FALSE(img.empty());
ASSERT_TRUE(img.channels() == 3);
// pixel is red in RGB
EXPECT_EQ(img.at<Vec3b>(0, 0), Vec3b(255, 0, 0));
EXPECT_EQ(img.at<Vec3b>(0, 1), Vec3b(255, 0, 0));
// Fourth Test : Read PNG without alpha, imread flag 1
img = imread(root + "readwrite/color_palette_no_alpha.png", IMREAD_COLOR);
ASSERT_FALSE(img.empty());
ASSERT_TRUE(img.channels() == 3);
// pixel is red in BGR
EXPECT_EQ(img.at<Vec3b>(0, 0), Vec3b(0, 0, 255));
EXPECT_EQ(img.at<Vec3b>(0, 1), Vec3b(0, 0, 255));
img = imread(root + "readwrite/color_palette_no_alpha.png", IMREAD_COLOR_RGB);
ASSERT_FALSE(img.empty());
ASSERT_TRUE(img.channels() == 3);
// pixel is red in RGB
EXPECT_EQ(img.at<Vec3b>(0, 0), Vec3b(255, 0, 0));
EXPECT_EQ(img.at<Vec3b>(0, 1), Vec3b(255, 0, 0));
}
// IHDR shall be first.
// See https://github.com/opencv/opencv/issues/27295
TEST(Imgcodecs_Png, decode_regression27295)
{
vector<uchar> buff;
Mat src = Mat::zeros(240, 180, CV_8UC3);
vector<int> param;
EXPECT_NO_THROW(imencode(".png", src, buff, param));
Mat img;
// If IHDR chunk found as the first chunk, output shall not be empty.
// 8 means PNG signature length.
// 4 means length field(uint32_t).
EXPECT_EQ(buff[8+4+0], 'I');
EXPECT_EQ(buff[8+4+1], 'H');
EXPECT_EQ(buff[8+4+2], 'D');
EXPECT_EQ(buff[8+4+3], 'R');
EXPECT_NO_THROW(img = imdecode(buff, IMREAD_COLOR));
EXPECT_FALSE(img.empty());
// If Non-IHDR chunk found as the first chunk, output shall be empty.
buff[8+4+0] = 'i'; // Not 'I'
buff[8+4+1] = 'H';
buff[8+4+2] = 'D';
buff[8+4+3] = 'R';
EXPECT_NO_THROW(img = imdecode(buff, IMREAD_COLOR));
EXPECT_TRUE(img.empty());
// If CgBI chunk (Apple private) found as the first chunk, output shall be empty with special message.
buff[8+4+0] = 'C';
buff[8+4+1] = 'g';
buff[8+4+2] = 'B';
buff[8+4+3] = 'I';
EXPECT_NO_THROW(img = imdecode(buff, IMREAD_COLOR));
EXPECT_TRUE(img.empty());
}
// The program must not crash even when decoding a corrupted APNG image.
// See https://github.com/opencv/opencv/issues/27744
#if defined(HAVE_PNG) // APNG is supported only with using libpng
TEST(Imgcodecs_Png, decode_regression27744)
{
// Create APNG stream
Animation anim;
for(size_t i = 0 ; i < 3 ; i++) {
Mat frame(120, 120, CV_8UC3, Scalar(0,0,0));
putText(frame, cv::format("%d", static_cast<int>(i)), Point(5, 28), FONT_HERSHEY_SIMPLEX, .5, Scalar(100, 255, 0, 255), 2);
anim.frames.push_back(frame);
anim.durations.push_back(30);
}
bool ret = false;
vector<uchar> buff;
EXPECT_NO_THROW(ret = imencodeanimation(".png", anim, buff));
ASSERT_TRUE(ret) << "imencodeanimation() returns false";
// Find IDAT chunk
const vector<uchar> IDAT = {'I', 'D', 'A', 'T' };
std::vector<uchar>::iterator it = std::search(buff.begin(), buff.end(), IDAT.begin(), IDAT.end());
ASSERT_FALSE(it == buff.end()) << "IDAT chunk not found";
// Determine the range to test
// APNG stream contains as { len0, len1, len2, len3, 'I', 'D', 'A' 'T', ... }
size_t idx = std::distance(buff.begin(), it); // 'I' position
size_t len = (buff[idx-4] << 24) + (buff[idx-3] << 16) +
(buff[idx-2] << 8) + (buff[idx-1]); // IDAT chunk length
idx = idx + 4; // Move to IDAT body
// Test
for(size_t i = 0; i < len; i++, idx++) {
vector<uint8_t> work = buff;
work[idx] = static_cast<uint8_t>((static_cast<uint32_t>(work[idx]) + 1) & 0xff);
Mat dst;
EXPECT_NO_THROW(dst = imdecode(work, cv::IMREAD_COLOR));
if(dst.empty()) {
// libpng detects some error, but the program is not crashed. Test is passed.
break;
}
}
}
#endif
typedef testing::TestWithParam<string> Imgcodecs_Png_PngSuite;
// Parameterized test for decoding PNG files from the PNGSuite test set
TEST_P(Imgcodecs_Png_PngSuite, decode)
{
// Construct full paths for the PNG image and corresponding ground truth XML file
const string root = cvtest::TS::ptr()->get_data_path();
const string filename = root + "pngsuite/" + GetParam() + ".png";
const string xml_filename = root + "pngsuite/" + GetParam() + ".xml";
// Load the XML file containing the ground truth data
FileStorage fs(xml_filename, FileStorage::READ);
ASSERT_TRUE(fs.isOpened()); // Ensure the file was opened successfully
// Load the image using IMREAD_UNCHANGED to preserve original format
Mat src = imread(filename, IMREAD_UNCHANGED);
ASSERT_FALSE(src.empty()); // Ensure the image was loaded successfully
// Load the ground truth matrix from XML
Mat gt;
fs.getFirstTopLevelNode() >> gt;
// Compare the image loaded with IMREAD_UNCHANGED to the ground truth
EXPECT_PRED_FORMAT2(cvtest::MatComparator(0, 0), src, gt);
// Declare matrices for ground truth in different imread flag combinations
Mat gt_0, gt_1, gt_2, gt_3, gt_256, gt_258;
// Handle grayscale 8-bit and 16-bit images
if (gt.channels() == 1)
{
gt.copyTo(gt_2); // For IMREAD_ANYDEPTH
if (gt.depth() == CV_16U)
gt_2.convertTo(gt_0, CV_8U, 1. / 256);
else
gt_0 = gt_2; // For IMREAD_GRAYSCALE
cvtColor(gt_2, gt_3, COLOR_GRAY2BGR); // For IMREAD_COLOR | IMREAD_ANYDEPTH
if (gt.depth() == CV_16U)
gt_3.convertTo(gt_1, CV_8U, 1. / 256);
else
gt_1 = gt_3; // For IMREAD_COLOR
gt_256 = gt_1; // For IMREAD_COLOR_RGB
gt_258 = gt_3; // For IMREAD_COLOR_RGB | IMREAD_ANYDEPTH
}
// Handle color images (3 or 4 channels) with 8-bit and 16-bit depth
if (gt.channels() > 1)
{
// Convert to grayscale
cvtColor(gt, gt_2, COLOR_BGRA2GRAY);
if (gt.depth() == CV_16U)
gt_2.convertTo(gt_0, CV_8U, 1. / 256);
else
gt_0 = gt_2;
// Convert to 3-channel BGR
if (gt.channels() == 3)
gt.copyTo(gt_3);
else
cvtColor(gt, gt_3, COLOR_BGRA2BGR);
if (gt.depth() == CV_16U)
gt_3.convertTo(gt_1, CV_8U, 1. / 256);
else
gt_1 = gt_3;
// Convert to RGB for IMREAD_COLOR_RGB variants
cvtColor(gt_1, gt_256, COLOR_BGR2RGB);
cvtColor(gt_3, gt_258, COLOR_BGR2RGB);
}
const double epsGrayAnydepth = ((gt.depth() == CV_16U) && (gt.channels() > 1)) ? OPENCV_IMGCODECS_PNG_EPS_16BIT_GRAY: OPENCV_IMGCODECS_PNG_EPS_DEFAULT;
// Perform comparisons with different imread flags
EXPECT_PRED_FORMAT2(cvtest::MatComparator(1, 0), imread(filename, IMREAD_GRAYSCALE), gt_0);
EXPECT_PRED_FORMAT2(cvtest::MatComparator(1, 0), imread(filename, IMREAD_COLOR), gt_1);
EXPECT_PRED_FORMAT2(cvtest::MatComparator(epsGrayAnydepth, 0), imread(filename, IMREAD_ANYDEPTH), gt_2); // IMREAD_GRAYSCALE is used.
EXPECT_PRED_FORMAT2(cvtest::MatComparator(0, 0), imread(filename, IMREAD_COLOR | IMREAD_ANYDEPTH), gt_3);
EXPECT_PRED_FORMAT2(cvtest::MatComparator(1, 0), imread(filename, IMREAD_COLOR_RGB), gt_256);
EXPECT_PRED_FORMAT2(cvtest::MatComparator(0, 0), imread(filename, IMREAD_COLOR_RGB | IMREAD_ANYDEPTH), gt_258);
// Uncomment this block to write out the decoded images for visual/manual inspection
// or for regenerating expected ground truth PNGs (for example, after changing decoder logic).
#if 0
imwrite(filename + "_0.png", imread(filename, IMREAD_GRAYSCALE));
imwrite(filename + "_1.png", imread(filename, IMREAD_COLOR));
imwrite(filename + "_2.png", imread(filename, IMREAD_ANYDEPTH));
imwrite(filename + "_3.png", imread(filename, IMREAD_COLOR | IMREAD_ANYDEPTH));
imwrite(filename + "_256.png", imread(filename, IMREAD_COLOR_RGB));
imwrite(filename + "_258.png", imread(filename, IMREAD_COLOR_RGB | IMREAD_ANYDEPTH));
#endif
// Uncomment this block to verify that saved images (from above) load identically
// when read back with IMREAD_UNCHANGED. Helps ensure write-read symmetry.
#if 0
EXPECT_PRED_FORMAT2(cvtest::MatComparator(0, 0), imread(filename, IMREAD_GRAYSCALE), imread(filename + "_0.png", IMREAD_UNCHANGED));
EXPECT_PRED_FORMAT2(cvtest::MatComparator(0, 0), imread(filename, IMREAD_COLOR), imread(filename + "_1.png", IMREAD_UNCHANGED));
EXPECT_PRED_FORMAT2(cvtest::MatComparator(0, 0), imread(filename, IMREAD_ANYDEPTH), imread(filename + "_2.png", IMREAD_UNCHANGED));
EXPECT_PRED_FORMAT2(cvtest::MatComparator(0, 0), imread(filename, IMREAD_COLOR | IMREAD_ANYDEPTH), imread(filename + "_3.png", IMREAD_UNCHANGED));
EXPECT_PRED_FORMAT2(cvtest::MatComparator(0, 0), imread(filename, IMREAD_COLOR_RGB), imread(filename + "_256.png", IMREAD_UNCHANGED));
EXPECT_PRED_FORMAT2(cvtest::MatComparator(0, 0), imread(filename, IMREAD_COLOR_RGB | IMREAD_ANYDEPTH), imread(filename + "_258.png", IMREAD_UNCHANGED));
#endif
}
const string pngsuite_files[] =
{
"basi0g01",
"basi0g02",
"basi0g04",
"basi0g08",
"basi0g16",
"basi2c08",
"basi2c16",
"basi3p01",
"basi3p02",
"basi3p04",
"basi3p08",
"basi4a08",
"basi4a16",
"basi6a08",
"basi6a16",
"basn0g01",
"basn0g02",
"basn0g04",
"basn0g08",
"basn0g16",
"basn2c08",
"basn2c16",
"basn3p01",
"basn3p02",
"basn3p04",
"basn3p08",
"basn4a08",
"basn4a16",
"basn6a08",
"basn6a16",
"bgai4a08",
"bgai4a16",
"bgan6a08",
"bgan6a16",
"bgbn4a08",
"bggn4a16",
"bgwn6a08",
"bgyn6a16",
"ccwn2c08",
"ccwn3p08",
"cdfn2c08",
"cdhn2c08",
"cdsn2c08",
"cdun2c08",
"ch1n3p04",
"ch2n3p08",
"cm0n0g04",
"cm7n0g04",
"cm9n0g04",
"cs3n2c16",
"cs3n3p08",
"cs5n2c08",
"cs5n3p08",
"cs8n2c08",
"cs8n3p08",
"ct0n0g04",
"ct1n0g04",
"cten0g04",
"ctfn0g04",
"ctgn0g04",
"cthn0g04",
"ctjn0g04",
"ctzn0g04",
"exif2c08",
"f00n0g08",
"f00n2c08",
"f01n0g08",
"f01n2c08",
"f02n0g08",
"f02n2c08",
"f03n0g08",
"f03n2c08",
"f04n0g08",
"f04n2c08",
"f99n0g04",
"g03n0g16",
"g04n0g16",
"g05n0g16",
"g07n0g16",
"g10n0g16",
"g10n2c08",
"g10n3p04",
"g25n0g16",
"oi1n0g16",
"oi1n2c16",
"oi2n0g16",
"oi2n2c16",
"oi4n0g16",
"oi4n2c16",
"oi9n0g16",
"oi9n2c16",
"pp0n2c16",
"pp0n6a08",
"ps1n0g08",
"ps1n2c16",
"ps2n0g08",
"ps2n2c16",
"s01i3p01",
"s01n3p01",
"s02i3p01",
"s02n3p01",
"s03i3p01",
"s03n3p01",
"s04i3p01",
"s04n3p01",
"s05i3p02",
"s05n3p02",
"s06i3p02",
"s06n3p02",
"s07i3p02",
"s07n3p02",
"s08i3p02",
"s08n3p02",
"s09i3p02",
"s09n3p02",
"s32i3p04",
"s32n3p04",
"s33i3p04",
"s33n3p04",
"s34i3p04",
"s34n3p04",
"s35i3p04",
"s35n3p04",
"s36i3p04",
"s36n3p04",
"s37i3p04",
"s37n3p04",
"s38i3p04",
"s38n3p04",
"s39i3p04",
"s39n3p04",
"s40i3p04",
"s40n3p04",
"tbbn0g04",
"tbbn2c16",
"tbbn3p08",
"tbgn2c16",
"tbgn3p08",
"tbrn2c08",
"tbwn0g16",
"tbwn3p08",
"tbyn3p08",
"tm3n3p02",
"tp0n0g08",
"tp0n2c08",
"tp0n3p08",
"tp1n3p08",
"z00n2c08",
"z03n2c08",
"z06n2c08",
"z09n2c08",
};
INSTANTIATE_TEST_CASE_P(/*nothing*/, Imgcodecs_Png_PngSuite,
testing::ValuesIn(pngsuite_files));
typedef testing::TestWithParam<string> Imgcodecs_Png_PngSuite_Gamma;
// Parameterized test for decoding PNG files from the PNGSuite test set
TEST_P(Imgcodecs_Png_PngSuite_Gamma, decode)
{
// Construct full paths for the PNG image and corresponding ground truth XML file
const string root = cvtest::TS::ptr()->get_data_path();
const string filename = root + "pngsuite/" + GetParam() + ".png";
const string xml_filename = root + "pngsuite/" + GetParam() + ".xml";
// Load the XML file containing the ground truth data
FileStorage fs(xml_filename, FileStorage::READ);
ASSERT_TRUE(fs.isOpened()); // Ensure the file was opened successfully
// Load the image using IMREAD_UNCHANGED to preserve original format
Mat src = imread(filename, IMREAD_UNCHANGED);
ASSERT_FALSE(src.empty()); // Ensure the image was loaded successfully
// Load the ground truth matrix from XML
Mat gt;
fs.getFirstTopLevelNode() >> gt;
// Compare the image loaded with IMREAD_UNCHANGED to the ground truth
EXPECT_PRED_FORMAT2(cvtest::MatComparator(0, 0), src, gt);
}
const string pngsuite_files_gamma[] =
{
"g03n2c08",
"g03n3p04",
"g04n2c08",
"g04n3p04",
"g05n2c08",
"g05n3p04",
"g07n2c08",
"g07n3p04",
"g25n2c08",
"g25n3p04"
};
INSTANTIATE_TEST_CASE_P(/*nothing*/, Imgcodecs_Png_PngSuite_Gamma,
testing::ValuesIn(pngsuite_files_gamma));
typedef testing::TestWithParam<string> Imgcodecs_Png_PngSuite_Corrupted;
TEST_P(Imgcodecs_Png_PngSuite_Corrupted, decode)
{
const string root = cvtest::TS::ptr()->get_data_path();
const string filename = root + "pngsuite/" + GetParam() + ".png";
Mat src = imread(filename, IMREAD_UNCHANGED);
// Corrupted files should not be read
EXPECT_TRUE(src.empty());
}
const string pngsuite_files_corrupted[] = {
"xc1n0g08",
"xc9n2c08",
"xcrn0g04",
"xcsn0g01",
"xd0n2c08",
"xd3n2c08",
"xd9n2c08",
"xdtn0g01",
"xhdn0g08",
"xlfn0g04",
"xs1n0g01",
"xs2n0g01",
"xs4n0g01",
"xs7n0g01",
};
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<testing::tuple<string, PNGStrategy, PNGFilters, int>> 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<uchar> file_buf;
readFileBytes(filename, file_buf);
Mat src = imdecode(file_buf, IMREAD_UNCHANGED);
EXPECT_FALSE(src.empty()) << "Cannot decode test image " << filename;
vector<uchar> 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<testing::tuple<string, int, size_t>> Imgcodecs_Png_ImwriteFlags;
TEST_P(Imgcodecs_Png_ImwriteFlags, compression_level)
{
const string root = cvtest::TS::ptr()->get_data_path();
const string filename = root + get<0>(GetParam());
const int compression_level = get<1>(GetParam());
const size_t compression_level_output_size = get<2>(GetParam());
Mat src = imread(filename, IMREAD_UNCHANGED);
EXPECT_FALSE(src.empty()) << "Cannot read test image " << filename;
vector<uchar> buf;
imencode(".png", src, buf, { IMWRITE_PNG_COMPRESSION, compression_level });
EXPECT_EQ(buf.size(), compression_level_output_size);
}
INSTANTIATE_TEST_CASE_P(/**/,
Imgcodecs_Png_ImwriteFlags,
testing::Values(
make_tuple("../perf/512x512.png", 0, 788279),
make_tuple("../perf/512x512.png", 1, 179503),
make_tuple("../perf/512x512.png", 2, 176007),
make_tuple("../perf/512x512.png", 3, 170497),
make_tuple("../perf/512x512.png", 4, 163357),
make_tuple("../perf/512x512.png", 5, 159190),
make_tuple("../perf/512x512.png", 6, 156621),
make_tuple("../perf/512x512.png", 7, 155696),
make_tuple("../perf/512x512.png", 8, 153708),
make_tuple("../perf/512x512.png", 9, 152181)));
// See https://github.com/opencv/opencv/issues/27614
typedef testing::TestWithParam<int> Imgcodecs_Png_ZLIBBUFFER_SIZE;
TEST_P(Imgcodecs_Png_ZLIBBUFFER_SIZE, encode_regression_27614)
{
Mat img(320,240,CV_8UC3,cv::Scalar(64,76,43));
vector<uint8_t> buff;
bool status = false;
ASSERT_NO_THROW(status = imencode(".png", img, buff, { IMWRITE_PNG_ZLIBBUFFER_SIZE, GetParam() }));
ASSERT_TRUE(status);
}
INSTANTIATE_TEST_CASE_P(/*nothing*/, Imgcodecs_Png_ZLIBBUFFER_SIZE,
testing::Values(5,
6, // Minimum limit
8192, // Default value
131072, // 128 KiB
262144, // 256 KiB
1048576, // Maximum limit
1048577));
#endif // HAVE_PNG
}} // namespace