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#[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 pub fn rgb_image(&self) -> &RgbImage {
61 self.rgb_image.as_ref()
62 }
63
64 pub fn raw_matte(&self) -> &GrayImage {
66 self.raw_matte.as_ref()
67 }
68
69 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#[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 pub fn to_raw_matte(&self) -> GrayImage {
135 (*self.raw_matte).clone()
136 }
137
138 pub fn as_raw_matte(&self) -> &GrayImage {
140 self.raw_matte.as_ref()
141 }
142
143 pub fn dimensions(&self) -> (u32, u32) {
145 self.raw_matte.dimensions()
146 }
147
148 #[deprecated(note = "use to_raw_matte()")]
150 pub fn raw(&self) -> GrayImage {
151 self.to_raw_matte()
152 }
153
154 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 pub fn save(&self, path: impl AsRef<Path>) -> OutlineResult<()> {
165 self.resolved_matte().save(path)?;
166 Ok(())
167 }
168
169 pub fn bounding_box(&self) -> Option<BoundingBox> {
171 self.bounding_box_with(1)
172 }
173
174 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 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 pub fn blur_with(mut self, sigma: f32) -> Self {
189 self.operations.push(MaskOperation::Blur { sigma });
190 self
191 }
192
193 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 pub fn threshold_with(mut self, value: u8) -> Self {
202 self.operations.push(MaskOperation::Threshold { value });
203 self
204 }
205
206 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 pub fn dilate_with(mut self, radius: f32) -> Self {
221 self.operations.push(MaskOperation::Dilate { radius });
222 self
223 }
224
225 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 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 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 pub fn fill_holes(self) -> Self {
269 let threshold = self.mask_processing_defaults.mask_threshold;
270 self.fill_holes_with(threshold)
271 }
272
273 pub fn fill_holes_with(mut self, threshold: u8) -> Self {
278 self.operations.push(MaskOperation::FillHoles { threshold });
279 self
280 }
281
282 pub fn keep_largest_component(self) -> Self {
287 self.keep_n_largest_components(1)
288 }
289
290 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 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 pub fn processed(self) -> OutlineResult<MaskHandle> {
322 self.process_with_pipeline(None)
323 }
324
325 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 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 pub fn colorize(&self, color: impl Into<MaskColor>) -> RgbaImage {
356 let mask = self.resolved_matte();
357 colorize_mask(&mask, color)
358 }
359
360 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 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 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 pub fn crop_to_content(self) -> Option<Self> {
410 self.crop_to_content_with(1)
411 }
412
413 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}