summaryrefslogtreecommitdiffstats
path: root/third_party/jpeg-xl/lib/jxl/gradient_test.cc
blob: 055a419f5b9755f4a16169762bc9ec16634a03fe (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
// Copyright (c) the JPEG XL Project Authors. All rights reserved.
//
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

#include <jxl/cms.h>
#include <math.h>
#include <stddef.h>
#include <stdint.h>

#include <algorithm>
#include <array>
#include <cmath>
#include <utility>
#include <vector>

#include "lib/jxl/base/common.h"
#include "lib/jxl/base/compiler_specific.h"
#include "lib/jxl/base/data_parallel.h"
#include "lib/jxl/base/span.h"
#include "lib/jxl/codec_in_out.h"
#include "lib/jxl/color_encoding_internal.h"
#include "lib/jxl/enc_params.h"
#include "lib/jxl/image.h"
#include "lib/jxl/image_bundle.h"
#include "lib/jxl/image_ops.h"
#include "lib/jxl/test_utils.h"
#include "lib/jxl/testing.h"

namespace jxl {

struct AuxOut;

namespace {

// Returns distance of point p to line p0..p1, the result is signed and is not
// normalized.
double PointLineDist(double x0, double y0, double x1, double y1, double x,
                     double y) {
  return (y1 - y0) * x - (x1 - x0) * y + x1 * y0 - y1 * x0;
}

// Generates a test image with a gradient from one color to another.
// Angle in degrees, colors can be given in hex as 0xRRGGBB. The angle is the
// angle in which the change direction happens.
Image3F GenerateTestGradient(uint32_t color0, uint32_t color1, double angle,
                             size_t xsize, size_t ysize) {
  Image3F image(xsize, ysize);

  double x0 = xsize / 2;
  double y0 = ysize / 2;
  double x1 = x0 + std::sin(angle / 360.0 * 2.0 * kPi);
  double y1 = y0 + std::cos(angle / 360.0 * 2.0 * kPi);

  double maxdist =
      std::max<double>(fabs(PointLineDist(x0, y0, x1, y1, 0, 0)),
                       fabs(PointLineDist(x0, y0, x1, y1, xsize, 0)));

  for (size_t c = 0; c < 3; ++c) {
    float c0 = ((color0 >> (8 * (2 - c))) & 255);
    float c1 = ((color1 >> (8 * (2 - c))) & 255);
    for (size_t y = 0; y < ysize; ++y) {
      float* row = image.PlaneRow(c, y);
      for (size_t x = 0; x < xsize; ++x) {
        double dist = PointLineDist(x0, y0, x1, y1, x, y);
        double v = ((dist / maxdist) + 1.0) / 2.0;
        float color = c0 * (1.0 - v) + c1 * v;
        row[x] = color;
      }
    }
  }

  return image;
}

// Computes the max of the horizontal and vertical second derivative for each
// pixel, where second derivative means absolute value of difference of left
// delta and right delta (top/bottom for vertical direction).
// The radius over which the derivative is computed is only 1 pixel and it only
// checks two angles (hor and ver), but this approximation works well enough.
static ImageF Gradient2(const ImageF& image) {
  size_t xsize = image.xsize();
  size_t ysize = image.ysize();
  ImageF image2(image.xsize(), image.ysize());
  for (size_t y = 1; y + 1 < ysize; y++) {
    const auto* JXL_RESTRICT row0 = image.Row(y - 1);
    const auto* JXL_RESTRICT row1 = image.Row(y);
    const auto* JXL_RESTRICT row2 = image.Row(y + 1);
    auto* row_out = image2.Row(y);
    for (size_t x = 1; x + 1 < xsize; x++) {
      float ddx = (row1[x] - row1[x - 1]) - (row1[x + 1] - row1[x]);
      float ddy = (row1[x] - row0[x]) - (row2[x] - row1[x]);
      row_out[x] = std::max(fabsf(ddx), fabsf(ddy));
    }
  }
  // Copy to the borders
  if (ysize > 2) {
    auto* JXL_RESTRICT row0 = image2.Row(0);
    const auto* JXL_RESTRICT row1 = image2.Row(1);
    const auto* JXL_RESTRICT row2 = image2.Row(ysize - 2);
    auto* JXL_RESTRICT row3 = image2.Row(ysize - 1);
    for (size_t x = 1; x + 1 < xsize; x++) {
      row0[x] = row1[x];
      row3[x] = row2[x];
    }
  } else {
    const auto* row0_in = image.Row(0);
    const auto* row1_in = image.Row(ysize - 1);
    auto* row0_out = image2.Row(0);
    auto* row1_out = image2.Row(ysize - 1);
    for (size_t x = 1; x + 1 < xsize; x++) {
      // Image too narrow, take first derivative instead
      row0_out[x] = row1_out[x] = fabsf(row0_in[x] - row1_in[x]);
    }
  }
  if (xsize > 2) {
    for (size_t y = 0; y < ysize; y++) {
      auto* row = image2.Row(y);
      row[0] = row[1];
      row[xsize - 1] = row[xsize - 2];
    }
  } else {
    for (size_t y = 0; y < ysize; y++) {
      const auto* JXL_RESTRICT row_in = image.Row(y);
      auto* row_out = image2.Row(y);
      // Image too narrow, take first derivative instead
      row_out[0] = row_out[xsize - 1] = fabsf(row_in[0] - row_in[xsize - 1]);
    }
  }
  return image2;
}

static Image3F Gradient2(const Image3F& image) {
  return Image3F(Gradient2(image.Plane(0)), Gradient2(image.Plane(1)),
                 Gradient2(image.Plane(2)));
}

/*
Tests if roundtrip with jxl on a gradient image doesn't cause banding.
Only tests if use_gradient is true. Set to false for debugging to see the
distance values.
Angle in degrees, colors can be given in hex as 0xRRGGBB.
*/
void TestGradient(ThreadPool* pool, uint32_t color0, uint32_t color1,
                  size_t xsize, size_t ysize, float angle, bool fast_mode,
                  float butteraugli_distance, bool use_gradient = true) {
  CompressParams cparams;
  cparams.butteraugli_distance = butteraugli_distance;
  if (fast_mode) {
    cparams.speed_tier = SpeedTier::kSquirrel;
  }
  Image3F gradient = GenerateTestGradient(color0, color1, angle, xsize, ysize);

  CodecInOut io;
  io.metadata.m.SetUintSamples(8);
  io.metadata.m.color_encoding = ColorEncoding::SRGB();
  io.SetFromImage(std::move(gradient), io.metadata.m.color_encoding);

  CodecInOut io2;

  std::vector<uint8_t> compressed;
  EXPECT_TRUE(test::EncodeFile(cparams, &io, &compressed, pool));
  EXPECT_TRUE(test::DecodeFile({}, Bytes(compressed), &io2, pool));
  EXPECT_TRUE(io2.Main().TransformTo(io2.metadata.m.color_encoding,
                                     *JxlGetDefaultCms(), pool));

  if (use_gradient) {
    // Test that the gradient map worked. For that, we take a second derivative
    // of the image with Gradient2 to measure how linear the change is in x and
    // y direction. For a well handled gradient, we expect max values around
    // 0.1, while if there is noticeable banding, which means the gradient map
    // failed, the values are around 0.5-1.0 (regardless of
    // butteraugli_distance).
    Image3F gradient2 = Gradient2(*io2.Main().color());

    std::array<float, 3> image_max;
    Image3Max(gradient2, &image_max);

    // TODO(jyrki): These values used to work with 0.2, 0.2, 0.2.
    EXPECT_LE(image_max[0], 3.15);
    EXPECT_LE(image_max[1], 1.72);
    EXPECT_LE(image_max[2], 5.05);
  }
}

static constexpr bool fast_mode = true;

TEST(GradientTest, SteepGradient) {
  test::ThreadPoolForTests pool(8);
  // Relatively steep gradients, colors from the sky of stp.png
  TestGradient(&pool, 0xd99d58, 0x889ab1, 512, 512, 90, fast_mode, 3.0);
}

TEST(GradientTest, SubtleGradient) {
  test::ThreadPoolForTests pool(8);
  // Very subtle gradient
  TestGradient(&pool, 0xb89b7b, 0xa89b8d, 512, 512, 90, fast_mode, 4.0);
}

}  // namespace
}  // namespace jxl