1use image::RgbImage;
12use metrale_config::VisionConfig;
13
14pub fn smart_resize(h: u32, w: u32, factor: u32, min_pixels: u64, max_pixels: u64) -> (u32, u32) {
23 let f = factor.max(1) as f64;
24 let (hf, wf) = (h.max(1) as f64, w.max(1) as f64);
25 let snap = |v: f64| ((v / f).round() as u32).max(1) * factor;
26 let (mut h_bar, mut w_bar) = (snap(hf), snap(wf));
27
28 let area = |a: u32, b: u32| u64::from(a) * u64::from(b);
29 if area(h_bar, w_bar) > max_pixels && max_pixels > 0 {
30 let beta = (hf * wf / max_pixels as f64).sqrt();
31 h_bar = (((hf / beta) / f).floor() as u32).max(1) * factor;
32 w_bar = (((wf / beta) / f).floor() as u32).max(1) * factor;
33 } else if area(h_bar, w_bar) < min_pixels {
34 let beta = (min_pixels as f64 / (hf * wf)).sqrt();
35 h_bar = (((hf * beta) / f).ceil() as u32).max(1) * factor;
36 w_bar = (((wf * beta) / f).ceil() as u32).max(1) * factor;
37 }
38 (h_bar, w_bar)
39}
40
41pub fn pixel_bounds(vcfg: &VisionConfig, operator_max: Option<usize>) -> (u64, u64) {
55 let factor = (vcfg.patch_size * vcfg.spatial_merge_size).max(1) as u64;
56 let per_token = factor * factor;
57 let min_pixels = vcfg.min_image_tokens as u64 * per_token;
58 let model_max = vcfg.max_image_tokens as u64 * per_token;
59 let (encoder_patches, _) = crate::layers::vision_encoder::enc_impl::init::derive_max_patches(
60 vcfg.max_pixels.or(operator_max),
61 vcfg.patch_size,
62 );
63 let encoder_max =
64 encoder_patches as u64 * (vcfg.patch_size.max(1) * vcfg.patch_size.max(1)) as u64;
65 let mut max_pixels = encoder_max;
66 if model_max > 0 {
67 max_pixels = max_pixels.min(model_max);
68 }
69 if let Some(p) = operator_max.filter(|&p| p > 0) {
70 max_pixels = max_pixels.min(p as u64);
71 }
72 (min_pixels, max_pixels.max(per_token))
73}
74
75pub fn resize_and_pad(
86 img: &RgbImage,
87 vcfg: &VisionConfig,
88 operator_max: Option<usize>,
89) -> RgbImage {
90 let factor = (vcfg.patch_size * vcfg.spatial_merge_size) as u32;
91 let (min_pixels, max_pixels) = pixel_bounds(vcfg, operator_max);
92 let (target_h, target_w) =
93 smart_resize(img.height(), img.width(), factor, min_pixels, max_pixels);
94
95 let scale = (target_h as f64 / img.height().max(1) as f64)
99 .min(target_w as f64 / img.width().max(1) as f64)
100 .min(1.0);
101 let content_h = ((img.height() as f64 * scale).round() as u32).clamp(1, target_h);
102 let content_w = ((img.width() as f64 * scale).round() as u32).clamp(1, target_w);
103
104 let fitted = if (content_h, content_w) == (img.height(), img.width()) {
107 img.clone()
108 } else {
109 image::imageops::resize(
110 img,
111 content_w,
112 content_h,
113 image::imageops::FilterType::CatmullRom,
114 )
115 };
116 if content_h == target_h && content_w == target_w {
117 return fitted;
118 }
119 let mut canvas = RgbImage::new(target_w, target_h);
120 image::imageops::replace(&mut canvas, &fitted, 0, 0);
121 canvas
122}
123
124pub fn block_major_patch_index(
131 patch_row: usize,
132 patch_col: usize,
133 grid_w: usize,
134 merge: usize,
135) -> usize {
136 let m = merge.max(1);
137 let w_blocks = grid_w / m;
138 (((patch_row / m) * w_blocks + patch_col / m) * m + patch_row % m) * m + patch_col % m
139}
140
141#[cfg(test)]
142mod tests {
143 use super::*;
144
145 #[test]
148 fn smart_resize_reproduces_the_golden_non_square_canvas() {
149 let (h, w) = smart_resize(360, 640, 28, 16 * 784, 8000 * 784);
150 assert_eq!((h, w), (364, 644));
151 assert_eq!((h / 14) * (w / 14), 26 * 46);
152 assert_eq!((h / 28) * (w / 28), 299);
153 }
154
155 #[test]
158 fn an_aligned_square_is_left_alone() {
159 assert_eq!(smart_resize(448, 448, 28, 16 * 784, 8000 * 784), (448, 448));
160 }
161
162 #[test]
165 fn an_over_budget_image_is_floored_inside_the_bound() {
166 let max = 100u64 * 784;
167 let (h, w) = smart_resize(4000, 4000, 28, 16 * 784, max);
168 assert_eq!(h % 28, 0);
169 assert_eq!(w % 28, 0);
170 assert!(
171 u64::from(h) * u64::from(w) <= max,
172 "{h}x{w} exceeds the {max}px budget"
173 );
174 }
175
176 #[test]
179 fn a_tiny_image_is_raised_to_the_minimum_token_count() {
180 let min = 16u64 * 784;
181 let (h, w) = smart_resize(1, 1, 28, min, 8000 * 784);
182 assert!(u64::from(h) * u64::from(w) >= min, "{h}x{w} under {min}");
183 assert_eq!((h % 28, w % 28), (0, 0));
184 }
185
186 fn glm_cfg() -> VisionConfig {
187 VisionConfig {
188 patch_size: 14,
189 spatial_merge_size: 2,
190 min_image_tokens: 16,
191 max_image_tokens: 8000,
192 ..VisionConfig::default()
193 }
194 }
195
196 #[test]
201 fn the_operator_bound_moves_the_budget_only_as_far_as_the_encoder_follows() {
202 let vcfg = glm_cfg();
203 let (_, tight) = pixel_bounds(&vcfg, Some(512 * 512));
204 assert!(
205 tight <= 512 * 512,
206 "a tighter flag must lower the bound: {tight}"
207 );
208 assert_eq!(tight / (14 * 14), 1337, "512x512 at patch 14 is 1337 rows");
209
210 let (_, loose) = pixel_bounds(&vcfg, Some(64 * 1024 * 1024));
211 let (_, none) = pixel_bounds(&vcfg, None);
212 assert!(
213 loose > none,
214 "a looser flag may raise the bound: {loose} vs {none}"
215 );
216 assert_eq!(
218 loose / (14 * 14),
219 16_384,
220 "the encoder's CEILING_MAX_PATCHES"
221 );
222 assert!(
223 loose < 8000 * 784,
224 "never as far as the declared token budget"
225 );
226 }
227
228 #[test]
231 fn the_encoder_capacity_caps_the_checkpoints_own_token_budget() {
232 let vcfg = glm_cfg();
233 let (_, resolved) = pixel_bounds(&vcfg, None);
234 let declared = 8000u64 * 784;
235 assert!(
236 resolved < declared,
237 "GLM declares {declared}px (8000 merged tokens); the encoder's default \
238 allocation cannot hold that, so the host bound must be lower, got {resolved}"
239 );
240 assert_eq!(resolved / (14 * 14), 6400);
243 let side = (resolved as f64).sqrt() as u64;
246 assert!((side / 14) * (side / 14) <= 6400);
247 }
248
249 #[test]
252 fn a_checkpoint_without_a_token_budget_still_gets_a_bound() {
253 let vcfg = VisionConfig {
254 max_image_tokens: 0,
255 ..glm_cfg()
256 };
257 let (_, resolved) = pixel_bounds(&vcfg, None);
258 assert_eq!(resolved / (14 * 14), 6400);
259 let (_, with_flag) = pixel_bounds(&vcfg, Some(512 * 512));
260 assert!(with_flag <= 512 * 512);
261 assert_eq!(with_flag / (14 * 14), 1337);
262 }
263
264 #[test]
267 fn a_canvas_larger_than_the_input_pads_rather_than_upscales() {
268 let vcfg = glm_cfg();
269 let img = RgbImage::new(640, 360);
270 let out = resize_and_pad(&img, &vcfg, None);
271 assert_eq!(
272 (out.height(), out.width()),
273 (364, 644),
274 "the smart_resize canvas"
275 );
276 assert_eq!((out.height() / 14) * (out.width() / 14), 26 * 46);
279 }
280
281 #[test]
284 fn block_major_index_is_a_permutation_grouping_2x2_blocks() {
285 let (gh, gw, m) = (26usize, 46usize, 2usize);
286 let mut seen = vec![usize::MAX; gh * gw];
287 for ph in 0..gh {
288 for pw in 0..gw {
289 let i = block_major_patch_index(ph, pw, gw, m);
290 assert!(i < gh * gw, "index {i} out of range");
291 assert_eq!(seen[i], usize::MAX, "index {i} used twice");
292 seen[i] = ph * gw + pw;
293 }
294 }
295 assert_eq!(&seen[0..4], &[0, 1, gw, gw + 1]);
298 assert_eq!(seen[4], 2);
300 }
301
302 #[test]
305 fn block_major_differs_from_raster_on_any_real_grid() {
306 for (row, col) in [(0, 0), (0, 1), (1, 0), (1, 1)] {
308 assert_eq!(
309 block_major_patch_index(row, col, 2, 2),
310 row * 2 + col,
311 "1 block wide must equal raster at ({row}, {col})"
312 );
313 }
314 assert_eq!(block_major_patch_index(0, 2, 4, 2), 4);
317 assert_eq!(block_major_patch_index(1, 0, 4, 2), 2);
318 }
319}