use super::core_warping::{warp_rgb_image, BoundaryMethod, InterpolationMethod};
use super::stereo_rectification::matrix_multiply;
use crate::error::{Result, VisionError};
use crate::registration::{identity_transform, TransformMatrix};
use image::{DynamicImage, Rgb, RgbImage};
use scirs2_core::ndarray::Array2;
#[allow(dead_code)]
pub fn stitch_images(
images: &[DynamicImage],
transforms: &[TransformMatrix],
output_size: (u32, u32),
) -> Result<DynamicImage> {
let pixel_count = (output_size.0 * output_size.1) as usize;
if pixel_count > 16_777_216 {
stitch_images_streaming(images, transforms, output_size)
} else {
stitch_images_traditional(images, transforms, output_size)
}
}
#[allow(dead_code)]
pub fn stitch_images_streaming(
images: &[DynamicImage],
transforms: &[TransformMatrix],
output_size: (u32, u32),
) -> Result<DynamicImage> {
if images.len() != transforms.len() {
return Err(VisionError::InvalidParameter(
"Number of images must match number of transforms".to_string(),
));
}
let tile_config = TileConfig::for_output_size(output_size);
let mut panorama_processor =
StreamingPanoramaProcessor::new(output_size, tile_config, BlendingMode::MultiBandBlending)?;
for (image, transform) in images.iter().zip(transforms.iter()) {
panorama_processor.add_image_streaming(image, transform)?;
}
panorama_processor.finalize()
}
#[allow(dead_code)]
fn stitch_images_traditional(
images: &[DynamicImage],
transforms: &[TransformMatrix],
output_size: (u32, u32),
) -> Result<DynamicImage> {
if images.len() != transforms.len() {
return Err(VisionError::InvalidParameter(
"Number of images must match number of transforms".to_string(),
));
}
let (width, height) = output_size;
let mut output = RgbImage::new(width, height);
let mut weight_map = Array2::<f32>::zeros((height as usize, width as usize));
for y in 0..height {
for x in 0..width {
output.put_pixel(x, y, Rgb([0, 0, 0]));
}
}
for (image, transform) in images.iter().zip(transforms.iter()) {
let rgb_image = image.to_rgb8();
let warped = warp_rgb_image(
&rgb_image,
transform,
output_size,
InterpolationMethod::Bilinear,
BoundaryMethod::Zero,
)?;
for y in 0..height {
for x in 0..width {
let warped_pixel = warped.get_pixel(x, y);
let output_pixel = output.get_pixel_mut(x, y);
if warped_pixel[0] > 0 || warped_pixel[1] > 0 || warped_pixel[2] > 0 {
let weight = weight_map[[y as usize, x as usize]];
let new_weight = weight + 1.0;
for c in 0..3 {
let old_value = output_pixel[c] as f32;
let new_value = warped_pixel[c] as f32;
let blended: f32 = (old_value * weight + new_value) / new_weight;
output_pixel[c] = blended as u8;
}
weight_map[[y as usize, x as usize]] = new_weight;
}
}
}
}
Ok(DynamicImage::ImageRgb8(output))
}
#[derive(Debug, Clone)]
pub struct TileConfig {
pub tile_size: (u32, u32),
pub overlap: u32,
pub tile_count: (u32, u32),
pub memory_budget: usize,
}
impl TileConfig {
pub fn for_output_size(output_size: (u32, u32)) -> Self {
let (width, height) = output_size;
let target_tile_pixels = 16_777_216; let tile_dimension = (target_tile_pixels as f64).sqrt() as u32;
let tile_width = tile_dimension.min(width).max(512);
let tile_height = tile_dimension.min(height).max(512);
let tiles_x = width.div_ceil(tile_width);
let tiles_y = height.div_ceil(tile_height);
let overlap = 64; let memory_budget = 1_073_741_824;
Self {
tile_size: (tile_width, tile_height),
overlap,
tile_count: (tiles_x, tiles_y),
memory_budget,
}
}
}
#[derive(Debug, Clone, Copy)]
pub enum BlendingMode {
Linear,
MultiBandBlending,
GraphCutSeaming,
}
pub struct StreamingPanoramaProcessor {
output_size: (u32, u32),
tile_config: TileConfig,
blending_mode: BlendingMode,
tile_cache: TileCache,
processed_images: usize,
}
impl StreamingPanoramaProcessor {
pub fn new(
output_size: (u32, u32),
tile_config: TileConfig,
blending_mode: BlendingMode,
) -> Result<Self> {
let tile_cache = TileCache::new(&tile_config)?;
Ok(Self {
output_size,
tile_config,
blending_mode,
tile_cache,
processed_images: 0,
})
}
pub fn add_image_streaming(
&mut self,
image: &DynamicImage,
transform: &TransformMatrix,
) -> Result<()> {
let rgb_image = image.to_rgb8();
for tile_y in 0..self.tile_config.tile_count.1 {
for tile_x in 0..self.tile_config.tile_count.0 {
self.process_tile_for_image(tile_x, tile_y, &rgb_image, transform)?;
}
}
self.processed_images += 1;
Ok(())
}
fn process_tile_for_image(
&mut self,
tile_x: u32,
tile_y: u32,
image: &RgbImage,
transform: &TransformMatrix,
) -> Result<()> {
let tile_bounds = self.calculate_tile_bounds(tile_x, tile_y);
let warped_tile = self.warp_image_for_tile(image, transform, &tile_bounds)?;
self.blend_tile(tile_x, tile_y, &warped_tile)?;
Ok(())
}
fn calculate_tile_bounds(&self, tile_x: u32, tile_y: u32) -> (u32, u32, u32, u32) {
let (tile_width, tile_height) = self.tile_config.tile_size;
let overlap = self.tile_config.overlap;
let start_x = tile_x * tile_width;
let start_y = tile_y * tile_height;
let actual_width = (tile_width + overlap).min(self.output_size.0 - start_x);
let actual_height = (tile_height + overlap).min(self.output_size.1 - start_y);
(start_x, start_y, actual_width, actual_height)
}
fn warp_image_for_tile(
&self,
image: &RgbImage,
transform: &TransformMatrix,
tile_bounds: &(u32, u32, u32, u32),
) -> Result<RgbImage> {
let (tile_x, tile_y, tile_width, tile_height) = *tile_bounds;
let tile_transform = self.create_tile_transform(transform, tile_x, tile_y);
warp_rgb_image(
image,
&tile_transform,
(tile_width, tile_height),
InterpolationMethod::Bilinear,
BoundaryMethod::Zero,
)
}
fn create_tile_transform(
&self,
base_transform: &TransformMatrix,
tile_x: u32,
tile_y: u32,
) -> TransformMatrix {
let mut tile_offset = identity_transform();
tile_offset[[0, 2]] = tile_x as f64;
tile_offset[[1, 2]] = tile_y as f64;
matrix_multiply(&tile_offset, base_transform).unwrap_or_else(|_| base_transform.clone())
}
fn blend_tile(&mut self, tile_x: u32, tile_y: u32, warped_tile: &RgbImage) -> Result<()> {
match self.blending_mode {
BlendingMode::Linear => self.blend_tile_linear(tile_x, tile_y, warped_tile),
BlendingMode::MultiBandBlending => {
self.blend_tile_multiband(tile_x, tile_y, warped_tile)
}
BlendingMode::GraphCutSeaming => self.blend_tile_graphcut(tile_x, tile_y, warped_tile),
}
}
fn blend_tile_linear(
&mut self,
tile_x: u32,
tile_y: u32,
warped_tile: &RgbImage,
) -> Result<()> {
let tile_id = TileId {
x: tile_x,
y: tile_y,
};
let existing_tile = self.tile_cache.get_or_create_tile(tile_id)?;
let (tile_width, tile_height) = warped_tile.dimensions();
for y in 0..tile_height {
for x in 0..tile_width {
let new_pixel = warped_tile.get_pixel(x, y);
let existing_pixel = existing_tile.get_pixel_mut(x, y);
if new_pixel[0] > 0 || new_pixel[1] > 0 || new_pixel[2] > 0 {
for c in 0..3 {
let old_value = existing_pixel[c] as f32;
let new_value = new_pixel[c] as f32;
let blended = if old_value > 0.0 {
(old_value + new_value) / 2.0
} else {
new_value
};
existing_pixel[c] = blended as u8;
}
}
}
}
Ok(())
}
fn blend_tile_multiband(
&mut self,
tile_x: u32,
tile_y: u32,
warped_tile: &RgbImage,
) -> Result<()> {
self.blend_tile_linear(tile_x, tile_y, warped_tile)
}
fn blend_tile_graphcut(
&mut self,
tile_x: u32,
tile_y: u32,
warped_tile: &RgbImage,
) -> Result<()> {
self.blend_tile_linear(tile_x, tile_y, warped_tile)
}
pub fn finalize(self) -> Result<DynamicImage> {
let (width, height) = self.output_size;
let mut output = RgbImage::new(width, height);
for tile_y in 0..self.tile_config.tile_count.1 {
for tile_x in 0..self.tile_config.tile_count.0 {
let tile_id = TileId {
x: tile_x,
y: tile_y,
};
if let Ok(tile) = self.tile_cache.get_tile(tile_id) {
self.copy_tile_to_output(tile, tile_x, tile_y, &mut output)?;
}
}
}
Ok(DynamicImage::ImageRgb8(output))
}
fn copy_tile_to_output(
&self,
tile: &RgbImage,
tile_x: u32,
tile_y: u32,
output: &mut RgbImage,
) -> Result<()> {
let tile_bounds = self.calculate_tile_bounds(tile_x, tile_y);
let (start_x, start_y, tile_width, tile_height) = tile_bounds;
for y in 0..tile_height {
for x in 0..tile_width {
let output_x = start_x + x;
let output_y = start_y + y;
if output_x < self.output_size.0 && output_y < self.output_size.1 {
let pixel = tile.get_pixel(x, y);
output.put_pixel(output_x, output_y, *pixel);
}
}
}
Ok(())
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
struct TileId {
x: u32,
y: u32,
}
struct TileCache {
tiles: std::collections::HashMap<TileId, RgbImage>,
config: TileConfig,
memory_usage: usize,
}
impl TileCache {
fn new(config: &TileConfig) -> Result<Self> {
Ok(Self {
tiles: std::collections::HashMap::new(),
config: config.clone(),
memory_usage: 0,
})
}
#[allow(clippy::map_entry)]
fn get_or_create_tile(&mut self, tile_id: TileId) -> Result<&mut RgbImage> {
if !self.tiles.contains_key(&tile_id) {
self.ensure_memory_budget()?;
let (tile_width, tile_height) = self.config.tile_size;
let tile = RgbImage::new(tile_width, tile_height);
let tile_memory = (tile_width * tile_height * 3) as usize;
self.memory_usage += tile_memory;
self.tiles.insert(tile_id, tile);
}
Ok(self.tiles.get_mut(&tile_id).expect("Operation failed"))
}
fn get_tile(&self, tile_id: TileId) -> Result<&RgbImage> {
self.tiles
.get(&tile_id)
.ok_or_else(|| VisionError::OperationError(format!("Tile {tile_id:?} not found")))
}
fn ensure_memory_budget(&mut self) -> Result<()> {
while self.memory_usage > self.config.memory_budget && !self.tiles.is_empty() {
if let Some((tile_id, _)) = self.tiles.iter().next() {
let tile_id = *tile_id;
let (tile_width, tile_height) = self.config.tile_size;
let tile_memory = (tile_width * tile_height * 3) as usize;
self.tiles.remove(&tile_id);
self.memory_usage = self.memory_usage.saturating_sub(tile_memory);
} else {
break;
}
}
Ok(())
}
}