jpegli_test.cc (14523B)
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 #include <stdint.h> 13 14 #include <cstddef> 15 #include <cstdint> 16 #include <cstdio> 17 #include <cstring> 18 #include <memory> 19 #include <ostream> 20 #include <sstream> 21 #include <string> 22 #include <utility> 23 #include <vector> 24 25 #include "lib/extras/dec/color_hints.h" 26 #include "lib/extras/dec/decode.h" 27 #include "lib/extras/dec/jpg.h" 28 #include "lib/extras/enc/encode.h" 29 #include "lib/extras/enc/jpegli.h" 30 #include "lib/extras/enc/jpg.h" 31 #include "lib/extras/packed_image.h" 32 #include "lib/jxl/base/span.h" 33 #include "lib/jxl/base/status.h" 34 #include "lib/jxl/color_encoding_internal.h" 35 #include "lib/jxl/test_image.h" 36 #include "lib/jxl/test_utils.h" 37 #include "lib/jxl/testing.h" 38 39 namespace jxl { 40 namespace extras { 41 namespace { 42 43 using test::Butteraugli3Norm; 44 using test::ButteraugliDistance; 45 using test::TestImage; 46 47 Status ReadTestImage(const std::string& pathname, PackedPixelFile* ppf) { 48 const std::vector<uint8_t> encoded = jxl::test::ReadTestData(pathname); 49 ColorHints color_hints; 50 if (pathname.find(".ppm") != std::string::npos) { 51 color_hints.Add("color_space", "RGB_D65_SRG_Rel_SRG"); 52 } else if (pathname.find(".pgm") != std::string::npos) { 53 color_hints.Add("color_space", "Gra_D65_Rel_SRG"); 54 } 55 return DecodeBytes(Bytes(encoded), color_hints, ppf); 56 } 57 58 std::vector<uint8_t> GetAppData(const std::vector<uint8_t>& compressed) { 59 std::vector<uint8_t> result; 60 size_t pos = 2; // After SOI 61 while (pos + 4 < compressed.size()) { 62 if (compressed[pos] != 0xff || compressed[pos + 1] < 0xe0 || 63 compressed[pos + 1] > 0xf0) { 64 break; 65 } 66 size_t len = (compressed[pos + 2] << 8) + compressed[pos + 3] + 2; 67 if (pos + len > compressed.size()) { 68 break; 69 } 70 result.insert(result.end(), &compressed[pos], &compressed[pos] + len); 71 pos += len; 72 } 73 return result; 74 } 75 76 Status DecodeWithLibjpeg(const std::vector<uint8_t>& compressed, 77 PackedPixelFile* ppf, 78 const JPGDecompressParams* dparams = nullptr) { 79 return DecodeImageJPG(Bytes(compressed), ColorHints(), ppf, 80 /*constraints=*/nullptr, dparams); 81 } 82 83 Status EncodeWithLibjpeg(const PackedPixelFile& ppf, int quality, 84 std::vector<uint8_t>* compressed) { 85 std::unique_ptr<Encoder> encoder = GetJPEGEncoder(); 86 encoder->SetOption("q", std::to_string(quality)); 87 EncodedImage encoded; 88 JXL_RETURN_IF_ERROR(encoder->Encode(ppf, &encoded, nullptr)); 89 JXL_RETURN_IF_ERROR(!encoded.bitstreams.empty()); 90 *compressed = std::move(encoded.bitstreams[0]); 91 return true; 92 } 93 94 std::string Description(const JxlColorEncoding& color_encoding) { 95 ColorEncoding c_enc; 96 JXL_CHECK(c_enc.FromExternal(color_encoding)); 97 return Description(c_enc); 98 } 99 100 float BitsPerPixel(const PackedPixelFile& ppf, 101 const std::vector<uint8_t>& compressed) { 102 const size_t num_pixels = ppf.info.xsize * ppf.info.ysize; 103 return compressed.size() * 8.0 / num_pixels; 104 } 105 106 TEST(JpegliTest, JpegliSRGBDecodeTest) { 107 TEST_LIBJPEG_SUPPORT(); 108 std::string testimage = "jxl/flower/flower_small.rgb.depth8.ppm"; 109 PackedPixelFile ppf0; 110 ASSERT_TRUE(ReadTestImage(testimage, &ppf0)); 111 EXPECT_EQ("RGB_D65_SRG_Rel_SRG", Description(ppf0.color_encoding)); 112 EXPECT_EQ(8, ppf0.info.bits_per_sample); 113 114 std::vector<uint8_t> compressed; 115 ASSERT_TRUE(EncodeWithLibjpeg(ppf0, 90, &compressed)); 116 117 PackedPixelFile ppf1; 118 ASSERT_TRUE(DecodeWithLibjpeg(compressed, &ppf1)); 119 PackedPixelFile ppf2; 120 JpegDecompressParams dparams; 121 ASSERT_TRUE(DecodeJpeg(compressed, dparams, nullptr, &ppf2)); 122 EXPECT_LT(ButteraugliDistance(ppf0, ppf2), ButteraugliDistance(ppf0, ppf1)); 123 } 124 125 TEST(JpegliTest, JpegliGrayscaleDecodeTest) { 126 TEST_LIBJPEG_SUPPORT(); 127 std::string testimage = "jxl/flower/flower_small.g.depth8.pgm"; 128 PackedPixelFile ppf0; 129 ASSERT_TRUE(ReadTestImage(testimage, &ppf0)); 130 EXPECT_EQ("Gra_D65_Rel_SRG", Description(ppf0.color_encoding)); 131 EXPECT_EQ(8, ppf0.info.bits_per_sample); 132 133 std::vector<uint8_t> compressed; 134 ASSERT_TRUE(EncodeWithLibjpeg(ppf0, 90, &compressed)); 135 136 PackedPixelFile ppf1; 137 ASSERT_TRUE(DecodeWithLibjpeg(compressed, &ppf1)); 138 PackedPixelFile ppf2; 139 JpegDecompressParams dparams; 140 ASSERT_TRUE(DecodeJpeg(compressed, dparams, nullptr, &ppf2)); 141 EXPECT_LT(ButteraugliDistance(ppf0, ppf2), ButteraugliDistance(ppf0, ppf1)); 142 } 143 144 TEST(JpegliTest, JpegliXYBEncodeTest) { 145 TEST_LIBJPEG_SUPPORT(); 146 std::string testimage = "jxl/flower/flower_small.rgb.depth8.ppm"; 147 PackedPixelFile ppf_in; 148 ASSERT_TRUE(ReadTestImage(testimage, &ppf_in)); 149 EXPECT_EQ("RGB_D65_SRG_Rel_SRG", Description(ppf_in.color_encoding)); 150 EXPECT_EQ(8, ppf_in.info.bits_per_sample); 151 152 std::vector<uint8_t> compressed; 153 JpegSettings settings; 154 settings.xyb = true; 155 ASSERT_TRUE(EncodeJpeg(ppf_in, settings, nullptr, &compressed)); 156 157 PackedPixelFile ppf_out; 158 ASSERT_TRUE(DecodeWithLibjpeg(compressed, &ppf_out)); 159 EXPECT_THAT(BitsPerPixel(ppf_in, compressed), IsSlightlyBelow(1.45f)); 160 EXPECT_THAT(ButteraugliDistance(ppf_in, ppf_out), IsSlightlyBelow(1.32f)); 161 } 162 163 TEST(JpegliTest, JpegliDecodeTestLargeSmoothArea) { 164 TEST_LIBJPEG_SUPPORT(); 165 TestImage t; 166 const size_t xsize = 2070; 167 const size_t ysize = 1063; 168 t.SetDimensions(xsize, ysize).SetChannels(3); 169 t.SetAllBitDepths(8).SetEndianness(JXL_NATIVE_ENDIAN); 170 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 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_THAT(BitsPerPixel(ppf_in, compressed), IsSlightlyBelow(1.7f)); 209 EXPECT_THAT(ButteraugliDistance(ppf_in, ppf_out), IsSlightlyBelow(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_THAT(BitsPerPixel(ppf_in, compressed), IsSlightlyBelow(1.85f)); 251 EXPECT_THAT(ButteraugliDistance(ppf_in, ppf_out), IsSlightlyBelow(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("RGB_D65_202_Rel_HLG", 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_THAT(BitsPerPixel(ppf_in, compressed), IsSlightlyBelow(2.95f)); 271 EXPECT_THAT(ButteraugliDistance(ppf_in, ppf_out), IsSlightlyBelow(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