mirror of
https://github.com/opencv/opencv.git
synced 2026-07-29 15:23:05 +04:00
Merge pull request #27499 from vpisarev:image_io_with_metadata
Extend image I/O API with metadata support #27499 Covered with the PR: * AVIF encoder can write exif, xmp, icc * AVIF decoder can read exif * JPEG encoder can write exif * JPEG decoder can read exif * PNG encoder can write exif * PNG decoder can read exif This PR is a sort of preamble for #27488. I suggest to merge this one first to OpenCV 4.x, then promote this change to OpenCV 5.x and then provide extra API to read and write metadata in 5.x (or maybe 4.x) in a style similar to #27488. Maybe in that PR exif packing/unpacking should be done using a separate external API. That is, metadata reading and writing can/should be done in 2 steps: * [1] pack and then [2] embed exif into image at the encoding stage. * [1] extract and then [2] unpack exif at the decoding stage. ### Pull Request Readiness Checklist See details at https://github.com/opencv/opencv/wiki/How_to_contribute#making-a-good-pull-request - [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 - [x] There is a reference to the original bug report and related work - [x] 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
This commit is contained in:
@@ -148,7 +148,246 @@ const std::vector<std::string> exif_files
|
||||
};
|
||||
|
||||
INSTANTIATE_TEST_CASE_P(Imgcodecs, Exif,
|
||||
testing::ValuesIn(exif_files));
|
||||
testing::ValuesIn(exif_files));
|
||||
|
||||
static Mat makeCirclesImage(Size size, int type, int nbits)
|
||||
{
|
||||
Mat img(size, type);
|
||||
img.setTo(Scalar::all(0));
|
||||
RNG& rng = theRNG();
|
||||
int maxval = (int)(1 << nbits);
|
||||
for (int i = 0; i < 100; i++) {
|
||||
int x = rng.uniform(0, img.cols);
|
||||
int y = rng.uniform(0, img.rows);
|
||||
int radius = rng.uniform(5, std::min(img.cols, img.rows)/5);
|
||||
int b = rng.uniform(0, maxval);
|
||||
int g = rng.uniform(0, maxval);
|
||||
int r = rng.uniform(0, maxval);
|
||||
circle(img, Point(x, y), radius, Scalar(b, g, r), -1, LINE_AA);
|
||||
}
|
||||
return img;
|
||||
}
|
||||
|
||||
#ifdef HAVE_AVIF
|
||||
TEST(Imgcodecs_Avif, ReadWriteWithExif)
|
||||
{
|
||||
static const uchar exif_data[] = {
|
||||
'M', 'M', 0, '*', 0, 0, 0, 8, 0, 10, 1, 0, 0, 4, 0, 0, 0, 1, 0, 0, 5,
|
||||
0, 1, 1, 0, 4, 0, 0, 0, 1, 0, 0, 2, 208, 1, 2, 0, 3, 0, 0, 0, 1,
|
||||
0, 10, 0, 0, 1, 18, 0, 3, 0, 0, 0, 1, 0, 1, 0, 0, 1, 14, 0, 2, 0, 0,
|
||||
0, '"', 0, 0, 0, 176, 1, '1', 0, 2, 0, 0, 0, 7, 0, 0, 0, 210, 1, 26,
|
||||
0, 5, 0, 0, 0, 1, 0, 0, 0, 218, 1, 27, 0, 5, 0, 0, 0, 1, 0, 0, 0,
|
||||
226, 1, '(', 0, 3, 0, 0, 0, 1, 0, 2, 0, 0, 135, 'i', 0, 4, 0, 0, 0,
|
||||
1, 0, 0, 0, 134, 0, 0, 0, 0, 0, 3, 144, 0, 0, 7, 0, 0, 0, 4, '0', '2',
|
||||
'2', '1', 160, 2, 0, 4, 0, 0, 0, 1, 0, 0, 5, 0, 160, 3, 0, 4, 0, 0,
|
||||
0, 1, 0, 0, 2, 208, 0, 0, 0, 0, 'S', 'a', 'm', 'p', 'l', 'e', ' ', '1', '0',
|
||||
'-', 'b', 'i', 't', ' ', 'i', 'm', 'a', 'g', 'e', ' ', 'w', 'i', 't', 'h', ' ',
|
||||
'm', 'e', 't', 'a', 'd', 'a', 't', 'a', 0, 'O', 'p', 'e', 'n', 'C', 'V', 0, 0,
|
||||
0, 0, 0, 'H', 0, 0, 0, 1, 0, 0, 0, 'H', 0, 0, 0, 1
|
||||
};
|
||||
|
||||
int avif_nbits = 10;
|
||||
int avif_speed = 10;
|
||||
int avif_quality = 85;
|
||||
int imgdepth = avif_nbits > 8 ? CV_16U : CV_8U;
|
||||
int imgtype = CV_MAKETYPE(imgdepth, 3);
|
||||
const string outputname = cv::tempfile(".avif");
|
||||
Mat img = makeCirclesImage(Size(1280, 720), imgtype, avif_nbits);
|
||||
|
||||
std::vector<int> metadata_types = {IMAGE_METADATA_EXIF};
|
||||
std::vector<std::vector<uchar> > metadata(1);
|
||||
metadata[0].assign(exif_data, exif_data + sizeof(exif_data));
|
||||
|
||||
std::vector<int> write_params = {
|
||||
IMWRITE_AVIF_DEPTH, avif_nbits,
|
||||
IMWRITE_AVIF_SPEED, avif_speed,
|
||||
IMWRITE_AVIF_QUALITY, avif_quality
|
||||
};
|
||||
|
||||
imwriteWithMetadata(outputname, img, metadata_types, metadata, write_params);
|
||||
std::vector<uchar> compressed;
|
||||
imencodeWithMetadata(outputname, img, metadata_types, metadata, compressed, write_params);
|
||||
|
||||
std::vector<int> read_metadata_types, read_metadata_types2;
|
||||
std::vector<std::vector<uchar> > read_metadata, read_metadata2;
|
||||
Mat img2 = imreadWithMetadata(outputname, read_metadata_types, read_metadata, IMREAD_UNCHANGED);
|
||||
Mat img3 = imdecodeWithMetadata(compressed, read_metadata_types2, read_metadata2, IMREAD_UNCHANGED);
|
||||
EXPECT_EQ(img2.cols, img.cols);
|
||||
EXPECT_EQ(img2.rows, img.rows);
|
||||
EXPECT_EQ(img2.type(), imgtype);
|
||||
EXPECT_EQ(read_metadata_types, read_metadata_types2);
|
||||
EXPECT_GE(read_metadata_types.size(), 1u);
|
||||
EXPECT_EQ(read_metadata, read_metadata2);
|
||||
EXPECT_EQ(read_metadata_types[0], IMAGE_METADATA_EXIF);
|
||||
EXPECT_EQ(read_metadata_types.size(), read_metadata.size());
|
||||
EXPECT_EQ(read_metadata[0], metadata[0]);
|
||||
EXPECT_EQ(cv::norm(img2, img3, NORM_INF), 0.);
|
||||
double mse = cv::norm(img, img2, NORM_L2SQR)/(img.rows*img.cols);
|
||||
EXPECT_LT(mse, 1500);
|
||||
remove(outputname.c_str());
|
||||
}
|
||||
#endif // HAVE_AVIF
|
||||
|
||||
TEST(Imgcodecs_Jpeg, ReadWriteWithExif)
|
||||
{
|
||||
static const uchar exif_data[] = {
|
||||
'M', 'M', 0, '*', 0, 0, 0, 8, 0, 10, 1, 0, 0, 4, 0, 0, 0, 1, 0, 0, 5,
|
||||
0, 1, 1, 0, 4, 0, 0, 0, 1, 0, 0, 2, 208, 1, 2, 0, 3, 0, 0, 0, 1,
|
||||
0, 8, 0, 0, 1, 18, 0, 3, 0, 0, 0, 1, 0, 1, 0, 0, 1, 14, 0, 2, 0, 0,
|
||||
0, '!', 0, 0, 0, 176, 1, '1', 0, 2, 0, 0, 0, 7, 0, 0, 0, 210, 1, 26,
|
||||
0, 5, 0, 0, 0, 1, 0, 0, 0, 218, 1, 27, 0, 5, 0, 0, 0, 1, 0, 0, 0,
|
||||
226, 1, '(', 0, 3, 0, 0, 0, 1, 0, 2, 0, 0, 135, 'i', 0, 4, 0, 0, 0,
|
||||
1, 0, 0, 0, 134, 0, 0, 0, 0, 0, 3, 144, 0, 0, 7, 0, 0, 0, 4, '0', '2',
|
||||
'2', '1', 160, 2, 0, 4, 0, 0, 0, 1, 0, 0, 5, 0, 160, 3, 0, 4, 0, 0,
|
||||
0, 1, 0, 0, 2, 208, 0, 0, 0, 0, 'S', 'a', 'm', 'p', 'l', 'e', ' ', '8', '-',
|
||||
'b', 'i', 't', ' ', 'i', 'm', 'a', 'g', 'e', ' ', 'w', 'i', 't', 'h', ' ', 'm',
|
||||
'e', 't', 'a', 'd', 'a', 't', 'a', 0, 0, 'O', 'p', 'e', 'n', 'C', 'V', 0, 0,
|
||||
0, 0, 0, 'H', 0, 0, 0, 1, 0, 0, 0, 'H', 0, 0, 0, 1
|
||||
};
|
||||
|
||||
int jpeg_quality = 95;
|
||||
int imgtype = CV_MAKETYPE(CV_8U, 3);
|
||||
const string outputname = cv::tempfile(".jpeg");
|
||||
Mat img = makeCirclesImage(Size(1280, 720), imgtype, 8);
|
||||
|
||||
std::vector<int> metadata_types = {IMAGE_METADATA_EXIF};
|
||||
std::vector<std::vector<uchar> > metadata(1);
|
||||
metadata[0].assign(exif_data, exif_data + sizeof(exif_data));
|
||||
|
||||
std::vector<int> write_params = {
|
||||
IMWRITE_JPEG_QUALITY, jpeg_quality
|
||||
};
|
||||
|
||||
imwriteWithMetadata(outputname, img, metadata_types, metadata, write_params);
|
||||
std::vector<uchar> compressed;
|
||||
imencodeWithMetadata(outputname, img, metadata_types, metadata, compressed, write_params);
|
||||
|
||||
std::vector<int> read_metadata_types, read_metadata_types2;
|
||||
std::vector<std::vector<uchar> > read_metadata, read_metadata2;
|
||||
Mat img2 = imreadWithMetadata(outputname, read_metadata_types, read_metadata, IMREAD_UNCHANGED);
|
||||
Mat img3 = imdecodeWithMetadata(compressed, read_metadata_types2, read_metadata2, IMREAD_UNCHANGED);
|
||||
EXPECT_EQ(img2.cols, img.cols);
|
||||
EXPECT_EQ(img2.rows, img.rows);
|
||||
EXPECT_EQ(img2.type(), imgtype);
|
||||
EXPECT_EQ(read_metadata_types, read_metadata_types2);
|
||||
EXPECT_GE(read_metadata_types.size(), 1u);
|
||||
EXPECT_EQ(read_metadata, read_metadata2);
|
||||
EXPECT_EQ(read_metadata_types[0], IMAGE_METADATA_EXIF);
|
||||
EXPECT_EQ(read_metadata_types.size(), read_metadata.size());
|
||||
EXPECT_EQ(read_metadata[0], metadata[0]);
|
||||
EXPECT_EQ(cv::norm(img2, img3, NORM_INF), 0.);
|
||||
double mse = cv::norm(img, img2, NORM_L2SQR)/(img.rows*img.cols);
|
||||
EXPECT_LT(mse, 80);
|
||||
remove(outputname.c_str());
|
||||
}
|
||||
|
||||
TEST(Imgcodecs_Png, ReadWriteWithExif)
|
||||
{
|
||||
static const uchar exif_data[] = {
|
||||
'M', 'M', 0, '*', 0, 0, 0, 8, 0, 10, 1, 0, 0, 4, 0, 0, 0, 1, 0, 0, 5,
|
||||
0, 1, 1, 0, 4, 0, 0, 0, 1, 0, 0, 2, 208, 1, 2, 0, 3, 0, 0, 0, 1,
|
||||
0, 8, 0, 0, 1, 18, 0, 3, 0, 0, 0, 1, 0, 1, 0, 0, 1, 14, 0, 2, 0, 0,
|
||||
0, '!', 0, 0, 0, 176, 1, '1', 0, 2, 0, 0, 0, 7, 0, 0, 0, 210, 1, 26,
|
||||
0, 5, 0, 0, 0, 1, 0, 0, 0, 218, 1, 27, 0, 5, 0, 0, 0, 1, 0, 0, 0,
|
||||
226, 1, '(', 0, 3, 0, 0, 0, 1, 0, 2, 0, 0, 135, 'i', 0, 4, 0, 0, 0,
|
||||
1, 0, 0, 0, 134, 0, 0, 0, 0, 0, 3, 144, 0, 0, 7, 0, 0, 0, 4, '0', '2',
|
||||
'2', '1', 160, 2, 0, 4, 0, 0, 0, 1, 0, 0, 5, 0, 160, 3, 0, 4, 0, 0,
|
||||
0, 1, 0, 0, 2, 208, 0, 0, 0, 0, 'S', 'a', 'm', 'p', 'l', 'e', ' ', '8', '-',
|
||||
'b', 'i', 't', ' ', 'i', 'm', 'a', 'g', 'e', ' ', 'w', 'i', 't', 'h', ' ', 'm',
|
||||
'e', 't', 'a', 'd', 'a', 't', 'a', 0, 0, 'O', 'p', 'e', 'n', 'C', 'V', 0, 0,
|
||||
0, 0, 0, 'H', 0, 0, 0, 1, 0, 0, 0, 'H', 0, 0, 0, 1
|
||||
};
|
||||
|
||||
int png_compression = 3;
|
||||
int imgtype = CV_MAKETYPE(CV_8U, 3);
|
||||
const string outputname = cv::tempfile(".png");
|
||||
Mat img = makeCirclesImage(Size(1280, 720), imgtype, 8);
|
||||
|
||||
std::vector<int> metadata_types = {IMAGE_METADATA_EXIF};
|
||||
std::vector<std::vector<uchar> > metadata(1);
|
||||
metadata[0].assign(exif_data, exif_data + sizeof(exif_data));
|
||||
|
||||
std::vector<int> write_params = {
|
||||
IMWRITE_PNG_COMPRESSION, png_compression
|
||||
};
|
||||
|
||||
imwriteWithMetadata(outputname, img, metadata_types, metadata, write_params);
|
||||
std::vector<uchar> compressed;
|
||||
imencodeWithMetadata(outputname, img, metadata_types, metadata, compressed, write_params);
|
||||
|
||||
std::vector<int> read_metadata_types, read_metadata_types2;
|
||||
std::vector<std::vector<uchar> > read_metadata, read_metadata2;
|
||||
Mat img2 = imreadWithMetadata(outputname, read_metadata_types, read_metadata, IMREAD_UNCHANGED);
|
||||
Mat img3 = imdecodeWithMetadata(compressed, read_metadata_types2, read_metadata2, IMREAD_UNCHANGED);
|
||||
EXPECT_EQ(img2.cols, img.cols);
|
||||
EXPECT_EQ(img2.rows, img.rows);
|
||||
EXPECT_EQ(img2.type(), imgtype);
|
||||
EXPECT_EQ(read_metadata_types, read_metadata_types2);
|
||||
EXPECT_GE(read_metadata_types.size(), 1u);
|
||||
EXPECT_EQ(read_metadata, read_metadata2);
|
||||
EXPECT_EQ(read_metadata_types[0], IMAGE_METADATA_EXIF);
|
||||
EXPECT_EQ(read_metadata_types.size(), read_metadata.size());
|
||||
EXPECT_EQ(read_metadata[0], metadata[0]);
|
||||
EXPECT_EQ(cv::norm(img2, img3, NORM_INF), 0.);
|
||||
double mse = cv::norm(img, img2, NORM_L2SQR)/(img.rows*img.cols);
|
||||
EXPECT_EQ(mse, 0); // png is lossless
|
||||
remove(outputname.c_str());
|
||||
}
|
||||
|
||||
static size_t locateString(const uchar* exif, size_t exif_size, const std::string& pattern)
|
||||
{
|
||||
size_t plen = pattern.size();
|
||||
for (size_t i = 0; i + plen <= exif_size; i++) {
|
||||
if (exif[i] == pattern[0] && memcmp(&exif[i], pattern.c_str(), plen) == 0)
|
||||
return i;
|
||||
}
|
||||
return 0xFFFFFFFFu;
|
||||
}
|
||||
|
||||
typedef std::tuple<std::string, size_t, std::string, size_t> ReadExif_Sanity_Params;
|
||||
typedef testing::TestWithParam<ReadExif_Sanity_Params> ReadExif_Sanity;
|
||||
|
||||
TEST_P(ReadExif_Sanity, Check)
|
||||
{
|
||||
std::string filename = get<0>(GetParam());
|
||||
size_t exif_size = get<1>(GetParam());
|
||||
std::string pattern = get<2>(GetParam());
|
||||
size_t ploc = get<3>(GetParam());
|
||||
|
||||
const string root = cvtest::TS::ptr()->get_data_path();
|
||||
filename = root + filename;
|
||||
|
||||
std::vector<int> metadata_types;
|
||||
std::vector<Mat> metadata;
|
||||
Mat img = imreadWithMetadata(filename, metadata_types, metadata, 1);
|
||||
|
||||
EXPECT_EQ(img.type(), CV_8UC3);
|
||||
ASSERT_GE(metadata_types.size(), 1u);
|
||||
EXPECT_EQ(metadata_types.size(), metadata.size());
|
||||
const Mat& exif = metadata[IMAGE_METADATA_EXIF];
|
||||
EXPECT_EQ(exif.type(), CV_8U);
|
||||
EXPECT_EQ(exif.total(), exif_size);
|
||||
ASSERT_GE(exif_size, 26u); // minimal exif should take at least 26 bytes
|
||||
// (the header + IDF0 with at least 1 entry).
|
||||
EXPECT_TRUE(exif.data[0] == 'I' || exif.data[0] == 'M');
|
||||
EXPECT_EQ(exif.data[0], exif.data[1]);
|
||||
EXPECT_EQ(locateString(exif.data, exif_size, pattern), ploc);
|
||||
}
|
||||
|
||||
static const std::vector<ReadExif_Sanity_Params> exif_sanity_params
|
||||
{
|
||||
#ifdef HAVE_JPEG
|
||||
{"readwrite/testExifOrientation_3.jpg", 916, "Photoshop", 120},
|
||||
#endif
|
||||
#ifdef OPENCV_IMGCODECS_PNG_WITH_EXIF
|
||||
{"readwrite/testExifOrientation_5.png", 112, "ExifTool", 102},
|
||||
#endif
|
||||
#ifdef HAVE_AVIF
|
||||
{"readwrite/testExifOrientation_7.avif", 913, "Photoshop", 120},
|
||||
#endif
|
||||
};
|
||||
|
||||
INSTANTIATE_TEST_CASE_P(Imgcodecs, ReadExif_Sanity,
|
||||
testing::ValuesIn(exif_sanity_params));
|
||||
|
||||
}}
|
||||
|
||||
Reference in New Issue
Block a user