Coverage Report

Created: 2026-10-14 06:45

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/src/libjxl/lib/jxl/enc_cluster.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_cluster.h"
7
8
#include <algorithm>
9
#include <cstddef>
10
#include <cstdint>
11
#include <limits>
12
#include <map>
13
#include <numeric>
14
#include <queue>
15
#include <tuple>
16
#include <vector>
17
18
#include "lib/jxl/base/status.h"
19
#include "lib/jxl/enc_ans_params.h"
20
21
#undef HWY_TARGET_INCLUDE
22
#define HWY_TARGET_INCLUDE "lib/jxl/enc_cluster.cc"
23
#include <hwy/foreach_target.h>
24
#include <hwy/highway.h>
25
26
#include "lib/jxl/base/fast_math-inl.h"
27
HWY_BEFORE_NAMESPACE();
28
namespace jxl {
29
namespace HWY_NAMESPACE {
30
31
// These templates are not found via ADL.
32
using hwy::HWY_NAMESPACE::AllTrue;
33
using hwy::HWY_NAMESPACE::Eq;
34
using hwy::HWY_NAMESPACE::GetLane;
35
using hwy::HWY_NAMESPACE::IfThenZeroElse;
36
using hwy::HWY_NAMESPACE::SumOfLanes;
37
using hwy::HWY_NAMESPACE::Zero;
38
39
template <class V>
40
0
V Entropy(V count, V inv_total, V total) {
41
0
  const HWY_CAPPED(float, Histogram::kRounding) d;
42
0
  const auto zero = Set(d, 0.0f);
43
  // TODO(eustas): why (0 - x) instead of Neg(x)?
44
0
  return IfThenZeroElse(
45
0
      Eq(count, total),
46
0
      Sub(zero, Mul(count, FastLog2f(d, Mul(inv_total, count)))));
47
0
}
48
49
0
void HistogramCondition(Histogram& a) {
50
0
  const HWY_CAPPED(int32_t, Histogram::kRounding) di;
51
0
  const auto kZero = Zero(di);
52
0
  auto total = kZero;
53
0
  int nz_pos = -static_cast<int>(Lanes(di));
54
0
  for (size_t i = 0; i < a.counts.size(); i += Lanes(di)) {
55
0
    const auto counts = LoadU(di, &a.counts[i]);
56
0
    const bool nz = !AllTrue(di, Eq(counts, kZero));
57
0
    total = Add(total, counts);
58
0
    if (nz) nz_pos = i;
59
0
  }
60
0
  a.counts.resize(nz_pos + Lanes(di));
61
0
  a.total_count = GetLane(SumOfLanes(di, total));
62
0
}
63
64
0
void HistogramEntropy(const Histogram& a) {
65
0
  a.entropy = 0.0f;
66
0
  if (a.total_count == 0) return;
67
68
0
  const HWY_CAPPED(float, Histogram::kRounding) df;
69
0
  const HWY_CAPPED(int32_t, Histogram::kRounding) di;
70
71
0
  const auto inv_tot = Set(df, 1.0f / a.total_count);
72
0
  auto entropy_lanes = Zero(df);
73
0
  auto total = Set(df, a.total_count);
74
75
0
  for (size_t i = 0; i < a.counts.size(); i += Lanes(di)) {
76
0
    const auto counts = LoadU(di, &a.counts[i]);
77
0
    entropy_lanes =
78
0
        Add(entropy_lanes, Entropy(ConvertTo(df, counts), inv_tot, total));
79
0
  }
80
0
  a.entropy += GetLane(SumOfLanes(df, entropy_lanes));
81
0
}
82
83
0
float HistogramDistance(const Histogram& a, const Histogram& b) {
84
0
  if (a.total_count == 0 || b.total_count == 0) return 0;
85
86
0
  const HWY_CAPPED(float, Histogram::kRounding) df;
87
0
  const HWY_CAPPED(int32_t, Histogram::kRounding) di;
88
89
0
  const auto inv_tot = Set(df, 1.0f / (a.total_count + b.total_count));
90
0
  auto distance_lanes = Zero(df);
91
0
  auto total = Set(df, a.total_count + b.total_count);
92
93
0
  for (size_t i = 0; i < std::max(a.counts.size(), b.counts.size());
94
0
       i += Lanes(di)) {
95
0
    const auto a_counts =
96
0
        a.counts.size() > i ? LoadU(di, &a.counts[i]) : Zero(di);
97
0
    const auto b_counts =
98
0
        b.counts.size() > i ? LoadU(di, &b.counts[i]) : Zero(di);
99
0
    const auto counts = ConvertTo(df, Add(a_counts, b_counts));
100
0
    distance_lanes = Add(distance_lanes, Entropy(counts, inv_tot, total));
101
0
  }
102
0
  const float total_distance = GetLane(SumOfLanes(df, distance_lanes));
103
0
  return total_distance - a.entropy - b.entropy;
104
0
}
105
106
constexpr const float kInfinity = std::numeric_limits<float>::infinity();
107
108
// With prefix codes, a histogram with a single symbol costs nothing per
109
// symbol, and one with more costs at least one bit per symbol. The Shannon
110
// entropy misses that: a mostly-zero 2-symbol histogram looks nearly free to
111
// merge into large all-zero ones, which then cost a bit per zero. So such a
112
// merge costs at least a bit per symbol of the single-symbol side(s).
113
0
size_t NumSymbols(const Histogram& a, const Histogram* b) {
114
0
  size_t n = 0;
115
0
  const size_t size =
116
0
      std::max(a.counts.size(), b ? b->counts.size() : size_t{0});
117
0
  for (size_t i = 0; i < size; i++) {
118
0
    const int32_t count = (i < a.counts.size() ? a.counts[i] : 0) +
119
0
                          (b && i < b->counts.size() ? b->counts[i] : 0);
120
0
    n += count > 0;
121
0
  }
122
0
  return n;
123
0
}
124
float ClusterDistance(const Histogram& a, const Histogram& b,
125
0
                      bool prefix_codes) {
126
0
  const float distance = HistogramDistance(a, b);
127
0
  if (!prefix_codes || a.total_count == 0 || b.total_count == 0 ||
128
0
      NumSymbols(a, &b) <= 1) {
129
0
    return distance;
130
0
  }
131
0
  float single = 0;
132
0
  if (NumSymbols(a, nullptr) <= 1) single += a.total_count;
133
0
  if (NumSymbols(b, nullptr) <= 1) single += b.total_count;
134
0
  return std::max(distance, single);
135
0
}
136
137
0
float HistogramKLDivergence(const Histogram& actual, const Histogram& coding) {
138
0
  if (actual.total_count == 0) return 0;
139
0
  if (coding.total_count == 0) return kInfinity;
140
141
0
  const HWY_CAPPED(float, Histogram::kRounding) df;
142
0
  const HWY_CAPPED(int32_t, Histogram::kRounding) di;
143
144
0
  const auto coding_inv = Set(df, 1.0f / coding.total_count);
145
0
  auto cost_lanes = Zero(df);
146
147
0
  for (size_t i = 0; i < actual.counts.size(); i += Lanes(di)) {
148
0
    const auto counts = LoadU(di, &actual.counts[i]);
149
0
    const auto coding_counts =
150
0
        coding.counts.size() > i ? LoadU(di, &coding.counts[i]) : Zero(di);
151
0
    const auto coding_probs = Mul(ConvertTo(df, coding_counts), coding_inv);
152
0
    const auto neg_coding_cost = BitCast(
153
0
        df,
154
0
        IfThenZeroElse(Eq(counts, Zero(di)),
155
0
                       IfThenElse(Eq(coding_counts, Zero(di)),
156
0
                                  BitCast(di, Set(df, -kInfinity)),
157
0
                                  BitCast(di, FastLog2f(df, coding_probs)))));
158
0
    cost_lanes = NegMulAdd(ConvertTo(df, counts), neg_coding_cost, cost_lanes);
159
0
  }
160
0
  const float total_cost = GetLane(SumOfLanes(df, cost_lanes));
161
0
  return total_cost - actual.entropy;
162
0
}
163
164
// First step of a k-means clustering with a fancy distance metric.
165
Status FastClusterHistograms(const std::vector<Histogram>& in,
166
                             size_t max_histograms, bool prefix_codes,
167
                             std::vector<Histogram>* out,
168
0
                             std::vector<uint32_t>* histogram_symbols) {
169
0
  const size_t prev_histograms = out->size();
170
0
  out->reserve(max_histograms);
171
0
  histogram_symbols->clear();
172
0
  histogram_symbols->resize(in.size(), max_histograms);
173
174
0
  std::vector<float> dists(in.size(), std::numeric_limits<float>::max());
175
0
  size_t largest_idx = 0;
176
0
  for (size_t i = 0; i < in.size(); i++) {
177
0
    if (in[i].total_count == 0) {
178
0
      (*histogram_symbols)[i] = 0;
179
0
      dists[i] = 0.0f;
180
0
      continue;
181
0
    }
182
0
    HistogramEntropy(in[i]);
183
0
    if (in[i].total_count > in[largest_idx].total_count) {
184
0
      largest_idx = i;
185
0
    }
186
0
  }
187
188
0
  if (prev_histograms > 0) {
189
0
    for (size_t j = 0; j < prev_histograms; ++j) {
190
0
      HistogramEntropy((*out)[j]);
191
0
    }
192
0
    for (size_t i = 0; i < in.size(); i++) {
193
0
      if (dists[i] == 0.0f) continue;
194
0
      for (size_t j = 0; j < prev_histograms; ++j) {
195
0
        dists[i] = std::min(HistogramKLDivergence(in[i], (*out)[j]), dists[i]);
196
0
      }
197
0
    }
198
0
    auto max_dist = std::max_element(dists.begin(), dists.end());
199
0
    if (*max_dist > 0.0f) {
200
0
      largest_idx = max_dist - dists.begin();
201
0
    }
202
0
  }
203
204
0
  constexpr float kMinDistanceForDistinct = 48.0f;
205
0
  while (out->size() < max_histograms) {
206
0
    (*histogram_symbols)[largest_idx] = out->size();
207
0
    out->push_back(in[largest_idx]);
208
0
    dists[largest_idx] = 0.0f;
209
0
    largest_idx = 0;
210
0
    for (size_t i = 0; i < in.size(); i++) {
211
0
      if (dists[i] == 0.0f) continue;
212
0
      dists[i] =
213
0
          std::min(ClusterDistance(in[i], out->back(), prefix_codes), dists[i]);
214
0
      if (dists[i] > dists[largest_idx]) largest_idx = i;
215
0
    }
216
0
    if (dists[largest_idx] < kMinDistanceForDistinct) break;
217
0
  }
218
219
0
  for (size_t i = 0; i < in.size(); i++) {
220
0
    if ((*histogram_symbols)[i] != max_histograms) continue;
221
0
    size_t best = 0;
222
0
    float best_dist = std::numeric_limits<float>::max();
223
0
    for (size_t j = 0; j < out->size(); j++) {
224
0
      float dist = j < prev_histograms
225
0
                       ? HistogramKLDivergence(in[i], (*out)[j])
226
0
                       : ClusterDistance(in[i], (*out)[j], prefix_codes);
227
0
      if (dist < best_dist) {
228
0
        best = j;
229
0
        best_dist = dist;
230
0
      }
231
0
    }
232
0
    JXL_ENSURE(best_dist < std::numeric_limits<float>::max());
233
0
    if (best >= prev_histograms) {
234
0
      (*out)[best].AddHistogram(in[i]);
235
0
      HistogramEntropy((*out)[best]);
236
0
    }
237
0
    (*histogram_symbols)[i] = best;
238
0
  }
239
0
  return true;
240
0
}
241
242
// NOLINTNEXTLINE(google-readability-namespace-comments)
243
}  // namespace HWY_NAMESPACE
244
}  // namespace jxl
245
HWY_AFTER_NAMESPACE();
246
247
#if HWY_ONCE
248
namespace jxl {
249
HWY_EXPORT(FastClusterHistograms);  // Local function
250
HWY_EXPORT(HistogramEntropy);       // Local function
251
HWY_EXPORT(HistogramCondition);     // Local function
252
253
0
void Histogram::Condition() { HWY_DYNAMIC_DISPATCH(HistogramCondition)(*this); }
254
255
0
float Histogram::ShannonEntropy() const {
256
0
  HWY_DYNAMIC_DISPATCH(HistogramEntropy)(*this);
257
0
  return entropy;
258
0
}
259
260
namespace {
261
// -----------------------------------------------------------------------------
262
// Histogram refinement
263
264
// Reorder histograms in *out so that the new symbols in *symbols come in
265
// increasing order.
266
void HistogramReindex(std::vector<Histogram>* out, size_t prev_histograms,
267
0
                      std::vector<uint32_t>* symbols) {
268
0
  std::vector<Histogram> tmp(*out);
269
0
  std::map<int, int> new_index;
270
0
  for (size_t i = 0; i < prev_histograms; ++i) {
271
0
    new_index[i] = i;
272
0
  }
273
0
  int next_index = prev_histograms;
274
0
  for (uint32_t symbol : *symbols) {
275
0
    if (new_index.find(symbol) == new_index.end()) {
276
0
      new_index[symbol] = next_index;
277
0
      (*out)[next_index] = tmp[symbol];
278
0
      ++next_index;
279
0
    }
280
0
  }
281
0
  out->resize(next_index);
282
0
  for (uint32_t& symbol : *symbols) {
283
0
    symbol = new_index[symbol];
284
0
  }
285
0
}
286
287
}  // namespace
288
289
// Clusters similar histograms in 'in' together, the selected histograms are
290
// placed in 'out', and for each index in 'in', *histogram_symbols will
291
// indicate which of the 'out' histograms is the best approximation.
292
Status ClusterHistograms(const HistogramParams& params,
293
                         const std::vector<Histogram>& in,
294
                         size_t max_histograms, std::vector<Histogram>* out,
295
0
                         std::vector<uint32_t>* histogram_symbols) {
296
0
  size_t prev_histograms = out->size();
297
0
  max_histograms = std::min(max_histograms, params.max_histograms);
298
0
  max_histograms = std::min(max_histograms, in.size());
299
0
  if (params.clustering == HistogramParams::ClusteringType::kFastest) {
300
0
    max_histograms = std::min(max_histograms, static_cast<size_t>(4));
301
0
  }
302
303
0
  JXL_RETURN_IF_ERROR(HWY_DYNAMIC_DISPATCH(FastClusterHistograms)(
304
0
      in, prev_histograms + max_histograms, params.force_huffman, out,
305
0
      histogram_symbols));
306
307
0
  if (prev_histograms == 0 &&
308
0
      params.clustering == HistogramParams::ClusteringType::kBest) {
309
0
    for (auto& histo : *out) {
310
0
      JXL_ASSIGN_OR_RETURN(histo.entropy, histo.ANSPopulationCost());
311
0
    }
312
0
    uint32_t next_version = 2;
313
0
    std::vector<uint32_t> version(out->size(), 1);
314
0
    std::vector<uint32_t> renumbering(out->size());
315
0
    std::iota(renumbering.begin(), renumbering.end(), 0);
316
317
    // Try to pair up clusters if doing so reduces the total cost.
318
319
0
    struct HistogramPair {
320
      // validity of a pair: p.version == max(version[i], version[j])
321
0
      float cost;
322
0
      uint32_t first;
323
0
      uint32_t second;
324
0
      uint32_t version;
325
      // We use > because priority queues sort in *decreasing* order, but we
326
      // want lower cost elements to appear first.
327
0
      bool operator<(const HistogramPair& other) const {
328
0
        return std::make_tuple(cost, first, second, version) >
329
0
               std::make_tuple(other.cost, other.first, other.second,
330
0
                               other.version);
331
0
      }
332
0
    };
333
334
    // Create list of all pairs by increasing merging cost.
335
0
    std::priority_queue<HistogramPair> pairs_to_merge;
336
0
    for (uint32_t i = 0; i < out->size(); i++) {
337
0
      for (uint32_t j = i + 1; j < out->size(); j++) {
338
0
        Histogram histo;
339
0
        histo.AddHistogram((*out)[i]);
340
0
        histo.AddHistogram((*out)[j]);
341
0
        JXL_ASSIGN_OR_RETURN(float cost, histo.ANSPopulationCost());
342
0
        cost -= (*out)[i].entropy + (*out)[j].entropy;
343
        // Avoid enqueueing pairs that are not advantageous to merge.
344
0
        if (cost >= 0) continue;
345
0
        pairs_to_merge.push(
346
0
            HistogramPair{cost, i, j, std::max(version[i], version[j])});
347
0
      }
348
0
    }
349
350
    // Merge the best pair to merge, add new pairs that get formed as a
351
    // consequence.
352
0
    while (!pairs_to_merge.empty()) {
353
0
      uint32_t first = pairs_to_merge.top().first;
354
0
      uint32_t second = pairs_to_merge.top().second;
355
0
      uint32_t ver = pairs_to_merge.top().version;
356
0
      pairs_to_merge.pop();
357
0
      if (ver != std::max(version[first], version[second]) ||
358
0
          version[first] == 0 || version[second] == 0) {
359
0
        continue;
360
0
      }
361
0
      (*out)[first].AddHistogram((*out)[second]);
362
0
      JXL_ASSIGN_OR_RETURN((*out)[first].entropy,
363
0
                           (*out)[first].ANSPopulationCost());
364
0
      for (uint32_t& item : renumbering) {
365
0
        if (item == second) {
366
0
          item = first;
367
0
        }
368
0
      }
369
0
      version[second] = 0;
370
0
      version[first] = next_version++;
371
0
      for (uint32_t j = 0; j < out->size(); j++) {
372
0
        if (j == first) continue;
373
0
        if (version[j] == 0) continue;
374
0
        Histogram histo;
375
0
        histo.AddHistogram((*out)[first]);
376
0
        histo.AddHistogram((*out)[j]);
377
0
        JXL_ASSIGN_OR_RETURN(float merge_cost, histo.ANSPopulationCost());
378
0
        merge_cost -= (*out)[first].entropy + (*out)[j].entropy;
379
        // Avoid enqueueing pairs that are not advantageous to merge.
380
0
        if (merge_cost >= 0) continue;
381
0
        pairs_to_merge.push(
382
0
            HistogramPair{merge_cost, std::min(first, j), std::max(first, j),
383
0
                          std::max(version[first], version[j])});
384
0
      }
385
0
    }
386
0
    std::vector<uint32_t> reverse_renumbering(out->size(), -1);
387
0
    size_t num_alive = 0;
388
0
    for (size_t i = 0; i < out->size(); i++) {
389
0
      if (version[i] == 0) continue;
390
0
      (*out)[num_alive++] = (*out)[i];
391
0
      reverse_renumbering[i] = num_alive - 1;
392
0
    }
393
0
    out->resize(num_alive);
394
0
    for (uint32_t& item : *histogram_symbols) {
395
0
      item = reverse_renumbering[renumbering[item]];
396
0
    }
397
0
  }
398
399
  // Convert the context map to a canonical form.
400
0
  HistogramReindex(out, prev_histograms, histogram_symbols);
401
0
  return true;
402
0
}
403
404
}  // namespace jxl
405
#endif  // HWY_ONCE