gradient_test.cc (7626B)
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 #include <jxl/cms.h> 7 #include <jxl/memory_manager.h> 8 9 #include <algorithm> 10 #include <cmath> 11 #include <cstddef> 12 #include <cstdint> 13 #include <utility> 14 #include <vector> 15 16 #include "lib/jxl/base/common.h" 17 #include "lib/jxl/base/compiler_specific.h" 18 #include "lib/jxl/base/data_parallel.h" 19 #include "lib/jxl/base/span.h" 20 #include "lib/jxl/codec_in_out.h" 21 #include "lib/jxl/color_encoding_internal.h" 22 #include "lib/jxl/common.h" // SpeedTier 23 #include "lib/jxl/enc_params.h" 24 #include "lib/jxl/image.h" 25 #include "lib/jxl/image_bundle.h" 26 #include "lib/jxl/image_ops.h" 27 #include "lib/jxl/test_memory_manager.h" 28 #include "lib/jxl/test_utils.h" 29 #include "lib/jxl/testing.h" 30 31 namespace jxl { 32 33 struct AuxOut; 34 35 namespace { 36 37 // Returns distance of point p to line p0..p1, the result is signed and is not 38 // normalized. 39 double PointLineDist(double x0, double y0, double x1, double y1, double x, 40 double y) { 41 return (y1 - y0) * x - (x1 - x0) * y + x1 * y0 - y1 * x0; 42 } 43 44 // Generates a test image with a gradient from one color to another. 45 // Angle in degrees, colors can be given in hex as 0xRRGGBB. The angle is the 46 // angle in which the change direction happens. 47 Image3F GenerateTestGradient(uint32_t color0, uint32_t color1, double angle, 48 size_t xsize, size_t ysize) { 49 JXL_TEST_ASSIGN_OR_DIE( 50 Image3F image, Image3F::Create(jxl::test::MemoryManager(), xsize, ysize)); 51 52 double x0 = xsize / 2.0; 53 double y0 = ysize / 2.0; 54 double x1 = x0 + std::sin(angle / 360.0 * 2.0 * kPi); 55 double y1 = y0 + std::cos(angle / 360.0 * 2.0 * kPi); 56 57 double maxdist = 58 std::max<double>(fabs(PointLineDist(x0, y0, x1, y1, 0, 0)), 59 fabs(PointLineDist(x0, y0, x1, y1, xsize, 0))); 60 61 for (size_t c = 0; c < 3; ++c) { 62 float c0 = ((color0 >> (8 * (2 - c))) & 255); 63 float c1 = ((color1 >> (8 * (2 - c))) & 255); 64 for (size_t y = 0; y < ysize; ++y) { 65 float* row = image.PlaneRow(c, y); 66 for (size_t x = 0; x < xsize; ++x) { 67 double dist = PointLineDist(x0, y0, x1, y1, x, y); 68 double v = ((dist / maxdist) + 1.0) / 2.0; 69 float color = c0 * (1.0 - v) + c1 * v; 70 row[x] = color; 71 } 72 } 73 } 74 75 return image; 76 } 77 78 // Computes the max of the horizontal and vertical second derivative for each 79 // pixel, where second derivative means absolute value of difference of left 80 // delta and right delta (top/bottom for vertical direction). 81 // The radius over which the derivative is computed is only 1 pixel and it only 82 // checks two angles (hor and ver), but this approximation works well enough. 83 Image3F Gradient2(const Image3F& image) { 84 JxlMemoryManager* memory_manager = jxl::test::MemoryManager(); 85 size_t xsize = image.xsize(); 86 size_t ysize = image.ysize(); 87 JXL_TEST_ASSIGN_OR_DIE(Image3F image2, 88 Image3F::Create(memory_manager, xsize, ysize)); 89 for (size_t c = 0; c < 3; ++c) { 90 for (size_t y = 1; y + 1 < ysize; y++) { 91 const auto* JXL_RESTRICT row0 = image.ConstPlaneRow(c, y - 1); 92 const auto* JXL_RESTRICT row1 = image.ConstPlaneRow(c, y); 93 const auto* JXL_RESTRICT row2 = image.ConstPlaneRow(c, y + 1); 94 auto* row_out = image2.PlaneRow(c, y); 95 for (size_t x = 1; x + 1 < xsize; x++) { 96 float ddx = (row1[x] - row1[x - 1]) - (row1[x + 1] - row1[x]); 97 float ddy = (row1[x] - row0[x]) - (row2[x] - row1[x]); 98 row_out[x] = std::max(fabsf(ddx), fabsf(ddy)); 99 } 100 } 101 // Copy to the borders 102 if (ysize > 2) { 103 auto* JXL_RESTRICT row0 = image2.PlaneRow(c, 0); 104 const auto* JXL_RESTRICT row1 = image2.PlaneRow(c, 1); 105 const auto* JXL_RESTRICT row2 = image2.PlaneRow(c, ysize - 2); 106 auto* JXL_RESTRICT row3 = image2.PlaneRow(c, ysize - 1); 107 for (size_t x = 1; x + 1 < xsize; x++) { 108 row0[x] = row1[x]; 109 row3[x] = row2[x]; 110 } 111 } else { 112 const auto* row0_in = image.ConstPlaneRow(c, 0); 113 const auto* row1_in = image.ConstPlaneRow(c, ysize - 1); 114 auto* row0_out = image2.PlaneRow(c, 0); 115 auto* row1_out = image2.PlaneRow(c, ysize - 1); 116 for (size_t x = 1; x + 1 < xsize; x++) { 117 // Image too narrow, take first derivative instead 118 row0_out[x] = row1_out[x] = fabsf(row0_in[x] - row1_in[x]); 119 } 120 } 121 if (xsize > 2) { 122 for (size_t y = 0; y < ysize; y++) { 123 auto* row = image2.PlaneRow(c, y); 124 row[0] = row[1]; 125 row[xsize - 1] = row[xsize - 2]; 126 } 127 } else { 128 for (size_t y = 0; y < ysize; y++) { 129 const auto* JXL_RESTRICT row_in = image.ConstPlaneRow(c, y); 130 auto* row_out = image2.PlaneRow(c, y); 131 // Image too narrow, take first derivative instead 132 row_out[0] = row_out[xsize - 1] = fabsf(row_in[0] - row_in[xsize - 1]); 133 } 134 } 135 } 136 return image2; 137 } 138 139 /* 140 Tests if roundtrip with jxl on a gradient image doesn't cause banding. 141 Only tests if use_gradient is true. Set to false for debugging to see the 142 distance values. 143 Angle in degrees, colors can be given in hex as 0xRRGGBB. 144 */ 145 void TestGradient(ThreadPool* pool, uint32_t color0, uint32_t color1, 146 size_t xsize, size_t ysize, float angle, bool fast_mode, 147 float butteraugli_distance, bool use_gradient = true) { 148 JxlMemoryManager* memory_manager = jxl::test::MemoryManager(); 149 CompressParams cparams; 150 cparams.butteraugli_distance = butteraugli_distance; 151 if (fast_mode) { 152 cparams.speed_tier = SpeedTier::kSquirrel; 153 } 154 Image3F gradient = GenerateTestGradient(color0, color1, angle, xsize, ysize); 155 156 CodecInOut io{memory_manager}; 157 io.metadata.m.SetUintSamples(8); 158 io.metadata.m.color_encoding = ColorEncoding::SRGB(); 159 ASSERT_TRUE( 160 io.SetFromImage(std::move(gradient), io.metadata.m.color_encoding)); 161 162 CodecInOut io2{memory_manager}; 163 164 std::vector<uint8_t> compressed; 165 EXPECT_TRUE(test::EncodeFile(cparams, &io, &compressed, pool)); 166 EXPECT_TRUE(test::DecodeFile({}, Bytes(compressed), &io2, pool)); 167 EXPECT_TRUE(io2.Main().TransformTo(io2.metadata.m.color_encoding, 168 *JxlGetDefaultCms(), pool)); 169 170 if (use_gradient) { 171 // Test that the gradient map worked. For that, we take a second derivative 172 // of the image with Gradient2 to measure how linear the change is in x and 173 // y direction. For a well handled gradient, we expect max values around 174 // 0.1, while if there is noticeable banding, which means the gradient map 175 // failed, the values are around 0.5-1.0 (regardless of 176 // butteraugli_distance). 177 Image3F gradient2 = Gradient2(*io2.Main().color()); 178 179 // TODO(jyrki): These values used to work with 0.2, 0.2, 0.2. 180 float image_min; 181 float image_max; 182 ImageMinMax(gradient2.Plane(0), &image_min, &image_max); 183 EXPECT_LE(image_max, 3.15); 184 ImageMinMax(gradient2.Plane(1), &image_min, &image_max); 185 EXPECT_LE(image_max, 1.72); 186 ImageMinMax(gradient2.Plane(2), &image_min, &image_max); 187 EXPECT_LE(image_max, 5.05); 188 } 189 } 190 191 constexpr bool fast_mode = true; 192 193 TEST(GradientTest, SteepGradient) { 194 test::ThreadPoolForTests pool(8); 195 // Relatively steep gradients, colors from the sky of stp.png 196 TestGradient(pool.get(), 0xd99d58, 0x889ab1, 512, 512, 90, fast_mode, 3.0); 197 } 198 199 TEST(GradientTest, SubtleGradient) { 200 test::ThreadPoolForTests pool(8); 201 // Very subtle gradient 202 TestGradient(pool.get(), 0xb89b7b, 0xa89b8d, 512, 512, 90, fast_mode, 4.0); 203 } 204 205 } // namespace 206 } // namespace jxl