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> &parameters,
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(&parameters, 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(&parameters, 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