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 #ifndef LIB_JXL_MODULAR_TRANSFORM_SQUEEZE_H_
7 #define LIB_JXL_MODULAR_TRANSFORM_SQUEEZE_H_
8
9 // Haar-like transform: halves the resolution in one direction
10 // A B -> (A+B)>>1 in one channel (average) -> same range as
11 // original channel
12 // A-B - tendency in a new channel ('residual' needed to make
13 // the transform reversible)
14 // -> theoretically range could be 2.5
15 // times larger (2 times without the
16 // 'tendency'), but there should be lots
17 // of zeroes
18 // Repeated application (alternating horizontal and vertical squeezes) results
19 // in downscaling
20 //
21 // The default coefficient ordering is low-frequency to high-frequency, as in
22 // M. Antonini, M. Barlaud, P. Mathieu and I. Daubechies, "Image coding using
23 // wavelet transform", IEEE Transactions on Image Processing, vol. 1, no. 2, pp.
24 // 205-220, April 1992, doi: 10.1109/83.136597.
25
26 #include <stdlib.h>
27
28 #include "lib/jxl/base/data_parallel.h"
29 #include "lib/jxl/common.h"
30 #include "lib/jxl/modular/modular_image.h"
31 #include "lib/jxl/modular/transform/transform.h"
32
33 #define JXL_MAX_FIRST_PREVIEW_SIZE 8
34
35 namespace jxl {
36
37 /*
38 int avg=(A+B)>>1;
39 int diff=(A-B);
40 int rA=(diff+(avg<<1)+(diff&1))>>1;
41 int rB=rA-diff;
42
43 */
44 // |A B|C D|E F|
45 // p a n p=avg(A,B), a=avg(C,D), n=avg(E,F)
46 //
47 // Goal: estimate C-D (avoiding ringing artifacts)
48 // (ensuring that in smooth areas, a zero residual corresponds to a smooth
49 // gradient)
50
51 // best estimate for C: (B + 2*a)/3
52 // best estimate for D: (n + 3*a)/4
53 // best estimate for C-D: 4*B - 3*n - a /12
54
55 // avoid ringing by 1) only doing this if B <= a <= n or B >= a >= n
56 // (otherwise, this is not a smooth area and we cannot really estimate C-D)
57 // 2) making sure that B <= C <= D <= n or B >= C >= D >= n
58
SmoothTendency(pixel_type_w B,pixel_type_w a,pixel_type_w n)59 inline pixel_type_w SmoothTendency(pixel_type_w B, pixel_type_w a,
60 pixel_type_w n) {
61 pixel_type_w diff = 0;
62 if (B >= a && a >= n) {
63 diff = (4 * B - 3 * n - a + 6) / 12;
64 // 2C = a<<1 + diff - diff&1 <= 2B so diff - diff&1 <= 2B - 2a
65 // 2D = a<<1 - diff - diff&1 >= 2n so diff + diff&1 <= 2a - 2n
66 if (diff - (diff & 1) > 2 * (B - a)) diff = 2 * (B - a) + 1;
67 if (diff + (diff & 1) > 2 * (a - n)) diff = 2 * (a - n);
68 } else if (B <= a && a <= n) {
69 diff = (4 * B - 3 * n - a - 6) / 12;
70 // 2C = a<<1 + diff + diff&1 >= 2B so diff + diff&1 >= 2B - 2a
71 // 2D = a<<1 - diff + diff&1 <= 2n so diff - diff&1 >= 2a - 2n
72 if (diff + (diff & 1) < 2 * (B - a)) diff = 2 * (B - a) - 1;
73 if (diff - (diff & 1) < 2 * (a - n)) diff = 2 * (a - n);
74 }
75 return diff;
76 }
77
InvHSqueeze(Image & input,int c,int rc,ThreadPool * pool)78 void InvHSqueeze(Image &input, int c, int rc, ThreadPool *pool) {
79 const Channel &chin = input.channel[c];
80 const Channel &chin_residual = input.channel[rc];
81 // These must be valid since we ran MetaApply already.
82 JXL_ASSERT(chin.w == DivCeil(chin.w + chin_residual.w, 2));
83 JXL_ASSERT(chin.h == chin_residual.h);
84
85 if (chin_residual.w == 0 || chin_residual.h == 0) {
86 input.channel[c].resize(chin.w + chin_residual.w, chin.h);
87 input.channel[c].hshift--;
88 input.channel[c].hcshift--;
89 return;
90 }
91
92 Channel chout(chin.w + chin_residual.w, chin.h, chin.hshift - 1, chin.vshift,
93 chin.hcshift - 1, chin.vcshift);
94 JXL_DEBUG_V(4,
95 "Undoing horizontal squeeze of channel %i using residuals in "
96 "channel %i (going from width %zu to %zu)",
97 c, rc, chin.w, chout.w);
98 RunOnPool(
99 pool, 0, chin.h, ThreadPool::SkipInit(),
100 [&](const int task, const int thread) {
101 const size_t y = task;
102 const pixel_type *JXL_RESTRICT p_residual = chin_residual.Row(y);
103 const pixel_type *JXL_RESTRICT p_avg = chin.Row(y);
104 pixel_type *JXL_RESTRICT p_out = chout.Row(y);
105
106 // special case for x=0 so we don't have to check x>0
107 pixel_type_w avg = p_avg[0];
108 pixel_type_w next_avg = (1 < chin.w ? p_avg[1] : avg);
109 pixel_type_w tendency = SmoothTendency(avg, avg, next_avg);
110 pixel_type_w diff = p_residual[0] + tendency;
111 pixel_type_w A =
112 ((avg * 2) + diff + (diff > 0 ? -(diff & 1) : (diff & 1))) >> 1;
113 pixel_type_w B = A - diff;
114 p_out[0] = ClampToRange<pixel_type>(A);
115 p_out[1] = ClampToRange<pixel_type>(B);
116
117 for (size_t x = 1; x < chin_residual.w; x++) {
118 pixel_type_w diff_minus_tendency = p_residual[x];
119 pixel_type_w avg = p_avg[x];
120 pixel_type_w next_avg = (x + 1 < chin.w ? p_avg[x + 1] : avg);
121 pixel_type_w left = p_out[(x << 1) - 1];
122 pixel_type_w tendency = SmoothTendency(left, avg, next_avg);
123 pixel_type_w diff = diff_minus_tendency + tendency;
124 pixel_type_w A =
125 ((avg * 2) + diff + (diff > 0 ? -(diff & 1) : (diff & 1))) >> 1;
126 p_out[x << 1] = ClampToRange<pixel_type>(A);
127 pixel_type_w B = A - diff;
128 p_out[(x << 1) + 1] = ClampToRange<pixel_type>(B);
129 }
130 if (chout.w & 1) p_out[chout.w - 1] = p_avg[chin.w - 1];
131 },
132 "InvHorizontalSqueeze");
133 input.channel[c] = std::move(chout);
134 }
135
FwdHSqueeze(Image & input,int c,int rc)136 void FwdHSqueeze(Image &input, int c, int rc) {
137 const Channel &chin = input.channel[c];
138
139 JXL_DEBUG_V(4, "Doing horizontal squeeze of channel %i to new channel %i", c,
140 rc);
141
142 Channel chout((chin.w + 1) / 2, chin.h, chin.hshift + 1, chin.vshift,
143 chin.hcshift + 1, chin.vcshift);
144 Channel chout_residual(chin.w - chout.w, chout.h, chin.hshift + 1,
145 chin.vshift, chin.hcshift, chin.vcshift);
146
147 for (size_t y = 0; y < chout.h; y++) {
148 const pixel_type *JXL_RESTRICT p_in = chin.Row(y);
149 pixel_type *JXL_RESTRICT p_out = chout.Row(y);
150 pixel_type *JXL_RESTRICT p_res = chout_residual.Row(y);
151 for (size_t x = 0; x < chout_residual.w; x++) {
152 pixel_type A = p_in[x * 2];
153 pixel_type B = p_in[x * 2 + 1];
154 pixel_type avg = (A + B + (A > B)) >> 1;
155 p_out[x] = avg;
156
157 pixel_type diff = A - B;
158
159 pixel_type next_avg = avg;
160 if (x + 1 < chout_residual.w) {
161 next_avg = (p_in[x * 2 + 2] + p_in[x * 2 + 3] +
162 (p_in[x * 2 + 2] > p_in[x * 2 + 3])) >>
163 1; // which will be chout.value(y,x+1)
164 } else if (chin.w & 1)
165 next_avg = p_in[x * 2 + 2];
166 pixel_type left = (x > 0 ? p_in[x * 2 - 1] : avg);
167 pixel_type tendency = SmoothTendency(left, avg, next_avg);
168
169 p_res[x] = diff - tendency;
170 }
171 if (chin.w & 1) {
172 int x = chout.w - 1;
173 p_out[x] = p_in[x * 2];
174 }
175 }
176 input.channel[c] = std::move(chout);
177 input.channel.insert(input.channel.begin() + rc, std::move(chout_residual));
178 }
179
InvVSqueeze(Image & input,int c,int rc,ThreadPool * pool)180 void InvVSqueeze(Image &input, int c, int rc, ThreadPool *pool) {
181 const Channel &chin = input.channel[c];
182 const Channel &chin_residual = input.channel[rc];
183 // These must be valid since we ran MetaApply already.
184 JXL_ASSERT(chin.h == DivCeil(chin.h + chin_residual.h, 2));
185 JXL_ASSERT(chin.w == chin_residual.w);
186
187 if (chin_residual.w == 0 || chin_residual.h == 0) {
188 input.channel[c].resize(chin.w, chin.h + chin_residual.h);
189 input.channel[c].vshift--;
190 input.channel[c].vcshift--;
191 return;
192 }
193
194 // Note: chin.h >= chin_residual.h and at most 1 different.
195 Channel chout(chin.w, chin.h + chin_residual.h, chin.hshift, chin.vshift - 1,
196 chin.hcshift, chin.vcshift - 1);
197 JXL_DEBUG_V(
198 4,
199 "Undoing vertical squeeze of channel %i using residuals in channel "
200 "%i (going from height %zu to %zu)",
201 c, rc, chin.h, chout.h);
202
203 intptr_t onerow_in = chin.plane.PixelsPerRow();
204 intptr_t onerow_out = chout.plane.PixelsPerRow();
205 constexpr int kColsPerThread = 64;
206 RunOnPool(
207 pool, 0, DivCeil(chin.w, kColsPerThread), ThreadPool::SkipInit(),
208 [&](const int task, const int thread) {
209 const size_t x0 = task * kColsPerThread;
210 const size_t x1 = std::min((size_t)(task + 1) * kColsPerThread, chin.w);
211 // We only iterate up to std::min(chin_residual.h, chin.h) which is
212 // always chin_residual.h.
213 for (size_t y = 0; y < chin_residual.h; y++) {
214 const pixel_type *JXL_RESTRICT p_residual = chin_residual.Row(y);
215 const pixel_type *JXL_RESTRICT p_avg = chin.Row(y);
216 pixel_type *JXL_RESTRICT p_out = chout.Row(y << 1);
217 for (size_t x = x0; x < x1; x++) {
218 pixel_type_w diff_minus_tendency = p_residual[x];
219 pixel_type_w avg = p_avg[x];
220
221 pixel_type_w next_avg = avg;
222 if (y + 1 < chin.h) next_avg = p_avg[x + onerow_in];
223 pixel_type_w top =
224 (y > 0 ? p_out[static_cast<ssize_t>(x) - onerow_out] : avg);
225 pixel_type_w tendency = SmoothTendency(top, avg, next_avg);
226 pixel_type_w diff = diff_minus_tendency + tendency;
227 pixel_type_w out =
228 ((avg * 2) + diff + (diff > 0 ? -(diff & 1) : (diff & 1))) >> 1;
229
230 p_out[x] = ClampToRange<pixel_type>(out);
231 // If the chin_residual.h == chin.h, the output has an even number
232 // of rows so the next line is fine. Otherwise, this loop won't
233 // write to the last output row which is handled separately.
234 p_out[x + onerow_out] = ClampToRange<pixel_type>(p_out[x] - diff);
235 }
236 }
237 },
238 "InvVertSqueeze");
239
240 if (chout.h & 1) {
241 size_t y = chin.h - 1;
242 const pixel_type *p_avg = chin.Row(y);
243 pixel_type *p_out = chout.Row(y << 1);
244 for (size_t x = 0; x < chin.w; x++) {
245 p_out[x] = p_avg[x];
246 }
247 }
248 input.channel[c] = std::move(chout);
249 }
250
FwdVSqueeze(Image & input,int c,int rc)251 void FwdVSqueeze(Image &input, int c, int rc) {
252 const Channel &chin = input.channel[c];
253
254 JXL_DEBUG_V(4, "Doing vertical squeeze of channel %i to new channel %i", c,
255 rc);
256
257 Channel chout(chin.w, (chin.h + 1) / 2, chin.hshift, chin.vshift + 1,
258 chin.hcshift, chin.vcshift + 1);
259 Channel chout_residual(chin.w, chin.h - chout.h, chin.hshift, chin.vshift + 1,
260 chin.hcshift, chin.vcshift);
261 intptr_t onerow_in = chin.plane.PixelsPerRow();
262 for (size_t y = 0; y < chout_residual.h; y++) {
263 const pixel_type *JXL_RESTRICT p_in = chin.Row(y * 2);
264 pixel_type *JXL_RESTRICT p_out = chout.Row(y);
265 pixel_type *JXL_RESTRICT p_res = chout_residual.Row(y);
266 for (size_t x = 0; x < chout.w; x++) {
267 pixel_type A = p_in[x];
268 pixel_type B = p_in[x + onerow_in];
269 pixel_type avg = (A + B + (A > B)) >> 1;
270 p_out[x] = avg;
271
272 pixel_type diff = A - B;
273
274 pixel_type next_avg = avg;
275 if (y + 1 < chout_residual.h) {
276 next_avg = (p_in[x + 2 * onerow_in] + p_in[x + 3 * onerow_in] +
277 (p_in[x + 2 * onerow_in] > p_in[x + 3 * onerow_in])) >>
278 1; // which will be chout.value(y+1,x)
279 } else if (chin.h & 1) {
280 next_avg = p_in[x + 2 * onerow_in];
281 }
282 pixel_type top =
283 (y > 0 ? p_in[static_cast<ssize_t>(x) - onerow_in] : avg);
284 pixel_type tendency = SmoothTendency(top, avg, next_avg);
285
286 p_res[x] = diff - tendency;
287 }
288 }
289 if (chin.h & 1) {
290 size_t y = chout.h - 1;
291 const pixel_type *p_in = chin.Row(y * 2);
292 pixel_type *p_out = chout.Row(y);
293 for (size_t x = 0; x < chout.w; x++) {
294 p_out[x] = p_in[x];
295 }
296 }
297 input.channel[c] = std::move(chout);
298 input.channel.insert(input.channel.begin() + rc, std::move(chout_residual));
299 }
300
DefaultSqueezeParameters(std::vector<SqueezeParams> * parameters,const Image & image)301 void DefaultSqueezeParameters(std::vector<SqueezeParams> *parameters,
302 const Image &image) {
303 int nb_channels = image.nb_channels;
304 // maybe other transforms have been applied before, but let's assume the first
305 // nb_channels channels still contain the 'main' data
306
307 parameters->clear();
308 size_t w = image.channel[image.nb_meta_channels].w;
309 size_t h = image.channel[image.nb_meta_channels].h;
310 JXL_DEBUG_V(7, "Default squeeze parameters for %zux%zu image: ", w, h);
311
312 bool wide =
313 (w >
314 h); // do horizontal first on wide images; vertical first on tall images
315
316 if (nb_channels > 2 && image.channel[image.nb_meta_channels + 1].w == w &&
317 image.channel[image.nb_meta_channels + 1].h == h) {
318 // assume channels 1 and 2 are chroma, and can be squeezed first for 4:2:0
319 // previews
320 JXL_DEBUG_V(7, "(4:2:0 chroma), %zux%zu image", w, h);
321 // if (!wide) {
322 // parameters.push_back(0+2); // vertical chroma squeeze
323 // parameters.push_back(image.nb_meta_channels+1);
324 // parameters.push_back(image.nb_meta_channels+2);
325 // }
326 SqueezeParams params;
327 // horizontal chroma squeeze
328 params.horizontal = true;
329 params.in_place = false;
330 params.begin_c = image.nb_meta_channels + 1;
331 params.num_c = 2;
332 parameters->push_back(params);
333 params.horizontal = false;
334 // vertical chroma squeeze
335 parameters->push_back(params);
336 }
337 SqueezeParams params;
338 params.begin_c = image.nb_meta_channels;
339 params.num_c = nb_channels;
340 params.in_place = true;
341
342 if (!wide) {
343 if (h > JXL_MAX_FIRST_PREVIEW_SIZE) {
344 params.horizontal = false;
345 parameters->push_back(params);
346 h = (h + 1) / 2;
347 JXL_DEBUG_V(7, "Vertical (%zux%zu), ", w, h);
348 }
349 }
350 while (w > JXL_MAX_FIRST_PREVIEW_SIZE || h > JXL_MAX_FIRST_PREVIEW_SIZE) {
351 if (w > JXL_MAX_FIRST_PREVIEW_SIZE) {
352 params.horizontal = true;
353 parameters->push_back(params);
354 w = (w + 1) / 2;
355 JXL_DEBUG_V(7, "Horizontal (%zux%zu), ", w, h);
356 }
357 if (h > JXL_MAX_FIRST_PREVIEW_SIZE) {
358 params.horizontal = false;
359 parameters->push_back(params);
360 h = (h + 1) / 2;
361 JXL_DEBUG_V(7, "Vertical (%zux%zu), ", w, h);
362 }
363 }
364 JXL_DEBUG_V(7, "that's it");
365 }
366
CheckMetaSqueezeParams(const std::vector<SqueezeParams> & parameters,int num_channels)367 Status CheckMetaSqueezeParams(const std::vector<SqueezeParams> ¶meters,
368 int num_channels) {
369 for (size_t i = 0; i < parameters.size(); i++) {
370 int c1 = parameters[i].begin_c;
371 int c2 = parameters[i].begin_c + parameters[i].num_c - 1;
372 if (c1 < 0 || c1 > num_channels || c2 < 0 || c2 >= num_channels ||
373 c2 < c1) {
374 return JXL_FAILURE("Invalid channel range");
375 }
376 }
377 return true;
378 }
379
MetaSqueeze(Image & image,std::vector<SqueezeParams> * parameters)380 Status MetaSqueeze(Image &image, std::vector<SqueezeParams> *parameters) {
381 if (parameters->empty()) {
382 DefaultSqueezeParameters(parameters, image);
383 }
384 JXL_RETURN_IF_ERROR(
385 CheckMetaSqueezeParams(*parameters, image.channel.size()));
386
387 for (size_t i = 0; i < parameters->size(); i++) {
388 bool horizontal = (*parameters)[i].horizontal;
389 bool in_place = (*parameters)[i].in_place;
390 uint32_t beginc = (*parameters)[i].begin_c;
391 uint32_t endc = (*parameters)[i].begin_c + (*parameters)[i].num_c - 1;
392
393 uint32_t offset;
394 if (in_place) {
395 offset = endc + 1;
396 } else {
397 offset = image.channel.size();
398 }
399 for (uint32_t c = beginc; c <= endc; c++) {
400 Channel dummy;
401 dummy.hcshift = image.channel[c].hcshift;
402 dummy.vcshift = image.channel[c].vcshift;
403 if (image.channel[c].hshift > 30 || image.channel[c].vshift > 30) {
404 return JXL_FAILURE("Too many squeezes: shift > 30");
405 }
406 if (horizontal) {
407 size_t w = image.channel[c].w;
408 image.channel[c].w = (w + 1) / 2;
409 image.channel[c].hshift++;
410 image.channel[c].hcshift++;
411 dummy.w = w - (w + 1) / 2;
412 dummy.h = image.channel[c].h;
413 } else {
414 size_t h = image.channel[c].h;
415 image.channel[c].h = (h + 1) / 2;
416 image.channel[c].vshift++;
417 image.channel[c].vcshift++;
418 dummy.h = h - (h + 1) / 2;
419 dummy.w = image.channel[c].w;
420 }
421 dummy.hshift = image.channel[c].hshift;
422 dummy.vshift = image.channel[c].vshift;
423
424 image.channel.insert(image.channel.begin() + offset + (c - beginc),
425 std::move(dummy));
426 }
427 }
428 return true;
429 }
430
InvSqueeze(Image & input,std::vector<SqueezeParams> parameters,ThreadPool * pool)431 Status InvSqueeze(Image &input, std::vector<SqueezeParams> parameters,
432 ThreadPool *pool) {
433 if (parameters.empty()) {
434 DefaultSqueezeParameters(¶meters, input);
435 }
436 JXL_RETURN_IF_ERROR(CheckMetaSqueezeParams(parameters, input.channel.size()));
437
438 for (int i = parameters.size() - 1; i >= 0; i--) {
439 bool horizontal = parameters[i].horizontal;
440 bool in_place = parameters[i].in_place;
441 uint32_t beginc = parameters[i].begin_c;
442 uint32_t endc = parameters[i].begin_c + parameters[i].num_c - 1;
443 uint32_t offset;
444 if (in_place) {
445 offset = endc + 1;
446 } else {
447 offset = input.channel.size() + beginc - endc - 1;
448 }
449 for (uint32_t c = beginc; c <= endc; c++) {
450 uint32_t rc = offset + c - beginc;
451 if ((input.channel[c].w < input.channel[rc].w) ||
452 (input.channel[c].h < input.channel[rc].h)) {
453 return JXL_FAILURE("Corrupted squeeze transform");
454 }
455 if (input.channel[rc].is_empty()) {
456 input.channel[rc].resize(); // assume all zeroes
457 }
458 if (horizontal) {
459 InvHSqueeze(input, c, rc, pool);
460 } else {
461 InvVSqueeze(input, c, rc, pool);
462 }
463 }
464 input.channel.erase(input.channel.begin() + offset,
465 input.channel.begin() + offset + (endc - beginc + 1));
466 }
467 return true;
468 }
469
FwdSqueeze(Image & input,std::vector<SqueezeParams> parameters,ThreadPool * pool)470 Status FwdSqueeze(Image &input, std::vector<SqueezeParams> parameters,
471 ThreadPool *pool) {
472 if (parameters.empty()) {
473 DefaultSqueezeParameters(¶meters, input);
474 }
475 JXL_RETURN_IF_ERROR(CheckMetaSqueezeParams(parameters, input.channel.size()));
476
477 for (size_t i = 0; i < parameters.size(); i++) {
478 bool horizontal = parameters[i].horizontal;
479 bool in_place = parameters[i].in_place;
480 uint32_t beginc = parameters[i].begin_c;
481 uint32_t endc = parameters[i].begin_c + parameters[i].num_c - 1;
482 uint32_t offset;
483 if (in_place) {
484 offset = endc + 1;
485 } else {
486 offset = input.channel.size();
487 }
488 for (uint32_t c = beginc; c <= endc; c++) {
489 if (horizontal) {
490 FwdHSqueeze(input, c, offset + c - beginc);
491 } else {
492 FwdVSqueeze(input, c, offset + c - beginc);
493 }
494 }
495 }
496 return true;
497 }
498
499 } // namespace jxl
500
501 #endif // LIB_JXL_MODULAR_TRANSFORM_SQUEEZE_H_
502