Coverage Report

Created: 2026-05-24 07:45

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/src/libjxl/lib/jxl/enc_patch_dictionary.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 "lib/jxl/enc_patch_dictionary.h"
7
8
#include <jxl/cms_interface.h>
9
#include <jxl/memory_manager.h>
10
#include <jxl/types.h>
11
12
#include <algorithm>
13
#include <atomic>
14
#include <cmath>
15
#include <cstdint>
16
#include <cstdlib>
17
#include <utility>
18
#include <vector>
19
20
#include "lib/jxl/base/common.h"
21
#include "lib/jxl/base/compiler_specific.h"
22
#include "lib/jxl/base/data_parallel.h"
23
#include "lib/jxl/base/override.h"
24
#include "lib/jxl/base/printf_macros.h"
25
#include "lib/jxl/base/random.h"
26
#include "lib/jxl/base/rect.h"
27
#include "lib/jxl/base/span.h"
28
#include "lib/jxl/base/status.h"
29
#include "lib/jxl/common.h"
30
#include "lib/jxl/dec_cache.h"
31
#include "lib/jxl/dec_frame.h"
32
#include "lib/jxl/dec_patch_dictionary.h"
33
#include "lib/jxl/enc_ans.h"
34
#include "lib/jxl/enc_ans_params.h"
35
#include "lib/jxl/enc_aux_out.h"
36
#include "lib/jxl/enc_bit_writer.h"
37
#include "lib/jxl/enc_cache.h"
38
#include "lib/jxl/enc_debug_image.h"
39
#include "lib/jxl/enc_dot_dictionary.h"
40
#include "lib/jxl/enc_frame.h"
41
#include "lib/jxl/enc_params.h"
42
#include "lib/jxl/frame_header.h"
43
#include "lib/jxl/image.h"
44
#include "lib/jxl/image_bundle.h"
45
#include "lib/jxl/image_ops.h"
46
#include "lib/jxl/modular/options.h"
47
#include "lib/jxl/pack_signed.h"
48
#include "lib/jxl/patch_dictionary_internal.h"
49
50
namespace jxl {
51
52
static constexpr size_t kPatchFrameReferenceId = 3;
53
54
// static
55
Status PatchDictionaryEncoder::Encode(const PatchDictionary& pdic,
56
                                      BitWriter* writer, LayerType layer,
57
1.84k
                                      AuxOut* aux_out) {
58
1.84k
  JXL_ENSURE(pdic.HasAny());
59
1.84k
  JxlMemoryManager* memory_manager = writer->memory_manager();
60
1.84k
  std::vector<std::vector<Token>> tokens(1);
61
62
6.54M
  auto add_num = [&](int context, size_t num) {
63
6.54M
    tokens[0].emplace_back(context, static_cast<uint32_t>(num));
64
6.54M
  };
65
1.84k
  size_t num_ref_patch = 0;
66
138k
  for (size_t i = 0; i < pdic.positions_.size();) {
67
136k
    size_t ref_pos_idx = pdic.positions_[i].ref_pos_idx;
68
2.04M
    while (i < pdic.positions_.size() &&
69
2.04M
           pdic.positions_[i].ref_pos_idx == ref_pos_idx) {
70
1.90M
      i++;
71
1.90M
    }
72
136k
    num_ref_patch++;
73
136k
  }
74
1.84k
  add_num(kNumRefPatchContext, num_ref_patch);
75
1.84k
  size_t blend_pos = 0;
76
1.84k
  size_t blending_stride = pdic.blendings_stride_;
77
  // blending_stride == num_ec + 1; num_ec > 1 =>
78
1.84k
  bool choose_alpha = (blending_stride > 1 + 1);
79
138k
  for (size_t i = 0; i < pdic.positions_.size();) {
80
136k
    size_t i_start = i;
81
136k
    size_t ref_pos_idx = pdic.positions_[i].ref_pos_idx;
82
136k
    const auto& ref_pos = pdic.ref_positions_[ref_pos_idx];
83
2.04M
    while (i < pdic.positions_.size() &&
84
2.04M
           pdic.positions_[i].ref_pos_idx == ref_pos_idx) {
85
1.90M
      i++;
86
1.90M
    }
87
136k
    size_t num = i - i_start;
88
136k
    JXL_ENSURE(num > 0);
89
136k
    add_num(kReferenceFrameContext, ref_pos.ref);
90
136k
    add_num(kPatchReferencePositionContext, ref_pos.x0);
91
136k
    add_num(kPatchReferencePositionContext, ref_pos.y0);
92
136k
    add_num(kPatchSizeContext, ref_pos.xsize - 1);
93
136k
    add_num(kPatchSizeContext, ref_pos.ysize - 1);
94
136k
    add_num(kPatchCountContext, num - 1);
95
2.04M
    for (size_t j = i_start; j < i; j++) {
96
1.90M
      const PatchPosition& pos = pdic.positions_[j];
97
1.90M
      if (j == i_start) {
98
136k
        add_num(kPatchPositionContext, pos.x);
99
136k
        add_num(kPatchPositionContext, pos.y);
100
1.77M
      } else {
101
1.77M
        add_num(kPatchOffsetContext,
102
1.77M
                PackSigned(pos.x - pdic.positions_[j - 1].x));
103
1.77M
        add_num(kPatchOffsetContext,
104
1.77M
                PackSigned(pos.y - pdic.positions_[j - 1].y));
105
1.77M
      }
106
3.81M
      for (size_t k = 0; k < blending_stride; ++k, ++blend_pos) {
107
1.90M
        const PatchBlending& info = pdic.blendings_[blend_pos];
108
1.90M
        add_num(kPatchBlendModeContext, static_cast<uint32_t>(info.mode));
109
1.90M
        if (UsesAlpha(info.mode) && choose_alpha) {
110
0
          add_num(kPatchAlphaChannelContext, info.alpha_channel);
111
0
        }
112
1.90M
        if (UsesClamp(info.mode)) {
113
0
          add_num(kPatchClampContext, TO_JXL_BOOL(info.clamp));
114
0
        }
115
1.90M
      }
116
1.90M
    }
117
136k
  }
118
119
1.84k
  EntropyEncodingData codes;
120
1.84k
  JXL_ASSIGN_OR_RETURN(
121
1.84k
      size_t cost, BuildAndEncodeHistograms(memory_manager, HistogramParams(),
122
1.84k
                                            kNumPatchDictionaryContexts, tokens,
123
1.84k
                                            &codes, writer, layer, aux_out));
124
1.84k
  (void)cost;
125
1.84k
  JXL_RETURN_IF_ERROR(WriteTokens(tokens[0], codes, 0, writer, layer, aux_out));
126
1.84k
  return true;
127
1.84k
}
128
129
// static
130
Status PatchDictionaryEncoder::SubtractFrom(const PatchDictionary& pdic,
131
5.75k
                                            Image3F* opsin) {
132
  // TODO(veluca): this can likely be optimized knowing it runs on full images.
133
1.35M
  for (size_t y = 0; y < opsin->ysize(); y++) {
134
1.35M
    float* JXL_RESTRICT rows[3] = {
135
1.35M
        opsin->PlaneRow(0, y),
136
1.35M
        opsin->PlaneRow(1, y),
137
1.35M
        opsin->PlaneRow(2, y),
138
1.35M
    };
139
1.35M
    size_t blending_stride = pdic.blendings_stride_;
140
4.23M
    for (size_t pos_idx : pdic.GetPatchesForRow(y)) {
141
4.23M
      const size_t blending_idx = pos_idx * blending_stride;
142
4.23M
      const PatchPosition& pos = pdic.positions_[pos_idx];
143
4.23M
      const PatchReferencePosition& ref_pos =
144
4.23M
          pdic.ref_positions_[pos.ref_pos_idx];
145
4.23M
      const PatchBlendMode mode = pdic.blendings_[blending_idx].mode;
146
4.23M
      size_t by = pos.y;
147
4.23M
      size_t bx = pos.x;
148
4.23M
      size_t xsize = ref_pos.xsize;
149
4.23M
      JXL_ENSURE(y >= by);
150
4.23M
      JXL_ENSURE(y < by + ref_pos.ysize);
151
4.23M
      size_t iy = y - by;
152
4.23M
      size_t ref = ref_pos.ref;
153
4.23M
      const float* JXL_RESTRICT ref_rows[3] = {
154
4.23M
          pdic.reference_frames_->at(ref).frame->color()->ConstPlaneRow(
155
4.23M
              0, ref_pos.y0 + iy) +
156
4.23M
              ref_pos.x0,
157
4.23M
          pdic.reference_frames_->at(ref).frame->color()->ConstPlaneRow(
158
4.23M
              1, ref_pos.y0 + iy) +
159
4.23M
              ref_pos.x0,
160
4.23M
          pdic.reference_frames_->at(ref).frame->color()->ConstPlaneRow(
161
4.23M
              2, ref_pos.y0 + iy) +
162
4.23M
              ref_pos.x0,
163
4.23M
      };
164
29.4M
      for (size_t ix = 0; ix < xsize; ix++) {
165
101M
        for (size_t c = 0; c < 3; c++) {
166
75.7M
          if (mode == PatchBlendMode::kAdd) {
167
75.7M
            rows[c][bx + ix] -= ref_rows[c][ix];
168
75.7M
          } else if (mode == PatchBlendMode::kReplace) {
169
0
            rows[c][bx + ix] = 0;
170
0
          } else if (mode == PatchBlendMode::kNone) {
171
            // Nothing to do.
172
0
          } else {
173
0
            return JXL_UNREACHABLE("blending mode %u not yet implemented",
174
0
                                   static_cast<uint32_t>(mode));
175
0
          }
176
75.7M
        }
177
25.2M
      }
178
4.23M
    }
179
1.35M
  }
180
5.75k
  return true;
181
5.75k
}
182
183
namespace {
184
185
struct PatchColorspaceInfo {
186
  float kChannelDequant[3];
187
  float kChannelWeights[3];
188
189
3.91k
  explicit PatchColorspaceInfo(bool is_xyb) {
190
3.91k
    if (is_xyb) {
191
3.91k
      kChannelDequant[0] = 0.01615;
192
3.91k
      kChannelDequant[1] = 0.08875;
193
3.91k
      kChannelDequant[2] = 0.1922;
194
3.91k
      kChannelWeights[0] = 30.0;
195
3.91k
      kChannelWeights[1] = 3.0;
196
3.91k
      kChannelWeights[2] = 1.0;
197
3.91k
    } else {
198
0
      kChannelDequant[0] = 20.0f / 255;
199
0
      kChannelDequant[1] = 22.0f / 255;
200
0
      kChannelDequant[2] = 20.0f / 255;
201
0
      kChannelWeights[0] = 0.017 * 255;
202
0
      kChannelWeights[1] = 0.02 * 255;
203
0
      kChannelWeights[2] = 0.017 * 255;
204
0
    }
205
3.91k
  }
206
207
263M
  float ScaleForQuantization(float val, size_t c) {
208
263M
    return val / kChannelDequant[c];
209
263M
  }
210
211
263M
  int Quantize(float val, size_t c) {
212
263M
    float scaled = ScaleForQuantization(val, c);
213
    // Clamping allows values outside of target range (int8_t); caller should
214
    // deal with out-of-range values.
215
263M
    scaled = jxl::Clamp1(scaled, -32768.0f, 32767.0f);
216
263M
    return std::trunc(scaled);
217
263M
  }
218
219
673M
  bool is_similar_v(const Color& v1, const Color& v2, float threshold) {
220
673M
    float distance = 0;
221
2.69G
    for (size_t c = 0; c < 3; c++) {
222
2.01G
      distance += std::abs(v1[c] - v2[c]) * kChannelWeights[c];
223
2.01G
    }
224
673M
    return distance <= threshold;
225
673M
  }
226
};
227
228
using XY = std::pair<int32_t, int32_t>;
229
constexpr const size_t kPatchSide = 4;
230
231
StatusOr<std::vector<PatchInfo>> FindTextLikePatches(
232
    const CompressParams& cparams, const Image3F& opsin,
233
    const PassesEncoderState* JXL_RESTRICT state, ThreadPool* pool,
234
5.75k
    AuxOut* aux_out, bool is_xyb) {
235
5.75k
  std::vector<PatchInfo> info;
236
5.75k
  if (state->cparams.patches == Override::kOff) return info;
237
3.91k
  const auto& frame_dim = state->shared.frame_dim;
238
3.91k
  JxlMemoryManager* memory_manager = opsin.memory_manager();
239
240
3.91k
  PatchColorspaceInfo pci(is_xyb);
241
3.91k
  float kSimilarThreshold = 0.8f;
242
243
3.91k
  auto is_similar_impl = [&pci](const XY& p1, const XY& p2,
244
3.91k
                                const float* JXL_RESTRICT rows[3],
245
440M
                                size_t stride, float threshold) {
246
440M
    size_t offset1 = p1.second * stride + p1.first;
247
440M
    Color v1{rows[0][offset1], rows[1][offset1], rows[2][offset1]};
248
440M
    size_t offset2 = p2.second * stride + p2.first;
249
440M
    Color v2{rows[0][offset2], rows[1][offset2], rows[2][offset2]};
250
440M
    return pci.is_similar_v(v1, v2, threshold);
251
440M
  };
252
253
3.91k
  std::atomic<uint32_t> screenshot_area_seeds{0};
254
3.91k
  const size_t opsin_stride = opsin.PixelsPerRow();
255
3.91k
  const float* JXL_RESTRICT opsin_rows[3] = {opsin.ConstPlaneRow(0, 0),
256
3.91k
                                             opsin.ConstPlaneRow(1, 0),
257
3.91k
                                             opsin.ConstPlaneRow(2, 0)};
258
18.7M
  const auto pick = [&opsin_rows, opsin_stride](const XY& p) -> Color {
259
18.7M
    size_t offset = p.second * opsin_stride + p.first;
260
18.7M
    return {opsin_rows[0][offset], opsin_rows[1][offset],
261
18.7M
            opsin_rows[2][offset]};
262
18.7M
  };
263
3.91k
  const auto is_same_color = [&opsin_rows, opsin_stride](
264
204M
                                 const XY& p, const Color& c) -> size_t {
265
204M
    const size_t offset = p.second * opsin_stride + p.first;
266
765M
    for (size_t i = 0; i < c.size(); ++i) {
267
579M
      if (std::fabs(c[i] - opsin_rows[i][offset]) > 1e-4) {
268
18.6M
        return 0;
269
18.6M
      }
270
579M
    }
271
186M
    return 1;
272
204M
  };
273
274
297M
  auto is_similar = [&](const XY& p1, const XY& p2) {
275
297M
    return is_similar_impl(p1, p2, opsin_rows, opsin_stride, kSimilarThreshold);
276
297M
  };
277
278
  // Look for kPatchSide size squares, naturally aligned, that all have the same
279
  // pixel values.
280
3.91k
  JXL_ASSIGN_OR_RETURN(
281
3.91k
      ImageB is_screenshot_like,
282
3.91k
      ImageB::Create(memory_manager, DivCeil(frame_dim.xsize, kPatchSide),
283
3.91k
                     DivCeil(frame_dim.ysize, kPatchSide)));
284
3.91k
  ZeroFillImage(&is_screenshot_like);
285
3.91k
  const size_t pw = frame_dim.xsize / kPatchSide;
286
3.91k
  const size_t ph = frame_dim.ysize / kPatchSide;
287
288
18.7M
  const auto flat_patch = [&](const XY& o, const Color& base) -> bool {
289
48.5M
    for (size_t iy = 0; iy < kPatchSide; iy++) {
290
182M
      for (size_t ix = 0; ix < kPatchSide; ix++) {
291
152M
        XY p = {static_cast<int32_t>(o.first + ix),
292
152M
                static_cast<int32_t>(o.second + iy)};
293
152M
        if (!is_same_color(p, base)) {
294
12.9M
          return false;
295
12.9M
        }
296
152M
      }
297
42.7M
    }
298
5.78M
    return true;
299
18.7M
  };
300
301
  // TODO(eustas): should do this in 2 phases:
302
  //   1) if patches are not enabled do sampling run for has_screenshot_areas
303
  //   2) if patches forced or not disables + has_screenshot_areas do
304
  //      SIMDified full scan for is_screenshot_like
305
3.91k
  const auto process_row = [&](const uint32_t py,
306
295k
                               size_t /* thread */) -> Status {
307
295k
    uint32_t found = 0;
308
19.0M
    for (size_t px = 1; px <= pw - 2; px++) {
309
18.7M
      XY o = {static_cast<uint32_t>(px * kPatchSide),
310
18.7M
              static_cast<uint32_t>(py * kPatchSide)};
311
18.7M
      Color base = pick(o);
312
18.7M
      if (!flat_patch(o, base)) continue;
313
5.78M
      size_t num_same = 0;
314
23.1M
      for (size_t y = (py - 1) * kPatchSide; y <= (py + 1) * kPatchSide;
315
17.3M
           y += kPatchSide) {
316
69.4M
        for (size_t x = (px - 1) * kPatchSide; x <= (px + 1) * kPatchSide;
317
52.0M
             x += kPatchSide) {
318
52.0M
          XY p = {static_cast<uint32_t>(x), static_cast<uint32_t>(y)};
319
52.0M
          num_same += is_same_color(p, base);
320
52.0M
        }
321
17.3M
      }
322
      // Too few equal pixels nearby.
323
5.78M
      if (num_same < 8) continue;
324
4.08M
      is_screenshot_like.Row(py)[px] = 1;
325
4.08M
      found++;
326
4.08M
    }
327
295k
    screenshot_area_seeds.fetch_add(found);
328
295k
    return true;
329
295k
  };
330
3.91k
  bool can_have_seeds = ((pw >= 3) && (ph >= 3));
331
3.91k
  if (can_have_seeds) {
332
3.65k
    JXL_RETURN_IF_ERROR(RunOnPool(pool, 1, ph - 2, ThreadPool::NoInit,
333
3.65k
                                  process_row, "IsScreenshotLike"));
334
3.65k
  }
335
336
  // TODO(veluca): also parallelize the rest of this function.
337
3.91k
  if (WantDebugOutput(cparams)) {
338
0
    JXL_RETURN_IF_ERROR(
339
0
        DumpPlaneNormalized(cparams, "screenshot_like", is_screenshot_like));
340
0
  }
341
342
3.91k
  constexpr int kSearchRadius = 1;
343
344
3.91k
  size_t num_seeds = screenshot_area_seeds.load();
345
3.91k
  if (!ApplyOverride(state->cparams.patches, (num_seeds > 0))) {
346
784
    return info;
347
784
  }
348
349
  // Search for "similar enough" pixels near the screenshot-like areas.
350
6.26k
  JXL_ASSIGN_OR_RETURN(
351
6.26k
      ImageB is_background,
352
6.26k
      ImageB::Create(memory_manager, frame_dim.xsize, frame_dim.ysize));
353
6.26k
  ZeroFillImage(&is_background);
354
6.26k
  JXL_ASSIGN_OR_RETURN(
355
3.13k
      Image3F background,
356
3.13k
      Image3F::Create(memory_manager, frame_dim.xsize, frame_dim.ysize));
357
3.13k
  ZeroFillImage(&background);
358
3.13k
  constexpr size_t kDistanceLimit = 50;
359
3.13k
  float* JXL_RESTRICT background_rows[3] = {
360
3.13k
      background.PlaneRow(0, 0),
361
3.13k
      background.PlaneRow(1, 0),
362
3.13k
      background.PlaneRow(2, 0),
363
3.13k
  };
364
3.13k
  const size_t background_stride = background.PixelsPerRow();
365
3.13k
  uint8_t* JXL_RESTRICT is_background_row = is_background.Row(0);
366
3.13k
  const size_t is_background_stride = is_background.PixelsPerRow();
367
1.99G
  const auto is_bg = [&](const XY& p) -> uint8_t& {
368
1.99G
    return is_background_row[p.second * is_background_stride + p.first];
369
1.99G
  };
370
3.13k
  std::vector<std::pair<XY, XY>> queue;
371
3.13k
  queue.reserve(2 * num_seeds * kPatchSide * kPatchSide);
372
3.13k
  size_t queue_front = 0;
373
  // TODO(eustas): coalesce neighbours, leave only border.
374
3.13k
  if (can_have_seeds) {
375
273k
    for (size_t py = 1; py < ph - 1; py++) {
376
270k
      uint8_t* JXL_RESTRICT screenshot_row = is_screenshot_like.Row(py);
377
17.4M
      for (size_t px = 1; px < pw - 1; px++) {
378
17.2M
        if (!screenshot_row[px]) continue;
379
20.4M
        for (size_t y = py * kPatchSide; y < (py + 1) * kPatchSide; ++y) {
380
81.7M
          for (size_t x = px * kPatchSide; x < (px + 1) * kPatchSide; ++x) {
381
65.4M
            XY p = {static_cast<uint32_t>(x), static_cast<uint32_t>(y)};
382
65.4M
            queue.emplace_back(p, p);
383
65.4M
            is_bg(p) = 1;
384
65.4M
          }
385
16.3M
        }
386
4.08M
      }
387
270k
    }
388
3.13k
  }
389
215M
  while (queue_front < queue.size()) {
390
215M
    XY cur = queue[queue_front].first;
391
215M
    XY src = queue[queue_front].second;
392
215M
    queue_front++;
393
215M
    Color src_color;
394
862M
    for (size_t c = 0; c < 3; c++) {
395
646M
      float clr = opsin_rows[c][src.second * opsin_stride + src.first];
396
646M
      src_color[c] = clr;
397
646M
      background_rows[c][cur.second * background_stride + cur.first] = clr;
398
646M
    }
399
862M
    for (int dx = -kSearchRadius; dx <= kSearchRadius; dx++) {
400
2.58G
      for (int dy = -kSearchRadius; dy <= kSearchRadius; dy++) {
401
1.94G
        XY next{cur.first + dx, cur.second + dy};
402
1.94G
        if (next.first < 0 || next.second < 0 ||
403
1.93G
            static_cast<uint32_t>(next.first) >= frame_dim.xsize ||
404
1.93G
            static_cast<uint32_t>(next.second) >= frame_dim.ysize) {
405
6.85M
          continue;
406
6.85M
        }
407
1.93G
        uint8_t& bg = is_bg(next);
408
1.93G
        if (bg) continue;
409
299M
        if (static_cast<uint32_t>(
410
299M
                std::abs(next.first - static_cast<int>(src.first)) +
411
299M
                std::abs(next.second - static_cast<int>(src.second))) >
412
299M
            kDistanceLimit) {
413
1.70M
          continue;
414
1.70M
        }
415
297M
        if (is_similar(src, next)) {
416
150M
          queue.emplace_back(next, src);
417
150M
          bg = 1;
418
150M
        }
419
297M
      }
420
646M
    }
421
215M
  }
422
3.13k
  queue.clear();
423
424
3.13k
  ImageF ccs;
425
3.13k
  Rng rng(0);
426
3.13k
  bool paint_ccs = false;
427
3.13k
  if (WantDebugOutput(cparams)) {
428
0
    JXL_RETURN_IF_ERROR(
429
0
        DumpPlaneNormalized(cparams, "is_background", is_background));
430
0
    if (is_xyb) {
431
0
      JXL_RETURN_IF_ERROR(DumpXybImage(cparams, "background", background));
432
0
    } else {
433
0
      JXL_RETURN_IF_ERROR(DumpImage(cparams, "background", background));
434
0
    }
435
0
    JXL_ASSIGN_OR_RETURN(
436
0
        ccs, ImageF::Create(memory_manager, frame_dim.xsize, frame_dim.ysize));
437
0
    ZeroFillImage(&ccs);
438
0
    paint_ccs = true;
439
0
  }
440
441
3.13k
  constexpr float kVerySimilarThreshold = 0.03f;
442
3.13k
  constexpr float kHasSimilarThreshold = 0.03f;
443
444
3.13k
  const float* JXL_RESTRICT const_background_rows[3] = {
445
3.13k
      background_rows[0], background_rows[1], background_rows[2]};
446
142M
  auto is_similar_b = [&](std::pair<int, int> p1, std::pair<int, int> p2) {
447
142M
    return is_similar_impl(p1, p2, const_background_rows, background_stride,
448
142M
                           kVerySimilarThreshold);
449
142M
  };
450
451
3.13k
  constexpr int kMinPeak = 2;
452
3.13k
  constexpr int kHasSimilarRadius = 2;
453
454
  // Find small CC outside the "similar enough" areas, compute bounding boxes,
455
  // and run heuristics to exclude some patches.
456
3.13k
  JXL_ASSIGN_OR_RETURN(
457
3.13k
      ImageB visited,
458
3.13k
      ImageB::Create(memory_manager, frame_dim.xsize, frame_dim.ysize));
459
3.13k
  ZeroFillImage(&visited);
460
3.13k
  uint8_t* JXL_RESTRICT visited_row = visited.Row(0);
461
3.13k
  const size_t visited_stride = visited.PixelsPerRow();
462
3.13k
  std::vector<std::pair<uint32_t, uint32_t>> cc;
463
3.13k
  std::vector<std::pair<uint32_t, uint32_t>> stack;
464
1.11M
  for (size_t y = 0; y < frame_dim.ysize; y++) {
465
294M
    for (size_t x = 0; x < frame_dim.xsize; x++) {
466
293M
      if (is_background_row[y * is_background_stride + x]) continue;
467
77.5M
      cc.clear();
468
77.5M
      stack.clear();
469
77.5M
      stack.emplace_back(static_cast<uint32_t>(x), static_cast<uint32_t>(y));
470
77.5M
      size_t min_x = x;
471
77.5M
      size_t max_x = x;
472
77.5M
      size_t min_y = y;
473
77.5M
      size_t max_y = y;
474
77.5M
      std::pair<uint32_t, uint32_t> reference;
475
77.5M
      bool found_border = false;
476
77.5M
      bool all_similar = true;
477
624M
      while (!stack.empty()) {
478
547M
        std::pair<uint32_t, uint32_t> cur = stack.back();
479
547M
        stack.pop_back();
480
547M
        if (visited_row[cur.second * visited_stride + cur.first]) continue;
481
77.5M
        visited_row[cur.second * visited_stride + cur.first] = 1;
482
77.5M
        if (cur.first < min_x) min_x = cur.first;
483
77.5M
        if (cur.first > max_x) max_x = cur.first;
484
77.5M
        if (cur.second < min_y) min_y = cur.second;
485
77.5M
        if (cur.second > max_y) max_y = cur.second;
486
77.5M
        if (paint_ccs) {
487
0
          cc.push_back(cur);
488
0
        }
489
310M
        for (int dx = -kSearchRadius; dx <= kSearchRadius; dx++) {
490
930M
          for (int dy = -kSearchRadius; dy <= kSearchRadius; dy++) {
491
698M
            if (dx == 0 && dy == 0) continue;
492
620M
            int next_first = static_cast<int32_t>(cur.first) + dx;
493
620M
            int next_second = static_cast<int32_t>(cur.second) + dy;
494
620M
            if (next_first < 0 || next_second < 0 ||
495
619M
                static_cast<uint32_t>(next_first) >= frame_dim.xsize ||
496
617M
                static_cast<uint32_t>(next_second) >= frame_dim.ysize) {
497
4.55M
              continue;
498
4.55M
            }
499
616M
            std::pair<uint32_t, uint32_t> next{next_first, next_second};
500
616M
            if (!is_background_row[next.second * is_background_stride +
501
616M
                                   next.first]) {
502
469M
              stack.push_back(next);
503
469M
            } else {
504
146M
              if (!found_border) {
505
3.36M
                reference = next;
506
3.36M
                found_border = true;
507
142M
              } else {
508
142M
                if (!is_similar_b(next, reference)) all_similar = false;
509
142M
              }
510
146M
            }
511
616M
          }
512
232M
        }
513
77.5M
      }
514
77.5M
      if (!found_border || !all_similar || max_x - min_x >= kMaxPatchSize ||
515
74.2M
          max_y - min_y >= kMaxPatchSize) {
516
74.2M
        continue;
517
74.2M
      }
518
3.29M
      size_t bpos = background_stride * reference.second + reference.first;
519
3.29M
      Color ref = {background_rows[0][bpos], background_rows[1][bpos],
520
3.29M
                   background_rows[2][bpos]};
521
3.29M
      bool has_similar = false;
522
3.29M
      for (size_t iy = std::max<int>(
523
3.29M
               static_cast<int32_t>(min_y) - kHasSimilarRadius, 0);
524
27.6M
           iy < std::min(max_y + kHasSimilarRadius + 1, frame_dim.ysize);
525
24.3M
           iy++) {
526
24.3M
        for (size_t ix = std::max<int>(
527
24.3M
                 static_cast<int32_t>(min_x) - kHasSimilarRadius, 0);
528
257M
             ix < std::min(max_x + kHasSimilarRadius + 1, frame_dim.xsize);
529
232M
             ix++) {
530
232M
          size_t opos = opsin_stride * iy + ix;
531
232M
          Color px = {opsin_rows[0][opos], opsin_rows[1][opos],
532
232M
                      opsin_rows[2][opos]};
533
232M
          if (pci.is_similar_v(ref, px, kHasSimilarThreshold)) {
534
144M
            has_similar = true;
535
144M
          }
536
232M
        }
537
24.3M
      }
538
3.29M
      if (!has_similar) continue;
539
3.29M
      info.emplace_back();
540
3.29M
      info.back().second.emplace_back(static_cast<uint32_t>(min_x),
541
3.29M
                                      static_cast<uint32_t>(min_y));
542
3.29M
      QuantizedPatch& patch = info.back().first;
543
3.29M
      patch.xsize = max_x - min_x + 1;
544
3.29M
      patch.ysize = max_y - min_y + 1;
545
3.29M
      bool too_big = false;
546
3.29M
      bool too_small = true;
547
9.87M
      for (size_t c : {1, 0, 2}) {
548
43.6M
        for (size_t iy = min_y; iy <= max_y; iy++) {
549
296M
          for (size_t ix = min_x; ix <= max_x; ix++) {
550
263M
            size_t offset = (iy - min_y) * patch.xsize + ix - min_x;
551
263M
            float fval = opsin_rows[c][iy * opsin_stride + ix] - ref[c];
552
263M
            patch.fpixels[c][offset] = fval;
553
263M
            int val = pci.Quantize(patch.fpixels[c][offset], c);
554
263M
            int8_t qval = static_cast<int8_t>(val);
555
263M
            patch.pixels[c][offset] = qval;
556
263M
            too_big |= (val != static_cast<int>(qval));
557
263M
            too_small &= (val < kMinPeak) && (val > -kMinPeak);
558
263M
          }
559
33.7M
        }
560
9.87M
      }
561
3.29M
      if (too_small || too_big) {
562
19.7k
        info.pop_back();
563
19.7k
        continue;
564
19.7k
      }
565
3.27M
      if (paint_ccs) {
566
0
        float cc_color = rng.UniformF(0.5, 1.0);
567
0
        for (std::pair<uint32_t, uint32_t> p : cc) {
568
0
          ccs.Row(p.second)[p.first] = cc_color;
569
0
        }
570
0
      }
571
3.27M
    }
572
1.11M
  }
573
574
3.13k
  if (paint_ccs) {
575
0
    JXL_ENSURE(WantDebugOutput(cparams));
576
0
    JXL_RETURN_IF_ERROR(DumpPlaneNormalized(cparams, "ccs", ccs));
577
0
  }
578
3.13k
  if (info.empty()) {
579
448
    return info;
580
448
  }
581
582
  // Remove duplicates.
583
2.68k
  constexpr size_t kMinPatchOccurrences = 2;
584
2.68k
  std::sort(info.begin(), info.end());
585
2.68k
  size_t unique = 0;
586
3.27M
  for (size_t i = 1; i < info.size(); i++) {
587
3.26M
    if (info[i].first == info[unique].first) {
588
1.93M
      info[unique].second.insert(info[unique].second.end(),
589
1.93M
                                 info[i].second.begin(), info[i].second.end());
590
1.93M
    } else {
591
1.33M
      if (info[unique].second.size() >= kMinPatchOccurrences) {
592
156k
        unique++;
593
156k
      }
594
1.33M
      info[unique] = info[i];
595
1.33M
    }
596
3.26M
  }
597
2.68k
  if (info[unique].second.size() >= kMinPatchOccurrences) {
598
444
    unique++;
599
444
  }
600
2.68k
  info.resize(unique);
601
602
2.68k
  size_t max_patch_size = 0;
603
604
156k
  for (const auto& patch : info) {
605
156k
    size_t pixels = patch.first.xsize * patch.first.ysize;
606
156k
    if (pixels > max_patch_size) max_patch_size = pixels;
607
156k
  }
608
609
  // don't use patches if all patches are smaller than this
610
2.68k
  constexpr size_t kMinMaxPatchSize = 20;
611
2.68k
  if (max_patch_size < kMinMaxPatchSize) {
612
840
    info.clear();
613
840
  }
614
615
2.68k
  return info;
616
3.13k
}
617
618
}  // namespace
619
620
Status FindBestPatchDictionary(const Image3F& opsin,
621
                               PassesEncoderState* JXL_RESTRICT state,
622
                               const JxlCmsInterface& cms, ThreadPool* pool,
623
5.75k
                               AuxOut* aux_out, bool is_xyb) {
624
5.75k
  JXL_ASSIGN_OR_RETURN(
625
5.75k
      std::vector<PatchInfo> info,
626
5.75k
      FindTextLikePatches(state->cparams, opsin, state, pool, aux_out, is_xyb));
627
5.75k
  JxlMemoryManager* memory_manager = opsin.memory_manager();
628
629
  // TODO(veluca): this doesn't work if both dots and patches are enabled.
630
  // For now, since dots and patches are not likely to occur in the same kind of
631
  // images, disable dots if some patches were found.
632
5.75k
  if (info.empty() &&
633
3.91k
      ApplyOverride(
634
3.91k
          state->cparams.dots,
635
3.91k
          state->cparams.speed_tier <= SpeedTier::kSquirrel &&
636
3.91k
              state->cparams.butteraugli_distance >= kMinButteraugliForDots &&
637
0
              !state->cparams.disable_perceptual_optimizations)) {
638
0
    Rect rect(0, 0, state->shared.frame_dim.xsize,
639
0
              state->shared.frame_dim.ysize);
640
0
    JXL_ASSIGN_OR_RETURN(info,
641
0
                         FindDotDictionary(state->cparams, opsin, rect,
642
0
                                           state->shared.cmap.base(), pool));
643
0
  }
644
645
5.75k
  if (info.empty()) return true;
646
647
1.84k
  std::sort(
648
670k
      info.begin(), info.end(), [&](const PatchInfo& a, const PatchInfo& b) {
649
670k
        return a.first.xsize * a.first.ysize > b.first.xsize * b.first.ysize;
650
670k
      });
651
652
1.84k
  size_t max_x_size = 0;
653
1.84k
  size_t max_y_size = 0;
654
1.84k
  size_t total_pixels = 0;
655
656
136k
  for (const auto& patch : info) {
657
136k
    size_t pixels = patch.first.xsize * patch.first.ysize;
658
136k
    if (max_x_size < patch.first.xsize) max_x_size = patch.first.xsize;
659
136k
    if (max_y_size < patch.first.ysize) max_y_size = patch.first.ysize;
660
136k
    total_pixels += pixels;
661
136k
  }
662
663
  // Bin-packing & conversion of patches.
664
1.84k
  constexpr float kBinPackingSlackness = 1.05f;
665
1.84k
  size_t ref_xsize = std::max<float>(max_x_size, std::sqrt(total_pixels));
666
1.84k
  size_t ref_ysize = std::max<float>(max_y_size, std::sqrt(total_pixels));
667
1.84k
  std::vector<std::pair<size_t, size_t>> ref_positions(info.size());
668
  // TODO(veluca): allow partial overlaps of patches that have the same pixels.
669
1.84k
  size_t max_y = 0;
670
2.51k
  do {
671
2.51k
    max_y = 0;
672
    // Increase packed image size.
673
2.51k
    ref_xsize = ref_xsize * kBinPackingSlackness + 1;
674
2.51k
    ref_ysize = ref_ysize * kBinPackingSlackness + 1;
675
676
2.51k
    JXL_ASSIGN_OR_RETURN(ImageB occupied,
677
2.51k
                         ImageB::Create(memory_manager, ref_xsize, ref_ysize));
678
2.51k
    ZeroFillImage(&occupied);
679
2.51k
    uint8_t* JXL_RESTRICT occupied_rows = occupied.Row(0);
680
2.51k
    size_t occupied_stride = occupied.PixelsPerRow();
681
682
2.51k
    bool success = true;
683
    // For every patch...
684
145k
    for (size_t patch = 0; patch < info.size(); patch++) {
685
143k
      size_t x0 = 0;
686
143k
      size_t y0 = 0;
687
143k
      size_t xsize = info[patch].first.xsize;
688
143k
      size_t ysize = info[patch].first.ysize;
689
143k
      bool found = false;
690
      // For every possible start position ...
691
7.43M
      for (; y0 + ysize <= ref_ysize; y0++) {
692
7.43M
        x0 = 0;
693
875M
        for (; x0 + xsize <= ref_xsize; x0++) {
694
868M
          bool has_occupied_pixel = false;
695
868M
          size_t x = x0;
696
          // Check if it is possible to place the patch in this position in the
697
          // reference frame.
698
6.63G
          for (size_t y = y0; y < y0 + ysize; y++) {
699
5.76G
            x = x0;
700
6.44G
            for (; x < x0 + xsize; x++) {
701
6.40G
              if (occupied_rows[y * occupied_stride + x]) {
702
5.72G
                has_occupied_pixel = true;
703
5.72G
                break;
704
5.72G
              }
705
6.40G
            }
706
5.76G
          }  // end of positioning check
707
868M
          if (!has_occupied_pixel) {
708
142k
            found = true;
709
142k
            break;
710
142k
          }
711
868M
          x0 = x;  // Jump to next pixel after the occupied one.
712
868M
        }
713
7.43M
        if (found) break;
714
7.43M
      }  // end of start position checking
715
716
      // We didn't find a possible position: repeat from the beginning with a
717
      // larger reference frame size.
718
143k
      if (!found) {
719
673
        success = false;
720
673
        break;
721
673
      }
722
723
      // We found a position: mark the corresponding positions in the reference
724
      // image as used.
725
142k
      ref_positions[patch] = {x0, y0};
726
899k
      for (size_t y = y0; y < y0 + ysize; y++) {
727
9.11M
        for (size_t x = x0; x < x0 + xsize; x++) {
728
8.36M
          occupied_rows[y * occupied_stride + x] = JXL_TRUE;
729
8.36M
        }
730
756k
      }
731
142k
      max_y = std::max(max_y, y0 + ysize);
732
142k
    }
733
734
2.51k
    if (success) break;
735
2.51k
  } while (true);
736
737
1.84k
  JXL_ENSURE(ref_ysize >= max_y);
738
739
1.84k
  ref_ysize = max_y;
740
741
1.84k
  JXL_ASSIGN_OR_RETURN(Image3F reference_frame,
742
1.84k
                       Image3F::Create(memory_manager, ref_xsize, ref_ysize));
743
  // TODO(veluca): figure out a better way to fill the image.
744
1.84k
  ZeroFillImage(&reference_frame);
745
1.84k
  std::vector<PatchPosition> positions;
746
1.84k
  std::vector<PatchReferencePosition> pref_positions;
747
1.84k
  std::vector<PatchBlending> blendings;
748
1.84k
  float* JXL_RESTRICT ref_rows[3] = {
749
1.84k
      reference_frame.PlaneRow(0, 0),
750
1.84k
      reference_frame.PlaneRow(1, 0),
751
1.84k
      reference_frame.PlaneRow(2, 0),
752
1.84k
  };
753
1.84k
  size_t ref_stride = reference_frame.PixelsPerRow();
754
1.84k
  size_t num_ec = state->shared.metadata->m.num_extra_channels;
755
756
138k
  for (size_t i = 0; i < info.size(); i++) {
757
136k
    PatchReferencePosition ref_pos;
758
136k
    ref_pos.xsize = info[i].first.xsize;
759
136k
    ref_pos.ysize = info[i].first.ysize;
760
136k
    ref_pos.x0 = ref_positions[i].first;
761
136k
    ref_pos.y0 = ref_positions[i].second;
762
136k
    ref_pos.ref = kPatchFrameReferenceId;
763
846k
    for (size_t y = 0; y < ref_pos.ysize; y++) {
764
8.52M
      for (size_t x = 0; x < ref_pos.xsize; x++) {
765
31.2M
        for (size_t c = 0; c < 3; c++) {
766
23.4M
          ref_rows[c][(y + ref_pos.y0) * ref_stride + x + ref_pos.x0] =
767
23.4M
              info[i].first.fpixels[c][y * ref_pos.xsize + x];
768
23.4M
        }
769
7.81M
      }
770
709k
    }
771
1.90M
    for (const auto& pos : info[i].second) {
772
1.90M
      JXL_DEBUG_V(4, "Patch %" PRIuS "x%" PRIuS " at position %u,%u",
773
1.90M
                  ref_pos.xsize, ref_pos.ysize, pos.first, pos.second);
774
1.90M
      positions.emplace_back(
775
1.90M
          PatchPosition{pos.first, pos.second, pref_positions.size()});
776
      // Add blending for color channels, ignore other channels.
777
1.90M
      blendings.push_back({PatchBlendMode::kAdd, 0, false});
778
1.90M
      for (size_t j = 0; j < num_ec; ++j) {
779
0
        blendings.push_back({PatchBlendMode::kNone, 0, false});
780
0
      }
781
1.90M
    }
782
136k
    pref_positions.emplace_back(ref_pos);
783
136k
  }
784
785
1.84k
  CompressParams cparams = state->cparams;
786
  // Recursive application of patches could create very weird issues.
787
1.84k
  cparams.patches = Override::kOff;
788
789
1.84k
  if (WantDebugOutput(cparams)) {
790
0
    if (is_xyb) {
791
0
      JXL_RETURN_IF_ERROR(
792
0
          DumpXybImage(cparams, "patch_reference", reference_frame));
793
0
    } else {
794
0
      JXL_RETURN_IF_ERROR(
795
0
          DumpImage(cparams, "patch_reference", reference_frame));
796
0
    }
797
0
  }
798
799
1.84k
  JXL_RETURN_IF_ERROR(RoundtripPatchFrame(&reference_frame, state,
800
1.84k
                                          kPatchFrameReferenceId, cparams, cms,
801
1.84k
                                          pool, aux_out, /*subtract=*/true));
802
803
  // TODO(veluca): this assumes that applying patches is commutative, which is
804
  // not true for all blending modes. This code only produces kAdd patches, so
805
  // this works out.
806
1.84k
  PatchDictionaryEncoder::SetPositions(
807
1.84k
      &state->shared.image_features.patches, std::move(positions),
808
1.84k
      std::move(pref_positions), std::move(blendings), num_ec + 1);
809
1.84k
  return true;
810
1.84k
}
811
812
Status RoundtripPatchFrame(Image3F* reference_frame,
813
                           PassesEncoderState* JXL_RESTRICT state, int idx,
814
                           CompressParams& cparams, const JxlCmsInterface& cms,
815
1.84k
                           ThreadPool* pool, AuxOut* aux_out, bool subtract) {
816
1.84k
  JxlMemoryManager* memory_manager = state->memory_manager();
817
1.84k
  FrameInfo patch_frame_info;
818
1.84k
  cparams.resampling = 1;
819
1.84k
  cparams.ec_resampling = 1;
820
1.84k
  cparams.dots = Override::kOff;
821
1.84k
  cparams.noise = Override::kOff;
822
1.84k
  cparams.modular_mode = true;
823
1.84k
  cparams.responsive = 0;
824
1.84k
  cparams.progressive_dc = 0;
825
1.84k
  cparams.progressive_mode = Override::kOff;
826
1.84k
  cparams.qprogressive_mode = Override::kOff;
827
  // Use gradient predictor and not Predictor::Best.
828
1.84k
  cparams.options.predictor = Predictor::Gradient;
829
1.84k
  patch_frame_info.save_as_reference = idx;  // always saved.
830
1.84k
  patch_frame_info.frame_type = FrameType::kReferenceOnly;
831
1.84k
  patch_frame_info.save_before_color_transform = true;
832
1.84k
  ImageBundle ib(memory_manager, &state->shared.metadata->m);
833
  // TODO(veluca): metadata.color_encoding is a lie: ib is in XYB, but there is
834
  // no simple way to express that yet.
835
1.84k
  patch_frame_info.ib_needs_color_transform = false;
836
1.84k
  JXL_RETURN_IF_ERROR(ib.SetFromImage(
837
1.84k
      std::move(*reference_frame), state->shared.metadata->m.color_encoding));
838
1.84k
  if (!ib.metadata()->extra_channel_info.empty()) {
839
    // Add placeholder extra channels to the patch image: patch encoding does
840
    // not yet support extra channels, but the codec expects that the amount of
841
    // extra channels in frames matches that in the metadata of the codestream.
842
0
    std::vector<ImageF> extra_channels;
843
0
    extra_channels.reserve(ib.metadata()->extra_channel_info.size());
844
0
    for (size_t i = 0; i < ib.metadata()->extra_channel_info.size(); i++) {
845
0
      JXL_ASSIGN_OR_RETURN(
846
0
          ImageF ch, ImageF::Create(memory_manager, ib.xsize(), ib.ysize()));
847
0
      extra_channels.emplace_back(std::move(ch));
848
      // Must initialize the image with data to not affect blending with
849
      // uninitialized memory.
850
      // TODO(lode): patches must copy and use the real extra channels instead.
851
0
      ZeroFillImage(&extra_channels.back());
852
0
    }
853
0
    JXL_RETURN_IF_ERROR(ib.SetExtraChannels(std::move(extra_channels)));
854
0
  }
855
1.84k
  auto special_frame = jxl::make_unique<BitWriter>(memory_manager);
856
1.84k
  AuxOut patch_aux_out;
857
1.84k
  JXL_RETURN_IF_ERROR(EncodeFrame(
858
1.84k
      memory_manager, cparams, patch_frame_info, state->shared.metadata, ib,
859
1.84k
      cms, pool, special_frame.get(), aux_out ? &patch_aux_out : nullptr));
860
1.84k
  if (aux_out) {
861
0
    for (const auto& l : patch_aux_out.layers) {
862
0
      aux_out->layer(LayerType::Dictionary).Assimilate(l);
863
0
    }
864
0
  }
865
1.84k
  const Span<const uint8_t> encoded = special_frame->GetSpan();
866
1.84k
  state->special_frames.emplace_back(std::move(special_frame));
867
1.84k
  if (subtract) {
868
1.84k
    ImageBundle decoded(memory_manager, &state->shared.metadata->m);
869
1.84k
    auto dec_state = jxl::make_unique<PassesDecoderState>(memory_manager);
870
1.84k
    JXL_RETURN_IF_ERROR(dec_state->output_encoding_info.SetFromMetadata(
871
1.84k
        *state->shared.metadata));
872
1.84k
    const uint8_t* frame_start = encoded.data();
873
1.84k
    size_t encoded_size = encoded.size();
874
1.84k
    JXL_RETURN_IF_ERROR(DecodeFrame(
875
1.84k
        dec_state.get(), pool, frame_start, encoded_size,
876
1.84k
        /*frame_header=*/nullptr, &decoded, *state->shared.metadata));
877
1.84k
    frame_start += decoded.decoded_bytes();
878
1.84k
    encoded_size -= decoded.decoded_bytes();
879
1.84k
    size_t ref_xsize =
880
1.84k
        dec_state->shared_storage.reference_frames[idx].frame->color()->xsize();
881
    // if the frame itself uses patches, we need to decode another frame
882
1.84k
    if (!ref_xsize) {
883
0
      JXL_RETURN_IF_ERROR(DecodeFrame(
884
0
          dec_state.get(), pool, frame_start, encoded_size,
885
0
          /*frame_header=*/nullptr, &decoded, *state->shared.metadata));
886
0
    }
887
1.84k
    JXL_ENSURE(encoded_size == 0);
888
1.84k
    state->shared.reference_frames[idx] =
889
1.84k
        std::move(dec_state->shared_storage.reference_frames[idx]);
890
1.84k
  } else {
891
0
    *state->shared.reference_frames[idx].frame = std::move(ib);
892
0
  }
893
1.84k
  return true;
894
1.84k
}
895
896
}  // namespace jxl