Skip to main content

outline/
matte.rs

1use std::borrow::Cow;
2use std::path::Path;
3use std::sync::Arc;
4
5use image::{GrayImage, RgbImage, RgbaImage};
6
7use crate::config::{ErosionBorderMode, MaskProcessingDefaults};
8use crate::foreground::{ForegroundHandle, compose_foreground};
9use crate::geometry::{
10    BoundingBox, Padding, crop_bounds_fit_image, crop_gray_image, crop_rgb_image,
11    mask_bounding_box, pad_gray_image, pad_rgb_image,
12};
13use crate::mask::{
14    MaskColor, MaskHandle, MaskOperation, MaskPipeline, apply_operations, colorize_mask,
15};
16use crate::{MaskVectorizer, OutlineResult};
17
18/// Inference result containing the original RGB image and raw matte prediction.
19///
20/// Returned by an Outline inference backend after running model inference.
21///
22/// # Example
23/// ```no_run
24/// use outline::Outline;
25///
26/// let outline = Outline::new("model.onnx");
27/// let session = outline.for_image("input.png")?;
28/// let matte = session.matte();
29///
30/// // Access the original image and raw matte directly
31/// let rgb = session.rgb_image();
32/// let raw_matte = session.raw_matte();
33///
34/// // Compose the foreground from the raw matte
35/// let foreground = matte.foreground()?;
36/// foreground.save("foreground.png")?;
37/// # Ok::<_, outline::OutlineError>(())
38/// ```
39#[derive(Debug, Clone)]
40pub struct InferencedMatte {
41    rgb_image: Arc<RgbImage>,
42    raw_matte: Arc<GrayImage>,
43    mask_processing_defaults: MaskProcessingDefaults,
44}
45
46impl InferencedMatte {
47    pub(crate) fn new(
48        rgb_image: RgbImage,
49        raw_matte: GrayImage,
50        mask_processing_defaults: MaskProcessingDefaults,
51    ) -> Self {
52        Self {
53            rgb_image: Arc::new(rgb_image),
54            raw_matte: Arc::new(raw_matte),
55            mask_processing_defaults,
56        }
57    }
58
59    /// Get a reference to the original RGB image.
60    pub fn rgb_image(&self) -> &RgbImage {
61        self.rgb_image.as_ref()
62    }
63
64    /// Get a reference to the raw grayscale matte.
65    pub fn raw_matte(&self) -> &GrayImage {
66        self.raw_matte.as_ref()
67    }
68
69    /// Begin building a mask processing pipeline from the raw matte.
70    pub fn matte(&self) -> MatteHandle {
71        MatteHandle {
72            rgb_image: Arc::clone(&self.rgb_image),
73            raw_matte: Arc::clone(&self.raw_matte),
74            mask_processing_defaults: self.mask_processing_defaults.clone(),
75            operations: Vec::new(),
76        }
77    }
78}
79
80/// Builder for chaining mask processing operations on the raw matte.
81///
82/// The raw matte is the soft, grayscale alpha prediction from the model.
83///
84/// # Example
85/// ```no_run
86/// use outline::Outline;
87///
88/// let outline = Outline::new("model.onnx");
89/// let session = outline.for_image("input.jpg")?;
90///
91/// // Chain operations and execute them
92/// let mask = session.matte()
93///     .blur_with(6.0)          // Smooth edges
94///     .threshold_with(120)     // Convert to binary
95///     .dilate_with(5.0)        // Expand slightly
96///     .processed()?;           // Execute operations
97///
98/// mask.save("mask.png")?;
99/// # Ok::<_, outline::OutlineError>(())
100/// ```
101#[derive(Debug, Clone)]
102pub struct MatteHandle {
103    rgb_image: Arc<RgbImage>,
104    raw_matte: Arc<GrayImage>,
105    mask_processing_defaults: MaskProcessingDefaults,
106    operations: Vec<MaskOperation>,
107}
108
109impl MatteHandle {
110    fn resolved_matte(&self) -> Cow<'_, GrayImage> {
111        if self.operations.is_empty() {
112            Cow::Borrowed(self.raw_matte.as_ref())
113        } else {
114            Cow::Owned(apply_operations(self.raw_matte.as_ref(), &self.operations))
115        }
116    }
117
118    fn resolve_pending_operations(mut self) -> Self {
119        if self.operations.is_empty() {
120            return self;
121        }
122
123        let operations = std::mem::take(&mut self.operations);
124        let matte = apply_operations(self.raw_matte.as_ref(), &operations);
125        Self {
126            rgb_image: self.rgb_image,
127            raw_matte: Arc::new(matte),
128            mask_processing_defaults: self.mask_processing_defaults,
129            operations: Vec::new(),
130        }
131    }
132
133    /// Clone and return the raw grayscale matte.
134    pub fn to_raw_matte(&self) -> GrayImage {
135        (*self.raw_matte).clone()
136    }
137
138    /// Get a reference to the raw grayscale matte.
139    pub fn as_raw_matte(&self) -> &GrayImage {
140        self.raw_matte.as_ref()
141    }
142
143    /// Return the current matte canvas dimensions.
144    pub fn dimensions(&self) -> (u32, u32) {
145        self.raw_matte.dimensions()
146    }
147
148    /// Get the raw grayscale matte.
149    #[deprecated(note = "use to_raw_matte()")]
150    pub fn raw(&self) -> GrayImage {
151        self.to_raw_matte()
152    }
153
154    /// Consume the handle and return the current matte as a grayscale image.
155    pub fn into_image(self) -> GrayImage {
156        if self.operations.is_empty() {
157            Arc::unwrap_or_clone(self.raw_matte)
158        } else {
159            apply_operations(self.raw_matte.as_ref(), &self.operations)
160        }
161    }
162
163    /// Save the current matte to the specified path.
164    pub fn save(&self, path: impl AsRef<Path>) -> OutlineResult<()> {
165        self.resolved_matte().save(path)?;
166        Ok(())
167    }
168
169    /// Compute the bounding box of the current matte using a non-zero threshold.
170    pub fn bounding_box(&self) -> Option<BoundingBox> {
171        self.bounding_box_with(1)
172    }
173
174    /// Compute the bounding box of the current matte at or above `threshold`.
175    pub fn bounding_box_with(&self, threshold: u8) -> Option<BoundingBox> {
176        let mask = self.resolved_matte();
177        mask_bounding_box(&mask, threshold)
178    }
179
180    /// Add a blur operation using the default sigma.
181    pub fn blur(mut self) -> Self {
182        let sigma = self.mask_processing_defaults.blur_sigma;
183        self.operations.push(MaskOperation::Blur { sigma });
184        self
185    }
186
187    /// Add a blur operation with a custom sigma.
188    pub fn blur_with(mut self, sigma: f32) -> Self {
189        self.operations.push(MaskOperation::Blur { sigma });
190        self
191    }
192
193    /// Add a threshold operation using the default mask threshold.
194    pub fn threshold(mut self) -> Self {
195        let value = self.mask_processing_defaults.mask_threshold;
196        self.operations.push(MaskOperation::Threshold { value });
197        self
198    }
199
200    /// Add a threshold operation with a custom value.
201    pub fn threshold_with(mut self, value: u8) -> Self {
202        self.operations.push(MaskOperation::Threshold { value });
203        self
204    }
205
206    /// Add a dilation operation using the default radius.
207    ///
208    /// **Note**: Dilation typically works best on binary masks. Consider calling
209    /// [`threshold`](MatteHandle::threshold) before `dilate` if working with a soft matte.
210    pub fn dilate(mut self) -> Self {
211        let radius = self.mask_processing_defaults.dilation_radius;
212        self.operations.push(MaskOperation::Dilate { radius });
213        self
214    }
215
216    /// Add a dilation operation with a custom radius.
217    ///
218    /// **Note**: Dilation typically works best on binary masks. Consider calling
219    /// [`threshold`](MatteHandle::threshold) before `dilate` if working with a soft matte.
220    pub fn dilate_with(mut self, radius: f32) -> Self {
221        self.operations.push(MaskOperation::Dilate { radius });
222        self
223    }
224
225    /// Add an erosion operation using the default radius.
226    ///
227    /// **Note**: Erosion typically works best on binary masks. Consider calling
228    /// [`threshold`](MatteHandle::threshold) before `erode` if working with a soft matte.
229    pub fn erode(mut self) -> Self {
230        let radius = self.mask_processing_defaults.erosion_radius;
231        let border_mode = self.mask_processing_defaults.erosion_border_mode;
232        self.operations.push(MaskOperation::Erode {
233            radius,
234            border_mode,
235        });
236        self
237    }
238
239    /// Add an erosion operation with a custom radius.
240    ///
241    /// **Note**: Erosion typically works best on binary masks. Consider calling
242    /// [`threshold`](MatteHandle::threshold) before `erode` if working with a soft matte.
243    pub fn erode_with(mut self, radius: f32) -> Self {
244        let border_mode = self.mask_processing_defaults.erosion_border_mode;
245        self.operations.push(MaskOperation::Erode {
246            radius,
247            border_mode,
248        });
249        self
250    }
251
252    /// Add an erosion operation with a custom radius and boundary behavior.
253    ///
254    /// **Note**: Erosion typically works best on binary masks. Consider calling
255    /// [`threshold`](MatteHandle::threshold) before `erode` if working with a soft matte.
256    pub fn erode_with_border_mode(mut self, radius: f32, border_mode: ErosionBorderMode) -> Self {
257        self.operations.push(MaskOperation::Erode {
258            radius,
259            border_mode,
260        });
261        self
262    }
263
264    /// Add a hole-filling operation to the processing pipeline.
265    ///
266    /// **Note**: Hole-filling typically works best on binary masks. Consider calling
267    /// [`threshold`](MatteHandle::threshold) before `fill_holes` if working with a soft matte.
268    pub fn fill_holes(self) -> Self {
269        let threshold = self.mask_processing_defaults.mask_threshold;
270        self.fill_holes_with(threshold)
271    }
272
273    /// Add a hole-filling operation with a custom threshold.
274    ///
275    /// **Note**: Hole-filling typically works best on binary masks. Consider calling
276    /// [`threshold`](MatteHandle::threshold) before `fill_holes` if working with a soft matte.
277    pub fn fill_holes_with(mut self, threshold: u8) -> Self {
278        self.operations.push(MaskOperation::FillHoles { threshold });
279        self
280    }
281
282    /// Keep only the largest connected foreground component.
283    ///
284    /// **Note**: Component filtering typically works best on binary masks. Consider calling
285    /// [`threshold`](MatteHandle::threshold) before `keep_largest_component` if working with a soft matte.
286    pub fn keep_largest_component(self) -> Self {
287        self.keep_n_largest_components(1)
288    }
289
290    /// Keep the largest `count` connected foreground components.
291    ///
292    /// **Note**: Component filtering typically works best on binary masks. Consider calling
293    /// [`threshold`](MatteHandle::threshold) before `keep_n_largest_components` if working with a soft matte.
294    pub fn keep_n_largest_components(mut self, count: usize) -> Self {
295        self.operations.push(MaskOperation::KeepNLargestComponents {
296            count,
297            threshold: self.mask_processing_defaults.mask_threshold,
298        });
299        self
300    }
301
302    /// Drop connected foreground components below `min_area` pixels.
303    ///
304    /// **Note**: Component filtering typically works best on binary masks. Consider calling
305    /// [`threshold`](MatteHandle::threshold) before `drop_components_below_area` if working with a soft matte.
306    ///
307    /// # Panics
308    ///
309    /// Panics if `min_area` is negative or NaN.
310    pub fn drop_components_below_area(mut self, min_area: f32) -> Self {
311        crate::mask::assert_nonnegative_area(min_area);
312        self.operations
313            .push(MaskOperation::DropComponentsBelowArea {
314                min_area,
315                threshold: self.mask_processing_defaults.mask_threshold,
316            });
317        self
318    }
319
320    /// Process the raw matte with the accumulated operations.
321    pub fn processed(self) -> OutlineResult<MaskHandle> {
322        self.process_with_pipeline(None)
323    }
324
325    /// Process the raw matte with the accumulated operations and a custom pipeline.
326    pub fn processed_with(self, pipeline: &MaskPipeline) -> OutlineResult<MaskHandle> {
327        self.process_with_pipeline(Some(pipeline))
328    }
329
330    fn process_with_pipeline(
331        mut self,
332        pipeline: Option<&MaskPipeline>,
333    ) -> OutlineResult<MaskHandle> {
334        let mut ops = std::mem::take(&mut self.operations);
335        if let Some(custom) = pipeline {
336            ops.extend_from_slice(custom.operations());
337        }
338
339        let mask = apply_operations(self.raw_matte.as_ref(), &ops);
340        Ok(MaskHandle::new(
341            Arc::clone(&self.rgb_image),
342            mask,
343            self.mask_processing_defaults,
344        ))
345    }
346
347    /// Compose the RGBA foreground image from the RGB image and the current matte.
348    pub fn foreground(&self) -> OutlineResult<ForegroundHandle> {
349        let mask = self.resolved_matte();
350        let rgba = compose_foreground(self.rgb_image.as_ref(), &mask)?;
351        Ok(ForegroundHandle::new(rgba))
352    }
353
354    /// Colorize the current matte into a flat-color RGBA image.
355    pub fn colorize(&self, color: impl Into<MaskColor>) -> RgbaImage {
356        let mask = self.resolved_matte();
357        colorize_mask(&mask, color)
358    }
359
360    /// Trace the current matte using the specified vectorizer and options.
361    pub fn trace<V>(&self, vectorizer: &V, options: &V::Options) -> OutlineResult<V::Output>
362    where
363        V: MaskVectorizer,
364    {
365        let mask = self.resolved_matte();
366        vectorizer.vectorize(&mask, options)
367    }
368
369    /// Expand the matte canvas by the given padding while keeping content at the same offset.
370    ///
371    /// Any pending matte operations are applied before padding so method call order is preserved.
372    /// The underlying RGB canvas is padded with black to keep [`foreground`](MatteHandle::foreground)
373    /// aligned with the matte.
374    pub fn pad(self, padding: impl Into<Padding>) -> Self {
375        let this = self.resolve_pending_operations();
376        let padding = padding.into();
377        let matte = Arc::new(pad_gray_image(this.raw_matte.as_ref(), padding, 0));
378        let rgb = Arc::new(pad_rgb_image(this.rgb_image.as_ref(), padding, [0, 0, 0]));
379        Self {
380            rgb_image: rgb,
381            raw_matte: matte,
382            mask_processing_defaults: this.mask_processing_defaults,
383            operations: Vec::new(),
384        }
385    }
386
387    /// Crop the matte and source RGB image to `bounds`.
388    ///
389    /// Returns `None` when `bounds` is empty or outside the current matte canvas.
390    pub fn crop(self, bounds: BoundingBox) -> Option<Self> {
391        let this = self.resolve_pending_operations();
392        if !crop_bounds_fit_image(bounds, this.raw_matte.width(), this.raw_matte.height()) {
393            return None;
394        }
395
396        let matte = Arc::new(crop_gray_image(this.raw_matte.as_ref(), bounds));
397        let rgb = Arc::new(crop_rgb_image(this.rgb_image.as_ref(), bounds));
398        Some(Self {
399            rgb_image: rgb,
400            raw_matte: matte,
401            mask_processing_defaults: this.mask_processing_defaults,
402            operations: Vec::new(),
403        })
404    }
405
406    /// Crop the matte and source RGB image to the smallest non-zero content box.
407    ///
408    /// Returns `None` when the current matte has no non-zero pixels.
409    pub fn crop_to_content(self) -> Option<Self> {
410        self.crop_to_content_with(1)
411    }
412
413    /// Crop the matte and source RGB image to the smallest content box at or above `threshold`.
414    ///
415    /// Returns `None` when the current matte has no pixels at or above `threshold`.
416    pub fn crop_to_content_with(self, threshold: u8) -> Option<Self> {
417        let this = self.resolve_pending_operations();
418        let bounds = mask_bounding_box(this.raw_matte.as_ref(), threshold)?;
419        this.crop(bounds)
420    }
421}
422
423#[cfg(test)]
424mod tests {
425    use super::*;
426    use image::{Luma, Rgb};
427
428    fn matte_handle() -> MatteHandle {
429        MatteHandle {
430            rgb_image: Arc::new(RgbImage::from_pixel(1, 1, Rgb([255, 255, 255]))),
431            raw_matte: Arc::new(GrayImage::from_pixel(1, 1, Luma([255]))),
432            mask_processing_defaults: MaskProcessingDefaults::default(),
433            operations: Vec::new(),
434        }
435    }
436
437    fn single_pixel_matte_handle() -> MatteHandle {
438        MatteHandle {
439            rgb_image: Arc::new(RgbImage::from_pixel(5, 5, Rgb([10, 20, 30]))),
440            raw_matte: Arc::new(GrayImage::from_fn(5, 5, |x, y| {
441                if x == 2 && y == 2 {
442                    Luma([255])
443                } else {
444                    Luma([0])
445                }
446            })),
447            mask_processing_defaults: MaskProcessingDefaults::default(),
448            operations: Vec::new(),
449        }
450    }
451
452    fn matte_handle_with_images(rgb_image: RgbImage, raw_matte: GrayImage) -> MatteHandle {
453        MatteHandle {
454            rgb_image: Arc::new(rgb_image),
455            raw_matte: Arc::new(raw_matte),
456            mask_processing_defaults: MaskProcessingDefaults::default(),
457            operations: Vec::new(),
458        }
459    }
460
461    struct BoundingBoxVectorizer;
462
463    impl MaskVectorizer for BoundingBoxVectorizer {
464        type Options = ();
465        type Output = Option<BoundingBox>;
466
467        fn vectorize(
468            &self,
469            mask: &GrayImage,
470            _options: &Self::Options,
471        ) -> OutlineResult<Self::Output> {
472            Ok(mask_bounding_box(mask, 1))
473        }
474    }
475
476    #[test]
477    fn matte_handle_dimensions_reports_current_canvas() {
478        let cropped = single_pixel_matte_handle()
479            .crop(BoundingBox::new(1, 1, 2, 3))
480            .expect("bounds are inside the image");
481        assert_eq!(cropped.dimensions(), (2, 3));
482
483        let padded = cropped.pad(Padding::new(1, 2, 3, 4));
484        assert_eq!(padded.dimensions(), (6, 9));
485    }
486
487    #[test]
488    fn matte_handle_raw_accessors_return_original_matte() {
489        let handle = single_pixel_matte_handle().dilate_with(1.0);
490
491        assert_eq!(handle.as_raw_matte().dimensions(), (5, 5));
492        assert_eq!(handle.as_raw_matte().get_pixel(2, 2).0[0], 255);
493        assert_eq!(handle.as_raw_matte().get_pixel(1, 2).0[0], 0);
494
495        let cloned = handle.to_raw_matte();
496        assert_eq!(cloned.as_raw(), handle.as_raw_matte().as_raw());
497    }
498
499    #[test]
500    fn matte_handle_erode_uses_default_radius() {
501        let handle = matte_handle().erode();
502        assert!(matches!(
503            handle.operations.as_slice(),
504            [MaskOperation::Erode { radius, border_mode }]
505                if (*radius - MaskProcessingDefaults::default().erosion_radius).abs() < f32::EPSILON
506                    && *border_mode == MaskProcessingDefaults::default().erosion_border_mode
507        ));
508    }
509
510    #[test]
511    fn matte_handle_erode_with_uses_custom_radius() {
512        let handle = matte_handle().erode_with(2.5);
513        assert!(matches!(
514            handle.operations.as_slice(),
515            [MaskOperation::Erode { radius, border_mode }]
516                if (*radius - 2.5).abs() < f32::EPSILON
517                    && *border_mode == MaskProcessingDefaults::default().erosion_border_mode
518        ));
519    }
520
521    #[test]
522    fn matte_handle_erode_with_border_mode_uses_custom_mode() {
523        let handle =
524            matte_handle().erode_with_border_mode(2.5, ErosionBorderMode::OutsideIsUnknown);
525        assert!(matches!(
526            handle.operations.as_slice(),
527            [MaskOperation::Erode { radius, border_mode }]
528                if (*radius - 2.5).abs() < f32::EPSILON
529                    && *border_mode == ErosionBorderMode::OutsideIsUnknown
530        ));
531    }
532
533    #[test]
534    fn matte_handle_fill_holes_with_uses_custom_threshold() {
535        let handle = matte_handle().fill_holes_with(180);
536        assert!(matches!(
537            handle.operations.as_slice(),
538            [MaskOperation::FillHoles { threshold: 180 }]
539        ));
540    }
541
542    #[test]
543    fn matte_handle_component_filtering_methods_add_operations() {
544        let handle = matte_handle()
545            .keep_largest_component()
546            .keep_n_largest_components(2)
547            .drop_components_below_area(8.0);
548
549        assert!(matches!(
550            handle.operations.as_slice(),
551            [
552                MaskOperation::KeepNLargestComponents {
553                    count: 1,
554                    threshold
555                },
556                MaskOperation::KeepNLargestComponents {
557                    count: 2,
558                    threshold: second_threshold
559                },
560                MaskOperation::DropComponentsBelowArea {
561                    min_area,
562                    threshold: area_threshold
563                }
564            ] if *threshold == MaskProcessingDefaults::default().mask_threshold
565                && *second_threshold == MaskProcessingDefaults::default().mask_threshold
566                && (*min_area - 8.0).abs() < f32::EPSILON
567                && *area_threshold == MaskProcessingDefaults::default().mask_threshold
568        ));
569    }
570
571    #[test]
572    fn matte_handle_chain_and_pipeline_are_equivalent() {
573        let pipeline = MaskPipeline::new()
574            .threshold_with(128)
575            .dilate_with(1.0)
576            .erode_with(1.0);
577
578        let chained = single_pixel_matte_handle()
579            .threshold_with(128)
580            .dilate_with(1.0)
581            .erode_with(1.0)
582            .processed()
583            .expect("chain should process")
584            .into_image();
585        let piped = single_pixel_matte_handle()
586            .processed_with(&pipeline)
587            .expect("pipeline should process")
588            .into_image();
589
590        assert_eq!(chained.as_raw(), piped.as_raw());
591    }
592
593    #[test]
594    fn matte_handle_processed_without_chained_operations_is_identity() {
595        let source = single_pixel_matte_handle().into_image();
596        let handle = MatteHandle {
597            rgb_image: Arc::new(RgbImage::from_pixel(5, 5, Rgb([10, 20, 30]))),
598            raw_matte: Arc::new(source.clone()),
599            mask_processing_defaults: MaskProcessingDefaults::default(),
600            operations: Vec::new(),
601        };
602
603        let mask = handle
604            .processed()
605            .expect("empty processing should succeed")
606            .into_image();
607
608        assert_eq!(mask.as_raw(), source.as_raw());
609    }
610
611    #[test]
612    fn matte_handle_bounding_box_applies_pending_operations() {
613        let bounds = single_pixel_matte_handle()
614            .dilate_with(1.0)
615            .bounding_box_with(1)
616            .expect("expected bounding box");
617
618        assert_eq!(bounds, BoundingBox::new(1, 1, 3, 3));
619    }
620
621    #[test]
622    fn matte_handle_bounding_box_uses_non_zero_threshold() {
623        let bounds = single_pixel_matte_handle()
624            .bounding_box()
625            .expect("expected bounding box");
626
627        assert_eq!(bounds, BoundingBox::new(2, 2, 1, 1));
628    }
629
630    #[test]
631    fn matte_handle_pad_updates_matte_and_foreground_canvas() {
632        let rgb = RgbImage::from_pixel(2, 2, Rgb([10, 20, 30]));
633        let mut matte = GrayImage::from_pixel(2, 2, Luma([0]));
634        matte.put_pixel(1, 1, Luma([255]));
635
636        let padded = matte_handle_with_images(rgb, matte).pad(Padding::new(1, 2, 3, 4));
637
638        assert_eq!(padded.dimensions(), (6, 8));
639        let foreground = padded.foreground().expect("foreground should compose");
640        assert_eq!(foreground.dimensions(), (6, 8));
641        assert_eq!(foreground.image().get_pixel(2, 3)[3], 255);
642    }
643
644    #[test]
645    fn matte_handle_pad_applies_pending_operations_first() {
646        let padded = single_pixel_matte_handle()
647            .dilate_with(1.0)
648            .pad(Padding::new(1, 2, 0, 0));
649
650        assert_eq!(
651            mask_bounding_box(padded.as_raw_matte(), 1),
652            Some(BoundingBox::new(2, 3, 3, 3))
653        );
654    }
655
656    #[test]
657    fn matte_handle_crop_to_content_crops_matte_and_rgb_together() {
658        let rgb = RgbImage::from_fn(4, 4, |x, y| Rgb([x as u8, y as u8, 0]));
659        let mut matte = GrayImage::from_pixel(4, 4, Luma([0]));
660        matte.put_pixel(2, 1, Luma([255]));
661        matte.put_pixel(2, 2, Luma([255]));
662
663        let cropped = matte_handle_with_images(rgb, matte)
664            .crop_to_content()
665            .expect("matte has content");
666
667        assert_eq!(cropped.dimensions(), (1, 2));
668        let foreground = cropped.foreground().expect("foreground should compose");
669        assert_eq!(foreground.dimensions(), (1, 2));
670        assert_eq!(foreground.image().get_pixel(0, 0)[0], 2);
671        assert_eq!(foreground.image().get_pixel(0, 0)[1], 1);
672        assert_eq!(foreground.image().get_pixel(0, 0)[3], 255);
673    }
674
675    #[test]
676    fn matte_handle_crop_uses_requested_bounds() {
677        let rgb = RgbImage::from_fn(4, 4, |x, y| Rgb([x as u8, y as u8, 0]));
678        let mut matte = GrayImage::from_pixel(4, 4, Luma([0]));
679        matte.put_pixel(2, 1, Luma([255]));
680
681        let cropped = matte_handle_with_images(rgb, matte)
682            .crop(BoundingBox::new(1, 1, 2, 2))
683            .expect("bounds are inside the image");
684
685        assert_eq!(cropped.dimensions(), (2, 2));
686        assert_eq!(cropped.as_raw_matte().get_pixel(1, 0)[0], 255);
687        let foreground = cropped.foreground().expect("foreground should compose");
688        assert_eq!(foreground.image().get_pixel(1, 0)[0], 2);
689        assert_eq!(foreground.image().get_pixel(1, 0)[1], 1);
690        assert_eq!(foreground.image().get_pixel(1, 0)[3], 255);
691    }
692
693    #[test]
694    fn matte_handle_crop_rejects_invalid_bounds() {
695        assert!(
696            single_pixel_matte_handle()
697                .crop(BoundingBox::new(4, 4, 2, 2))
698                .is_none()
699        );
700        assert!(
701            single_pixel_matte_handle()
702                .crop(BoundingBox::new(0, 0, 0, 1))
703                .is_none()
704        );
705    }
706
707    #[test]
708    fn matte_handle_crop_to_content_with_ignores_low_values() {
709        let rgb = RgbImage::from_fn(5, 3, |x, y| Rgb([x as u8, y as u8, 0]));
710        let mut matte = GrayImage::from_pixel(5, 3, Luma([0]));
711        matte.put_pixel(1, 1, Luma([32]));
712        matte.put_pixel(3, 1, Luma([200]));
713
714        let cropped = matte_handle_with_images(rgb, matte)
715            .crop_to_content_with(128)
716            .expect("matte has content above threshold");
717
718        assert_eq!(cropped.dimensions(), (1, 1));
719        assert_eq!(cropped.as_raw_matte().get_pixel(0, 0)[0], 200);
720        let foreground = cropped.foreground().expect("foreground should compose");
721        assert_eq!(foreground.image().get_pixel(0, 0)[0], 3);
722        assert_eq!(foreground.image().get_pixel(0, 0)[1], 1);
723        assert_eq!(foreground.image().get_pixel(0, 0)[3], 200);
724    }
725
726    #[test]
727    fn matte_handle_into_image_applies_pending_operations() {
728        let mask = single_pixel_matte_handle().dilate_with(1.0).into_image();
729
730        assert_eq!(
731            mask_bounding_box(&mask, 1),
732            Some(BoundingBox::new(1, 1, 3, 3))
733        );
734    }
735
736    #[test]
737    fn matte_handle_save_applies_pending_operations() {
738        let temp_dir = tempfile::tempdir().expect("temp dir should be created");
739        let path = temp_dir.path().join("matte.png");
740
741        single_pixel_matte_handle()
742            .dilate_with(1.0)
743            .save(&path)
744            .expect("matte should save");
745
746        let saved = image::open(&path)
747            .expect("saved matte should load")
748            .to_luma8();
749
750        assert_eq!(
751            mask_bounding_box(&saved, 1),
752            Some(BoundingBox::new(1, 1, 3, 3))
753        );
754    }
755
756    #[test]
757    fn matte_handle_foreground_applies_pending_operations() {
758        let foreground = single_pixel_matte_handle()
759            .dilate_with(1.0)
760            .foreground()
761            .expect("foreground should compose");
762
763        assert_eq!(foreground.image().get_pixel(1, 2)[3], 255);
764        assert_eq!(foreground.image().get_pixel(0, 0)[3], 0);
765    }
766
767    #[test]
768    fn matte_handle_colorize_applies_pending_operations() {
769        let colorized = single_pixel_matte_handle()
770            .dilate_with(1.0)
771            .colorize([0, 180, 255, 255]);
772
773        assert_eq!(colorized.get_pixel(1, 2).0, [0, 180, 255, 255]);
774        assert_eq!(colorized.get_pixel(0, 0).0, [0, 180, 255, 0]);
775    }
776
777    #[test]
778    fn matte_handle_trace_applies_pending_operations() {
779        let bounds = single_pixel_matte_handle()
780            .dilate_with(1.0)
781            .trace(&BoundingBoxVectorizer, &())
782            .expect("trace should run");
783
784        assert_eq!(bounds, Some(BoundingBox::new(1, 1, 3, 3)));
785    }
786}