jpegli_test.cc (14528B)
1 // Copyright (c) the JPEG XL Project Authors. All rights reserved. 2 // 3 // Use of this source code is governed by a BSD-style 4 // license that can be found in the LICENSE file. 5 6 #if JPEGXL_ENABLE_JPEGLI 7 8 #include "lib/extras/dec/jpegli.h" 9 10 #include <jxl/color_encoding.h> 11 #include <jxl/types.h> 12 13 #include <cstddef> 14 #include <cstdint> 15 #include <cstdio> 16 #include <cstring> 17 #include <memory> 18 #include <ostream> 19 #include <sstream> 20 #include <string> 21 #include <utility> 22 #include <vector> 23 24 #include "lib/extras/dec/color_hints.h" 25 #include "lib/extras/dec/decode.h" 26 #include "lib/extras/dec/jpg.h" 27 #include "lib/extras/enc/encode.h" 28 #include "lib/extras/enc/jpegli.h" 29 #include "lib/extras/enc/jpg.h" 30 #include "lib/extras/packed_image.h" 31 #include "lib/jxl/base/span.h" 32 #include "lib/jxl/base/status.h" 33 #include "lib/jxl/color_encoding_internal.h" 34 #include "lib/jxl/test_image.h" 35 #include "lib/jxl/test_utils.h" 36 #include "lib/jxl/testing.h" 37 38 namespace jxl { 39 namespace extras { 40 namespace { 41 42 using ::jxl::test::Butteraugli3Norm; 43 using ::jxl::test::ButteraugliDistance; 44 using ::jxl::test::TestImage; 45 46 Status ReadTestImage(const std::string& pathname, PackedPixelFile* ppf) { 47 const std::vector<uint8_t> encoded = jxl::test::ReadTestData(pathname); 48 ColorHints color_hints; 49 if (pathname.find(".ppm") != std::string::npos) { 50 color_hints.Add("color_space", "RGB_D65_SRG_Rel_SRG"); 51 } else if (pathname.find(".pgm") != std::string::npos) { 52 color_hints.Add("color_space", "Gra_D65_Rel_SRG"); 53 } 54 return DecodeBytes(Bytes(encoded), color_hints, ppf); 55 } 56 57 std::vector<uint8_t> GetAppData(const std::vector<uint8_t>& compressed) { 58 std::vector<uint8_t> result; 59 size_t pos = 2; // After SOI 60 while (pos + 4 < compressed.size()) { 61 if (compressed[pos] != 0xff || compressed[pos + 1] < 0xe0 || 62 compressed[pos + 1] > 0xf0) { 63 break; 64 } 65 size_t len = (compressed[pos + 2] << 8) + compressed[pos + 3] + 2; 66 if (pos + len > compressed.size()) { 67 break; 68 } 69 result.insert(result.end(), &compressed[pos], &compressed[pos] + len); 70 pos += len; 71 } 72 return result; 73 } 74 75 Status DecodeWithLibjpeg(const std::vector<uint8_t>& compressed, 76 PackedPixelFile* ppf, 77 const JPGDecompressParams* dparams = nullptr) { 78 return DecodeImageJPG(Bytes(compressed), ColorHints(), ppf, 79 /*constraints=*/nullptr, dparams); 80 } 81 82 Status EncodeWithLibjpeg(const PackedPixelFile& ppf, int quality, 83 std::vector<uint8_t>* compressed) { 84 std::unique_ptr<Encoder> encoder = GetJPEGEncoder(); 85 encoder->SetOption("q", std::to_string(quality)); 86 EncodedImage encoded; 87 JXL_RETURN_IF_ERROR(encoder->Encode(ppf, &encoded, nullptr)); 88 JXL_RETURN_IF_ERROR(!encoded.bitstreams.empty()); 89 *compressed = std::move(encoded.bitstreams[0]); 90 return true; 91 } 92 93 std::string Description(const JxlColorEncoding& color_encoding) { 94 ColorEncoding c_enc; 95 EXPECT_TRUE(c_enc.FromExternal(color_encoding)); 96 return Description(c_enc); 97 } 98 99 float BitsPerPixel(const PackedPixelFile& ppf, 100 const std::vector<uint8_t>& compressed) { 101 const size_t num_pixels = ppf.info.xsize * ppf.info.ysize; 102 return compressed.size() * 8.0 / num_pixels; 103 } 104 105 TEST(JpegliTest, JpegliSRGBDecodeTest) { 106 TEST_LIBJPEG_SUPPORT(); 107 std::string testimage = "jxl/flower/flower_small.rgb.depth8.ppm"; 108 PackedPixelFile ppf0; 109 ASSERT_TRUE(ReadTestImage(testimage, &ppf0)); 110 EXPECT_EQ("RGB_D65_SRG_Rel_SRG", Description(ppf0.color_encoding)); 111 EXPECT_EQ(8, ppf0.info.bits_per_sample); 112 113 std::vector<uint8_t> compressed; 114 ASSERT_TRUE(EncodeWithLibjpeg(ppf0, 90, &compressed)); 115 116 PackedPixelFile ppf1; 117 ASSERT_TRUE(DecodeWithLibjpeg(compressed, &ppf1)); 118 PackedPixelFile ppf2; 119 JpegDecompressParams dparams; 120 ASSERT_TRUE(DecodeJpeg(compressed, dparams, nullptr, &ppf2)); 121 EXPECT_LT(ButteraugliDistance(ppf0, ppf2), ButteraugliDistance(ppf0, ppf1)); 122 } 123 124 TEST(JpegliTest, JpegliGrayscaleDecodeTest) { 125 TEST_LIBJPEG_SUPPORT(); 126 std::string testimage = "jxl/flower/flower_small.g.depth8.pgm"; 127 PackedPixelFile ppf0; 128 ASSERT_TRUE(ReadTestImage(testimage, &ppf0)); 129 EXPECT_EQ("Gra_D65_Rel_SRG", Description(ppf0.color_encoding)); 130 EXPECT_EQ(8, ppf0.info.bits_per_sample); 131 132 std::vector<uint8_t> compressed; 133 ASSERT_TRUE(EncodeWithLibjpeg(ppf0, 90, &compressed)); 134 135 PackedPixelFile ppf1; 136 ASSERT_TRUE(DecodeWithLibjpeg(compressed, &ppf1)); 137 PackedPixelFile ppf2; 138 JpegDecompressParams dparams; 139 ASSERT_TRUE(DecodeJpeg(compressed, dparams, nullptr, &ppf2)); 140 EXPECT_LT(ButteraugliDistance(ppf0, ppf2), ButteraugliDistance(ppf0, ppf1)); 141 } 142 143 TEST(JpegliTest, JpegliXYBEncodeTest) { 144 TEST_LIBJPEG_SUPPORT(); 145 std::string testimage = "jxl/flower/flower_small.rgb.depth8.ppm"; 146 PackedPixelFile ppf_in; 147 ASSERT_TRUE(ReadTestImage(testimage, &ppf_in)); 148 EXPECT_EQ("RGB_D65_SRG_Rel_SRG", Description(ppf_in.color_encoding)); 149 EXPECT_EQ(8, ppf_in.info.bits_per_sample); 150 151 std::vector<uint8_t> compressed; 152 JpegSettings settings; 153 settings.xyb = true; 154 ASSERT_TRUE(EncodeJpeg(ppf_in, settings, nullptr, &compressed)); 155 156 PackedPixelFile ppf_out; 157 ASSERT_TRUE(DecodeWithLibjpeg(compressed, &ppf_out)); 158 EXPECT_SLIGHTLY_BELOW(BitsPerPixel(ppf_in, compressed), 1.45f); 159 EXPECT_SLIGHTLY_BELOW(ButteraugliDistance(ppf_in, ppf_out), 1.32f); 160 } 161 162 TEST(JpegliTest, JpegliDecodeTestLargeSmoothArea) { 163 TEST_LIBJPEG_SUPPORT(); 164 TestImage t; 165 const size_t xsize = 2070; 166 const size_t ysize = 1063; 167 ASSERT_TRUE(t.SetDimensions(xsize, ysize)); 168 ASSERT_TRUE(t.SetChannels(3)); 169 t.SetAllBitDepths(8).SetEndianness(JXL_NATIVE_ENDIAN); 170 JXL_TEST_ASSIGN_OR_DIE(TestImage::Frame frame, t.AddFrame()); 171 frame.RandomFill(); 172 // Create a large smooth area in the top half of the image. This is to test 173 // that the bias statistics calculation can handle many blocks with all-zero 174 // AC coefficients. 175 for (size_t y = 0; y < ysize / 2; ++y) { 176 for (size_t x = 0; x < xsize; ++x) { 177 for (size_t c = 0; c < 3; ++c) { 178 ASSERT_TRUE(frame.SetValue(y, x, c, 0.5f)); 179 } 180 } 181 } 182 const PackedPixelFile& ppf0 = t.ppf(); 183 184 std::vector<uint8_t> compressed; 185 ASSERT_TRUE(EncodeWithLibjpeg(ppf0, 90, &compressed)); 186 187 PackedPixelFile ppf1; 188 JpegDecompressParams dparams; 189 ASSERT_TRUE(DecodeJpeg(compressed, dparams, nullptr, &ppf1)); 190 EXPECT_LT(ButteraugliDistance(ppf0, ppf1), 3.0f); 191 } 192 193 TEST(JpegliTest, JpegliYUVEncodeTest) { 194 TEST_LIBJPEG_SUPPORT(); 195 std::string testimage = "jxl/flower/flower_small.rgb.depth8.ppm"; 196 PackedPixelFile ppf_in; 197 ASSERT_TRUE(ReadTestImage(testimage, &ppf_in)); 198 EXPECT_EQ("RGB_D65_SRG_Rel_SRG", Description(ppf_in.color_encoding)); 199 EXPECT_EQ(8, ppf_in.info.bits_per_sample); 200 201 std::vector<uint8_t> compressed; 202 JpegSettings settings; 203 settings.xyb = false; 204 ASSERT_TRUE(EncodeJpeg(ppf_in, settings, nullptr, &compressed)); 205 206 PackedPixelFile ppf_out; 207 ASSERT_TRUE(DecodeWithLibjpeg(compressed, &ppf_out)); 208 EXPECT_SLIGHTLY_BELOW(BitsPerPixel(ppf_in, compressed), 1.7f); 209 EXPECT_SLIGHTLY_BELOW(ButteraugliDistance(ppf_in, ppf_out), 1.32f); 210 } 211 212 TEST(JpegliTest, JpegliYUVChromaSubsamplingEncodeTest) { 213 TEST_LIBJPEG_SUPPORT(); 214 std::string testimage = "jxl/flower/flower_small.rgb.depth8.ppm"; 215 PackedPixelFile ppf_in; 216 ASSERT_TRUE(ReadTestImage(testimage, &ppf_in)); 217 EXPECT_EQ("RGB_D65_SRG_Rel_SRG", Description(ppf_in.color_encoding)); 218 EXPECT_EQ(8, ppf_in.info.bits_per_sample); 219 220 std::vector<uint8_t> compressed; 221 JpegSettings settings; 222 for (const char* sampling : {"440", "422", "420"}) { 223 settings.xyb = false; 224 settings.chroma_subsampling = std::string(sampling); 225 ASSERT_TRUE(EncodeJpeg(ppf_in, settings, nullptr, &compressed)); 226 227 PackedPixelFile ppf_out; 228 ASSERT_TRUE(DecodeWithLibjpeg(compressed, &ppf_out)); 229 EXPECT_LE(BitsPerPixel(ppf_in, compressed), 1.55f); 230 EXPECT_LE(ButteraugliDistance(ppf_in, ppf_out), 1.82f); 231 } 232 } 233 234 TEST(JpegliTest, JpegliYUVEncodeTestNoAq) { 235 TEST_LIBJPEG_SUPPORT(); 236 std::string testimage = "jxl/flower/flower_small.rgb.depth8.ppm"; 237 PackedPixelFile ppf_in; 238 ASSERT_TRUE(ReadTestImage(testimage, &ppf_in)); 239 EXPECT_EQ("RGB_D65_SRG_Rel_SRG", Description(ppf_in.color_encoding)); 240 EXPECT_EQ(8, ppf_in.info.bits_per_sample); 241 242 std::vector<uint8_t> compressed; 243 JpegSettings settings; 244 settings.xyb = false; 245 settings.use_adaptive_quantization = false; 246 ASSERT_TRUE(EncodeJpeg(ppf_in, settings, nullptr, &compressed)); 247 248 PackedPixelFile ppf_out; 249 ASSERT_TRUE(DecodeWithLibjpeg(compressed, &ppf_out)); 250 EXPECT_SLIGHTLY_BELOW(BitsPerPixel(ppf_in, compressed), 1.85f); 251 EXPECT_SLIGHTLY_BELOW(ButteraugliDistance(ppf_in, ppf_out), 1.25f); 252 } 253 254 TEST(JpegliTest, JpegliHDRRoundtripTest) { 255 std::string testimage = "jxl/hdr_room.png"; 256 PackedPixelFile ppf_in; 257 ASSERT_TRUE(ReadTestImage(testimage, &ppf_in)); 258 EXPECT_EQ("Rec2100HLG", Description(ppf_in.color_encoding)); 259 EXPECT_EQ(16, ppf_in.info.bits_per_sample); 260 261 std::vector<uint8_t> compressed; 262 JpegSettings settings; 263 settings.xyb = false; 264 ASSERT_TRUE(EncodeJpeg(ppf_in, settings, nullptr, &compressed)); 265 266 PackedPixelFile ppf_out; 267 JpegDecompressParams dparams; 268 dparams.output_data_type = JXL_TYPE_UINT16; 269 ASSERT_TRUE(DecodeJpeg(compressed, dparams, nullptr, &ppf_out)); 270 EXPECT_SLIGHTLY_BELOW(BitsPerPixel(ppf_in, compressed), 2.95f); 271 EXPECT_SLIGHTLY_BELOW(ButteraugliDistance(ppf_in, ppf_out), 1.05f); 272 } 273 274 TEST(JpegliTest, JpegliSetAppData) { 275 std::string testimage = "jxl/flower/flower_small.rgb.depth8.ppm"; 276 PackedPixelFile ppf_in; 277 ASSERT_TRUE(ReadTestImage(testimage, &ppf_in)); 278 EXPECT_EQ("RGB_D65_SRG_Rel_SRG", Description(ppf_in.color_encoding)); 279 EXPECT_EQ(8, ppf_in.info.bits_per_sample); 280 281 std::vector<uint8_t> compressed; 282 JpegSettings settings; 283 settings.app_data = {0xff, 0xe3, 0, 4, 0, 1}; 284 EXPECT_TRUE(EncodeJpeg(ppf_in, settings, nullptr, &compressed)); 285 EXPECT_EQ(settings.app_data, GetAppData(compressed)); 286 287 settings.app_data = {0xff, 0xe3, 0, 6, 0, 1, 2, 3, 0xff, 0xef, 0, 4, 0, 1}; 288 EXPECT_TRUE(EncodeJpeg(ppf_in, settings, nullptr, &compressed)); 289 EXPECT_EQ(settings.app_data, GetAppData(compressed)); 290 291 settings.xyb = true; 292 EXPECT_TRUE(EncodeJpeg(ppf_in, settings, nullptr, &compressed)); 293 EXPECT_EQ(0, memcmp(settings.app_data.data(), GetAppData(compressed).data(), 294 settings.app_data.size())); 295 296 settings.xyb = false; 297 settings.app_data = {0}; 298 EXPECT_FALSE(EncodeJpeg(ppf_in, settings, nullptr, &compressed)); 299 300 settings.app_data = {0xff, 0xe0}; 301 EXPECT_FALSE(EncodeJpeg(ppf_in, settings, nullptr, &compressed)); 302 303 settings.app_data = {0xff, 0xe0, 0, 2}; 304 EXPECT_FALSE(EncodeJpeg(ppf_in, settings, nullptr, &compressed)); 305 306 settings.app_data = {0xff, 0xeb, 0, 4, 0}; 307 EXPECT_FALSE(EncodeJpeg(ppf_in, settings, nullptr, &compressed)); 308 309 settings.app_data = {0xff, 0xeb, 0, 4, 0, 1, 2, 3}; 310 EXPECT_FALSE(EncodeJpeg(ppf_in, settings, nullptr, &compressed)); 311 312 settings.app_data = {0xff, 0xab, 0, 4, 0, 1}; 313 EXPECT_FALSE(EncodeJpeg(ppf_in, settings, nullptr, &compressed)); 314 315 settings.xyb = false; 316 settings.app_data = { 317 0xff, 0xeb, 0, 4, 0, 1, // 318 0xff, 0xe2, 0, 20, 0x49, 0x43, 0x43, 0x5F, 0x50, // 319 0x52, 0x4F, 0x46, 0x49, 0x4C, 0x45, 0x00, 0, 1, // 320 0, 0, 0, 0, // 321 }; 322 EXPECT_TRUE(EncodeJpeg(ppf_in, settings, nullptr, &compressed)); 323 EXPECT_EQ(settings.app_data, GetAppData(compressed)); 324 325 settings.xyb = true; 326 EXPECT_FALSE(EncodeJpeg(ppf_in, settings, nullptr, &compressed)); 327 } 328 329 struct TestConfig { 330 int num_colors; 331 int passes; 332 int dither; 333 }; 334 335 class JpegliColorQuantTestParam : public ::testing::TestWithParam<TestConfig> { 336 }; 337 338 TEST_P(JpegliColorQuantTestParam, JpegliColorQuantizeTest) { 339 TEST_LIBJPEG_SUPPORT(); 340 TestConfig config = GetParam(); 341 std::string testimage = "jxl/flower/flower_small.rgb.depth8.ppm"; 342 PackedPixelFile ppf0; 343 ASSERT_TRUE(ReadTestImage(testimage, &ppf0)); 344 EXPECT_EQ("RGB_D65_SRG_Rel_SRG", Description(ppf0.color_encoding)); 345 EXPECT_EQ(8, ppf0.info.bits_per_sample); 346 347 std::vector<uint8_t> compressed; 348 ASSERT_TRUE(EncodeWithLibjpeg(ppf0, 90, &compressed)); 349 350 PackedPixelFile ppf1; 351 JPGDecompressParams dparams1; 352 dparams1.two_pass_quant = (config.passes == 2); 353 dparams1.num_colors = config.num_colors; 354 dparams1.dither_mode = config.dither; 355 ASSERT_TRUE(DecodeWithLibjpeg(compressed, &ppf1, &dparams1)); 356 357 PackedPixelFile ppf2; 358 JpegDecompressParams dparams2; 359 dparams2.two_pass_quant = (config.passes == 2); 360 dparams2.num_colors = config.num_colors; 361 dparams2.dither_mode = config.dither; 362 ASSERT_TRUE(DecodeJpeg(compressed, dparams2, nullptr, &ppf2)); 363 364 double dist1 = Butteraugli3Norm(ppf0, ppf1); 365 double dist2 = Butteraugli3Norm(ppf0, ppf2); 366 printf("distance: %f vs %f\n", dist2, dist1); 367 if (config.passes == 1) { 368 if (config.num_colors == 16 && config.dither == 2) { 369 // TODO(szabadka) Fix this case. 370 EXPECT_LT(dist2, dist1 * 1.5); 371 } else { 372 EXPECT_LT(dist2, dist1 * 1.05); 373 } 374 } else if (config.num_colors > 64) { 375 // TODO(szabadka) Fix 2pass quantization for <= 64 colors. 376 EXPECT_LT(dist2, dist1 * 1.1); 377 } else if (config.num_colors > 32) { 378 EXPECT_LT(dist2, dist1 * 1.2); 379 } else { 380 EXPECT_LT(dist2, dist1 * 1.7); 381 } 382 } 383 384 std::vector<TestConfig> GenerateTests() { 385 std::vector<TestConfig> all_tests; 386 for (int num_colors = 8; num_colors <= 256; num_colors *= 2) { 387 for (int passes = 1; passes <= 2; ++passes) { 388 for (int dither = 0; dither < 3; dither += passes) { 389 TestConfig config; 390 config.num_colors = num_colors; 391 config.passes = passes; 392 config.dither = dither; 393 all_tests.push_back(config); 394 } 395 } 396 } 397 return all_tests; 398 } 399 400 std::ostream& operator<<(std::ostream& os, const TestConfig& c) { 401 static constexpr const char* kDitherModeStr[] = {"No", "Ordered", "FS"}; 402 os << c.passes << "pass"; 403 os << c.num_colors << "colors"; 404 os << kDitherModeStr[c.dither] << "dither"; 405 return os; 406 } 407 408 std::string TestDescription(const testing::TestParamInfo<TestConfig>& info) { 409 std::stringstream name; 410 name << info.param; 411 return name.str(); 412 } 413 414 JXL_GTEST_INSTANTIATE_TEST_SUITE_P(JpegliColorQuantTest, 415 JpegliColorQuantTestParam, 416 testing::ValuesIn(GenerateTests()), 417 TestDescription); 418 419 } // namespace 420 } // namespace extras 421 } // namespace jxl 422 #endif // JPEGXL_ENABLE_JPEGLI