Coverage Report

Created: 2025-06-22 08:04

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