Coverage Report

Created: 2026-06-16 07:20

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