Coverage Report

Created: 2025-07-23 08:18

/src/libjxl/lib/jxl/enc_noise.cc
Line
Count
Source (jump to first uncovered line)
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 "lib/jxl/enc_noise.h"
7
8
#include <algorithm>
9
#include <cmath>
10
#include <cstdint>
11
#include <cstdio>
12
#include <cstdlib>
13
#include <numeric>
14
#include <utility>
15
#include <vector>
16
17
#include "lib/jxl/base/common.h"
18
#include "lib/jxl/base/status.h"
19
#include "lib/jxl/enc_aux_out.h"
20
#include "lib/jxl/enc_bit_writer.h"
21
#include "lib/jxl/enc_optimize.h"
22
#include "lib/jxl/image.h"
23
#include "lib/jxl/noise.h"
24
25
namespace jxl {
26
namespace {
27
28
using OptimizeArray = optimize::Array<double, NoiseParams::kNumNoisePoints>;
29
30
float GetScoreSumsOfAbsoluteDifferences(const Image3F& opsin, const int x,
31
0
                                        const int y, const int block_size) {
32
0
  const int small_bl_size_x = 3;
33
0
  const int small_bl_size_y = 4;
34
0
  const int kNumSAD =
35
0
      (block_size - small_bl_size_x) * (block_size - small_bl_size_y);
36
  // block_size x block_size reference pixels
37
0
  int counter = 0;
38
0
  const int offset = 2;
39
40
0
  std::vector<float> sad(kNumSAD, 0);
41
0
  for (int y_bl = 0; y_bl + small_bl_size_y < block_size; ++y_bl) {
42
0
    for (int x_bl = 0; x_bl + small_bl_size_x < block_size; ++x_bl) {
43
0
      float sad_sum = 0;
44
      // size of the center patch, we compare all the patches inside window with
45
      // the center one
46
0
      for (int cy = 0; cy < small_bl_size_y; ++cy) {
47
0
        for (int cx = 0; cx < small_bl_size_x; ++cx) {
48
0
          float wnd = 0.5f * (opsin.PlaneRow(1, y + y_bl + cy)[x + x_bl + cx] +
49
0
                              opsin.PlaneRow(0, y + y_bl + cy)[x + x_bl + cx]);
50
0
          float center =
51
0
              0.5f * (opsin.PlaneRow(1, y + offset + cy)[x + offset + cx] +
52
0
                      opsin.PlaneRow(0, y + offset + cy)[x + offset + cx]);
53
0
          sad_sum += std::abs(center - wnd);
54
0
        }
55
0
      }
56
0
      sad[counter++] = sad_sum;
57
0
    }
58
0
  }
59
0
  const int kSamples = (kNumSAD) / 2;
60
  // As with ROAD (rank order absolute distance), we keep the smallest half of
61
  // the values in SAD (we use here the more robust patch SAD instead of
62
  // absolute single-pixel differences).
63
0
  std::sort(sad.begin(), sad.end());
64
0
  const float total_sad_sum =
65
0
      std::accumulate(sad.begin(), sad.begin() + kSamples, 0.0f);
66
0
  return total_sad_sum / kSamples;
67
0
}
68
69
class NoiseHistogram {
70
 public:
71
  static constexpr int kBins = 256;
72
73
0
  NoiseHistogram() { std::fill(bins, bins + kBins, 0); }
74
75
0
  void Increment(const float x) { bins[Index(x)] += 1; }
76
0
  int Get(const float x) const { return bins[Index(x)]; }
77
0
  int Bin(const size_t bin) const { return bins[bin]; }
78
79
0
  int Mode() const {
80
0
    size_t max_idx = 0;
81
0
    for (size_t i = 0; i < kBins; i++) {
82
0
      if (bins[i] > bins[max_idx]) max_idx = i;
83
0
    }
84
0
    return max_idx;
85
0
  }
86
87
0
  double Quantile(double q01) const {
88
0
    const int64_t total = std::accumulate(bins, bins + kBins, int64_t{1});
89
0
    const int64_t target = static_cast<int64_t>(q01 * total);
90
0
    // Until sum >= target:
91
0
    int64_t sum = 0;
92
0
    size_t i = 0;
93
0
    for (; i < kBins; ++i) {
94
0
      sum += bins[i];
95
0
      // Exact match: assume middle of bin i
96
0
      if (sum == target) {
97
0
        return i + 0.5;
98
0
      }
99
0
      if (sum > target) break;
100
0
    }
101
0
102
0
    // Next non-empty bin (in case histogram is sparsely filled)
103
0
    size_t next = i + 1;
104
0
    while (next < kBins && bins[next] == 0) {
105
0
      ++next;
106
0
    }
107
0
108
0
    // Linear interpolation according to how far into next we went
109
0
    const double excess = target - sum;
110
0
    const double weight_next = bins[Index(next)] / excess;
111
0
    return ClampX(next * weight_next + i * (1.0 - weight_next));
112
0
  }
113
114
  // Inter-quartile range
115
0
  double IQR() const { return Quantile(0.75) - Quantile(0.25); }
116
117
 private:
118
  template <typename T>
119
0
  T ClampX(const T x) const {
120
0
    return jxl::Clamp1<T>(x, 0, kBins - 1);
121
0
  }
Unexecuted instantiation: enc_noise.cc:int jxl::(anonymous namespace)::NoiseHistogram::ClampX<int>(int) const
Unexecuted instantiation: enc_noise.cc:double jxl::(anonymous namespace)::NoiseHistogram::ClampX<double>(double) const
122
0
  size_t Index(const float x) const { return ClampX(static_cast<int>(x)); }
123
124
  uint32_t bins[kBins];
125
};
126
127
std::vector<float> GetSADScoresForPatches(const Image3F& opsin,
128
                                          const size_t block_s,
129
                                          const size_t num_bin,
130
0
                                          NoiseHistogram* sad_histogram) {
131
0
  std::vector<float> sad_scores(
132
0
      (opsin.ysize() / block_s) * (opsin.xsize() / block_s), 0.0f);
133
134
0
  int block_index = 0;
135
136
0
  for (size_t y = 0; y + block_s <= opsin.ysize(); y += block_s) {
137
0
    for (size_t x = 0; x + block_s <= opsin.xsize(); x += block_s) {
138
0
      float sad_sc = GetScoreSumsOfAbsoluteDifferences(opsin, x, y, block_s);
139
0
      sad_scores[block_index++] = sad_sc;
140
0
      sad_histogram->Increment(sad_sc * num_bin);
141
0
    }
142
0
  }
143
0
  return sad_scores;
144
0
}
145
146
0
float GetSADThreshold(const NoiseHistogram& histogram, const int num_bin) {
147
  // Here we assume that the most patches with similar SAD value is a "flat"
148
  // patches. However, some images might contain regular texture part and
149
  // generate second strong peak at the histogram
150
  // TODO(user) handle bimodal and heavy-tailed case
151
0
  const int mode = histogram.Mode();
152
0
  return static_cast<float>(mode) / NoiseHistogram::kBins;
153
0
}
154
155
// loss = sum asym * (F(x) - nl)^2 + kReg * num_points * sum (w[i] - w[i+1])^2
156
// where asym = 1 if F(x) < nl, kAsym if F(x) > nl.
157
struct LossFunction {
158
0
  explicit LossFunction(std::vector<NoiseLevel> nl0) : nl(std::move(nl0)) {}
159
160
  double Compute(const OptimizeArray& w, OptimizeArray* df,
161
0
                 bool skip_regularization = false) const {
162
0
    constexpr double kReg = 0.005;
163
0
    constexpr double kAsym = 1.1;
164
0
    double loss_function = 0;
165
0
    for (size_t i = 0; i < w.size(); i++) {
166
0
      (*df)[i] = 0;
167
0
    }
168
0
    for (auto ind : nl) {
169
0
      std::pair<int, float> pos = IndexAndFrac(ind.intensity);
170
0
      JXL_DASSERT(pos.first >= 0 && static_cast<size_t>(pos.first) <
171
0
                                        NoiseParams::kNumNoisePoints - 1);
172
0
      double low = w[pos.first];
173
0
      double hi = w[pos.first + 1];
174
0
      double val = low * (1.0f - pos.second) + hi * pos.second;
175
0
      double dist = val - ind.noise_level;
176
0
      if (dist > 0) {
177
0
        loss_function += kAsym * dist * dist;
178
0
        (*df)[pos.first] -= kAsym * (1.0f - pos.second) * dist;
179
0
        (*df)[pos.first + 1] -= kAsym * pos.second * dist;
180
0
      } else {
181
0
        loss_function += dist * dist;
182
0
        (*df)[pos.first] -= (1.0f - pos.second) * dist;
183
0
        (*df)[pos.first + 1] -= pos.second * dist;
184
0
      }
185
0
    }
186
0
    if (skip_regularization) return loss_function;
187
0
    for (size_t i = 0; i + 1 < w.size(); i++) {
188
0
      double diff = w[i] - w[i + 1];
189
0
      loss_function += kReg * nl.size() * diff * diff;
190
0
      (*df)[i] -= kReg * diff * nl.size();
191
0
      (*df)[i + 1] += kReg * diff * nl.size();
192
0
    }
193
0
    return loss_function;
194
0
  }
195
196
  std::vector<NoiseLevel> nl;
197
};
198
199
void OptimizeNoiseParameters(const std::vector<NoiseLevel>& noise_level,
200
0
                             NoiseParams* noise_params, float mul) {
201
0
  constexpr double kMaxError = 1e-3;
202
0
  static const double kPrecision = 1e-8;
203
0
  static const int kMaxIter = 40;
204
205
0
  float avg = 0;
206
0
  for (const NoiseLevel& nl : noise_level) {
207
0
    avg += nl.noise_level;
208
0
  }
209
0
  avg /= noise_level.size();
210
211
0
  LossFunction loss_function(noise_level);
212
0
  OptimizeArray parameter_vector;
213
0
  for (size_t i = 0; i < parameter_vector.size(); i++) {
214
0
    parameter_vector[i] = avg;
215
0
  }
216
217
0
  parameter_vector = optimize::OptimizeWithScaledConjugateGradientMethod(
218
0
      loss_function, parameter_vector, kPrecision, kMaxIter);
219
220
  // Clamp here to account codestream limits.
221
0
  for (size_t i = 0; i < parameter_vector.size(); i++) {
222
0
    parameter_vector[i] =
223
0
        jxl::Clamp1<float>(parameter_vector[i] * mul, 0.0f, kNoiseLutMax);
224
0
  }
225
226
0
  OptimizeArray unused;
227
0
  float loss = loss_function.Compute(parameter_vector, &unused,
228
0
                                     /*skip_regularization=*/true) /
229
0
               noise_level.size();
230
231
  // Approximation went too badly: escape with no noise at all.
232
0
  if (loss > kMaxError) {
233
0
    noise_params->Clear();
234
0
    return;
235
0
  }
236
237
0
  for (size_t i = 0; i < parameter_vector.size(); i++) {
238
0
    noise_params->lut[i] = parameter_vector[i];
239
0
  }
240
0
}
241
242
std::vector<NoiseLevel> GetNoiseLevel(
243
    const Image3F& opsin, const std::vector<float>& texture_strength,
244
0
    const float threshold, const size_t block_s) {
245
0
  std::vector<NoiseLevel> noise_level_per_intensity;
246
247
0
  const int filt_size = 1;
248
0
  static const float kLaplFilter[filt_size * 2 + 1][filt_size * 2 + 1] = {
249
0
      {-0.25f, -1.0f, -0.25f},
250
0
      {-1.0f, 5.0f, -1.0f},
251
0
      {-0.25f, -1.0f, -0.25f},
252
0
  };
253
254
  // The noise model is built based on channel 0.5 * (X+Y) as we notice that it
255
  // is similar to the model 0.5 * (Y-X)
256
0
  size_t patch_index = 0;
257
258
0
  for (size_t y = 0; y + block_s <= opsin.ysize(); y += block_s) {
259
0
    for (size_t x = 0; x + block_s <= opsin.xsize(); x += block_s) {
260
0
      if (texture_strength[patch_index] <= threshold) {
261
        // Calculate mean value
262
0
        float mean_int = 0;
263
0
        for (size_t y_bl = 0; y_bl < block_s; ++y_bl) {
264
0
          for (size_t x_bl = 0; x_bl < block_s; ++x_bl) {
265
0
            mean_int += 0.5f * (opsin.PlaneRow(1, y + y_bl)[x + x_bl] +
266
0
                                opsin.PlaneRow(0, y + y_bl)[x + x_bl]);
267
0
          }
268
0
        }
269
0
        mean_int /= block_s * block_s;
270
271
        // Calculate Noise level
272
0
        float noise_level = 0;
273
0
        size_t count = 0;
274
0
        for (size_t y_bl = 0; y_bl < block_s; ++y_bl) {
275
0
          for (size_t x_bl = 0; x_bl < block_s; ++x_bl) {
276
0
            float filtered_value = 0;
277
0
            for (int y_f = -1 * filt_size; y_f <= filt_size; ++y_f) {
278
0
              if ((static_cast<ssize_t>(y_bl) + y_f) >= 0 &&
279
0
                  (y_bl + y_f) < block_s) {
280
0
                for (int x_f = -1 * filt_size; x_f <= filt_size; ++x_f) {
281
0
                  if ((static_cast<ssize_t>(x_bl) + x_f) >= 0 &&
282
0
                      (x_bl + x_f) < block_s) {
283
0
                    filtered_value +=
284
0
                        0.5f *
285
0
                        (opsin.PlaneRow(1, y + y_bl + y_f)[x + x_bl + x_f] +
286
0
                         opsin.PlaneRow(0, y + y_bl + y_f)[x + x_bl + x_f]) *
287
0
                        kLaplFilter[y_f + filt_size][x_f + filt_size];
288
0
                  } else {
289
0
                    filtered_value +=
290
0
                        0.5f *
291
0
                        (opsin.PlaneRow(1, y + y_bl + y_f)[x + x_bl - x_f] +
292
0
                         opsin.PlaneRow(0, y + y_bl + y_f)[x + x_bl - x_f]) *
293
0
                        kLaplFilter[y_f + filt_size][x_f + filt_size];
294
0
                  }
295
0
                }
296
0
              } else {
297
0
                for (int x_f = -1 * filt_size; x_f <= filt_size; ++x_f) {
298
0
                  if ((static_cast<ssize_t>(x_bl) + x_f) >= 0 &&
299
0
                      (x_bl + x_f) < block_s) {
300
0
                    filtered_value +=
301
0
                        0.5f *
302
0
                        (opsin.PlaneRow(1, y + y_bl - y_f)[x + x_bl + x_f] +
303
0
                         opsin.PlaneRow(0, y + y_bl - y_f)[x + x_bl + x_f]) *
304
0
                        kLaplFilter[y_f + filt_size][x_f + filt_size];
305
0
                  } else {
306
0
                    filtered_value +=
307
0
                        0.5f *
308
0
                        (opsin.PlaneRow(1, y + y_bl - y_f)[x + x_bl - x_f] +
309
0
                         opsin.PlaneRow(0, y + y_bl - y_f)[x + x_bl - x_f]) *
310
0
                        kLaplFilter[y_f + filt_size][x_f + filt_size];
311
0
                  }
312
0
                }
313
0
              }
314
0
            }
315
0
            noise_level += std::abs(filtered_value);
316
0
            ++count;
317
0
          }
318
0
        }
319
0
        noise_level /= count;
320
0
        NoiseLevel nl;
321
0
        nl.intensity = mean_int;
322
0
        nl.noise_level = noise_level;
323
0
        noise_level_per_intensity.push_back(nl);
324
0
      }
325
0
      ++patch_index;
326
0
    }
327
0
  }
328
0
  return noise_level_per_intensity;
329
0
}
330
331
0
Status EncodeFloatParam(float val, float precision, BitWriter* writer) {
332
0
  JXL_ENSURE(val >= 0);
333
0
  const int absval_quant = static_cast<int>(std::lround(val * precision));
334
0
  JXL_ENSURE(absval_quant < (1 << 10));
335
0
  writer->Write(10, absval_quant);
336
0
  return true;
337
0
}
338
339
}  // namespace
340
341
Status GetNoiseParameter(const Image3F& opsin, NoiseParams* noise_params,
342
0
                         float quality_coef) {
343
  // The size of a patch in decoder might be different from encoder's patch
344
  // size.
345
  // For encoder: the patch size should be big enough to estimate
346
  //              noise level, but, at the same time, it should be not too big
347
  //              to be able to estimate intensity value of the patch
348
0
  const size_t block_s = 8;
349
0
  const size_t kNumBin = 256;
350
0
  NoiseHistogram sad_histogram;
351
0
  std::vector<float> sad_scores =
352
0
      GetSADScoresForPatches(opsin, block_s, kNumBin, &sad_histogram);
353
0
  float sad_threshold = GetSADThreshold(sad_histogram, kNumBin);
354
  // If threshold is too large, the image has a strong pattern. This pattern
355
  // fools our model and it will add too much noise. Therefore, we do not add
356
  // noise for such images
357
0
  if (sad_threshold > 0.15f || sad_threshold <= 0.0f) {
358
0
    noise_params->Clear();
359
0
    return false;
360
0
  }
361
0
  std::vector<NoiseLevel> nl =
362
0
      GetNoiseLevel(opsin, sad_scores, sad_threshold, block_s);
363
364
0
  OptimizeNoiseParameters(nl, noise_params, quality_coef * 1.4f);
365
0
  return noise_params->HasAny();
366
0
}
367
368
Status EncodeNoise(const NoiseParams& noise_params, BitWriter* writer,
369
0
                   LayerType layer, AuxOut* aux_out) {
370
0
  JXL_ENSURE(noise_params.HasAny());
371
372
0
  return writer->WithMaxBits(
373
0
      NoiseParams::kNumNoisePoints * 16, layer, aux_out, [&]() -> Status {
374
0
        for (float i : noise_params.lut) {
375
0
          JXL_RETURN_IF_ERROR(EncodeFloatParam(i, kNoisePrecision, writer));
376
0
        }
377
0
        return true;
378
0
      });
379
0
}
380
381
}  // namespace jxl