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(
234 &self,
235 src_w: usize,
236 src_h: usize,
237 cfg: &TilingConfig,
238 ) -> Result<Vec<TilePlacement>> {
239 let span = tracing::trace_span!(
240 "image.plan_tiles",
241 tiles = tracing::field::Empty,
242 overlap = cfg.overlap_ratio,
243 );
244 let _s = span.enter();
245 cfg.validate()?;
246 let grid = tile_grid(src_h, src_w, cfg.tile_h, cfg.tile_w, cfg.overlap_ratio);
247 let count = grid.len();
248 span.record("tiles", count);
249 let crop = Crop::default().with_fit(cfg.fit);
250 Ok(grid
251 .iter()
252 .map(|t| {
253 let lb = placement_letterbox(
254 &crop.with_source(Some(t.source)),
255 t.source.width,
256 t.source.height,
257 cfg.tile_w,
258 cfg.tile_h,
259 );
260 TilePlacement {
261 index: t.index,
262 count,
263 origin: (t.source.x as f32, t.source.y as f32),
264 crop_size: (t.source.width as f32, t.source.height as f32),
265 letterbox: lb,
266 frame_dims: (src_w as f32, src_h as f32),
267 }
268 })
269 .collect())
270 }
271
272 pub fn tile_into(
316 &mut self,
317 src: &TensorDyn,
318 dst_batched: &mut TensorDyn,
319 cfg: &TilingConfig,
320 ) -> Result<Vec<TilePlacement>> {
321 let span = tracing::trace_span!("image.tile_into", tiles = tracing::field::Empty);
322 let _s = span.enter();
323 cfg.validate()?;
324 let (src_w, src_h) = (
325 src.width().ok_or(Error::NotAnImage)?,
326 src.height().ok_or(Error::NotAnImage)?,
327 );
328 let placements = self.plan_tiles(src_w, src_h, cfg)?;
329 let count = placements.len();
330 span.record("tiles", count);
331
332 let dst_h = dst_batched.height().ok_or(Error::NotAnImage)?;
333 let required_h = count.saturating_mul(cfg.tile_h);
334 if dst_h < required_h {
335 return Err(Error::InvalidShape(format!(
336 "tile_into dst height {dst_h} < count*tile_h {required_h}"
337 )));
338 }
339
340 for p in &placements {
341 self.render_tile(src, dst_batched, p, cfg)?;
342 }
343 self.flush()?;
344 Ok(placements)
345 }
346
347 pub fn tile_one(
359 &mut self,
360 src: &TensorDyn,
361 dst_slot: &mut TensorDyn,
362 placement: &TilePlacement,
363 cfg: &TilingConfig,
364 ) -> Result<()> {
365 let _s = tracing::trace_span!(
366 "image.tile_one",
367 index = placement.index,
368 count = placement.count,
369 )
370 .entered();
371 cfg.validate()?;
372 self.render_tile(src, dst_slot, placement, cfg)
373 }
374
375 fn render_tile(
378 &mut self,
379 src: &TensorDyn,
380 dst: &mut TensorDyn,
381 placement: &TilePlacement,
382 cfg: &TilingConfig,
383 ) -> Result<()> {
384 let source = placement_to_source_region(placement)?;
385 let crop = Crop::default().with_source(Some(source)).with_fit(cfg.fit);
386
387 let dst_h = dst.height().ok_or(Error::NotAnImage)?;
392 let batched = dst_h >= placement.count.saturating_mul(cfg.tile_h);
393 if batched {
394 let mut band = dst.view(Region::new(
395 0,
396 placement.index * cfg.tile_h,
397 cfg.tile_w,
398 cfg.tile_h,
399 ))?;
400 self.convert_deferred(src, &mut band, Rotation::None, Flip::None, crop)?;
401 } else {
402 self.convert_deferred(src, dst, Rotation::None, Flip::None, crop)?;
403 }
404 Ok(())
405 }
406}
407
408fn placement_to_source_region(placement: &TilePlacement) -> Result<Region> {
414 let (ox, oy) = placement.origin;
415 let (cw, ch) = placement.crop_size;
416 for (name, v) in [
417 ("origin.x", ox),
418 ("origin.y", oy),
419 ("crop.w", cw),
420 ("crop.h", ch),
421 ] {
422 if !v.is_finite() || v < 0.0 {
423 return Err(Error::CropInvalid(format!(
424 "tile placement {name} must be finite and non-negative, got {v}"
425 )));
426 }
427 }
428 let (ox_i, oy_i, cw_i, ch_i) = (ox as usize, oy as usize, cw as usize, ch as usize);
429 for (name, f, i) in [
431 ("origin.x", ox, ox_i),
432 ("origin.y", oy, oy_i),
433 ("crop.w", cw, cw_i),
434 ("crop.h", ch, ch_i),
435 ] {
436 if (f - i as f32).abs() > f32::EPSILON {
437 return Err(Error::CropInvalid(format!(
438 "tile placement {name} must be an integral pixel value, got {f}"
439 )));
440 }
441 }
442 if cw_i == 0 || ch_i == 0 {
443 return Err(Error::CropInvalid(
444 "tile placement crop size must be non-zero".into(),
445 ));
446 }
447 Ok(Region::new(ox_i, oy_i, cw_i, ch_i))
448}
449
450#[cfg(test)]
451mod tests {
452 use super::*;
453
454 #[test]
457 fn axis_origins_matches_adis_worked_example() {
458 assert_eq!(axis_origins(1920, 640, 0.1), vec![0, 427, 853, 1280]);
460 }
461
462 #[test]
463 fn axis_origins_4k_axes() {
464 let xs = axis_origins(3840, 640, 0.2);
466 assert_eq!(xs, vec![0, 457, 914, 1371, 1829, 2286, 2743, 3200]);
467 let ys = axis_origins(2160, 640, 0.2);
469 assert_eq!(ys, vec![0, 507, 1013, 1520]);
470 assert_eq!(*xs.last().unwrap(), 3840 - 640);
471 assert_eq!(*ys.last().unwrap(), 2160 - 640);
472 assert_eq!(xs[0], 0);
473 }
474
475 #[test]
476 fn axis_origins_frame_le_tile_single() {
477 assert_eq!(axis_origins(640, 640, 0.2), vec![0]); assert_eq!(axis_origins(400, 640, 0.2), vec![0]); }
480
481 #[test]
482 fn axis_origins_frame_tile_plus_one() {
483 assert_eq!(axis_origins(641, 640, 0.2), vec![0, 1]);
485 }
486
487 #[test]
488 fn axis_origins_overlap_zero_is_exact_tiling() {
489 assert_eq!(axis_origins(1920, 640, 0.0), vec![0, 640, 1280]);
491 }
492
493 #[test]
494 fn axis_origins_overlap_near_one_no_panic() {
495 let xs = axis_origins(1920, 640, 0.99);
497 assert_eq!(xs[0], 0);
498 assert_eq!(*xs.last().unwrap(), 1280);
499 assert!(xs.windows(2).all(|w| w[0] <= w[1]));
501 assert!(xs.len() > 8); }
503
504 #[test]
505 fn axis_origins_8k_no_overflow() {
506 let xs = axis_origins(7680, 640, 0.2);
508 assert_eq!(xs[0], 0);
509 assert_eq!(*xs.last().unwrap(), 7680 - 640);
510 assert!(xs.windows(2).all(|w| w[0] <= w[1]));
511 }
512
513 #[test]
514 fn tile_grid_4k_all_full_size_and_in_bounds() {
515 let grid = tile_grid(2160, 3840, 640, 640, 0.2);
516 assert_eq!(grid.len(), 8 * 4); for t in &grid {
518 assert_eq!(t.source.width, 640);
519 assert_eq!(t.source.height, 640);
520 assert!(t.source.x + 640 <= 3840);
521 assert!(t.source.y + 640 <= 2160);
522 }
523 assert_eq!(grid[0].source, Region::new(0, 0, 640, 640));
525 assert_eq!(grid[8].source, Region::new(0, 507, 640, 640)); }
527
528 #[test]
529 fn tile_grid_realized_overlap_at_least_requested() {
530 let grid = tile_grid(2160, 3840, 640, 640, 0.2);
531 let step = grid[1].source.x - grid[0].source.x;
533 let realized_overlap = 1.0 - (step as f64 / 640.0);
534 assert!(
535 realized_overlap >= 0.2 - 1e-9,
536 "realized {realized_overlap} < 0.2"
537 );
538 }
539
540 #[test]
541 fn tile_grid_frame_smaller_than_tile_single_whole_frame() {
542 let grid = tile_grid(400, 500, 640, 640, 0.2);
543 assert_eq!(grid.len(), 1);
544 assert_eq!(grid[0].source, Region::new(0, 0, 500, 400));
545 }
546
547 #[test]
548 fn rounding_uses_f32_path() {
549 let xs = axis_origins(4000, 640, 0.333);
553 assert_eq!(xs[0], 0);
554 assert_eq!(*xs.last().unwrap(), 4000 - 640);
555 assert!(xs.windows(2).all(|w| w[0] <= w[1]));
556 let max_step = xs.windows(2).map(|w| w[1] - w[0]).max().unwrap();
557 let realized = 1.0 - (max_step as f64 / 640.0);
558 assert!(
559 realized >= 0.333 - 1e-9,
560 "realized overlap {realized} < 0.333 (max_step={max_step})"
561 );
562 }
563
564 #[test]
565 fn with_fit_letterbox_syncs_pad() {
566 let pad = [1, 2, 3, 4];
567 let cfg = TilingConfig::new(640, 640).with_fit(Fit::Letterbox { pad });
568 assert_eq!(cfg.fit, Fit::Letterbox { pad });
569 assert_eq!(cfg.pad, pad);
570 }
571
572 #[test]
573 fn placement_to_source_region_rejects_invalid() {
574 let good = TilePlacement {
575 index: 0,
576 count: 1,
577 origin: (10.0, 20.0),
578 crop_size: (640.0, 640.0),
579 letterbox: None,
580 frame_dims: (3840.0, 2160.0),
581 };
582 assert!(placement_to_source_region(&good).is_ok());
583
584 let mut bad = good;
585 bad.origin.0 = -1.0;
586 assert!(placement_to_source_region(&bad).is_err());
587 bad = good;
588 bad.origin.0 = f32::NAN;
589 assert!(placement_to_source_region(&bad).is_err());
590 bad = good;
591 bad.origin.0 = 1.5;
592 assert!(placement_to_source_region(&bad).is_err());
593 bad = good;
594 bad.crop_size.0 = 0.0;
595 assert!(placement_to_source_region(&bad).is_err());
596 }
597
598 #[test]
599 fn tiling_config_validate_rejects_bad_overlap() {
600 assert!(TilingConfig::new(640, 640)
601 .with_overlap(1.0)
602 .validate()
603 .is_err());
604 assert!(TilingConfig::new(640, 640)
605 .with_overlap(-0.1)
606 .validate()
607 .is_err());
608 assert!(TilingConfig::new(640, 640)
609 .with_overlap(0.2)
610 .validate()
611 .is_ok());
612 }
613
614 #[test]
615 fn tiling_config_validate_rejects_zero_tile_size() {
616 assert!(TilingConfig::new(0, 640).validate().is_err());
617 assert!(TilingConfig::new(640, 0).validate().is_err());
618 }
619
620 struct SpanNameCapture {
627 names: std::sync::Arc<std::sync::Mutex<Vec<String>>>,
628 next_id: std::sync::atomic::AtomicU64,
629 }
630
631 impl tracing::Subscriber for SpanNameCapture {
632 fn enabled(&self, _metadata: &tracing::Metadata<'_>) -> bool {
633 true
634 }
635
636 fn new_span(&self, span: &tracing::span::Attributes<'_>) -> tracing::span::Id {
637 self.names
638 .lock()
639 .unwrap()
640 .push(span.metadata().name().to_string());
641 let id = self
642 .next_id
643 .fetch_add(1, std::sync::atomic::Ordering::Relaxed)
644 + 1;
645 tracing::span::Id::from_u64(id)
646 }
647
648 fn record(&self, _span: &tracing::span::Id, _values: &tracing::span::Record<'_>) {}
649 fn record_follows_from(&self, _span: &tracing::span::Id, _follows: &tracing::span::Id) {}
650 fn event(&self, _event: &tracing::Event<'_>) {}
651 fn enter(&self, _span: &tracing::span::Id) {}
652 fn exit(&self, _span: &tracing::span::Id) {}
653 }
654
655 #[test]
656 fn tiling_emits_spans() {
657 let names = std::sync::Arc::new(std::sync::Mutex::new(Vec::new()));
658 let subscriber = SpanNameCapture {
659 names: names.clone(),
660 next_id: std::sync::atomic::AtomicU64::new(0),
661 };
662
663 tracing::subscriber::with_default(subscriber, || {
664 let mut proc = ImageProcessor::new().expect("processor");
665 let cfg = TilingConfig::new(320, 240);
666 let placements = proc.plan_tiles(640, 480, &cfg).expect("plan_tiles");
667
668 let src = TensorDyn::image(
669 640,
670 480,
671 PixelFormat::Nv12,
672 DType::U8,
673 Some(TensorMemory::Mem),
674 CpuAccess::ReadWrite,
675 )
676 .expect("src");
677 let mut dst = TensorDyn::image(
678 320,
679 240,
680 PixelFormat::Rgb,
681 DType::U8,
682 Some(TensorMemory::Mem),
683 CpuAccess::ReadWrite,
684 )
685 .expect("dst");
686
687 proc.tile_one(&src, &mut dst, &placements[0], &cfg)
688 .expect("tile_one");
689 let _ = proc.flush();
690 });
691
692 let names = names.lock().unwrap();
693 assert!(
694 names.iter().any(|n| n == "image.plan_tiles"),
695 "expected image.plan_tiles in {names:?}"
696 );
697 assert!(
698 names.iter().any(|n| n == "image.tile_one"),
699 "expected image.tile_one in {names:?}"
700 );
701 }
702}