1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
// Copyright (c) Imazen LLC and the JPEG XL Project Authors.
// Algorithms and constants derived from libjxl (BSD-3-Clause).
// Licensed under AGPL-3.0-or-later. Commercial licenses at https://www.imazen.io/pricing
//! Per-tile quality measurement from butteraugli diffmap.
//!
//! Computes per-tile (8x8 block) quality metrics from a butteraugli distance map,
//! matching libjxl's `TileDistMap` approach for iterative rate control.
use super::ac_strategy::AcStrategyMap;
use super::common::*;
/// Per-tile distance map computed from butteraugli diffmap.
///
/// Each entry corresponds to one 8x8 block and represents the 16th-norm
/// of butteraugli distances within that block. This matches libjxl's
/// approach for computing per-block quality during rate control.
pub struct TileDistMap {
/// Per-block distances (16th-norm of butteraugli values).
pub distances: Vec<f32>,
/// Number of blocks in x direction.
pub xsize_blocks: usize,
/// Number of blocks in y direction.
pub ysize_blocks: usize,
}
impl TileDistMap {
/// Create a new tile distance map from a butteraugli diffmap.
///
/// The diffmap should be width × height pixels with one f32 per pixel
/// representing the local butteraugli distance.
///
/// For each 8x8 block, computes the 16th-norm of the pixel distances:
/// `block_dist = (sum(pixel_dist^16) / count)^(1/16)`
///
/// This is more sensitive to outliers than RMS, helping to catch
/// isolated bad pixels that affect perceptual quality.
pub fn from_diffmap(
diffmap: &[f32],
width: usize,
height: usize,
_ac_strategy: &AcStrategyMap,
) -> Self {
let xsize_blocks = div_ceil(width, BLOCK_DIM);
let ysize_blocks = div_ceil(height, BLOCK_DIM);
let mut distances = vec![0.0f32; xsize_blocks * ysize_blocks];
for by in 0..ysize_blocks {
for bx in 0..xsize_blocks {
let block_y_start = by * BLOCK_DIM;
let block_x_start = bx * BLOCK_DIM;
let mut sum_pow = 0.0f32;
let mut count = 0usize;
for py in 0..BLOCK_DIM {
let y = block_y_start + py;
if y >= height {
continue;
}
for px in 0..BLOCK_DIM {
let x = block_x_start + px;
if x >= width {
continue;
}
let pixel_dist = diffmap[y * width + x];
let clamped = pixel_dist.clamp(0.0, 100.0);
// x^16 = ((x^2)^2)^2)^2 — avoids expensive powf
let v2 = clamped * clamped;
let v4 = v2 * v2;
let v8 = v4 * v4;
let v16 = v8 * v8;
sum_pow += v16;
count += 1;
}
}
let block_dist = if count > 0 {
// x^(1/16) = sqrt(sqrt(sqrt(sqrt(x))))
(sum_pow / count as f32).sqrt().sqrt().sqrt().sqrt()
} else {
0.0
};
distances[by * xsize_blocks + bx] = block_dist;
}
}
Self {
distances,
xsize_blocks,
ysize_blocks,
}
}
/// Get the distance for a specific block.
#[inline]
pub fn get(&self, bx: usize, by: usize) -> f32 {
self.distances[by * self.xsize_blocks + bx]
}
/// Get the maximum distance across all blocks.
#[allow(dead_code)] // Used for debug logging
pub fn max(&self) -> f32 {
self.distances.iter().copied().fold(0.0f32, |a, b| a.max(b))
}
/// Get the 95th percentile distance (ignoring worst 5% of blocks).
///
/// This is useful for convergence checks since a few outlier blocks
/// shouldn't prevent declaring convergence.
pub fn percentile_95(&self) -> f32 {
if self.distances.is_empty() {
return 0.0;
}
let mut sorted = self.distances.clone();
sorted.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
let idx = (sorted.len() as f32 * 0.95).ceil() as usize;
let idx = idx.saturating_sub(1).min(sorted.len() - 1);
sorted[idx]
}
/// Get the mean distance across all blocks.
#[allow(dead_code)] // Used for debug logging
pub fn mean(&self) -> f32 {
if self.distances.is_empty() {
return 0.0;
}
let sum: f32 = self.distances.iter().sum();
sum / self.distances.len() as f32
}
/// Count blocks that exceed the target distance.
#[allow(dead_code)] // Used for debug logging
pub fn count_exceeding(&self, target: f32) -> usize {
self.distances.iter().filter(|&&d| d > target).count()
}
/// Get the fraction of blocks exceeding the target.
#[allow(dead_code)] // Used for debug logging
pub fn fraction_exceeding(&self, target: f32) -> f32 {
if self.distances.is_empty() {
return 0.0;
}
self.count_exceeding(target) as f32 / self.distances.len() as f32
}
}
/// Compute butteraugli diffmap between two linear RGB images.
///
/// Returns a per-pixel distance map where each value represents the
/// local butteraugli distance at that pixel.
///
/// Both images must be the same size and in linear RGB format.
pub fn compute_butteraugli_diffmap(
original: &[f32],
decoded: &[f32],
width: usize,
height: usize,
) -> Vec<f32> {
// Use the butteraugli crate's linear comparison function
// Both images should be in linear RGB (not sRGB)
use butteraugli::{ButteraugliParams, butteraugli_linear};
use imgref::Img;
use rgb::RGB;
// Convert flat arrays to RGB arrays
let orig_rgb: Vec<RGB<f32>> = original
.chunks(3)
.map(|c| RGB::new(c[0], c[1], c[2]))
.collect();
let decoded_rgb: Vec<RGB<f32>> = decoded
.chunks(3)
.map(|c| RGB::new(c[0], c[1], c[2]))
.collect();
let orig_img = Img::new(orig_rgb.as_slice(), width, height);
let decoded_img = Img::new(decoded_rgb.as_slice(), width, height);
// Create params with diffmap computation enabled
let params = ButteraugliParams::new().with_compute_diffmap(true);
// butteraugli_linear returns Result<ButteraugliResult, ButteraugliError>
match butteraugli_linear(orig_img, decoded_img, ¶ms) {
Ok(result) => {
if let Some(diffmap_img) = result.diffmap {
// Extract the buffer from ImgVec<f32>
diffmap_img.into_buf()
} else {
// Fallback: create uniform diffmap from score
vec![result.score as f32; width * height]
}
}
Err(_) => {
// On error, return a high-distance diffmap
vec![10.0; width * height]
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_tile_distmap_uniform() {
// Create a uniform diffmap
let width = 32;
let height = 32;
let diffmap = vec![1.5f32; width * height];
let ac_strategy = AcStrategyMap::new_dct8(div_ceil(width, 8), div_ceil(height, 8));
let tile_dist = TileDistMap::from_diffmap(&diffmap, width, height, &ac_strategy);
// With uniform values, each block should have the same distance
for d in &tile_dist.distances {
assert!((*d - 1.5).abs() < 0.01, "Expected ~1.5, got {}", d);
}
assert!((tile_dist.max() - 1.5).abs() < 0.01);
assert!((tile_dist.mean() - 1.5).abs() < 0.01);
}
#[test]
fn test_tile_distmap_outlier_sensitivity() {
// 16th-norm should be sensitive to outliers
let width = 8;
let height = 8;
let mut diffmap = vec![1.0f32; width * height];
// Add one outlier
diffmap[0] = 10.0;
let ac_strategy = AcStrategyMap::new_dct8(1, 1);
let tile_dist = TileDistMap::from_diffmap(&diffmap, width, height, &ac_strategy);
// With 16th-norm, the outlier should dominate
// (1^16 * 63 + 10^16) / 64 = (63 + 1e16) / 64 ≈ 1.56e14
// (1.56e14)^(1/16) ≈ 7.5
let dist = tile_dist.get(0, 0);
assert!(
dist > 5.0,
"Expected > 5.0, got {} (outlier should dominate)",
dist
);
assert!(dist < 10.0, "Expected < 10.0, got {}", dist);
}
}