/src/libjxl/lib/jxl/modular/encoding/enc_encoding.cc
Line | Count | Source |
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 <jxl/memory_manager.h> |
7 | | |
8 | | #include <algorithm> |
9 | | #include <array> |
10 | | #include <cstddef> |
11 | | #include <cstdint> |
12 | | #include <cstdlib> |
13 | | #include <limits> |
14 | | #include <queue> |
15 | | #include <utility> |
16 | | #include <vector> |
17 | | |
18 | | #include "lib/jxl/base/bits.h" |
19 | | #include "lib/jxl/base/common.h" |
20 | | #include "lib/jxl/base/compiler_specific.h" |
21 | | #include "lib/jxl/base/printf_macros.h" |
22 | | #include "lib/jxl/base/status.h" |
23 | | #include "lib/jxl/enc_ans.h" |
24 | | #include "lib/jxl/enc_ans_params.h" |
25 | | #include "lib/jxl/enc_aux_out.h" |
26 | | #include "lib/jxl/enc_bit_writer.h" |
27 | | #include "lib/jxl/enc_fields.h" |
28 | | #include "lib/jxl/fields.h" |
29 | | #include "lib/jxl/image.h" |
30 | | #include "lib/jxl/image_ops.h" |
31 | | #include "lib/jxl/modular/encoding/context_predict.h" |
32 | | #include "lib/jxl/modular/encoding/dec_ma.h" |
33 | | #include "lib/jxl/modular/encoding/enc_ma.h" |
34 | | #include "lib/jxl/modular/encoding/encoding.h" |
35 | | #include "lib/jxl/modular/encoding/ma_common.h" |
36 | | #include "lib/jxl/modular/modular_image.h" |
37 | | #include "lib/jxl/modular/options.h" |
38 | | #include "lib/jxl/pack_signed.h" |
39 | | |
40 | | namespace jxl { |
41 | | |
42 | | namespace { |
43 | | // Plot tree (if enabled) and predictor usage map. |
44 | | constexpr bool kWantDebug = true; |
45 | | // constexpr bool kPrintTree = false; |
46 | | |
47 | 256M | inline std::array<uint8_t, 3> PredictorColor(Predictor p) { |
48 | 256M | switch (p) { |
49 | 18.2M | case Predictor::Zero: |
50 | 18.2M | return {{0, 0, 0}}; |
51 | 5.71M | case Predictor::Left: |
52 | 5.71M | return {{255, 0, 0}}; |
53 | 0 | case Predictor::Top: |
54 | 0 | return {{0, 255, 0}}; |
55 | 0 | case Predictor::Average0: |
56 | 0 | return {{0, 0, 255}}; |
57 | 0 | case Predictor::Average4: |
58 | 0 | return {{192, 128, 128}}; |
59 | 0 | case Predictor::Select: |
60 | 0 | return {{255, 255, 0}}; |
61 | 233M | case Predictor::Gradient: |
62 | 233M | return {{255, 0, 255}}; |
63 | 23.8k | case Predictor::Weighted: |
64 | 23.8k | return {{0, 255, 255}}; |
65 | | // TODO(jon) |
66 | 0 | default: |
67 | 0 | return {{255, 255, 255}}; |
68 | 256M | }; |
69 | 0 | } |
70 | | |
71 | | // `cutoffs` must be sorted. |
72 | | Tree MakeFixedTree(int property, const std::vector<int32_t> &cutoffs, |
73 | 2.65k | Predictor pred, size_t num_pixels, int bitdepth) { |
74 | 2.65k | size_t log_px = CeilLog2Nonzero(num_pixels); |
75 | 2.65k | size_t min_gap = 0; |
76 | | // Reduce fixed tree height when encoding small images. |
77 | 2.65k | if (log_px < 14) { |
78 | 2.15k | min_gap = 8 * (14 - log_px); |
79 | 2.15k | } |
80 | 2.65k | const int shift = bitdepth > 11 ? std::min(4, bitdepth - 11) : 0; |
81 | 2.65k | const int mul = 1 << shift; |
82 | 2.65k | Tree tree; |
83 | 2.65k | struct NodeInfo { |
84 | 2.65k | size_t begin, end, pos; |
85 | 2.65k | }; |
86 | 2.65k | std::queue<NodeInfo> q; |
87 | | // Leaf IDs will be set by roundtrip decoding the tree. |
88 | 2.65k | tree.push_back(PropertyDecisionNode::Leaf(pred)); |
89 | 2.65k | q.push(NodeInfo{0, cutoffs.size(), 0}); |
90 | 41.1k | while (!q.empty()) { |
91 | 38.4k | NodeInfo info = q.front(); |
92 | 38.4k | q.pop(); |
93 | 38.4k | if (info.begin + min_gap >= info.end) continue; |
94 | 17.9k | uint32_t split = (info.begin + info.end) / 2; |
95 | 17.9k | int32_t cutoff = cutoffs[split] * mul; |
96 | 17.9k | tree[info.pos] = PropertyDecisionNode::Split(property, cutoff, tree.size()); |
97 | 17.9k | q.push(NodeInfo{split + 1, info.end, tree.size()}); |
98 | 17.9k | tree.push_back(PropertyDecisionNode::Leaf(pred)); |
99 | 17.9k | q.push(NodeInfo{info.begin, split, tree.size()}); |
100 | 17.9k | tree.push_back(PropertyDecisionNode::Leaf(pred)); |
101 | 17.9k | } |
102 | 2.65k | return tree; |
103 | 2.65k | } |
104 | | |
105 | | Status GatherTreeData(const Image &image, pixel_type chan, size_t group_id, |
106 | | const weighted::Header &wp_header, |
107 | | const ModularOptions &options, TreeSamples &tree_samples, |
108 | 5.80k | size_t *total_pixels) { |
109 | 5.80k | const Channel &channel = image.channel[chan]; |
110 | 5.80k | JxlMemoryManager *memory_manager = channel.memory_manager(); |
111 | | |
112 | 5.80k | JXL_DEBUG_V(7, "Learning %" PRIuS "x%" PRIuS " channel %d", channel.w, |
113 | 5.80k | channel.h, chan); |
114 | | |
115 | 5.80k | std::array<pixel_type, kNumStaticProperties> static_props = { |
116 | 5.80k | {chan, static_cast<int>(group_id)}}; |
117 | 5.80k | Properties properties(kNumNonrefProperties + |
118 | 5.80k | kExtraPropsPerChannel * options.max_properties); |
119 | 5.80k | double pixel_fraction = std::min(1.0f, options.nb_repeats); |
120 | | // a fraction of 0 is used to disable learning entirely. |
121 | 5.80k | if (pixel_fraction > 0) { |
122 | 5.80k | pixel_fraction = std::max(pixel_fraction, |
123 | 5.80k | std::min(1.0, 1024.0 / (channel.w * channel.h))); |
124 | 5.80k | } |
125 | 5.80k | uint64_t threshold = |
126 | 5.80k | (std::numeric_limits<uint64_t>::max() >> 32) * pixel_fraction; |
127 | 5.80k | uint64_t s[2] = {static_cast<uint64_t>(0x94D049BB133111EBull), |
128 | 5.80k | static_cast<uint64_t>(0xBF58476D1CE4E5B9ull)}; |
129 | | // Xorshift128+ adapted from xorshift128+-inl.h |
130 | 94.3M | auto use_sample = [&]() { |
131 | 94.3M | auto s1 = s[0]; |
132 | 94.3M | const auto s0 = s[1]; |
133 | 94.3M | const auto bits = s1 + s0; // b, c |
134 | 94.3M | s[0] = s0; |
135 | 94.3M | s1 ^= s1 << 23; |
136 | 94.3M | s1 ^= s0 ^ (s1 >> 18) ^ (s0 >> 5); |
137 | 94.3M | s[1] = s1; |
138 | 94.3M | return (bits >> 32) <= threshold; |
139 | 94.3M | }; |
140 | | |
141 | 5.80k | const ptrdiff_t onerow = channel.plane.PixelsPerRow(); |
142 | 5.80k | JXL_ASSIGN_OR_RETURN( |
143 | 5.80k | Channel references, |
144 | 5.80k | Channel::Create(memory_manager, properties.size() - kNumNonrefProperties, |
145 | 5.80k | channel.w)); |
146 | 5.80k | weighted::State wp_state(wp_header, channel.w, channel.h); |
147 | 5.80k | tree_samples.PrepareForSamples(pixel_fraction * channel.h * channel.w + 64); |
148 | 5.80k | const bool multiple_predictors = tree_samples.NumPredictors() != 1; |
149 | 4.20M | auto compute_sample = [&](const pixel_type *p, size_t x, size_t y) { |
150 | 4.20M | pixel_type_w pred[kNumModularPredictors]; |
151 | 4.20M | if (multiple_predictors) { |
152 | 0 | PredictLearnAll(&properties, channel.w, p + x, onerow, x, y, references, |
153 | 0 | &wp_state, pred); |
154 | 4.20M | } else { |
155 | 4.20M | pred[static_cast<int>(tree_samples.PredictorFromIndex(0))] = |
156 | 4.20M | PredictLearn(&properties, channel.w, p + x, onerow, x, y, |
157 | 4.20M | tree_samples.PredictorFromIndex(0), references, |
158 | 4.20M | &wp_state) |
159 | 4.20M | .guess; |
160 | 4.20M | } |
161 | 4.20M | (*total_pixels)++; |
162 | 4.20M | if (use_sample()) { |
163 | 2.30M | tree_samples.AddSample(p[x], properties, pred); |
164 | 2.30M | } |
165 | 4.20M | wp_state.UpdateErrors(p[x], x, y, channel.w); |
166 | 4.20M | }; |
167 | | |
168 | 561k | for (size_t y = 0; y < channel.h; y++) { |
169 | 555k | const pixel_type *JXL_RESTRICT p = channel.Row(y); |
170 | 555k | PrecomputeReferences(channel, y, image, chan, &references); |
171 | 555k | InitPropsRow(&properties, static_props, y); |
172 | | |
173 | | // TODO(veluca): avoid computing WP if we don't use its property or |
174 | | // predictions. |
175 | 555k | if (y > 1 && channel.w > 8 && references.w == 0) { |
176 | 1.60M | for (size_t x = 0; x < 2; x++) { |
177 | 1.06M | compute_sample(p, x, y); |
178 | 1.06M | } |
179 | 90.6M | for (size_t x = 2; x < channel.w - 2; x++) { |
180 | 90.1M | pixel_type_w pred[kNumModularPredictors]; |
181 | 90.1M | if (multiple_predictors) { |
182 | 0 | PredictLearnAllNEC(&properties, channel.w, p + x, onerow, x, y, |
183 | 0 | references, &wp_state, pred); |
184 | 90.1M | } else { |
185 | 90.1M | pred[static_cast<int>(tree_samples.PredictorFromIndex(0))] = |
186 | 90.1M | PredictLearnNEC(&properties, channel.w, p + x, onerow, x, y, |
187 | 90.1M | tree_samples.PredictorFromIndex(0), references, |
188 | 90.1M | &wp_state) |
189 | 90.1M | .guess; |
190 | 90.1M | } |
191 | 90.1M | (*total_pixels)++; |
192 | 90.1M | if (use_sample()) { |
193 | 45.4M | tree_samples.AddSample(p[x], properties, pred); |
194 | 45.4M | } |
195 | 90.1M | wp_state.UpdateErrors(p[x], x, y, channel.w); |
196 | 90.1M | } |
197 | 1.60M | for (size_t x = channel.w - 2; x < channel.w; x++) { |
198 | 1.06M | compute_sample(p, x, y); |
199 | 1.06M | } |
200 | 533k | } else { |
201 | 2.09M | for (size_t x = 0; x < channel.w; x++) { |
202 | 2.07M | compute_sample(p, x, y); |
203 | 2.07M | } |
204 | 21.5k | } |
205 | 555k | } |
206 | 5.80k | return true; |
207 | 5.80k | } |
208 | | |
209 | | StatusOr<Tree> LearnTree( |
210 | | TreeSamples &&tree_samples, size_t total_pixels, |
211 | | const ModularOptions &options, |
212 | | const std::vector<ModularMultiplierInfo> &multiplier_info = {}, |
213 | 2.53k | StaticPropRange static_prop_range = {}) { |
214 | 2.53k | Tree tree; |
215 | 7.59k | for (size_t i = 0; i < kNumStaticProperties; i++) { |
216 | 5.06k | if (static_prop_range[i][1] == 0) { |
217 | 0 | static_prop_range[i][1] = std::numeric_limits<uint32_t>::max(); |
218 | 0 | } |
219 | 5.06k | } |
220 | 2.53k | if (!tree_samples.HasSamples()) { |
221 | 119 | tree.emplace_back(); |
222 | 119 | tree.back().predictor = tree_samples.PredictorFromIndex(0); |
223 | 119 | tree.back().property = -1; |
224 | 119 | tree.back().predictor_offset = 0; |
225 | 119 | tree.back().multiplier = 1; |
226 | 119 | return tree; |
227 | 119 | } |
228 | 2.41k | float pixel_fraction = tree_samples.NumSamples() * 1.0f / total_pixels; |
229 | 2.41k | float required_cost = pixel_fraction * 0.9 + 0.1; |
230 | 2.41k | tree_samples.AllSamplesDone(); |
231 | 2.41k | JXL_RETURN_IF_ERROR(ComputeBestTree( |
232 | 2.41k | tree_samples, options.splitting_heuristics_node_threshold * required_cost, |
233 | 2.41k | multiplier_info, static_prop_range, options.fast_decode_multiplier, |
234 | 2.41k | &tree)); |
235 | 2.41k | return tree; |
236 | 2.41k | } |
237 | | |
238 | | Status EncodeModularChannelMAANS(const Image &image, pixel_type chan, |
239 | | const weighted::Header &wp_header, |
240 | | const Tree &global_tree, Token **tokenpp, |
241 | 24.3k | size_t group_id, bool skip_encoder_fast_path) { |
242 | 24.3k | const Channel &channel = image.channel[chan]; |
243 | 24.3k | JxlMemoryManager *memory_manager = channel.memory_manager(); |
244 | 24.3k | Token *tokenp = *tokenpp; |
245 | 24.3k | JXL_ENSURE(channel.w != 0 && channel.h != 0); |
246 | | |
247 | 24.3k | Image3F predictor_img; |
248 | 24.3k | if (kWantDebug) { |
249 | 24.3k | JXL_ASSIGN_OR_RETURN(predictor_img, |
250 | 24.3k | Image3F::Create(memory_manager, channel.w, channel.h)); |
251 | 24.3k | } |
252 | | |
253 | 24.3k | JXL_DEBUG_V(6, |
254 | 24.3k | "Encoding %" PRIuS "x%" PRIuS |
255 | 24.3k | " channel %d, " |
256 | 24.3k | "(shift=%i,%i)", |
257 | 24.3k | channel.w, channel.h, chan, channel.hshift, channel.vshift); |
258 | | |
259 | 24.3k | std::array<pixel_type, kNumStaticProperties> static_props = { |
260 | 24.3k | {chan, static_cast<int>(group_id)}}; |
261 | 24.3k | bool use_wp; |
262 | 24.3k | bool is_wp_only; |
263 | 24.3k | bool is_gradient_only; |
264 | 24.3k | size_t num_props; |
265 | 24.3k | FlatTree tree = FilterTree(global_tree, static_props, &num_props, &use_wp, |
266 | 24.3k | &is_wp_only, &is_gradient_only); |
267 | 24.3k | MATreeLookup tree_lookup(tree); |
268 | 24.3k | JXL_DEBUG_V(3, "Encoding using a MA tree with %" PRIuS " nodes", tree.size()); |
269 | | |
270 | | // Check if this tree is a WP-only tree with a small enough property value |
271 | | // range. |
272 | | // Initialized to avoid clang-tidy complaining. |
273 | 24.3k | auto tree_lut = jxl::make_unique<TreeLut<uint16_t, false, false>>(); |
274 | 24.3k | if (is_wp_only) { |
275 | 7.95k | is_wp_only = TreeToLookupTable(tree, *tree_lut); |
276 | 7.95k | } |
277 | 24.3k | if (is_gradient_only) { |
278 | 4.01k | is_gradient_only = TreeToLookupTable(tree, *tree_lut); |
279 | 4.01k | } |
280 | | |
281 | 24.3k | if (is_wp_only && !skip_encoder_fast_path) { |
282 | 31.8k | for (size_t c = 0; c < 3; c++) { |
283 | 23.8k | FillImage(static_cast<float>(PredictorColor(Predictor::Weighted)[c]), |
284 | 23.8k | &predictor_img.Plane(c)); |
285 | 23.8k | } |
286 | 7.95k | const ptrdiff_t onerow = channel.plane.PixelsPerRow(); |
287 | 7.95k | weighted::State wp_state(wp_header, channel.w, channel.h); |
288 | 7.95k | Properties properties(1); |
289 | 7.95k | bool unhealthy = false; |
290 | 277k | for (size_t y = 0; y < channel.h; y++) { |
291 | 269k | const pixel_type *JXL_RESTRICT r = channel.Row(y); |
292 | 13.2M | for (size_t x = 0; x < channel.w; x++) { |
293 | 12.9M | size_t offset = 0; |
294 | 12.9M | pixel_type_w left = (x ? r[x - 1] : y ? *(r + x - onerow) : 0); |
295 | 12.9M | pixel_type_w top = (y ? *(r + x - onerow) : left); |
296 | 12.9M | pixel_type_w topleft = (x && y ? *(r + x - 1 - onerow) : left); |
297 | 12.9M | pixel_type_w topright = |
298 | 12.9M | (x + 1 < channel.w && y ? *(r + x + 1 - onerow) : top); |
299 | 12.9M | pixel_type_w toptop = (y > 1 ? *(r + x - onerow - onerow) : top); |
300 | 12.9M | int32_t guess = wp_state.Predict</*compute_properties=*/true>( |
301 | 12.9M | x, y, channel.w, top, left, topright, topleft, toptop, &properties, |
302 | 12.9M | offset); |
303 | 12.9M | uint32_t pos = |
304 | 12.9M | kPropRangeFast + |
305 | 12.9M | jxl::Clamp1(properties[0], -kPropRangeFast, kPropRangeFast - 1); |
306 | 12.9M | uint32_t ctx_id = tree_lut->context_lookup[pos]; |
307 | 12.9M | int32_t residual; |
308 | 12.9M | unhealthy |= SubOverflow(r[x], guess, residual); |
309 | 12.9M | *tokenp++ = Token(ctx_id, PackSigned(residual)); |
310 | 12.9M | wp_state.UpdateErrors(r[x], x, y, channel.w); |
311 | 12.9M | } |
312 | 269k | } |
313 | 7.95k | if (unhealthy) { |
314 | 0 | return JXL_FAILURE("Residual overflow"); |
315 | 0 | } |
316 | 16.4k | } else if (tree.size() == 1 && tree[0].predictor == Predictor::Gradient && |
317 | 3.33k | tree[0].multiplier == 1 && tree[0].predictor_offset == 0 && |
318 | 3.33k | !skip_encoder_fast_path) { |
319 | 13.3k | for (size_t c = 0; c < 3; c++) { |
320 | 10.0k | FillImage(static_cast<float>(PredictorColor(Predictor::Gradient)[c]), |
321 | 10.0k | &predictor_img.Plane(c)); |
322 | 10.0k | } |
323 | 3.33k | const ptrdiff_t onerow = channel.plane.PixelsPerRow(); |
324 | 109k | for (size_t y = 0; y < channel.h; y++) { |
325 | 105k | const pixel_type *JXL_RESTRICT r = channel.Row(y); |
326 | 15.4M | for (size_t x = 0; x < channel.w; x++) { |
327 | 15.3M | pixel_type_w left = (x ? r[x - 1] : y ? *(r + x - onerow) : 0); |
328 | 15.3M | pixel_type_w top = (y ? *(r + x - onerow) : left); |
329 | 15.3M | pixel_type_w topleft = (x && y ? *(r + x - 1 - onerow) : left); |
330 | 15.3M | int32_t guess = ClampedGradient(top, left, topleft); |
331 | 15.3M | int32_t residual = r[x] - guess; |
332 | 15.3M | *tokenp++ = Token(tree[0].childID, PackSigned(residual)); |
333 | 15.3M | } |
334 | 105k | } |
335 | 13.0k | } else if (is_gradient_only && !skip_encoder_fast_path) { |
336 | 2.71k | for (size_t c = 0; c < 3; c++) { |
337 | 2.03k | FillImage(static_cast<float>(PredictorColor(Predictor::Gradient)[c]), |
338 | 2.03k | &predictor_img.Plane(c)); |
339 | 2.03k | } |
340 | 678 | const ptrdiff_t onerow = channel.plane.PixelsPerRow(); |
341 | 19.9k | for (size_t y = 0; y < channel.h; y++) { |
342 | 19.2k | const pixel_type *JXL_RESTRICT r = channel.Row(y); |
343 | 1.47M | for (size_t x = 0; x < channel.w; x++) { |
344 | 1.45M | pixel_type_w left = (x ? r[x - 1] : y ? *(r + x - onerow) : 0); |
345 | 1.45M | pixel_type_w top = (y ? *(r + x - onerow) : left); |
346 | 1.45M | pixel_type_w topleft = (x && y ? *(r + x - 1 - onerow) : left); |
347 | 1.45M | int32_t guess = ClampedGradient(top, left, topleft); |
348 | 1.45M | uint32_t pos = |
349 | 1.45M | kPropRangeFast + |
350 | 1.45M | std::min<pixel_type_w>( |
351 | 1.45M | std::max<pixel_type_w>(-kPropRangeFast, top + left - topleft), |
352 | 1.45M | kPropRangeFast - 1); |
353 | 1.45M | uint32_t ctx_id = tree_lut->context_lookup[pos]; |
354 | 1.45M | int32_t residual = r[x] - guess; |
355 | 1.45M | *tokenp++ = Token(ctx_id, PackSigned(residual)); |
356 | 1.45M | } |
357 | 19.2k | } |
358 | 12.3k | } else if (tree.size() == 1 && tree[0].predictor == Predictor::Zero && |
359 | 0 | tree[0].multiplier == 1 && tree[0].predictor_offset == 0 && |
360 | 0 | !skip_encoder_fast_path) { |
361 | 0 | for (size_t c = 0; c < 3; c++) { |
362 | 0 | FillImage(static_cast<float>(PredictorColor(Predictor::Zero)[c]), |
363 | 0 | &predictor_img.Plane(c)); |
364 | 0 | } |
365 | 0 | for (size_t y = 0; y < channel.h; y++) { |
366 | 0 | const pixel_type *JXL_RESTRICT p = channel.Row(y); |
367 | 0 | for (size_t x = 0; x < channel.w; x++) { |
368 | 0 | *tokenp++ = Token(tree[0].childID, PackSigned(p[x])); |
369 | 0 | } |
370 | 0 | } |
371 | 12.3k | } else if (tree.size() == 1 && tree[0].predictor != Predictor::Weighted && |
372 | 6.49k | (tree[0].multiplier & (tree[0].multiplier - 1)) == 0 && |
373 | 6.49k | tree[0].predictor_offset == 0 && !skip_encoder_fast_path) { |
374 | | // multiplier is a power of 2. |
375 | 25.9k | for (size_t c = 0; c < 3; c++) { |
376 | 19.4k | FillImage(static_cast<float>(PredictorColor(tree[0].predictor)[c]), |
377 | 19.4k | &predictor_img.Plane(c)); |
378 | 19.4k | } |
379 | 6.49k | uint32_t mul_shift = |
380 | 6.49k | FloorLog2Nonzero(static_cast<uint32_t>(tree[0].multiplier)); |
381 | 6.49k | const ptrdiff_t onerow = channel.plane.PixelsPerRow(); |
382 | 33.8k | for (size_t y = 0; y < channel.h; y++) { |
383 | 27.3k | const pixel_type *JXL_RESTRICT r = channel.Row(y); |
384 | 268k | for (size_t x = 0; x < channel.w; x++) { |
385 | 241k | PredictionResult pred = PredictNoTreeNoWP(channel.w, r + x, onerow, x, |
386 | 241k | y, tree[0].predictor); |
387 | 241k | pixel_type_w residual = r[x] - pred.guess; |
388 | 241k | JXL_DASSERT((residual >> mul_shift) * tree[0].multiplier == residual); |
389 | 241k | *tokenp++ = Token(tree[0].childID, PackSigned(residual >> mul_shift)); |
390 | 241k | } |
391 | 27.3k | } |
392 | | |
393 | 6.49k | } else if (!use_wp && !skip_encoder_fast_path) { |
394 | 2.72k | const ptrdiff_t onerow = channel.plane.PixelsPerRow(); |
395 | 2.72k | Properties properties(num_props); |
396 | 2.72k | JXL_ASSIGN_OR_RETURN( |
397 | 2.72k | Channel references, |
398 | 2.72k | Channel::Create(memory_manager, |
399 | 2.72k | properties.size() - kNumNonrefProperties, channel.w)); |
400 | 119k | for (size_t y = 0; y < channel.h; y++) { |
401 | 116k | const pixel_type *JXL_RESTRICT p = channel.Row(y); |
402 | 116k | PrecomputeReferences(channel, y, image, chan, &references); |
403 | 116k | float *pred_img_row[3]; |
404 | 116k | if (kWantDebug) { |
405 | 465k | for (size_t c = 0; c < 3; c++) { |
406 | 349k | pred_img_row[c] = predictor_img.PlaneRow(c, y); |
407 | 349k | } |
408 | 116k | } |
409 | 116k | InitPropsRow(&properties, static_props, y); |
410 | 11.8M | for (size_t x = 0; x < channel.w; x++) { |
411 | 11.7M | PredictionResult res = |
412 | 11.7M | PredictTreeNoWP(&properties, channel.w, p + x, onerow, x, y, |
413 | 11.7M | tree_lookup, references); |
414 | 11.7M | if (kWantDebug) { |
415 | 46.9M | for (size_t i = 0; i < 3; i++) { |
416 | 35.2M | pred_img_row[i][x] = PredictorColor(res.predictor)[i]; |
417 | 35.2M | } |
418 | 11.7M | } |
419 | 11.7M | pixel_type_w residual = p[x] - res.guess; |
420 | 11.7M | JXL_DASSERT(residual % res.multiplier == 0); |
421 | 11.7M | *tokenp++ = Token(res.context, PackSigned(residual / res.multiplier)); |
422 | 11.7M | } |
423 | 116k | } |
424 | 3.18k | } else { |
425 | 3.18k | const ptrdiff_t onerow = channel.plane.PixelsPerRow(); |
426 | 3.18k | Properties properties(num_props); |
427 | 3.18k | JXL_ASSIGN_OR_RETURN( |
428 | 3.18k | Channel references, |
429 | 3.18k | Channel::Create(memory_manager, |
430 | 3.18k | properties.size() - kNumNonrefProperties, channel.w)); |
431 | 3.18k | weighted::State wp_state(wp_header, channel.w, channel.h); |
432 | 409k | for (size_t y = 0; y < channel.h; y++) { |
433 | 406k | const pixel_type *JXL_RESTRICT p = channel.Row(y); |
434 | 406k | PrecomputeReferences(channel, y, image, chan, &references); |
435 | 406k | float *pred_img_row[3]; |
436 | 406k | if (kWantDebug) { |
437 | 1.62M | for (size_t c = 0; c < 3; c++) { |
438 | 1.21M | pred_img_row[c] = predictor_img.PlaneRow(c, y); |
439 | 1.21M | } |
440 | 406k | } |
441 | 406k | InitPropsRow(&properties, static_props, y); |
442 | 74.3M | for (size_t x = 0; x < channel.w; x++) { |
443 | 73.8M | PredictionResult res = |
444 | 73.8M | PredictTreeWP(&properties, channel.w, p + x, onerow, x, y, |
445 | 73.8M | tree_lookup, references, &wp_state); |
446 | 73.8M | if (kWantDebug) { |
447 | 295M | for (size_t i = 0; i < 3; i++) { |
448 | 221M | pred_img_row[i][x] = PredictorColor(res.predictor)[i]; |
449 | 221M | } |
450 | 73.8M | } |
451 | 73.8M | pixel_type_w residual = p[x] - res.guess; |
452 | 73.8M | JXL_DASSERT(residual % res.multiplier == 0); |
453 | 73.8M | *tokenp++ = Token(res.context, PackSigned(residual / res.multiplier)); |
454 | 73.8M | wp_state.UpdateErrors(p[x], x, y, channel.w); |
455 | 73.8M | } |
456 | 406k | } |
457 | 3.18k | } |
458 | | /* TODO(szabadka): Add cparams to the call stack here. |
459 | | if (kWantDebug && WantDebugOutput(cparams)) { |
460 | | DumpImage( |
461 | | cparams, |
462 | | ("pred_" + ToString(group_id) + "_" + ToString(chan)).c_str(), |
463 | | predictor_img); |
464 | | } |
465 | | */ |
466 | 24.3k | *tokenpp = tokenp; |
467 | 24.3k | return true; |
468 | 24.3k | } |
469 | | |
470 | | } // namespace |
471 | | |
472 | | Tree PredefinedTree(ModularOptions::TreeKind tree_kind, size_t total_pixels, |
473 | 5.30k | int bitdepth, int prevprop) { |
474 | 5.30k | switch (tree_kind) { |
475 | 0 | case ModularOptions::TreeKind::kJpegTranscodeACMeta: |
476 | | // All the data is 0, so no need for a fancy tree. |
477 | 0 | return {PropertyDecisionNode::Leaf(Predictor::Zero)}; |
478 | 0 | case ModularOptions::TreeKind::kTrivialTreeNoPredictor: |
479 | | // All the data is 0, so no need for a fancy tree. |
480 | 0 | return {PropertyDecisionNode::Leaf(Predictor::Zero)}; |
481 | 0 | case ModularOptions::TreeKind::kFalconACMeta: |
482 | | // All the data is 0 except the quant field. TODO(veluca): make that 0 |
483 | | // too. |
484 | 0 | return {PropertyDecisionNode::Leaf(Predictor::Left)}; |
485 | 2.65k | case ModularOptions::TreeKind::kACMeta: { |
486 | | // Small image. |
487 | 2.65k | if (total_pixels < 1024) { |
488 | 1.62k | return {PropertyDecisionNode::Leaf(Predictor::Left)}; |
489 | 1.62k | } |
490 | 1.02k | Tree tree; |
491 | | // 0: c > 1 |
492 | 1.02k | tree.push_back(PropertyDecisionNode::Split(0, 1, 1)); |
493 | | // 1: c > 2 |
494 | 1.02k | tree.push_back(PropertyDecisionNode::Split(0, 2, 3)); |
495 | | // 2: c > 0 |
496 | 1.02k | tree.push_back(PropertyDecisionNode::Split(0, 0, 5)); |
497 | | // 3: EPF control field (all 0 or 4), top > 3 |
498 | 1.02k | tree.push_back(PropertyDecisionNode::Split(6, 3, 21)); |
499 | | // 4: ACS+QF, y > 0 |
500 | 1.02k | tree.push_back(PropertyDecisionNode::Split(2, 0, 7)); |
501 | | // 5: CfL x |
502 | 1.02k | tree.push_back(PropertyDecisionNode::Leaf(Predictor::Gradient)); |
503 | | // 6: CfL b |
504 | 1.02k | tree.push_back(PropertyDecisionNode::Leaf(Predictor::Gradient)); |
505 | | // 7: QF: split according to the left quant value. |
506 | 1.02k | tree.push_back(PropertyDecisionNode::Split(7, 5, 9)); |
507 | | // 8: ACS: split in 4 segments (8x8 from 0 to 3, large square 4-5, large |
508 | | // rectangular 6-11, 8x8 12+), according to previous ACS value. |
509 | 1.02k | tree.push_back(PropertyDecisionNode::Split(7, 5, 15)); |
510 | | // QF |
511 | 1.02k | tree.push_back(PropertyDecisionNode::Split(7, 11, 11)); |
512 | 1.02k | tree.push_back(PropertyDecisionNode::Split(7, 3, 13)); |
513 | 1.02k | tree.push_back(PropertyDecisionNode::Leaf(Predictor::Left)); |
514 | 1.02k | tree.push_back(PropertyDecisionNode::Leaf(Predictor::Left)); |
515 | 1.02k | tree.push_back(PropertyDecisionNode::Leaf(Predictor::Left)); |
516 | 1.02k | tree.push_back(PropertyDecisionNode::Leaf(Predictor::Left)); |
517 | | // ACS |
518 | 1.02k | tree.push_back(PropertyDecisionNode::Split(7, 11, 17)); |
519 | 1.02k | tree.push_back(PropertyDecisionNode::Split(7, 3, 19)); |
520 | 1.02k | tree.push_back(PropertyDecisionNode::Leaf(Predictor::Zero)); |
521 | 1.02k | tree.push_back(PropertyDecisionNode::Leaf(Predictor::Zero)); |
522 | 1.02k | tree.push_back(PropertyDecisionNode::Leaf(Predictor::Zero)); |
523 | 1.02k | tree.push_back(PropertyDecisionNode::Leaf(Predictor::Zero)); |
524 | | // EPF, left > 3 |
525 | 1.02k | tree.push_back(PropertyDecisionNode::Split(7, 3, 23)); |
526 | 1.02k | tree.push_back(PropertyDecisionNode::Split(7, 3, 25)); |
527 | 1.02k | tree.push_back(PropertyDecisionNode::Leaf(Predictor::Zero)); |
528 | 1.02k | tree.push_back(PropertyDecisionNode::Leaf(Predictor::Zero)); |
529 | 1.02k | tree.push_back(PropertyDecisionNode::Leaf(Predictor::Zero)); |
530 | 1.02k | tree.push_back(PropertyDecisionNode::Leaf(Predictor::Zero)); |
531 | 1.02k | return tree; |
532 | 2.65k | } |
533 | 2.65k | case ModularOptions::TreeKind::kWPFixedDC: { |
534 | 2.65k | std::vector<int32_t> cutoffs = { |
535 | 2.65k | -500, -392, -255, -191, -127, -95, -63, -47, -31, -23, -15, |
536 | 2.65k | -11, -7, -4, -3, -1, 0, 1, 3, 5, 7, 11, |
537 | 2.65k | 15, 23, 31, 47, 63, 95, 127, 191, 255, 392, 500}; |
538 | 2.65k | return MakeFixedTree(kWPProp, cutoffs, Predictor::Weighted, total_pixels, |
539 | 2.65k | bitdepth); |
540 | 2.65k | } |
541 | 0 | case ModularOptions::TreeKind::kGradientFixedDC: { |
542 | 0 | std::vector<int32_t> cutoffs = { |
543 | 0 | -500, -392, -255, -191, -127, -95, -63, -47, -31, -23, -15, |
544 | 0 | -11, -7, -4, -3, -1, 0, 1, 3, 5, 7, 11, |
545 | 0 | 15, 23, 31, 47, 63, 95, 127, 191, 255, 392, 500}; |
546 | 0 | return MakeFixedTree( |
547 | 0 | prevprop > 0 ? kNumNonrefProperties + 2 : kGradientProp, cutoffs, |
548 | 0 | Predictor::Gradient, total_pixels, bitdepth); |
549 | 2.65k | } |
550 | 0 | case ModularOptions::TreeKind::kLearn: { |
551 | 0 | JXL_DEBUG_ABORT("internal: kLearn is not predefined tree"); |
552 | 0 | return {}; |
553 | 2.65k | } |
554 | 5.30k | } |
555 | 0 | JXL_DEBUG_ABORT("internal: unexpected TreeKind: %d", |
556 | 0 | static_cast<int>(tree_kind)); |
557 | 0 | return {}; |
558 | 5.30k | } |
559 | | |
560 | | StatusOr<Tree> LearnTree( |
561 | | const Image *images, const ModularOptions *options, const uint32_t start, |
562 | | const uint32_t stop, |
563 | 2.53k | const std::vector<ModularMultiplierInfo> &multiplier_info = {}) { |
564 | 2.53k | TreeSamples tree_samples; |
565 | 2.53k | JXL_RETURN_IF_ERROR(tree_samples.SetPredictor(options[start].predictor, |
566 | 2.53k | options[start].wp_tree_mode)); |
567 | 2.53k | JXL_RETURN_IF_ERROR( |
568 | 2.53k | tree_samples.SetProperties(options[start].splitting_heuristics_properties, |
569 | 2.53k | options[start].wp_tree_mode)); |
570 | 2.53k | uint32_t max_c = 0; |
571 | 2.53k | std::vector<pixel_type> pixel_samples; |
572 | 2.53k | std::vector<pixel_type> diff_samples; |
573 | 2.53k | std::vector<uint32_t> group_pixel_count; |
574 | 2.53k | std::vector<uint32_t> channel_pixel_count; |
575 | 6.06k | for (uint32_t i = start; i < stop; i++) { |
576 | 3.53k | max_c = std::max<uint32_t>(images[i].channel.size(), max_c); |
577 | 3.53k | CollectPixelSamples(images[i], options[i], i, group_pixel_count, |
578 | 3.53k | channel_pixel_count, pixel_samples, diff_samples); |
579 | 3.53k | } |
580 | 2.53k | StaticPropRange range; |
581 | 2.53k | range[0] = {{0, max_c}}; |
582 | 2.53k | range[1] = {{start, stop}}; |
583 | | |
584 | 2.53k | tree_samples.PreQuantizeProperties( |
585 | 2.53k | range, multiplier_info, group_pixel_count, channel_pixel_count, |
586 | 2.53k | pixel_samples, diff_samples, options[start].max_property_values); |
587 | | |
588 | 2.53k | size_t total_pixels = 0; |
589 | 7.20k | for (size_t i = 0; i < images[start].channel.size(); i++) { |
590 | 5.07k | if (i >= images[start].nb_meta_channels && |
591 | 4.50k | (images[start].channel[i].w > options[start].max_chan_size || |
592 | 4.12k | images[start].channel[i].h > options[start].max_chan_size)) { |
593 | 397 | break; |
594 | 397 | } |
595 | 4.67k | total_pixels += images[start].channel[i].w * images[start].channel[i].h; |
596 | 4.67k | } |
597 | 2.53k | total_pixels = std::max<size_t>(total_pixels, 1); |
598 | | |
599 | 2.53k | weighted::Header wp_header; |
600 | | |
601 | 6.06k | for (size_t i = start; i < stop; i++) { |
602 | 3.53k | size_t nb_channels = images[i].channel.size(); |
603 | | |
604 | 3.53k | if (images[i].w == 0 || images[i].h == 0 || nb_channels < 1) |
605 | 0 | continue; // is there any use for a zero-channel image? |
606 | 3.53k | if (images[i].error) return JXL_FAILURE("Invalid image"); |
607 | 3.53k | JXL_ENSURE(options[i].tree_kind == ModularOptions::TreeKind::kLearn); |
608 | | |
609 | 3.53k | JXL_DEBUG_V( |
610 | 3.53k | 2, "Encoding %" PRIuS "-channel, %i-bit, %" PRIuS "x%" PRIuS " image.", |
611 | 3.53k | nb_channels, images[i].bitdepth, images[i].w, images[i].h); |
612 | | |
613 | | // encode transforms |
614 | 3.53k | Bundle::Init(&wp_header); |
615 | 3.53k | if (PredictorHasWeighted(options[i].predictor)) { |
616 | 0 | weighted::PredictorMode(options[i].wp_mode, &wp_header); |
617 | 0 | } |
618 | | |
619 | | // Gather tree data |
620 | 9.34k | for (size_t c = 0; c < nb_channels; c++) { |
621 | 6.20k | if (c >= images[i].nb_meta_channels && |
622 | 5.50k | (images[i].channel[c].w > options[i].max_chan_size || |
623 | 5.13k | images[i].channel[c].h > options[i].max_chan_size)) { |
624 | 397 | break; |
625 | 397 | } |
626 | 5.80k | if (!images[i].channel[c].w || !images[i].channel[c].h) { |
627 | 0 | continue; // skip empty channels |
628 | 0 | } |
629 | 5.80k | JXL_RETURN_IF_ERROR(GatherTreeData(images[i], c, i, wp_header, options[i], |
630 | 5.80k | tree_samples, &total_pixels)); |
631 | 5.80k | } |
632 | 3.53k | } |
633 | | |
634 | | // TODO(veluca): parallelize more. |
635 | 2.53k | JXL_ASSIGN_OR_RETURN(Tree tree, |
636 | 2.53k | LearnTree(std::move(tree_samples), total_pixels, |
637 | 2.53k | options[start], multiplier_info, range)); |
638 | 2.53k | return tree; |
639 | 2.53k | } |
640 | | |
641 | | Status ModularCompress(const Image &image, const ModularOptions &options, |
642 | | size_t group_id, const Tree &tree, GroupHeader &header, |
643 | 76.8k | std::vector<Token> &tokens, size_t *width) { |
644 | 76.8k | size_t nb_channels = image.channel.size(); |
645 | | |
646 | 76.8k | if (image.w == 0 || image.h == 0 || nb_channels < 1) |
647 | 67.9k | return true; // is there any use for a zero-channel image? |
648 | 8.83k | if (image.error) return JXL_FAILURE("Invalid image"); |
649 | | |
650 | 8.83k | JXL_DEBUG_V( |
651 | 8.83k | 2, "Encoding %" PRIuS "-channel, %i-bit, %" PRIuS "x%" PRIuS " image.", |
652 | 8.83k | nb_channels, image.bitdepth, image.w, image.h); |
653 | | |
654 | | // encode transforms |
655 | 8.83k | Bundle::Init(&header); |
656 | 8.83k | if (PredictorHasWeighted(options.predictor)) { |
657 | 2.65k | weighted::PredictorMode(options.wp_mode, &header.wp_header); |
658 | 2.65k | } |
659 | 8.83k | header.transforms = image.transform; |
660 | 8.83k | header.use_global_tree = true; |
661 | | |
662 | 8.83k | size_t image_width = 0; |
663 | 8.83k | size_t total_tokens = 0; |
664 | 33.1k | for (size_t i = 0; i < nb_channels; i++) { |
665 | 24.7k | if (i >= image.nb_meta_channels && |
666 | 24.0k | (image.channel[i].w > options.max_chan_size || |
667 | 23.6k | image.channel[i].h > options.max_chan_size)) { |
668 | 397 | break; |
669 | 397 | } |
670 | 24.3k | if (image.channel[i].w > image_width) image_width = image.channel[i].w; |
671 | 24.3k | total_tokens += image.channel[i].w * image.channel[i].h; |
672 | 24.3k | } |
673 | 8.83k | if (options.zero_tokens) { |
674 | 0 | tokens.resize(tokens.size() + total_tokens, {0, 0}); |
675 | 8.83k | } else { |
676 | | // Do one big allocation for all the tokens we'll need, |
677 | | // to avoid reallocs that might require copying. |
678 | 8.83k | size_t pos = tokens.size(); |
679 | 8.83k | tokens.resize(pos + total_tokens); |
680 | 8.83k | Token *tokenp = tokens.data() + pos; |
681 | 33.1k | for (size_t i = 0; i < nb_channels; i++) { |
682 | 24.7k | if (i >= image.nb_meta_channels && |
683 | 24.0k | (image.channel[i].w > options.max_chan_size || |
684 | 23.6k | image.channel[i].h > options.max_chan_size)) { |
685 | 397 | break; |
686 | 397 | } |
687 | 24.3k | if (!image.channel[i].w || !image.channel[i].h) { |
688 | 0 | continue; // skip empty channels |
689 | 0 | } |
690 | 24.3k | JXL_RETURN_IF_ERROR( |
691 | 24.3k | EncodeModularChannelMAANS(image, i, header.wp_header, tree, &tokenp, |
692 | 24.3k | group_id, options.skip_encoder_fast_path)); |
693 | 24.3k | } |
694 | | // Make sure we actually wrote all tokens |
695 | 8.83k | JXL_ENSURE(tokenp == tokens.data() + tokens.size()); |
696 | 8.83k | } |
697 | | |
698 | 8.83k | *width = image_width; |
699 | | |
700 | 8.83k | return true; |
701 | 8.83k | } |
702 | | |
703 | | Status ModularGenericCompress(const Image &image, const ModularOptions &opts, |
704 | | BitWriter &writer, AuxOut *aux_out, |
705 | 754 | LayerType layer, size_t group_id) { |
706 | 754 | size_t nb_channels = image.channel.size(); |
707 | | |
708 | 754 | if (image.w == 0 || image.h == 0 || nb_channels < 1) |
709 | 0 | return true; // is there any use for a zero-channel image? |
710 | 754 | if (image.error) return JXL_FAILURE("Invalid image"); |
711 | | |
712 | 754 | ModularOptions options = opts; // Make a copy to modify it. |
713 | 754 | if (options.predictor == kUndefinedPredictor) { |
714 | 0 | options.predictor = Predictor::Gradient; |
715 | 0 | } |
716 | | |
717 | 754 | size_t bits = writer.BitsWritten(); |
718 | | |
719 | 754 | JxlMemoryManager *memory_manager = image.memory_manager(); |
720 | 754 | JXL_DEBUG_V( |
721 | 754 | 2, "Encoding %" PRIuS "-channel, %i-bit, %" PRIuS "x%" PRIuS " image.", |
722 | 754 | nb_channels, image.bitdepth, image.w, image.h); |
723 | | |
724 | | // encode transforms |
725 | 754 | GroupHeader header; |
726 | 754 | Bundle::Init(&header); |
727 | 754 | if (PredictorHasWeighted(options.predictor)) { |
728 | 126 | weighted::PredictorMode(options.wp_mode, &header.wp_header); |
729 | 126 | } |
730 | 754 | header.transforms = image.transform; |
731 | | |
732 | 754 | JXL_RETURN_IF_ERROR(Bundle::Write(header, &writer, layer, aux_out)); |
733 | | |
734 | | // Compute tree. |
735 | 754 | Tree tree; |
736 | 754 | if (options.tree_kind == ModularOptions::TreeKind::kLearn) { |
737 | 502 | JXL_ASSIGN_OR_RETURN(tree, LearnTree(&image, &options, 0, 1)); |
738 | 502 | } else { |
739 | 252 | size_t total_pixels = 0; |
740 | 1.13k | for (size_t i = 0; i < nb_channels; i++) { |
741 | 882 | if (i >= image.nb_meta_channels && |
742 | 882 | (image.channel[i].w > options.max_chan_size || |
743 | 882 | image.channel[i].h > options.max_chan_size)) { |
744 | 0 | break; |
745 | 0 | } |
746 | 882 | total_pixels += image.channel[i].w * image.channel[i].h; |
747 | 882 | } |
748 | 252 | total_pixels = std::max<size_t>(total_pixels, 1); |
749 | | |
750 | 252 | tree = PredefinedTree(options.tree_kind, total_pixels, image.bitdepth, |
751 | 252 | options.max_properties); |
752 | 252 | } |
753 | | |
754 | 754 | Tree decoded_tree; |
755 | 754 | std::vector<std::vector<Token>> tree_tokens(1); |
756 | 754 | JXL_RETURN_IF_ERROR(TokenizeTree(tree, tree_tokens.data(), &decoded_tree)); |
757 | 754 | JXL_ENSURE(tree.size() == decoded_tree.size()); |
758 | 754 | tree = std::move(decoded_tree); |
759 | | |
760 | | /* TODO(szabadka) Add text output callback |
761 | | if (kWantDebug && kPrintTree && WantDebugOutput(aux_out)) { |
762 | | PrintTree(*tree, aux_out->debug_prefix + "/tree_" + ToString(group_id)); |
763 | | } */ |
764 | | |
765 | | // Write tree |
766 | 754 | EntropyEncodingData code; |
767 | 754 | JXL_ASSIGN_OR_RETURN( |
768 | 754 | size_t cost, |
769 | 754 | BuildAndEncodeHistograms(memory_manager, options.histogram_params, |
770 | 754 | kNumTreeContexts, tree_tokens, &code, &writer, |
771 | 754 | LayerType::ModularTree, aux_out)); |
772 | 754 | JXL_RETURN_IF_ERROR(WriteTokens(tree_tokens[0], code, 0, &writer, |
773 | 754 | LayerType::ModularTree, aux_out)); |
774 | | |
775 | 754 | size_t image_width = 0; |
776 | 754 | std::vector<std::vector<Token>> tokens(1); |
777 | | // it puts `use_global_tree = true` in the header, but this is not used |
778 | | // further |
779 | 754 | JXL_RETURN_IF_ERROR(ModularCompress(image, options, group_id, tree, header, |
780 | 754 | tokens[0], &image_width)); |
781 | | |
782 | | // Write data |
783 | 754 | code = {}; |
784 | 754 | HistogramParams histo_params = options.histogram_params; |
785 | 754 | histo_params.image_widths.push_back(image_width); |
786 | 754 | JXL_ASSIGN_OR_RETURN( |
787 | 754 | cost, BuildAndEncodeHistograms(memory_manager, histo_params, |
788 | 754 | (tree.size() + 1) / 2, tokens, &code, |
789 | 754 | &writer, layer, aux_out)); |
790 | 754 | (void)cost; |
791 | 754 | JXL_RETURN_IF_ERROR(WriteTokens(tokens[0], code, 0, &writer, layer, aux_out)); |
792 | | |
793 | 754 | bits = writer.BitsWritten() - bits; |
794 | 754 | JXL_DEBUG_V(4, |
795 | 754 | "Modular-encoded a %" PRIuS "x%" PRIuS |
796 | 754 | " bitdepth=%i nbchans=%" PRIuS " image in %" PRIuS " bytes", |
797 | 754 | image.w, image.h, image.bitdepth, image.channel.size(), bits / 8); |
798 | 754 | (void)bits; |
799 | | |
800 | 754 | return true; |
801 | 754 | } |
802 | | |
803 | | } // namespace jxl |