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