Skip to main content

scirs2_vision/prompt_segmentation/
types.rs

1//! Core types for SAM-style prompt-based segmentation.
2
3use scirs2_core::ndarray::Array2;
4
5// ---------------------------------------------------------------------------
6// Prompt types
7// ---------------------------------------------------------------------------
8
9/// The kind of prompt that drives mask prediction.
10#[derive(Debug, Clone)]
11#[non_exhaustive]
12pub enum PromptType {
13    /// A single point with foreground/background label.
14    Point {
15        /// Horizontal coordinate (column).
16        x: usize,
17        /// Vertical coordinate (row).
18        y: usize,
19        /// `true` = foreground, `false` = background.
20        is_foreground: bool,
21    },
22    /// An axis-aligned bounding box.
23    BoundingBox {
24        /// Left column.
25        x1: usize,
26        /// Top row.
27        y1: usize,
28        /// Right column (exclusive).
29        x2: usize,
30        /// Bottom row (exclusive).
31        y2: usize,
32    },
33    /// A dense mask prompt (e.g. a rough user scribble).
34    MaskPrompt {
35        /// 2-D mask whose spatial size matches the input image.
36        mask: Array2<f64>,
37    },
38    /// Multiple points with per-point foreground/background labels.
39    MultiPoint {
40        /// Each entry is `(x, y, is_foreground)`.
41        points: Vec<(usize, usize, bool)>,
42    },
43}
44
45// ---------------------------------------------------------------------------
46// Configuration
47// ---------------------------------------------------------------------------
48
49/// Configuration for the SAM-style segmentation pipeline.
50#[derive(Debug, Clone)]
51pub struct SAMConfig {
52    /// Encoder input resolution (images are conceptually rescaled to this).
53    pub image_size: usize,
54    /// Embedding dimensionality throughout the pipeline.
55    pub embed_dim: usize,
56    /// Number of candidate masks produced by the decoder.
57    pub num_mask_outputs: usize,
58    /// Hidden size of the IoU prediction head.
59    pub iou_head_hidden: usize,
60    /// Number of encoder down-sampling stages (scales).
61    pub encoder_stages: usize,
62}
63
64impl Default for SAMConfig {
65    fn default() -> Self {
66        Self {
67            image_size: 1024,
68            embed_dim: 256,
69            num_mask_outputs: 3,
70            iou_head_hidden: 256,
71            encoder_stages: 3,
72        }
73    }
74}
75
76// ---------------------------------------------------------------------------
77// Results
78// ---------------------------------------------------------------------------
79
80/// Output of the mask decoder.
81#[derive(Debug, Clone)]
82pub struct SegmentationResult {
83    /// Predicted masks, one per candidate. Each mask has the same spatial size
84    /// as the input image and contains logit values (higher = more likely
85    /// foreground).
86    pub masks: Vec<Array2<f64>>,
87    /// Per-mask IoU predictions (model confidence).
88    pub iou_predictions: Vec<f64>,
89    /// Per-mask stability scores (IoU between high-threshold and
90    /// low-threshold binarisations).
91    pub stability_scores: Vec<f64>,
92}
93
94// ---------------------------------------------------------------------------
95// Prompt wrapper
96// ---------------------------------------------------------------------------
97
98/// A user-supplied segmentation prompt together with an optional label.
99#[derive(Debug, Clone)]
100pub struct SegmentationPrompt {
101    /// The prompt geometry.
102    pub prompt_type: PromptType,
103    /// Optional human-readable label for the prompt.
104    pub label: Option<String>,
105}
106
107impl SegmentationPrompt {
108    /// Create a new prompt without a label.
109    pub fn new(prompt_type: PromptType) -> Self {
110        Self {
111            prompt_type,
112            label: None,
113        }
114    }
115
116    /// Create a new prompt with a label.
117    pub fn with_label(prompt_type: PromptType, label: impl Into<String>) -> Self {
118        Self {
119            prompt_type,
120            label: Some(label.into()),
121        }
122    }
123}
124
125// ---------------------------------------------------------------------------
126// Tests
127// ---------------------------------------------------------------------------
128
129#[cfg(test)]
130mod tests {
131    use super::*;
132    use scirs2_core::ndarray::Array2;
133
134    #[test]
135    fn test_sam_config_default() {
136        let cfg = SAMConfig::default();
137        assert_eq!(cfg.image_size, 1024);
138        assert_eq!(cfg.embed_dim, 256);
139        assert_eq!(cfg.num_mask_outputs, 3);
140        assert_eq!(cfg.iou_head_hidden, 256);
141        assert_eq!(cfg.encoder_stages, 3);
142    }
143
144    #[test]
145    fn test_prompt_type_point() {
146        let p = PromptType::Point {
147            x: 10,
148            y: 20,
149            is_foreground: true,
150        };
151        if let PromptType::Point {
152            x,
153            y,
154            is_foreground,
155        } = &p
156        {
157            assert_eq!(*x, 10);
158            assert_eq!(*y, 20);
159            assert!(*is_foreground);
160        } else {
161            panic!("expected Point variant");
162        }
163    }
164
165    #[test]
166    fn test_prompt_type_bounding_box() {
167        let p = PromptType::BoundingBox {
168            x1: 5,
169            y1: 10,
170            x2: 50,
171            y2: 60,
172        };
173        if let PromptType::BoundingBox { x1, y1, x2, y2 } = &p {
174            assert_eq!(*x1, 5);
175            assert_eq!(*y1, 10);
176            assert_eq!(*x2, 50);
177            assert_eq!(*y2, 60);
178        } else {
179            panic!("expected BoundingBox variant");
180        }
181    }
182
183    #[test]
184    fn test_prompt_type_mask() {
185        let mask = Array2::<f64>::zeros((64, 64));
186        let p = PromptType::MaskPrompt { mask: mask.clone() };
187        if let PromptType::MaskPrompt { mask: m } = &p {
188            assert_eq!(m.dim(), (64, 64));
189        } else {
190            panic!("expected MaskPrompt variant");
191        }
192    }
193
194    #[test]
195    fn test_prompt_type_multipoint() {
196        let pts = vec![(1, 2, true), (3, 4, false)];
197        let p = PromptType::MultiPoint {
198            points: pts.clone(),
199        };
200        if let PromptType::MultiPoint { points } = &p {
201            assert_eq!(points.len(), 2);
202        } else {
203            panic!("expected MultiPoint variant");
204        }
205    }
206
207    #[test]
208    fn test_segmentation_prompt_new() {
209        let sp = SegmentationPrompt::new(PromptType::Point {
210            x: 0,
211            y: 0,
212            is_foreground: true,
213        });
214        assert!(sp.label.is_none());
215    }
216
217    #[test]
218    fn test_segmentation_prompt_with_label() {
219        let sp = SegmentationPrompt::with_label(
220            PromptType::BoundingBox {
221                x1: 0,
222                y1: 0,
223                x2: 10,
224                y2: 10,
225            },
226            "cat",
227        );
228        assert_eq!(sp.label.as_deref(), Some("cat"));
229    }
230}