Skip to main content

scirs2_vision/prompt_segmentation/
prompt_encoder.rs

1//! Prompt encoder: converts user prompts into dense/sparse embeddings.
2//!
3//! Supports point, bounding-box, mask and multi-point prompts using
4//! sinusoidal positional encoding and lightweight convolutions.
5
6use crate::error::{Result, VisionError};
7use scirs2_core::ndarray::{Array1, Array2};
8
9use super::types::{PromptType, SAMConfig, SegmentationPrompt};
10
11// ---------------------------------------------------------------------------
12// Positional encoding helpers
13// ---------------------------------------------------------------------------
14
15/// Number of frequency bands used in the sinusoidal encoding.
16const NUM_FREQUENCIES: usize = 64;
17
18/// Sinusoidal positional encoding for a 2-D coordinate normalised to `[0, 1]`.
19///
20/// Returns a vector of length `4 * NUM_FREQUENCIES` (sin+cos for each of x, y
21/// at `NUM_FREQUENCIES` frequencies).
22fn positional_encoding_2d(x_norm: f64, y_norm: f64) -> Array1<f64> {
23    let dim = 4 * NUM_FREQUENCIES;
24    let mut enc = Array1::zeros(dim);
25    for i in 0..NUM_FREQUENCIES {
26        let freq = std::f64::consts::PI * 2.0_f64.powi(i as i32);
27        enc[4 * i] = (x_norm * freq).sin();
28        enc[4 * i + 1] = (x_norm * freq).cos();
29        enc[4 * i + 2] = (y_norm * freq).sin();
30        enc[4 * i + 3] = (y_norm * freq).cos();
31    }
32    enc
33}
34
35/// Project a positional encoding vector to `embed_dim` via a simple linear
36/// layer (truncate or zero-pad).
37fn project_to_embed_dim(enc: &Array1<f64>, embed_dim: usize) -> Array1<f64> {
38    let src_len = enc.len();
39    let mut out = Array1::zeros(embed_dim);
40    let copy_len = src_len.min(embed_dim);
41    for i in 0..copy_len {
42        out[i] = enc[i];
43    }
44    out
45}
46
47// ---------------------------------------------------------------------------
48// PromptEmbedding
49// ---------------------------------------------------------------------------
50
51/// The output of the prompt encoder, split into sparse and dense components.
52#[derive(Debug, Clone)]
53pub struct PromptEmbedding {
54    /// Sparse embeddings from point / box prompts.
55    /// Shape: `[num_tokens, embed_dim]`.
56    pub sparse_embeddings: Array2<f64>,
57    /// Dense embeddings from mask prompts (or zeros if no mask prompt).
58    /// Shape: `[spatial_tokens, embed_dim]`.
59    pub dense_embeddings: Array2<f64>,
60}
61
62// ---------------------------------------------------------------------------
63// PromptEncoder
64// ---------------------------------------------------------------------------
65
66/// Encodes user-supplied prompts into embeddings that can be consumed by
67/// the mask decoder.
68#[derive(Debug, Clone)]
69pub struct PromptEncoder {
70    /// Pipeline configuration.
71    config: SAMConfig,
72    /// Learned foreground token (simulated).
73    fg_token: Array1<f64>,
74    /// Learned background token (simulated).
75    bg_token: Array1<f64>,
76}
77
78impl PromptEncoder {
79    /// Build a new prompt encoder from the given config.
80    pub fn new(config: &SAMConfig) -> Self {
81        // Initialise foreground / background tokens with small distinct values.
82        let mut fg = Array1::zeros(config.embed_dim);
83        let mut bg = Array1::zeros(config.embed_dim);
84        for i in 0..config.embed_dim {
85            fg[i] = 0.1 * ((i as f64 * 0.1).sin());
86            bg[i] = -0.1 * ((i as f64 * 0.1).cos());
87        }
88        Self {
89            config: config.clone(),
90            fg_token: fg,
91            bg_token: bg,
92        }
93    }
94
95    /// Encode a segmentation prompt.
96    ///
97    /// # Arguments
98    ///
99    /// * `prompt`     - The segmentation prompt to encode.
100    /// * `image_size` - `(height, width)` of the input image.
101    ///
102    /// # Returns
103    ///
104    /// A [`PromptEmbedding`] containing sparse and dense embeddings.
105    pub fn encode(
106        &self,
107        prompt: &SegmentationPrompt,
108        image_size: (usize, usize),
109    ) -> Result<PromptEmbedding> {
110        let (img_h, img_w) = image_size;
111        if img_h == 0 || img_w == 0 {
112            return Err(VisionError::InvalidParameter(
113                "prompt_encoder: image_size must be non-zero".into(),
114            ));
115        }
116
117        #[allow(unreachable_patterns)]
118        match &prompt.prompt_type {
119            PromptType::Point {
120                x,
121                y,
122                is_foreground,
123            } => self.encode_point(*x, *y, *is_foreground, img_h, img_w),
124            PromptType::BoundingBox { x1, y1, x2, y2 } => {
125                self.encode_box(*x1, *y1, *x2, *y2, img_h, img_w)
126            }
127            PromptType::MaskPrompt { mask } => self.encode_mask(mask, img_h, img_w),
128            PromptType::MultiPoint { points } => self.encode_multi_point(points, img_h, img_w),
129            _ => Err(VisionError::InvalidParameter(
130                "prompt_encoder: unknown prompt type variant".into(),
131            )),
132        }
133    }
134
135    // -- Point prompt -------------------------------------------------------
136
137    fn encode_point(
138        &self,
139        x: usize,
140        y: usize,
141        is_foreground: bool,
142        img_h: usize,
143        img_w: usize,
144    ) -> Result<PromptEmbedding> {
145        let x_norm = x as f64 / img_w.max(1) as f64;
146        let y_norm = y as f64 / img_h.max(1) as f64;
147
148        let pos_enc = positional_encoding_2d(x_norm, y_norm);
149        let mut token = project_to_embed_dim(&pos_enc, self.config.embed_dim);
150
151        // Add foreground / background token.
152        let label_token = if is_foreground {
153            &self.fg_token
154        } else {
155            &self.bg_token
156        };
157        for i in 0..self.config.embed_dim {
158            token[i] += label_token[i];
159        }
160
161        let sparse = Array2::from_shape_vec((1, self.config.embed_dim), token.to_vec())
162            .map_err(|e| VisionError::OperationError(format!("sparse reshape: {e}")))?;
163
164        Ok(PromptEmbedding {
165            sparse_embeddings: sparse,
166            dense_embeddings: Array2::zeros((0, self.config.embed_dim)),
167        })
168    }
169
170    // -- Bounding-box prompt ------------------------------------------------
171
172    fn encode_box(
173        &self,
174        x1: usize,
175        y1: usize,
176        x2: usize,
177        y2: usize,
178        img_h: usize,
179        img_w: usize,
180    ) -> Result<PromptEmbedding> {
181        // Encode as two corner points: top-left (foreground), bottom-right (background).
182        let tl = self.point_token(x1, y1, true, img_h, img_w);
183        let br = self.point_token(x2, y2, false, img_h, img_w);
184
185        let mut data = Vec::with_capacity(2 * self.config.embed_dim);
186        data.extend(tl.iter());
187        data.extend(br.iter());
188
189        let sparse = Array2::from_shape_vec((2, self.config.embed_dim), data)
190            .map_err(|e| VisionError::OperationError(format!("box sparse reshape: {e}")))?;
191
192        Ok(PromptEmbedding {
193            sparse_embeddings: sparse,
194            dense_embeddings: Array2::zeros((0, self.config.embed_dim)),
195        })
196    }
197
198    // -- Mask prompt --------------------------------------------------------
199
200    fn encode_mask(
201        &self,
202        mask: &Array2<f64>,
203        img_h: usize,
204        img_w: usize,
205    ) -> Result<PromptEmbedding> {
206        let (mh, mw) = mask.dim();
207        if mh == 0 || mw == 0 {
208            return Err(VisionError::InvalidParameter(
209                "prompt_encoder: mask must be non-empty".into(),
210            ));
211        }
212
213        // Down-sample mask by 4x using simple average pooling.
214        let ds_h = img_h.div_ceil(4);
215        let ds_w = img_w.div_ceil(4);
216        let num_spatial = ds_h * ds_w;
217
218        let mut dense = Array2::zeros((num_spatial, self.config.embed_dim));
219
220        for dy in 0..ds_h {
221            for dx in 0..ds_w {
222                // Average over the 4x4 source patch.
223                let sy = dy * 4;
224                let sx = dx * 4;
225                let mut sum = 0.0f64;
226                let mut count = 0usize;
227                for ky in 0..4 {
228                    for kx in 0..4 {
229                        let my = sy + ky;
230                        let mx = sx + kx;
231                        if my < mh && mx < mw {
232                            sum += mask[[my, mx]];
233                            count += 1;
234                        }
235                    }
236                }
237                let avg = if count > 0 { sum / count as f64 } else { 0.0 };
238
239                // Fill the embedding with the average value scaled by a
240                // positional encoding of the spatial location.
241                let x_norm = dx as f64 / ds_w.max(1) as f64;
242                let y_norm = dy as f64 / ds_h.max(1) as f64;
243                let pos = positional_encoding_2d(x_norm, y_norm);
244                let proj = project_to_embed_dim(&pos, self.config.embed_dim);
245                let idx = dy * ds_w + dx;
246                for c in 0..self.config.embed_dim {
247                    dense[[idx, c]] = avg * proj[c];
248                }
249            }
250        }
251
252        Ok(PromptEmbedding {
253            sparse_embeddings: Array2::zeros((0, self.config.embed_dim)),
254            dense_embeddings: dense,
255        })
256    }
257
258    // -- Multi-point prompt -------------------------------------------------
259
260    fn encode_multi_point(
261        &self,
262        points: &[(usize, usize, bool)],
263        img_h: usize,
264        img_w: usize,
265    ) -> Result<PromptEmbedding> {
266        if points.is_empty() {
267            return Err(VisionError::InvalidParameter(
268                "prompt_encoder: MultiPoint must have at least one point".into(),
269            ));
270        }
271
272        let n = points.len();
273        let mut data = Vec::with_capacity(n * self.config.embed_dim);
274        for &(x, y, is_fg) in points {
275            let tok = self.point_token(x, y, is_fg, img_h, img_w);
276            data.extend(tok.iter());
277        }
278
279        let sparse = Array2::from_shape_vec((n, self.config.embed_dim), data)
280            .map_err(|e| VisionError::OperationError(format!("multi-point reshape: {e}")))?;
281
282        Ok(PromptEmbedding {
283            sparse_embeddings: sparse,
284            dense_embeddings: Array2::zeros((0, self.config.embed_dim)),
285        })
286    }
287
288    // -- Helpers ------------------------------------------------------------
289
290    fn point_token(
291        &self,
292        x: usize,
293        y: usize,
294        is_fg: bool,
295        img_h: usize,
296        img_w: usize,
297    ) -> Array1<f64> {
298        let x_norm = x as f64 / img_w.max(1) as f64;
299        let y_norm = y as f64 / img_h.max(1) as f64;
300        let pos = positional_encoding_2d(x_norm, y_norm);
301        let mut tok = project_to_embed_dim(&pos, self.config.embed_dim);
302        let label = if is_fg {
303            &self.fg_token
304        } else {
305            &self.bg_token
306        };
307        for i in 0..self.config.embed_dim {
308            tok[i] += label[i];
309        }
310        tok
311    }
312}
313
314// ---------------------------------------------------------------------------
315// Tests
316// ---------------------------------------------------------------------------
317
318#[cfg(test)]
319mod tests {
320    use super::*;
321    use crate::prompt_segmentation::types::{PromptType, SAMConfig, SegmentationPrompt};
322    use scirs2_core::ndarray::Array2;
323
324    fn small_config() -> SAMConfig {
325        SAMConfig {
326            image_size: 32,
327            embed_dim: 16,
328            num_mask_outputs: 3,
329            iou_head_hidden: 16,
330            encoder_stages: 2,
331        }
332    }
333
334    #[test]
335    fn test_encode_point() {
336        let cfg = small_config();
337        let enc = PromptEncoder::new(&cfg);
338        let prompt = SegmentationPrompt::new(PromptType::Point {
339            x: 5,
340            y: 10,
341            is_foreground: true,
342        });
343        let emb = enc.encode(&prompt, (32, 32)).expect("encode point");
344        assert_eq!(emb.sparse_embeddings.dim(), (1, 16));
345        assert_eq!(emb.dense_embeddings.dim().0, 0);
346    }
347
348    #[test]
349    fn test_encode_box() {
350        let cfg = small_config();
351        let enc = PromptEncoder::new(&cfg);
352        let prompt = SegmentationPrompt::new(PromptType::BoundingBox {
353            x1: 2,
354            y1: 3,
355            x2: 20,
356            y2: 25,
357        });
358        let emb = enc.encode(&prompt, (32, 32)).expect("encode box");
359        // Box produces 2 tokens (top-left, bottom-right).
360        assert_eq!(emb.sparse_embeddings.dim(), (2, 16));
361    }
362
363    #[test]
364    fn test_encode_mask() {
365        let cfg = small_config();
366        let enc = PromptEncoder::new(&cfg);
367        let mask = Array2::from_elem((32, 32), 1.0);
368        let prompt = SegmentationPrompt::new(PromptType::MaskPrompt { mask });
369        let emb = enc.encode(&prompt, (32, 32)).expect("encode mask");
370        // Dense embeddings should have (32/4)*(32/4) = 64 spatial tokens.
371        assert_eq!(emb.dense_embeddings.dim(), (64, 16));
372        assert_eq!(emb.sparse_embeddings.dim().0, 0);
373    }
374
375    #[test]
376    fn test_encode_multipoint() {
377        let cfg = small_config();
378        let enc = PromptEncoder::new(&cfg);
379        let pts = vec![(1, 2, true), (10, 15, false), (5, 5, true)];
380        let prompt = SegmentationPrompt::new(PromptType::MultiPoint { points: pts });
381        let emb = enc.encode(&prompt, (32, 32)).expect("encode multipoint");
382        assert_eq!(emb.sparse_embeddings.dim(), (3, 16));
383    }
384
385    #[test]
386    fn test_encode_empty_multipoint_err() {
387        let cfg = small_config();
388        let enc = PromptEncoder::new(&cfg);
389        let prompt = SegmentationPrompt::new(PromptType::MultiPoint { points: vec![] });
390        assert!(enc.encode(&prompt, (32, 32)).is_err());
391    }
392
393    #[test]
394    fn test_encode_zero_image_size_err() {
395        let cfg = small_config();
396        let enc = PromptEncoder::new(&cfg);
397        let prompt = SegmentationPrompt::new(PromptType::Point {
398            x: 0,
399            y: 0,
400            is_foreground: true,
401        });
402        assert!(enc.encode(&prompt, (0, 0)).is_err());
403    }
404}