/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 |