Coverage Report

Created: 2025-06-16 07:00

/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) {
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
0
  OptimizeArray df = parameter_vector;
221
0
  float loss = loss_function.Compute(parameter_vector, &df,
222
0
                                     /*skip_regularization=*/true) /
223
0
               noise_level.size();
224
225
  // Approximation went too badly: escape with no noise at all.
226
0
  if (loss > kMaxError) {
227
0
    noise_params->Clear();
228
0
    return;
229
0
  }
230
231
0
  for (size_t i = 0; i < parameter_vector.size(); i++) {
232
0
    noise_params->lut[i] = std::max(parameter_vector[i], 0.0);
233
0
  }
234
0
}
235
236
std::vector<NoiseLevel> GetNoiseLevel(
237
    const Image3F& opsin, const std::vector<float>& texture_strength,
238
0
    const float threshold, const size_t block_s) {
239
0
  std::vector<NoiseLevel> noise_level_per_intensity;
240
241
0
  const int filt_size = 1;
242
0
  static const float kLaplFilter[filt_size * 2 + 1][filt_size * 2 + 1] = {
243
0
      {-0.25f, -1.0f, -0.25f},
244
0
      {-1.0f, 5.0f, -1.0f},
245
0
      {-0.25f, -1.0f, -0.25f},
246
0
  };
247
248
  // The noise model is built based on channel 0.5 * (X+Y) as we notice that it
249
  // is similar to the model 0.5 * (Y-X)
250
0
  size_t patch_index = 0;
251
252
0
  for (size_t y = 0; y + block_s <= opsin.ysize(); y += block_s) {
253
0
    for (size_t x = 0; x + block_s <= opsin.xsize(); x += block_s) {
254
0
      if (texture_strength[patch_index] <= threshold) {
255
        // Calculate mean value
256
0
        float mean_int = 0;
257
0
        for (size_t y_bl = 0; y_bl < block_s; ++y_bl) {
258
0
          for (size_t x_bl = 0; x_bl < block_s; ++x_bl) {
259
0
            mean_int += 0.5f * (opsin.PlaneRow(1, y + y_bl)[x + x_bl] +
260
0
                                opsin.PlaneRow(0, y + y_bl)[x + x_bl]);
261
0
          }
262
0
        }
263
0
        mean_int /= block_s * block_s;
264
265
        // Calculate Noise level
266
0
        float noise_level = 0;
267
0
        size_t count = 0;
268
0
        for (size_t y_bl = 0; y_bl < block_s; ++y_bl) {
269
0
          for (size_t x_bl = 0; x_bl < block_s; ++x_bl) {
270
0
            float filtered_value = 0;
271
0
            for (int y_f = -1 * filt_size; y_f <= filt_size; ++y_f) {
272
0
              if ((static_cast<ssize_t>(y_bl) + y_f) >= 0 &&
273
0
                  (y_bl + y_f) < block_s) {
274
0
                for (int x_f = -1 * filt_size; x_f <= filt_size; ++x_f) {
275
0
                  if ((static_cast<ssize_t>(x_bl) + x_f) >= 0 &&
276
0
                      (x_bl + x_f) < block_s) {
277
0
                    filtered_value +=
278
0
                        0.5f *
279
0
                        (opsin.PlaneRow(1, y + y_bl + y_f)[x + x_bl + x_f] +
280
0
                         opsin.PlaneRow(0, y + y_bl + y_f)[x + x_bl + x_f]) *
281
0
                        kLaplFilter[y_f + filt_size][x_f + filt_size];
282
0
                  } else {
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
                  }
289
0
                }
290
0
              } else {
291
0
                for (int x_f = -1 * filt_size; x_f <= filt_size; ++x_f) {
292
0
                  if ((static_cast<ssize_t>(x_bl) + x_f) >= 0 &&
293
0
                      (x_bl + x_f) < block_s) {
294
0
                    filtered_value +=
295
0
                        0.5f *
296
0
                        (opsin.PlaneRow(1, y + y_bl - y_f)[x + x_bl + x_f] +
297
0
                         opsin.PlaneRow(0, y + y_bl - y_f)[x + x_bl + x_f]) *
298
0
                        kLaplFilter[y_f + filt_size][x_f + filt_size];
299
0
                  } else {
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
                  }
306
0
                }
307
0
              }
308
0
            }
309
0
            noise_level += std::abs(filtered_value);
310
0
            ++count;
311
0
          }
312
0
        }
313
0
        noise_level /= count;
314
0
        NoiseLevel nl;
315
0
        nl.intensity = mean_int;
316
0
        nl.noise_level = noise_level;
317
0
        noise_level_per_intensity.push_back(nl);
318
0
      }
319
0
      ++patch_index;
320
0
    }
321
0
  }
322
0
  return noise_level_per_intensity;
323
0
}
324
325
0
Status EncodeFloatParam(float val, float precision, BitWriter* writer) {
326
0
  JXL_ENSURE(val >= 0);
327
0
  const int absval_quant = static_cast<int>(std::lround(val * precision));
328
0
  JXL_ENSURE(absval_quant < (1 << 10));
329
0
  writer->Write(10, absval_quant);
330
0
  return true;
331
0
}
332
333
}  // namespace
334
335
Status GetNoiseParameter(const Image3F& opsin, NoiseParams* noise_params,
336
0
                         float quality_coef) {
337
  // The size of a patch in decoder might be different from encoder's patch
338
  // size.
339
  // For encoder: the patch size should be big enough to estimate
340
  //              noise level, but, at the same time, it should be not too big
341
  //              to be able to estimate intensity value of the patch
342
0
  const size_t block_s = 8;
343
0
  const size_t kNumBin = 256;
344
0
  NoiseHistogram sad_histogram;
345
0
  std::vector<float> sad_scores =
346
0
      GetSADScoresForPatches(opsin, block_s, kNumBin, &sad_histogram);
347
0
  float sad_threshold = GetSADThreshold(sad_histogram, kNumBin);
348
  // If threshold is too large, the image has a strong pattern. This pattern
349
  // fools our model and it will add too much noise. Therefore, we do not add
350
  // noise for such images
351
0
  if (sad_threshold > 0.15f || sad_threshold <= 0.0f) {
352
0
    noise_params->Clear();
353
0
    return false;
354
0
  }
355
0
  std::vector<NoiseLevel> nl =
356
0
      GetNoiseLevel(opsin, sad_scores, sad_threshold, block_s);
357
358
0
  OptimizeNoiseParameters(nl, noise_params);
359
0
  for (float& i : noise_params->lut) {
360
0
    i *= quality_coef * 1.4;
361
0
  }
362
0
  return noise_params->HasAny();
363
0
}
364
365
Status EncodeNoise(const NoiseParams& noise_params, BitWriter* writer,
366
0
                   LayerType layer, AuxOut* aux_out) {
367
0
  JXL_ENSURE(noise_params.HasAny());
368
369
0
  return writer->WithMaxBits(
370
0
      NoiseParams::kNumNoisePoints * 16, layer, aux_out, [&]() -> Status {
371
0
        for (float i : noise_params.lut) {
372
0
          JXL_RETURN_IF_ERROR(EncodeFloatParam(i, kNoisePrecision, writer));
373
0
        }
374
0
        return true;
375
0
      });
376
0
}
377
378
}  // namespace jxl