use crate::{Crop, Fit, ImageProcessor, ImageProcessorTrait, Result};
use crate::{Error, Flip, Rotation};
use edgefirst_tensor::{CpuAccess, DType, PixelFormat, Region, TensorDyn, TensorMemory};
pub use edgefirst_decoder::tiling::TilePlacement;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct TileSpec {
pub source: Region,
pub index: usize,
pub row: usize,
pub col: usize,
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct TilingConfig {
pub tile_w: usize,
pub tile_h: usize,
pub overlap_ratio: f32,
pub pad: [u8; 4],
pub fit: Fit,
}
impl TilingConfig {
pub fn new(tile_w: usize, tile_h: usize) -> Self {
Self {
tile_w,
tile_h,
overlap_ratio: 0.2,
pad: [114, 114, 114, 255],
fit: Fit::Stretch,
}
}
pub fn with_overlap(mut self, overlap_ratio: f32) -> Self {
self.overlap_ratio = overlap_ratio;
self
}
pub fn with_fit(mut self, fit: Fit) -> Self {
if let Fit::Letterbox { pad } = fit {
self.pad = pad;
}
self.fit = fit;
self
}
pub fn validate(&self) -> Result<()> {
if !(self.overlap_ratio >= 0.0 && self.overlap_ratio < 1.0) {
return Err(Error::CropInvalid(format!(
"tiling overlap_ratio must be in [0.0, 1.0), got {}",
self.overlap_ratio
)));
}
if self.tile_w == 0 || self.tile_h == 0 {
return Err(Error::CropInvalid(
"tiling tile size must be non-zero".into(),
));
}
Ok(())
}
}
fn axis_origins(frame: usize, tile: usize, overlap: f64) -> Vec<usize> {
if frame <= tile {
return vec![0];
}
let last = frame - tile;
let max_step = ((1.0 - overlap) * tile as f64).floor().max(1.0) as usize;
let total = last.div_ceil(max_step);
(0..=total)
.map(|i| (i as f32 * last as f32 / total as f32).round() as usize)
.collect()
}
pub fn tile_grid(
frame_h: usize,
frame_w: usize,
tile_h: usize,
tile_w: usize,
overlap_ratio: f32,
) -> Vec<TileSpec> {
let cw = tile_w.min(frame_w);
let ch = tile_h.min(frame_h);
let xs = axis_origins(frame_w, tile_w, overlap_ratio as f64);
let ys = axis_origins(frame_h, tile_h, overlap_ratio as f64);
let mut tiles = Vec::with_capacity(xs.len() * ys.len());
let mut index = 0;
for (row, &oy) in ys.iter().enumerate() {
for (col, &ox) in xs.iter().enumerate() {
debug_assert!(
ox + cw <= frame_w && oy + ch <= frame_h,
"tile out of bounds"
);
tiles.push(TileSpec {
source: Region::new(ox, oy, cw, ch),
index,
row,
col,
});
index += 1;
}
}
tiles
}
fn placement_letterbox(
crop: &Crop,
src_w: usize,
src_h: usize,
tile_w: usize,
tile_h: usize,
) -> Option<[f32; 4]> {
let resolved = crop.resolve(src_w, src_h, tile_w, tile_h).ok()?;
resolved.dst_rect.map(|r| {
let (dw, dh) = (tile_w as f32, tile_h as f32);
[
r.left as f32 / dw,
r.top as f32 / dh,
(r.left + r.width) as f32 / dw,
(r.top + r.height) as f32 / dh,
]
})
}
impl ImageProcessor {
pub fn alloc_tile_batch(
&self,
n: usize,
cfg: &TilingConfig,
format: PixelFormat,
dtype: DType,
memory: Option<TensorMemory>,
access: CpuAccess,
) -> Result<TensorDyn> {
cfg.validate()?;
self.create_image(
cfg.tile_w,
n.saturating_mul(cfg.tile_h),
format,
dtype,
memory,
access,
)
}
pub fn plan_tiles(
&self,
src_w: usize,
src_h: usize,
cfg: &TilingConfig,
) -> Result<Vec<TilePlacement>> {
cfg.validate()?;
let grid = tile_grid(src_h, src_w, cfg.tile_h, cfg.tile_w, cfg.overlap_ratio);
let count = grid.len();
let crop = Crop::default().with_fit(cfg.fit);
Ok(grid
.iter()
.map(|t| {
let lb = placement_letterbox(
&crop.with_source(Some(t.source)),
t.source.width,
t.source.height,
cfg.tile_w,
cfg.tile_h,
);
TilePlacement {
index: t.index,
count,
origin: (t.source.x as f32, t.source.y as f32),
crop_size: (t.source.width as f32, t.source.height as f32),
letterbox: lb,
frame_dims: (src_w as f32, src_h as f32),
}
})
.collect())
}
pub fn tile_into(
&mut self,
src: &TensorDyn,
dst_batched: &mut TensorDyn,
cfg: &TilingConfig,
) -> Result<Vec<TilePlacement>> {
cfg.validate()?;
let (src_w, src_h) = (
src.width().ok_or(Error::NotAnImage)?,
src.height().ok_or(Error::NotAnImage)?,
);
let placements = self.plan_tiles(src_w, src_h, cfg)?;
let count = placements.len();
let dst_h = dst_batched.height().ok_or(Error::NotAnImage)?;
let required_h = count.saturating_mul(cfg.tile_h);
if dst_h < required_h {
return Err(Error::InvalidShape(format!(
"tile_into dst height {dst_h} < count*tile_h {required_h}"
)));
}
for p in &placements {
self.render_tile(src, dst_batched, p, cfg)?;
}
self.flush()?;
Ok(placements)
}
pub fn tile_one(
&mut self,
src: &TensorDyn,
dst_slot: &mut TensorDyn,
placement: &TilePlacement,
cfg: &TilingConfig,
) -> Result<()> {
cfg.validate()?;
self.render_tile(src, dst_slot, placement, cfg)
}
fn render_tile(
&mut self,
src: &TensorDyn,
dst: &mut TensorDyn,
placement: &TilePlacement,
cfg: &TilingConfig,
) -> Result<()> {
let source = placement_to_source_region(placement)?;
let crop = Crop::default().with_source(Some(source)).with_fit(cfg.fit);
let dst_h = dst.height().ok_or(Error::NotAnImage)?;
let batched = dst_h >= placement.count.saturating_mul(cfg.tile_h);
if batched {
let mut band = dst.view(Region::new(
0,
placement.index * cfg.tile_h,
cfg.tile_w,
cfg.tile_h,
))?;
self.convert_deferred(src, &mut band, Rotation::None, Flip::None, crop)?;
} else {
self.convert_deferred(src, dst, Rotation::None, Flip::None, crop)?;
}
Ok(())
}
}
fn placement_to_source_region(placement: &TilePlacement) -> Result<Region> {
let (ox, oy) = placement.origin;
let (cw, ch) = placement.crop_size;
for (name, v) in [
("origin.x", ox),
("origin.y", oy),
("crop.w", cw),
("crop.h", ch),
] {
if !v.is_finite() || v < 0.0 {
return Err(Error::CropInvalid(format!(
"tile placement {name} must be finite and non-negative, got {v}"
)));
}
}
let (ox_i, oy_i, cw_i, ch_i) = (ox as usize, oy as usize, cw as usize, ch as usize);
for (name, f, i) in [
("origin.x", ox, ox_i),
("origin.y", oy, oy_i),
("crop.w", cw, cw_i),
("crop.h", ch, ch_i),
] {
if (f - i as f32).abs() > f32::EPSILON {
return Err(Error::CropInvalid(format!(
"tile placement {name} must be an integral pixel value, got {f}"
)));
}
}
if cw_i == 0 || ch_i == 0 {
return Err(Error::CropInvalid(
"tile placement crop size must be non-zero".into(),
));
}
Ok(Region::new(ox_i, oy_i, cw_i, ch_i))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn axis_origins_matches_adis_worked_example() {
assert_eq!(axis_origins(1920, 640, 0.1), vec![0, 427, 853, 1280]);
}
#[test]
fn axis_origins_4k_axes() {
let xs = axis_origins(3840, 640, 0.2);
assert_eq!(xs, vec![0, 457, 914, 1371, 1829, 2286, 2743, 3200]);
let ys = axis_origins(2160, 640, 0.2);
assert_eq!(ys, vec![0, 507, 1013, 1520]);
assert_eq!(*xs.last().unwrap(), 3840 - 640);
assert_eq!(*ys.last().unwrap(), 2160 - 640);
assert_eq!(xs[0], 0);
}
#[test]
fn axis_origins_frame_le_tile_single() {
assert_eq!(axis_origins(640, 640, 0.2), vec![0]); assert_eq!(axis_origins(400, 640, 0.2), vec![0]); }
#[test]
fn axis_origins_frame_tile_plus_one() {
assert_eq!(axis_origins(641, 640, 0.2), vec![0, 1]);
}
#[test]
fn axis_origins_overlap_zero_is_exact_tiling() {
assert_eq!(axis_origins(1920, 640, 0.0), vec![0, 640, 1280]);
}
#[test]
fn axis_origins_overlap_near_one_no_panic() {
let xs = axis_origins(1920, 640, 0.99);
assert_eq!(xs[0], 0);
assert_eq!(*xs.last().unwrap(), 1280);
assert!(xs.windows(2).all(|w| w[0] <= w[1]));
assert!(xs.len() > 8); }
#[test]
fn axis_origins_8k_no_overflow() {
let xs = axis_origins(7680, 640, 0.2);
assert_eq!(xs[0], 0);
assert_eq!(*xs.last().unwrap(), 7680 - 640);
assert!(xs.windows(2).all(|w| w[0] <= w[1]));
}
#[test]
fn tile_grid_4k_all_full_size_and_in_bounds() {
let grid = tile_grid(2160, 3840, 640, 640, 0.2);
assert_eq!(grid.len(), 8 * 4); for t in &grid {
assert_eq!(t.source.width, 640);
assert_eq!(t.source.height, 640);
assert!(t.source.x + 640 <= 3840);
assert!(t.source.y + 640 <= 2160);
}
assert_eq!(grid[0].source, Region::new(0, 0, 640, 640));
assert_eq!(grid[8].source, Region::new(0, 507, 640, 640)); }
#[test]
fn tile_grid_realized_overlap_at_least_requested() {
let grid = tile_grid(2160, 3840, 640, 640, 0.2);
let step = grid[1].source.x - grid[0].source.x;
let realized_overlap = 1.0 - (step as f64 / 640.0);
assert!(
realized_overlap >= 0.2 - 1e-9,
"realized {realized_overlap} < 0.2"
);
}
#[test]
fn tile_grid_frame_smaller_than_tile_single_whole_frame() {
let grid = tile_grid(400, 500, 640, 640, 0.2);
assert_eq!(grid.len(), 1);
assert_eq!(grid[0].source, Region::new(0, 0, 500, 400));
}
#[test]
fn rounding_uses_f32_path() {
let xs = axis_origins(4000, 640, 0.333);
assert_eq!(xs[0], 0);
assert_eq!(*xs.last().unwrap(), 4000 - 640);
assert!(xs.windows(2).all(|w| w[0] <= w[1]));
let max_step = xs.windows(2).map(|w| w[1] - w[0]).max().unwrap();
let realized = 1.0 - (max_step as f64 / 640.0);
assert!(
realized >= 0.333 - 1e-9,
"realized overlap {realized} < 0.333 (max_step={max_step})"
);
}
#[test]
fn with_fit_letterbox_syncs_pad() {
let pad = [1, 2, 3, 4];
let cfg = TilingConfig::new(640, 640).with_fit(Fit::Letterbox { pad });
assert_eq!(cfg.fit, Fit::Letterbox { pad });
assert_eq!(cfg.pad, pad);
}
#[test]
fn placement_to_source_region_rejects_invalid() {
let good = TilePlacement {
index: 0,
count: 1,
origin: (10.0, 20.0),
crop_size: (640.0, 640.0),
letterbox: None,
frame_dims: (3840.0, 2160.0),
};
assert!(placement_to_source_region(&good).is_ok());
let mut bad = good;
bad.origin.0 = -1.0;
assert!(placement_to_source_region(&bad).is_err());
bad = good;
bad.origin.0 = f32::NAN;
assert!(placement_to_source_region(&bad).is_err());
bad = good;
bad.origin.0 = 1.5;
assert!(placement_to_source_region(&bad).is_err());
bad = good;
bad.crop_size.0 = 0.0;
assert!(placement_to_source_region(&bad).is_err());
}
#[test]
fn tiling_config_validate_rejects_bad_overlap() {
assert!(TilingConfig::new(640, 640)
.with_overlap(1.0)
.validate()
.is_err());
assert!(TilingConfig::new(640, 640)
.with_overlap(-0.1)
.validate()
.is_err());
assert!(TilingConfig::new(640, 640)
.with_overlap(0.2)
.validate()
.is_ok());
}
#[test]
fn tiling_config_validate_rejects_zero_tile_size() {
assert!(TilingConfig::new(0, 640).validate().is_err());
assert!(TilingConfig::new(640, 0).validate().is_err());
}
}