1use crate::{Crop, Fit, ImageProcessor, ImageProcessorTrait, Result};
18use crate::{Error, Flip, Rotation};
19use edgefirst_tensor::{CpuAccess, DType, PixelFormat, Region, TensorDyn, TensorMemory};
20
21pub use edgefirst_decoder::tiling::TilePlacement;
22
23#[derive(Debug, Clone, Copy, PartialEq, Eq)]
25pub struct TileSpec {
26 pub source: Region,
28 pub index: usize,
30 pub row: usize,
32 pub col: usize,
34}
35
36#[derive(Debug, Clone, Copy, PartialEq)]
38pub struct TilingConfig {
39 pub tile_w: usize,
41 pub tile_h: usize,
43 pub overlap_ratio: f32,
47 pub pad: [u8; 4],
49 pub fit: Fit,
53}
54
55impl TilingConfig {
56 pub fn new(tile_w: usize, tile_h: usize) -> Self {
59 Self {
60 tile_w,
61 tile_h,
62 overlap_ratio: 0.2,
63 pad: [114, 114, 114, 255],
64 fit: Fit::Stretch,
65 }
66 }
67
68 pub fn with_overlap(mut self, overlap_ratio: f32) -> Self {
70 self.overlap_ratio = overlap_ratio;
71 self
72 }
73
74 pub fn with_fit(mut self, fit: Fit) -> Self {
77 if let Fit::Letterbox { pad } = fit {
78 self.pad = pad;
79 }
80 self.fit = fit;
81 self
82 }
83
84 pub fn validate(&self) -> Result<()> {
92 if !(self.overlap_ratio >= 0.0 && self.overlap_ratio < 1.0) {
93 return Err(Error::CropInvalid(format!(
94 "tiling overlap_ratio must be in [0.0, 1.0), got {}",
95 self.overlap_ratio
96 )));
97 }
98 if self.tile_w == 0 || self.tile_h == 0 {
99 return Err(Error::CropInvalid(
100 "tiling tile size must be non-zero".into(),
101 ));
102 }
103 Ok(())
104 }
105}
106
107fn axis_origins(frame: usize, tile: usize, overlap: f64) -> Vec<usize> {
113 if frame <= tile {
114 return vec![0];
115 }
116 let last = frame - tile;
117 let max_step = ((1.0 - overlap) * tile as f64).floor().max(1.0) as usize;
123 let total = last.div_ceil(max_step);
124 (0..=total)
125 .map(|i| (i as f32 * last as f32 / total as f32).round() as usize)
126 .collect()
127}
128
129pub fn tile_grid(
134 frame_h: usize,
135 frame_w: usize,
136 tile_h: usize,
137 tile_w: usize,
138 overlap_ratio: f32,
139) -> Vec<TileSpec> {
140 let cw = tile_w.min(frame_w);
141 let ch = tile_h.min(frame_h);
142 let xs = axis_origins(frame_w, tile_w, overlap_ratio as f64);
143 let ys = axis_origins(frame_h, tile_h, overlap_ratio as f64);
144 let mut tiles = Vec::with_capacity(xs.len() * ys.len());
145 let mut index = 0;
146 for (row, &oy) in ys.iter().enumerate() {
147 for (col, &ox) in xs.iter().enumerate() {
148 debug_assert!(
149 ox + cw <= frame_w && oy + ch <= frame_h,
150 "tile out of bounds"
151 );
152 tiles.push(TileSpec {
153 source: Region::new(ox, oy, cw, ch),
154 index,
155 row,
156 col,
157 });
158 index += 1;
159 }
160 }
161 tiles
162}
163
164fn placement_letterbox(
167 crop: &Crop,
168 src_w: usize,
169 src_h: usize,
170 tile_w: usize,
171 tile_h: usize,
172) -> Option<[f32; 4]> {
173 let resolved = crop.resolve(src_w, src_h, tile_w, tile_h).ok()?;
174 resolved.dst_rect.map(|r| {
175 let (dw, dh) = (tile_w as f32, tile_h as f32);
176 [
177 r.left as f32 / dw,
178 r.top as f32 / dh,
179 (r.left + r.width) as f32 / dw,
180 (r.top + r.height) as f32 / dh,
181 ]
182 })
183}
184
185impl ImageProcessor {
186 pub fn alloc_tile_batch(
199 &self,
200 n: usize,
201 cfg: &TilingConfig,
202 format: PixelFormat,
203 dtype: DType,
204 memory: Option<TensorMemory>,
205 access: CpuAccess,
206 ) -> Result<TensorDyn> {
207 cfg.validate()?;
208 self.create_image(
209 cfg.tile_w,
210 n.saturating_mul(cfg.tile_h),
211 format,
212 dtype,
213 memory,
214 access,
215 )
216 }
217
218 pub fn plan_tiles(
226 &self,
227 src_w: usize,
228 src_h: usize,
229 cfg: &TilingConfig,
230 ) -> Result<Vec<TilePlacement>> {
231 cfg.validate()?;
232 let grid = tile_grid(src_h, src_w, cfg.tile_h, cfg.tile_w, cfg.overlap_ratio);
233 let count = grid.len();
234 let crop = Crop::default().with_fit(cfg.fit);
235 Ok(grid
236 .iter()
237 .map(|t| {
238 let lb = placement_letterbox(
239 &crop.with_source(Some(t.source)),
240 t.source.width,
241 t.source.height,
242 cfg.tile_w,
243 cfg.tile_h,
244 );
245 TilePlacement {
246 index: t.index,
247 count,
248 origin: (t.source.x as f32, t.source.y as f32),
249 crop_size: (t.source.width as f32, t.source.height as f32),
250 letterbox: lb,
251 frame_dims: (src_w as f32, src_h as f32),
252 }
253 })
254 .collect())
255 }
256
257 pub fn tile_into(
271 &mut self,
272 src: &TensorDyn,
273 dst_batched: &mut TensorDyn,
274 cfg: &TilingConfig,
275 ) -> Result<Vec<TilePlacement>> {
276 cfg.validate()?;
277 let (src_w, src_h) = (
278 src.width().ok_or(Error::NotAnImage)?,
279 src.height().ok_or(Error::NotAnImage)?,
280 );
281 let placements = self.plan_tiles(src_w, src_h, cfg)?;
282 let count = placements.len();
283
284 let dst_h = dst_batched.height().ok_or(Error::NotAnImage)?;
285 let required_h = count.saturating_mul(cfg.tile_h);
286 if dst_h < required_h {
287 return Err(Error::InvalidShape(format!(
288 "tile_into dst height {dst_h} < count*tile_h {required_h}"
289 )));
290 }
291
292 for p in &placements {
293 self.render_tile(src, dst_batched, p, cfg)?;
294 }
295 self.flush()?;
296 Ok(placements)
297 }
298
299 pub fn tile_one(
311 &mut self,
312 src: &TensorDyn,
313 dst_slot: &mut TensorDyn,
314 placement: &TilePlacement,
315 cfg: &TilingConfig,
316 ) -> Result<()> {
317 cfg.validate()?;
318 self.render_tile(src, dst_slot, placement, cfg)
319 }
320
321 fn render_tile(
324 &mut self,
325 src: &TensorDyn,
326 dst: &mut TensorDyn,
327 placement: &TilePlacement,
328 cfg: &TilingConfig,
329 ) -> Result<()> {
330 let source = placement_to_source_region(placement)?;
331 let crop = Crop::default().with_source(Some(source)).with_fit(cfg.fit);
332
333 let dst_h = dst.height().ok_or(Error::NotAnImage)?;
338 let batched = dst_h >= placement.count.saturating_mul(cfg.tile_h);
339 if batched {
340 let mut band = dst.view(Region::new(
341 0,
342 placement.index * cfg.tile_h,
343 cfg.tile_w,
344 cfg.tile_h,
345 ))?;
346 self.convert_deferred(src, &mut band, Rotation::None, Flip::None, crop)?;
347 } else {
348 self.convert_deferred(src, dst, Rotation::None, Flip::None, crop)?;
349 }
350 Ok(())
351 }
352}
353
354fn placement_to_source_region(placement: &TilePlacement) -> Result<Region> {
360 let (ox, oy) = placement.origin;
361 let (cw, ch) = placement.crop_size;
362 for (name, v) in [
363 ("origin.x", ox),
364 ("origin.y", oy),
365 ("crop.w", cw),
366 ("crop.h", ch),
367 ] {
368 if !v.is_finite() || v < 0.0 {
369 return Err(Error::CropInvalid(format!(
370 "tile placement {name} must be finite and non-negative, got {v}"
371 )));
372 }
373 }
374 let (ox_i, oy_i, cw_i, ch_i) = (ox as usize, oy as usize, cw as usize, ch as usize);
375 for (name, f, i) in [
377 ("origin.x", ox, ox_i),
378 ("origin.y", oy, oy_i),
379 ("crop.w", cw, cw_i),
380 ("crop.h", ch, ch_i),
381 ] {
382 if (f - i as f32).abs() > f32::EPSILON {
383 return Err(Error::CropInvalid(format!(
384 "tile placement {name} must be an integral pixel value, got {f}"
385 )));
386 }
387 }
388 if cw_i == 0 || ch_i == 0 {
389 return Err(Error::CropInvalid(
390 "tile placement crop size must be non-zero".into(),
391 ));
392 }
393 Ok(Region::new(ox_i, oy_i, cw_i, ch_i))
394}
395
396#[cfg(test)]
397mod tests {
398 use super::*;
399
400 #[test]
403 fn axis_origins_matches_adis_worked_example() {
404 assert_eq!(axis_origins(1920, 640, 0.1), vec![0, 427, 853, 1280]);
406 }
407
408 #[test]
409 fn axis_origins_4k_axes() {
410 let xs = axis_origins(3840, 640, 0.2);
412 assert_eq!(xs, vec![0, 457, 914, 1371, 1829, 2286, 2743, 3200]);
413 let ys = axis_origins(2160, 640, 0.2);
415 assert_eq!(ys, vec![0, 507, 1013, 1520]);
416 assert_eq!(*xs.last().unwrap(), 3840 - 640);
417 assert_eq!(*ys.last().unwrap(), 2160 - 640);
418 assert_eq!(xs[0], 0);
419 }
420
421 #[test]
422 fn axis_origins_frame_le_tile_single() {
423 assert_eq!(axis_origins(640, 640, 0.2), vec![0]); assert_eq!(axis_origins(400, 640, 0.2), vec![0]); }
426
427 #[test]
428 fn axis_origins_frame_tile_plus_one() {
429 assert_eq!(axis_origins(641, 640, 0.2), vec![0, 1]);
431 }
432
433 #[test]
434 fn axis_origins_overlap_zero_is_exact_tiling() {
435 assert_eq!(axis_origins(1920, 640, 0.0), vec![0, 640, 1280]);
437 }
438
439 #[test]
440 fn axis_origins_overlap_near_one_no_panic() {
441 let xs = axis_origins(1920, 640, 0.99);
443 assert_eq!(xs[0], 0);
444 assert_eq!(*xs.last().unwrap(), 1280);
445 assert!(xs.windows(2).all(|w| w[0] <= w[1]));
447 assert!(xs.len() > 8); }
449
450 #[test]
451 fn axis_origins_8k_no_overflow() {
452 let xs = axis_origins(7680, 640, 0.2);
454 assert_eq!(xs[0], 0);
455 assert_eq!(*xs.last().unwrap(), 7680 - 640);
456 assert!(xs.windows(2).all(|w| w[0] <= w[1]));
457 }
458
459 #[test]
460 fn tile_grid_4k_all_full_size_and_in_bounds() {
461 let grid = tile_grid(2160, 3840, 640, 640, 0.2);
462 assert_eq!(grid.len(), 8 * 4); for t in &grid {
464 assert_eq!(t.source.width, 640);
465 assert_eq!(t.source.height, 640);
466 assert!(t.source.x + 640 <= 3840);
467 assert!(t.source.y + 640 <= 2160);
468 }
469 assert_eq!(grid[0].source, Region::new(0, 0, 640, 640));
471 assert_eq!(grid[8].source, Region::new(0, 507, 640, 640)); }
473
474 #[test]
475 fn tile_grid_realized_overlap_at_least_requested() {
476 let grid = tile_grid(2160, 3840, 640, 640, 0.2);
477 let step = grid[1].source.x - grid[0].source.x;
479 let realized_overlap = 1.0 - (step as f64 / 640.0);
480 assert!(
481 realized_overlap >= 0.2 - 1e-9,
482 "realized {realized_overlap} < 0.2"
483 );
484 }
485
486 #[test]
487 fn tile_grid_frame_smaller_than_tile_single_whole_frame() {
488 let grid = tile_grid(400, 500, 640, 640, 0.2);
489 assert_eq!(grid.len(), 1);
490 assert_eq!(grid[0].source, Region::new(0, 0, 500, 400));
491 }
492
493 #[test]
494 fn rounding_uses_f32_path() {
495 let xs = axis_origins(4000, 640, 0.333);
499 assert_eq!(xs[0], 0);
500 assert_eq!(*xs.last().unwrap(), 4000 - 640);
501 assert!(xs.windows(2).all(|w| w[0] <= w[1]));
502 let max_step = xs.windows(2).map(|w| w[1] - w[0]).max().unwrap();
503 let realized = 1.0 - (max_step as f64 / 640.0);
504 assert!(
505 realized >= 0.333 - 1e-9,
506 "realized overlap {realized} < 0.333 (max_step={max_step})"
507 );
508 }
509
510 #[test]
511 fn with_fit_letterbox_syncs_pad() {
512 let pad = [1, 2, 3, 4];
513 let cfg = TilingConfig::new(640, 640).with_fit(Fit::Letterbox { pad });
514 assert_eq!(cfg.fit, Fit::Letterbox { pad });
515 assert_eq!(cfg.pad, pad);
516 }
517
518 #[test]
519 fn placement_to_source_region_rejects_invalid() {
520 let good = TilePlacement {
521 index: 0,
522 count: 1,
523 origin: (10.0, 20.0),
524 crop_size: (640.0, 640.0),
525 letterbox: None,
526 frame_dims: (3840.0, 2160.0),
527 };
528 assert!(placement_to_source_region(&good).is_ok());
529
530 let mut bad = good;
531 bad.origin.0 = -1.0;
532 assert!(placement_to_source_region(&bad).is_err());
533 bad = good;
534 bad.origin.0 = f32::NAN;
535 assert!(placement_to_source_region(&bad).is_err());
536 bad = good;
537 bad.origin.0 = 1.5;
538 assert!(placement_to_source_region(&bad).is_err());
539 bad = good;
540 bad.crop_size.0 = 0.0;
541 assert!(placement_to_source_region(&bad).is_err());
542 }
543
544 #[test]
545 fn tiling_config_validate_rejects_bad_overlap() {
546 assert!(TilingConfig::new(640, 640)
547 .with_overlap(1.0)
548 .validate()
549 .is_err());
550 assert!(TilingConfig::new(640, 640)
551 .with_overlap(-0.1)
552 .validate()
553 .is_err());
554 assert!(TilingConfig::new(640, 640)
555 .with_overlap(0.2)
556 .validate()
557 .is_ok());
558 }
559
560 #[test]
561 fn tiling_config_validate_rejects_zero_tile_size() {
562 assert!(TilingConfig::new(0, 640).validate().is_err());
563 assert!(TilingConfig::new(640, 0).validate().is_err());
564 }
565}