1use crate::Error;
18use crate::{Crop, Fit, ImageProcessor, ImageProcessorTrait, Result};
19use edgefirst_tensor::{CpuAccess, DType, PixelFormat, Region, TensorDyn, TensorMemory};
20
21pub use edgefirst_decoder_abi::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
174 .resolve(
175 src_w,
176 src_h,
177 tile_w,
178 tile_h,
179 crate::LetterboxPlacement::Center,
180 )
181 .ok()?;
182 resolved.dst_rect.map(|r| {
183 let (dw, dh) = (tile_w as f32, tile_h as f32);
184 [
185 r.left as f32 / dw,
186 r.top as f32 / dh,
187 (r.left + r.width) as f32 / dw,
188 (r.top + r.height) as f32 / dh,
189 ]
190 })
191}
192
193impl ImageProcessor {
194 pub fn alloc_tile_batch(
210 &self,
211 n: usize,
212 cfg: &TilingConfig,
213 format: PixelFormat,
214 dtype: DType,
215 memory: Option<TensorMemory>,
216 access: CpuAccess,
217 ) -> Result<TensorDyn> {
218 cfg.validate()?;
219 self.create_image_with(
220 cfg.tile_w,
221 n.saturating_mul(cfg.tile_h),
222 format,
223 dtype,
224 memory,
225 access,
226 true,
227 )
228 }
229
230 pub fn plan_tiles(
246 &self,
247 src_w: usize,
248 src_h: usize,
249 cfg: &TilingConfig,
250 ) -> Result<Vec<TilePlacement>> {
251 let span = tracing::trace_span!(
252 "image.plan_tiles",
253 tiles = tracing::field::Empty,
254 overlap = cfg.overlap_ratio,
255 );
256 let _s = span.enter();
257 cfg.validate()?;
258 let grid = tile_grid(src_h, src_w, cfg.tile_h, cfg.tile_w, cfg.overlap_ratio);
259 let count = grid.len();
260 span.record("tiles", count);
261 let crop = Crop::default().with_fit(cfg.fit);
262 Ok(grid
263 .iter()
264 .map(|t| {
265 let lb = placement_letterbox(
266 &crop.with_source(Some(t.source)),
267 t.source.width,
268 t.source.height,
269 cfg.tile_w,
270 cfg.tile_h,
271 );
272 TilePlacement {
273 index: t.index,
274 count,
275 origin: (t.source.x as f32, t.source.y as f32),
276 crop_size: (t.source.width as f32, t.source.height as f32),
277 letterbox: lb,
278 frame_dims: (src_w as f32, src_h as f32),
279 }
280 })
281 .collect())
282 }
283
284 pub fn tile_into(
328 &mut self,
329 src: &TensorDyn,
330 dst_batched: &mut TensorDyn,
331 cfg: &TilingConfig,
332 ) -> Result<Vec<TilePlacement>> {
333 let span = tracing::trace_span!("image.tile_into", tiles = tracing::field::Empty);
334 let _s = span.enter();
335 cfg.validate()?;
336 let (src_w, src_h) = (
337 src.width().ok_or(Error::NotAnImage)?,
338 src.height().ok_or(Error::NotAnImage)?,
339 );
340 let placements = self.plan_tiles(src_w, src_h, cfg)?;
341 let count = placements.len();
342 span.record("tiles", count);
343
344 let dst_h = dst_batched.height().ok_or(Error::NotAnImage)?;
345 let required_h = count.saturating_mul(cfg.tile_h);
346 if dst_h < required_h {
347 return Err(Error::InvalidShape(format!(
348 "tile_into dst height {dst_h} < count*tile_h {required_h}"
349 )));
350 }
351
352 for p in &placements {
353 self.render_tile(src, dst_batched, p, cfg)?;
354 }
355 self.flush()?;
356 Ok(placements)
357 }
358
359 pub fn tile_one(
371 &mut self,
372 src: &TensorDyn,
373 dst_slot: &mut TensorDyn,
374 placement: &TilePlacement,
375 cfg: &TilingConfig,
376 ) -> Result<()> {
377 let _s = tracing::trace_span!(
378 "image.tile_one",
379 index = placement.index,
380 count = placement.count,
381 )
382 .entered();
383 cfg.validate()?;
384 self.render_tile(src, dst_slot, placement, cfg)
385 }
386
387 fn render_tile(
390 &mut self,
391 src: &TensorDyn,
392 dst: &mut TensorDyn,
393 placement: &TilePlacement,
394 cfg: &TilingConfig,
395 ) -> Result<()> {
396 let source = placement_to_source_region(placement)?;
397 let crop = Crop::default().with_source(Some(source)).with_fit(cfg.fit);
398
399 let dst_h = dst.height().ok_or(Error::NotAnImage)?;
404 let batched = dst_h >= placement.count.saturating_mul(cfg.tile_h);
405 if batched {
406 let mut band = dst.view(Region::new(
407 0,
408 placement.index * cfg.tile_h,
409 cfg.tile_w,
410 cfg.tile_h,
411 ))?;
412 self.convert_deferred(
413 src,
414 &mut band,
415 Some(crate::ConvertOptions::new().crop(crop)),
416 )?;
417 } else {
418 self.convert_deferred(src, dst, Some(crate::ConvertOptions::new().crop(crop)))?;
419 }
420 Ok(())
421 }
422}
423
424fn placement_to_source_region(placement: &TilePlacement) -> Result<Region> {
430 let (ox, oy) = placement.origin;
431 let (cw, ch) = placement.crop_size;
432 for (name, v) in [
433 ("origin.x", ox),
434 ("origin.y", oy),
435 ("crop.w", cw),
436 ("crop.h", ch),
437 ] {
438 if !v.is_finite() || v < 0.0 {
439 return Err(Error::CropInvalid(format!(
440 "tile placement {name} must be finite and non-negative, got {v}"
441 )));
442 }
443 }
444 let (ox_i, oy_i, cw_i, ch_i) = (ox as usize, oy as usize, cw as usize, ch as usize);
445 for (name, f, i) in [
447 ("origin.x", ox, ox_i),
448 ("origin.y", oy, oy_i),
449 ("crop.w", cw, cw_i),
450 ("crop.h", ch, ch_i),
451 ] {
452 if (f - i as f32).abs() > f32::EPSILON {
453 return Err(Error::CropInvalid(format!(
454 "tile placement {name} must be an integral pixel value, got {f}"
455 )));
456 }
457 }
458 if cw_i == 0 || ch_i == 0 {
459 return Err(Error::CropInvalid(
460 "tile placement crop size must be non-zero".into(),
461 ));
462 }
463 Ok(Region::new(ox_i, oy_i, cw_i, ch_i))
464}
465
466#[cfg(test)]
467mod tests {
468 use super::*;
469
470 #[test]
473 fn axis_origins_matches_adis_worked_example() {
474 assert_eq!(axis_origins(1920, 640, 0.1), vec![0, 427, 853, 1280]);
476 }
477
478 #[test]
479 fn axis_origins_4k_axes() {
480 let xs = axis_origins(3840, 640, 0.2);
482 assert_eq!(xs, vec![0, 457, 914, 1371, 1829, 2286, 2743, 3200]);
483 let ys = axis_origins(2160, 640, 0.2);
485 assert_eq!(ys, vec![0, 507, 1013, 1520]);
486 assert_eq!(*xs.last().unwrap(), 3840 - 640);
487 assert_eq!(*ys.last().unwrap(), 2160 - 640);
488 assert_eq!(xs[0], 0);
489 }
490
491 #[test]
492 fn axis_origins_frame_le_tile_single() {
493 assert_eq!(axis_origins(640, 640, 0.2), vec![0]); assert_eq!(axis_origins(400, 640, 0.2), vec![0]); }
496
497 #[test]
498 fn axis_origins_frame_tile_plus_one() {
499 assert_eq!(axis_origins(641, 640, 0.2), vec![0, 1]);
501 }
502
503 #[test]
504 fn axis_origins_overlap_zero_is_exact_tiling() {
505 assert_eq!(axis_origins(1920, 640, 0.0), vec![0, 640, 1280]);
507 }
508
509 #[test]
510 fn axis_origins_overlap_near_one_no_panic() {
511 let xs = axis_origins(1920, 640, 0.99);
513 assert_eq!(xs[0], 0);
514 assert_eq!(*xs.last().unwrap(), 1280);
515 assert!(xs.windows(2).all(|w| w[0] <= w[1]));
517 assert!(xs.len() > 8); }
519
520 #[test]
521 fn axis_origins_8k_no_overflow() {
522 let xs = axis_origins(7680, 640, 0.2);
524 assert_eq!(xs[0], 0);
525 assert_eq!(*xs.last().unwrap(), 7680 - 640);
526 assert!(xs.windows(2).all(|w| w[0] <= w[1]));
527 }
528
529 #[test]
530 fn tile_grid_4k_all_full_size_and_in_bounds() {
531 let grid = tile_grid(2160, 3840, 640, 640, 0.2);
532 assert_eq!(grid.len(), 8 * 4); for t in &grid {
534 assert_eq!(t.source.width, 640);
535 assert_eq!(t.source.height, 640);
536 assert!(t.source.x + 640 <= 3840);
537 assert!(t.source.y + 640 <= 2160);
538 }
539 assert_eq!(grid[0].source, Region::new(0, 0, 640, 640));
541 assert_eq!(grid[8].source, Region::new(0, 507, 640, 640)); }
543
544 #[test]
545 fn tile_grid_realized_overlap_at_least_requested() {
546 let grid = tile_grid(2160, 3840, 640, 640, 0.2);
547 let step = grid[1].source.x - grid[0].source.x;
549 let realized_overlap = 1.0 - (step as f64 / 640.0);
550 assert!(
551 realized_overlap >= 0.2 - 1e-9,
552 "realized {realized_overlap} < 0.2"
553 );
554 }
555
556 #[test]
557 fn tile_grid_frame_smaller_than_tile_single_whole_frame() {
558 let grid = tile_grid(400, 500, 640, 640, 0.2);
559 assert_eq!(grid.len(), 1);
560 assert_eq!(grid[0].source, Region::new(0, 0, 500, 400));
561 }
562
563 #[test]
564 fn rounding_uses_f32_path() {
565 let xs = axis_origins(4000, 640, 0.333);
569 assert_eq!(xs[0], 0);
570 assert_eq!(*xs.last().unwrap(), 4000 - 640);
571 assert!(xs.windows(2).all(|w| w[0] <= w[1]));
572 let max_step = xs.windows(2).map(|w| w[1] - w[0]).max().unwrap();
573 let realized = 1.0 - (max_step as f64 / 640.0);
574 assert!(
575 realized >= 0.333 - 1e-9,
576 "realized overlap {realized} < 0.333 (max_step={max_step})"
577 );
578 }
579
580 #[test]
581 fn with_fit_letterbox_syncs_pad() {
582 let pad = [1, 2, 3, 4];
583 let cfg = TilingConfig::new(640, 640).with_fit(Fit::Letterbox { pad });
584 assert_eq!(cfg.fit, Fit::Letterbox { pad });
585 assert_eq!(cfg.pad, pad);
586 }
587
588 #[test]
589 fn placement_to_source_region_rejects_invalid() {
590 let good = TilePlacement {
591 index: 0,
592 count: 1,
593 origin: (10.0, 20.0),
594 crop_size: (640.0, 640.0),
595 letterbox: None,
596 frame_dims: (3840.0, 2160.0),
597 };
598 assert!(placement_to_source_region(&good).is_ok());
599
600 let mut bad = good;
601 bad.origin.0 = -1.0;
602 assert!(placement_to_source_region(&bad).is_err());
603 bad = good;
604 bad.origin.0 = f32::NAN;
605 assert!(placement_to_source_region(&bad).is_err());
606 bad = good;
607 bad.origin.0 = 1.5;
608 assert!(placement_to_source_region(&bad).is_err());
609 bad = good;
610 bad.crop_size.0 = 0.0;
611 assert!(placement_to_source_region(&bad).is_err());
612 }
613
614 #[test]
615 fn tiling_config_validate_rejects_bad_overlap() {
616 assert!(TilingConfig::new(640, 640)
617 .with_overlap(1.0)
618 .validate()
619 .is_err());
620 assert!(TilingConfig::new(640, 640)
621 .with_overlap(-0.1)
622 .validate()
623 .is_err());
624 assert!(TilingConfig::new(640, 640)
625 .with_overlap(0.2)
626 .validate()
627 .is_ok());
628 }
629
630 #[test]
631 fn tiling_config_validate_rejects_zero_tile_size() {
632 assert!(TilingConfig::new(0, 640).validate().is_err());
633 assert!(TilingConfig::new(640, 0).validate().is_err());
634 }
635
636 struct SpanNameCapture {
643 names: std::sync::Arc<std::sync::Mutex<Vec<String>>>,
644 next_id: std::sync::atomic::AtomicU64,
645 }
646
647 impl tracing::Subscriber for SpanNameCapture {
648 fn enabled(&self, _metadata: &tracing::Metadata<'_>) -> bool {
649 true
650 }
651
652 fn new_span(&self, span: &tracing::span::Attributes<'_>) -> tracing::span::Id {
653 self.names
654 .lock()
655 .unwrap()
656 .push(span.metadata().name().to_string());
657 let id = self
658 .next_id
659 .fetch_add(1, std::sync::atomic::Ordering::Relaxed)
660 + 1;
661 tracing::span::Id::from_u64(id)
662 }
663
664 fn record(&self, _span: &tracing::span::Id, _values: &tracing::span::Record<'_>) {}
665 fn record_follows_from(&self, _span: &tracing::span::Id, _follows: &tracing::span::Id) {}
666 fn event(&self, _event: &tracing::Event<'_>) {}
667 fn enter(&self, _span: &tracing::span::Id) {}
668 fn exit(&self, _span: &tracing::span::Id) {}
669 }
670
671 #[test]
672 fn tiling_emits_spans() {
673 let names = std::sync::Arc::new(std::sync::Mutex::new(Vec::new()));
674 let subscriber = SpanNameCapture {
675 names: names.clone(),
676 next_id: std::sync::atomic::AtomicU64::new(0),
677 };
678
679 tracing::subscriber::with_default(subscriber, || {
680 let mut proc = ImageProcessor::new().expect("processor");
681 let cfg = TilingConfig::new(320, 240);
682 let placements = proc.plan_tiles(640, 480, &cfg).expect("plan_tiles");
683
684 let src = TensorDyn::image(
685 640,
686 480,
687 PixelFormat::Nv12,
688 DType::U8,
689 Some(TensorMemory::Mem),
690 CpuAccess::ReadWrite,
691 )
692 .expect("src");
693 let mut dst = TensorDyn::image(
694 320,
695 240,
696 PixelFormat::Rgb,
697 DType::U8,
698 Some(TensorMemory::Mem),
699 CpuAccess::ReadWrite,
700 )
701 .expect("dst");
702
703 proc.tile_one(&src, &mut dst, &placements[0], &cfg)
704 .expect("tile_one");
705 let _ = proc.flush();
706 });
707
708 let names = names.lock().unwrap();
709 assert!(
710 names.iter().any(|n| n == "image.plan_tiles"),
711 "expected image.plan_tiles in {names:?}"
712 );
713 assert!(
714 names.iter().any(|n| n == "image.tile_one"),
715 "expected image.tile_one in {names:?}"
716 );
717 }
718}