mirror of
https://github.com/opencv/opencv.git
synced 2026-09-26 04:09:43 +03:00
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.
746 lines
25 KiB
C++
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
|