use std::borrow::Cow;
use std::path::Path;
use std::sync::Arc;
use image::{GrayImage, RgbImage, RgbaImage};
use crate::config::{ErosionBorderMode, MaskProcessingDefaults};
use crate::foreground::{ForegroundHandle, compose_foreground};
use crate::geometry::{
BoundingBox, Padding, crop_bounds_fit_image, crop_gray_image, crop_rgb_image,
mask_bounding_box, pad_gray_image, pad_rgb_image,
};
use crate::mask::{
MaskColor, MaskHandle, MaskOperation, MaskPipeline, apply_operations, colorize_mask,
};
use crate::{MaskVectorizer, OutlineResult};
#[derive(Debug, Clone)]
pub struct InferencedMatte {
rgb_image: Arc<RgbImage>,
raw_matte: Arc<GrayImage>,
mask_processing_defaults: MaskProcessingDefaults,
}
impl InferencedMatte {
pub(crate) fn new(
rgb_image: RgbImage,
raw_matte: GrayImage,
mask_processing_defaults: MaskProcessingDefaults,
) -> Self {
Self {
rgb_image: Arc::new(rgb_image),
raw_matte: Arc::new(raw_matte),
mask_processing_defaults,
}
}
pub fn rgb_image(&self) -> &RgbImage {
self.rgb_image.as_ref()
}
pub fn raw_matte(&self) -> &GrayImage {
self.raw_matte.as_ref()
}
pub fn matte(&self) -> MatteHandle {
MatteHandle {
rgb_image: Arc::clone(&self.rgb_image),
raw_matte: Arc::clone(&self.raw_matte),
mask_processing_defaults: self.mask_processing_defaults.clone(),
operations: Vec::new(),
}
}
}
#[derive(Debug, Clone)]
pub struct MatteHandle {
rgb_image: Arc<RgbImage>,
raw_matte: Arc<GrayImage>,
mask_processing_defaults: MaskProcessingDefaults,
operations: Vec<MaskOperation>,
}
impl MatteHandle {
fn resolved_matte(&self) -> Cow<'_, GrayImage> {
if self.operations.is_empty() {
Cow::Borrowed(self.raw_matte.as_ref())
} else {
Cow::Owned(apply_operations(self.raw_matte.as_ref(), &self.operations))
}
}
fn resolve_pending_operations(mut self) -> Self {
if self.operations.is_empty() {
return self;
}
let operations = std::mem::take(&mut self.operations);
let matte = apply_operations(self.raw_matte.as_ref(), &operations);
Self {
rgb_image: self.rgb_image,
raw_matte: Arc::new(matte),
mask_processing_defaults: self.mask_processing_defaults,
operations: Vec::new(),
}
}
pub fn to_raw_matte(&self) -> GrayImage {
(*self.raw_matte).clone()
}
pub fn as_raw_matte(&self) -> &GrayImage {
self.raw_matte.as_ref()
}
pub fn dimensions(&self) -> (u32, u32) {
self.raw_matte.dimensions()
}
#[deprecated(note = "use to_raw_matte()")]
pub fn raw(&self) -> GrayImage {
self.to_raw_matte()
}
pub fn into_image(self) -> GrayImage {
if self.operations.is_empty() {
Arc::unwrap_or_clone(self.raw_matte)
} else {
apply_operations(self.raw_matte.as_ref(), &self.operations)
}
}
pub fn save(&self, path: impl AsRef<Path>) -> OutlineResult<()> {
self.resolved_matte().save(path)?;
Ok(())
}
pub fn bounding_box(&self) -> Option<BoundingBox> {
self.bounding_box_with(1)
}
pub fn bounding_box_with(&self, threshold: u8) -> Option<BoundingBox> {
let mask = self.resolved_matte();
mask_bounding_box(&mask, threshold)
}
pub fn blur(mut self) -> Self {
let sigma = self.mask_processing_defaults.blur_sigma;
self.operations.push(MaskOperation::Blur { sigma });
self
}
pub fn blur_with(mut self, sigma: f32) -> Self {
self.operations.push(MaskOperation::Blur { sigma });
self
}
pub fn threshold(mut self) -> Self {
let value = self.mask_processing_defaults.mask_threshold;
self.operations.push(MaskOperation::Threshold { value });
self
}
pub fn threshold_with(mut self, value: u8) -> Self {
self.operations.push(MaskOperation::Threshold { value });
self
}
pub fn dilate(mut self) -> Self {
let radius = self.mask_processing_defaults.dilation_radius;
self.operations.push(MaskOperation::Dilate { radius });
self
}
pub fn dilate_with(mut self, radius: f32) -> Self {
self.operations.push(MaskOperation::Dilate { radius });
self
}
pub fn erode(mut self) -> Self {
let radius = self.mask_processing_defaults.erosion_radius;
let border_mode = self.mask_processing_defaults.erosion_border_mode;
self.operations.push(MaskOperation::Erode {
radius,
border_mode,
});
self
}
pub fn erode_with(mut self, radius: f32) -> Self {
let border_mode = self.mask_processing_defaults.erosion_border_mode;
self.operations.push(MaskOperation::Erode {
radius,
border_mode,
});
self
}
pub fn erode_with_border_mode(mut self, radius: f32, border_mode: ErosionBorderMode) -> Self {
self.operations.push(MaskOperation::Erode {
radius,
border_mode,
});
self
}
pub fn fill_holes(self) -> Self {
let threshold = self.mask_processing_defaults.mask_threshold;
self.fill_holes_with(threshold)
}
pub fn fill_holes_with(mut self, threshold: u8) -> Self {
self.operations.push(MaskOperation::FillHoles { threshold });
self
}
pub fn processed(self) -> OutlineResult<MaskHandle> {
self.process_with_pipeline(None)
}
pub fn processed_with(self, pipeline: &MaskPipeline) -> OutlineResult<MaskHandle> {
self.process_with_pipeline(Some(pipeline))
}
fn process_with_pipeline(
mut self,
pipeline: Option<&MaskPipeline>,
) -> OutlineResult<MaskHandle> {
let mut ops = std::mem::take(&mut self.operations);
if let Some(custom) = pipeline {
ops.extend_from_slice(custom.operations());
}
let mask = apply_operations(self.raw_matte.as_ref(), &ops);
Ok(MaskHandle::new(
Arc::clone(&self.rgb_image),
mask,
self.mask_processing_defaults,
))
}
pub fn foreground(&self) -> OutlineResult<ForegroundHandle> {
let mask = self.resolved_matte();
let rgba = compose_foreground(self.rgb_image.as_ref(), &mask)?;
Ok(ForegroundHandle::new(rgba))
}
pub fn colorize(&self, color: impl Into<MaskColor>) -> RgbaImage {
let mask = self.resolved_matte();
colorize_mask(&mask, color)
}
pub fn trace<V>(&self, vectorizer: &V, options: &V::Options) -> OutlineResult<V::Output>
where
V: MaskVectorizer,
{
let mask = self.resolved_matte();
vectorizer.vectorize(&mask, options)
}
pub fn pad(self, padding: impl Into<Padding>) -> Self {
let this = self.resolve_pending_operations();
let padding = padding.into();
let matte = Arc::new(pad_gray_image(this.raw_matte.as_ref(), padding, 0));
let rgb = Arc::new(pad_rgb_image(this.rgb_image.as_ref(), padding, [0, 0, 0]));
Self {
rgb_image: rgb,
raw_matte: matte,
mask_processing_defaults: this.mask_processing_defaults,
operations: Vec::new(),
}
}
pub fn crop(self, bounds: BoundingBox) -> Option<Self> {
let this = self.resolve_pending_operations();
if !crop_bounds_fit_image(bounds, this.raw_matte.width(), this.raw_matte.height()) {
return None;
}
let matte = Arc::new(crop_gray_image(this.raw_matte.as_ref(), bounds));
let rgb = Arc::new(crop_rgb_image(this.rgb_image.as_ref(), bounds));
Some(Self {
rgb_image: rgb,
raw_matte: matte,
mask_processing_defaults: this.mask_processing_defaults,
operations: Vec::new(),
})
}
pub fn crop_to_content(self) -> Option<Self> {
self.crop_to_content_with(1)
}
pub fn crop_to_content_with(self, threshold: u8) -> Option<Self> {
let this = self.resolve_pending_operations();
let bounds = mask_bounding_box(this.raw_matte.as_ref(), threshold)?;
this.crop(bounds)
}
}
#[cfg(test)]
mod tests {
use super::*;
use image::{Luma, Rgb};
fn matte_handle() -> MatteHandle {
MatteHandle {
rgb_image: Arc::new(RgbImage::from_pixel(1, 1, Rgb([255, 255, 255]))),
raw_matte: Arc::new(GrayImage::from_pixel(1, 1, Luma([255]))),
mask_processing_defaults: MaskProcessingDefaults::default(),
operations: Vec::new(),
}
}
fn single_pixel_matte_handle() -> MatteHandle {
MatteHandle {
rgb_image: Arc::new(RgbImage::from_pixel(5, 5, Rgb([10, 20, 30]))),
raw_matte: Arc::new(GrayImage::from_fn(5, 5, |x, y| {
if x == 2 && y == 2 {
Luma([255])
} else {
Luma([0])
}
})),
mask_processing_defaults: MaskProcessingDefaults::default(),
operations: Vec::new(),
}
}
fn matte_handle_with_images(rgb_image: RgbImage, raw_matte: GrayImage) -> MatteHandle {
MatteHandle {
rgb_image: Arc::new(rgb_image),
raw_matte: Arc::new(raw_matte),
mask_processing_defaults: MaskProcessingDefaults::default(),
operations: Vec::new(),
}
}
struct BoundingBoxVectorizer;
impl MaskVectorizer for BoundingBoxVectorizer {
type Options = ();
type Output = Option<BoundingBox>;
fn vectorize(
&self,
mask: &GrayImage,
_options: &Self::Options,
) -> OutlineResult<Self::Output> {
Ok(mask_bounding_box(mask, 1))
}
}
#[test]
fn matte_handle_dimensions_reports_current_canvas() {
let cropped = single_pixel_matte_handle()
.crop(BoundingBox::new(1, 1, 2, 3))
.expect("bounds are inside the image");
assert_eq!(cropped.dimensions(), (2, 3));
let padded = cropped.pad(Padding::new(1, 2, 3, 4));
assert_eq!(padded.dimensions(), (6, 9));
}
#[test]
fn matte_handle_raw_accessors_return_original_matte() {
let handle = single_pixel_matte_handle().dilate_with(1.0);
assert_eq!(handle.as_raw_matte().dimensions(), (5, 5));
assert_eq!(handle.as_raw_matte().get_pixel(2, 2).0[0], 255);
assert_eq!(handle.as_raw_matte().get_pixel(1, 2).0[0], 0);
let cloned = handle.to_raw_matte();
assert_eq!(cloned.as_raw(), handle.as_raw_matte().as_raw());
}
#[test]
fn matte_handle_erode_uses_default_radius() {
let handle = matte_handle().erode();
assert!(matches!(
handle.operations.as_slice(),
[MaskOperation::Erode { radius, border_mode }]
if (*radius - MaskProcessingDefaults::default().erosion_radius).abs() < f32::EPSILON
&& *border_mode == MaskProcessingDefaults::default().erosion_border_mode
));
}
#[test]
fn matte_handle_erode_with_uses_custom_radius() {
let handle = matte_handle().erode_with(2.5);
assert!(matches!(
handle.operations.as_slice(),
[MaskOperation::Erode { radius, border_mode }]
if (*radius - 2.5).abs() < f32::EPSILON
&& *border_mode == MaskProcessingDefaults::default().erosion_border_mode
));
}
#[test]
fn matte_handle_erode_with_border_mode_uses_custom_mode() {
let handle =
matte_handle().erode_with_border_mode(2.5, ErosionBorderMode::OutsideIsUnknown);
assert!(matches!(
handle.operations.as_slice(),
[MaskOperation::Erode { radius, border_mode }]
if (*radius - 2.5).abs() < f32::EPSILON
&& *border_mode == ErosionBorderMode::OutsideIsUnknown
));
}
#[test]
fn matte_handle_fill_holes_with_uses_custom_threshold() {
let handle = matte_handle().fill_holes_with(180);
assert!(matches!(
handle.operations.as_slice(),
[MaskOperation::FillHoles { threshold: 180 }]
));
}
#[test]
fn matte_handle_chain_and_pipeline_are_equivalent() {
let pipeline = MaskPipeline::new()
.threshold_with(128)
.dilate_with(1.0)
.erode_with(1.0);
let chained = single_pixel_matte_handle()
.threshold_with(128)
.dilate_with(1.0)
.erode_with(1.0)
.processed()
.expect("chain should process")
.into_image();
let piped = single_pixel_matte_handle()
.processed_with(&pipeline)
.expect("pipeline should process")
.into_image();
assert_eq!(chained.as_raw(), piped.as_raw());
}
#[test]
fn matte_handle_processed_without_chained_operations_is_identity() {
let source = single_pixel_matte_handle().into_image();
let handle = MatteHandle {
rgb_image: Arc::new(RgbImage::from_pixel(5, 5, Rgb([10, 20, 30]))),
raw_matte: Arc::new(source.clone()),
mask_processing_defaults: MaskProcessingDefaults::default(),
operations: Vec::new(),
};
let mask = handle
.processed()
.expect("empty processing should succeed")
.into_image();
assert_eq!(mask.as_raw(), source.as_raw());
}
#[test]
fn matte_handle_bounding_box_applies_pending_operations() {
let bounds = single_pixel_matte_handle()
.dilate_with(1.0)
.bounding_box_with(1)
.expect("expected bounding box");
assert_eq!(bounds, BoundingBox::new(1, 1, 3, 3));
}
#[test]
fn matte_handle_bounding_box_uses_non_zero_threshold() {
let bounds = single_pixel_matte_handle()
.bounding_box()
.expect("expected bounding box");
assert_eq!(bounds, BoundingBox::new(2, 2, 1, 1));
}
#[test]
fn matte_handle_pad_updates_matte_and_foreground_canvas() {
let rgb = RgbImage::from_pixel(2, 2, Rgb([10, 20, 30]));
let mut matte = GrayImage::from_pixel(2, 2, Luma([0]));
matte.put_pixel(1, 1, Luma([255]));
let padded = matte_handle_with_images(rgb, matte).pad(Padding::new(1, 2, 3, 4));
assert_eq!(padded.dimensions(), (6, 8));
let foreground = padded.foreground().expect("foreground should compose");
assert_eq!(foreground.dimensions(), (6, 8));
assert_eq!(foreground.image().get_pixel(2, 3)[3], 255);
}
#[test]
fn matte_handle_pad_applies_pending_operations_first() {
let padded = single_pixel_matte_handle()
.dilate_with(1.0)
.pad(Padding::new(1, 2, 0, 0));
assert_eq!(
mask_bounding_box(padded.as_raw_matte(), 1),
Some(BoundingBox::new(2, 3, 3, 3))
);
}
#[test]
fn matte_handle_crop_to_content_crops_matte_and_rgb_together() {
let rgb = RgbImage::from_fn(4, 4, |x, y| Rgb([x as u8, y as u8, 0]));
let mut matte = GrayImage::from_pixel(4, 4, Luma([0]));
matte.put_pixel(2, 1, Luma([255]));
matte.put_pixel(2, 2, Luma([255]));
let cropped = matte_handle_with_images(rgb, matte)
.crop_to_content()
.expect("matte has content");
assert_eq!(cropped.dimensions(), (1, 2));
let foreground = cropped.foreground().expect("foreground should compose");
assert_eq!(foreground.dimensions(), (1, 2));
assert_eq!(foreground.image().get_pixel(0, 0)[0], 2);
assert_eq!(foreground.image().get_pixel(0, 0)[1], 1);
assert_eq!(foreground.image().get_pixel(0, 0)[3], 255);
}
#[test]
fn matte_handle_crop_uses_requested_bounds() {
let rgb = RgbImage::from_fn(4, 4, |x, y| Rgb([x as u8, y as u8, 0]));
let mut matte = GrayImage::from_pixel(4, 4, Luma([0]));
matte.put_pixel(2, 1, Luma([255]));
let cropped = matte_handle_with_images(rgb, matte)
.crop(BoundingBox::new(1, 1, 2, 2))
.expect("bounds are inside the image");
assert_eq!(cropped.dimensions(), (2, 2));
assert_eq!(cropped.as_raw_matte().get_pixel(1, 0)[0], 255);
let foreground = cropped.foreground().expect("foreground should compose");
assert_eq!(foreground.image().get_pixel(1, 0)[0], 2);
assert_eq!(foreground.image().get_pixel(1, 0)[1], 1);
assert_eq!(foreground.image().get_pixel(1, 0)[3], 255);
}
#[test]
fn matte_handle_crop_rejects_invalid_bounds() {
assert!(
single_pixel_matte_handle()
.crop(BoundingBox::new(4, 4, 2, 2))
.is_none()
);
assert!(
single_pixel_matte_handle()
.crop(BoundingBox::new(0, 0, 0, 1))
.is_none()
);
}
#[test]
fn matte_handle_crop_to_content_with_ignores_low_values() {
let rgb = RgbImage::from_fn(5, 3, |x, y| Rgb([x as u8, y as u8, 0]));
let mut matte = GrayImage::from_pixel(5, 3, Luma([0]));
matte.put_pixel(1, 1, Luma([32]));
matte.put_pixel(3, 1, Luma([200]));
let cropped = matte_handle_with_images(rgb, matte)
.crop_to_content_with(128)
.expect("matte has content above threshold");
assert_eq!(cropped.dimensions(), (1, 1));
assert_eq!(cropped.as_raw_matte().get_pixel(0, 0)[0], 200);
let foreground = cropped.foreground().expect("foreground should compose");
assert_eq!(foreground.image().get_pixel(0, 0)[0], 3);
assert_eq!(foreground.image().get_pixel(0, 0)[1], 1);
assert_eq!(foreground.image().get_pixel(0, 0)[3], 200);
}
#[test]
fn matte_handle_into_image_applies_pending_operations() {
let mask = single_pixel_matte_handle().dilate_with(1.0).into_image();
assert_eq!(
mask_bounding_box(&mask, 1),
Some(BoundingBox::new(1, 1, 3, 3))
);
}
#[test]
fn matte_handle_save_applies_pending_operations() {
let temp_dir = tempfile::tempdir().expect("temp dir should be created");
let path = temp_dir.path().join("matte.png");
single_pixel_matte_handle()
.dilate_with(1.0)
.save(&path)
.expect("matte should save");
let saved = image::open(&path)
.expect("saved matte should load")
.to_luma8();
assert_eq!(
mask_bounding_box(&saved, 1),
Some(BoundingBox::new(1, 1, 3, 3))
);
}
#[test]
fn matte_handle_foreground_applies_pending_operations() {
let foreground = single_pixel_matte_handle()
.dilate_with(1.0)
.foreground()
.expect("foreground should compose");
assert_eq!(foreground.image().get_pixel(1, 2)[3], 255);
assert_eq!(foreground.image().get_pixel(0, 0)[3], 0);
}
#[test]
fn matte_handle_colorize_applies_pending_operations() {
let colorized = single_pixel_matte_handle()
.dilate_with(1.0)
.colorize([0, 180, 255, 255]);
assert_eq!(colorized.get_pixel(1, 2).0, [0, 180, 255, 255]);
assert_eq!(colorized.get_pixel(0, 0).0, [0, 180, 255, 0]);
}
#[test]
fn matte_handle_trace_applies_pending_operations() {
let bounds = single_pixel_matte_handle()
.dilate_with(1.0)
.trace(&BoundingBoxVectorizer, &())
.expect("trace should run");
assert_eq!(bounds, Some(BoundingBox::new(1, 1, 3, 3)));
}
}