Skip to main content

scirs2_vision/prompt_segmentation/
mask_decoder.rs

1//! Mask decoder: fuses image features and prompt embeddings to produce
2//! segmentation masks, IoU predictions and stability scores.
3//!
4//! The decoder applies simplified cross-attention (prompt tokens attend to
5//! image features), self-attention among tokens, and then up-samples through
6//! transposed convolutions to produce masks at the original image resolution.
7
8use crate::error::{Result, VisionError};
9use scirs2_core::ndarray::Array2;
10
11use super::prompt_encoder::PromptEmbedding;
12use super::types::{SAMConfig, SegmentationResult};
13
14// ---------------------------------------------------------------------------
15// Small helpers
16// ---------------------------------------------------------------------------
17
18/// Simplified single-head attention: `softmax(Q K^T / sqrt(d)) V`.
19///
20/// * `queries` – `[n_q, d]`
21/// * `keys`    – `[n_k, d]`
22/// * `values`  – `[n_k, d_v]`
23///
24/// Returns `[n_q, d_v]`.
25fn simple_attention(
26    queries: &Array2<f64>,
27    keys: &Array2<f64>,
28    values: &Array2<f64>,
29) -> Result<Array2<f64>> {
30    let (n_q, d) = queries.dim();
31    let (n_k, kd) = keys.dim();
32    let (n_v, d_v) = values.dim();
33
34    if d != kd {
35        return Err(VisionError::DimensionMismatch(format!(
36            "attention: query dim {d} != key dim {kd}"
37        )));
38    }
39    if n_k != n_v {
40        return Err(VisionError::DimensionMismatch(format!(
41            "attention: key count {n_k} != value count {n_v}"
42        )));
43    }
44
45    let scale = 1.0 / (d as f64).sqrt();
46
47    let mut out = Array2::zeros((n_q, d_v));
48    for q in 0..n_q {
49        // Compute attention scores.
50        let mut scores = vec![0.0f64; n_k];
51        let mut max_score = f64::NEG_INFINITY;
52        for k in 0..n_k {
53            let mut dot = 0.0f64;
54            for i in 0..d {
55                dot += queries[[q, i]] * keys[[k, i]];
56            }
57            scores[k] = dot * scale;
58            if scores[k] > max_score {
59                max_score = scores[k];
60            }
61        }
62
63        // Softmax.
64        let mut sum_exp = 0.0f64;
65        for s in &mut scores {
66            *s = (*s - max_score).exp();
67            sum_exp += *s;
68        }
69        if sum_exp > 0.0 {
70            for s in &mut scores {
71                *s /= sum_exp;
72            }
73        }
74
75        // Weighted sum of values.
76        for k in 0..n_k {
77            for v in 0..d_v {
78                out[[q, v]] += scores[k] * values[[k, v]];
79            }
80        }
81    }
82    Ok(out)
83}
84
85/// Simple MLP: `ReLU(x W1 + b1) W2 + b2`.
86///
87/// Uses deterministic pseudo-random initialisation.
88fn mlp_forward(input: &Array2<f64>, hidden: usize, out_dim: usize) -> Array2<f64> {
89    let (n, in_dim) = input.dim();
90
91    // Generate pseudo-random weights.
92    let gen = |total: usize, seed_start: u64, fan_in: usize| -> Vec<f64> {
93        let std_dev = (2.0 / fan_in as f64).sqrt();
94        let mut v = Vec::with_capacity(total);
95        let mut s = seed_start;
96        for _ in 0..total {
97            s = s.wrapping_mul(6_364_136_223_846_793_005).wrapping_add(1);
98            let u = (s >> 33) as f64 / (1u64 << 31) as f64 - 1.0;
99            v.push(u * std_dev);
100        }
101        v
102    };
103
104    let w1_data = gen(in_dim * hidden, 123, in_dim);
105    let w2_data = gen(hidden * out_dim, 456, hidden);
106
107    // First layer.
108    let mut h = Array2::zeros((n, hidden));
109    for i in 0..n {
110        for j in 0..hidden {
111            let mut val = 0.0f64;
112            for k in 0..in_dim {
113                val += input[[i, k]] * w1_data[k * hidden + j];
114            }
115            h[[i, j]] = val.max(0.0); // ReLU
116        }
117    }
118
119    // Second layer.
120    let mut out = Array2::zeros((n, out_dim));
121    for i in 0..n {
122        for j in 0..out_dim {
123            let mut val = 0.0f64;
124            for k in 0..hidden {
125                val += h[[i, k]] * w2_data[k * out_dim + j];
126            }
127            out[[i, j]] = val;
128        }
129    }
130    out
131}
132
133/// Bilinear 2x up-sample a feature map stored as `[H*W, C]`.
134///
135/// Returns `(2H, 2W, Array2<f64>[4*H*W, C])`.
136fn upsample_2x(input: &Array2<f64>, h: usize, w: usize) -> Result<(usize, usize, Array2<f64>)> {
137    let (n, c) = input.dim();
138    if n != h * w {
139        return Err(VisionError::DimensionMismatch(format!(
140            "upsample: n={n} != h*w={}",
141            h * w
142        )));
143    }
144
145    let oh = h * 2;
146    let ow = w * 2;
147    let mut out = Array2::zeros((oh * ow, c));
148
149    for oy in 0..oh {
150        for ox in 0..ow {
151            // Source coordinate (fractional).
152            let sy = (oy as f64) / 2.0;
153            let sx = (ox as f64) / 2.0;
154
155            let y0 = (sy.floor() as usize).min(h.saturating_sub(1));
156            let x0 = (sx.floor() as usize).min(w.saturating_sub(1));
157            let y1 = (y0 + 1).min(h.saturating_sub(1));
158            let x1 = (x0 + 1).min(w.saturating_sub(1));
159
160            let fy = sy - y0 as f64;
161            let fx = sx - x0 as f64;
162
163            let w00 = (1.0 - fy) * (1.0 - fx);
164            let w01 = (1.0 - fy) * fx;
165            let w10 = fy * (1.0 - fx);
166            let w11 = fy * fx;
167
168            let i00 = y0 * w + x0;
169            let i01 = y0 * w + x1;
170            let i10 = y1 * w + x0;
171            let i11 = y1 * w + x1;
172            let oi = oy * ow + ox;
173
174            for ch in 0..c {
175                out[[oi, ch]] = w00 * input[[i00, ch]]
176                    + w01 * input[[i01, ch]]
177                    + w10 * input[[i10, ch]]
178                    + w11 * input[[i11, ch]];
179            }
180        }
181    }
182    Ok((oh, ow, out))
183}
184
185// ---------------------------------------------------------------------------
186// MaskDecoder
187// ---------------------------------------------------------------------------
188
189/// Decodes image features + prompt embeddings into segmentation masks.
190#[derive(Debug, Clone)]
191pub struct MaskDecoder {
192    /// Pipeline configuration.
193    config: SAMConfig,
194}
195
196impl MaskDecoder {
197    /// Create a new mask decoder.
198    pub fn new(config: &SAMConfig) -> Self {
199        Self {
200            config: config.clone(),
201        }
202    }
203
204    /// Decode masks from image features and prompt embeddings.
205    ///
206    /// # Arguments
207    ///
208    /// * `image_features`  - Encoder output `[spatial_tokens, channels]`.
209    /// * `prompt_embedding` - Output of the prompt encoder.
210    /// * `image_size`      - Original image `(height, width)`.
211    ///
212    /// # Returns
213    ///
214    /// A [`SegmentationResult`] with `num_mask_outputs` candidate masks at the
215    /// original image resolution, together with IoU predictions and stability
216    /// scores.
217    pub fn decode(
218        &self,
219        image_features: &Array2<f64>,
220        prompt_embedding: &PromptEmbedding,
221        image_size: (usize, usize),
222    ) -> Result<SegmentationResult> {
223        let (img_h, img_w) = image_size;
224        if img_h == 0 || img_w == 0 {
225            return Err(VisionError::InvalidParameter(
226                "mask_decoder: image_size must be non-zero".into(),
227            ));
228        }
229
230        let embed_dim = self.config.embed_dim;
231        let n_masks = self.config.num_mask_outputs;
232
233        // --- Prepare tokens ------------------------------------------------
234        // Combine sparse prompt tokens with learnable mask output tokens.
235        let n_sparse = prompt_embedding.sparse_embeddings.dim().0;
236        let total_tokens = n_sparse + n_masks;
237
238        let mut tokens = Array2::zeros((total_tokens, embed_dim));
239
240        // Copy sparse embeddings.
241        for i in 0..n_sparse {
242            let src_cols = prompt_embedding.sparse_embeddings.dim().1.min(embed_dim);
243            for j in 0..src_cols {
244                tokens[[i, j]] = prompt_embedding.sparse_embeddings[[i, j]];
245            }
246        }
247
248        // Initialise mask output tokens (small distinct values).
249        for m in 0..n_masks {
250            let row = n_sparse + m;
251            for j in 0..embed_dim {
252                tokens[[row, j]] = 0.01 * ((m * embed_dim + j) as f64 * 0.07).sin();
253            }
254        }
255
256        // --- Cross-attention: tokens attend to image features ---------------
257        let feat_cols = image_features.dim().1;
258        let proj_features = if feat_cols != embed_dim {
259            // Simple linear projection to embed_dim.
260            project_features(image_features, embed_dim)
261        } else {
262            image_features.clone()
263        };
264
265        let tokens = simple_attention(&tokens, &proj_features, &proj_features)?;
266
267        // --- Self-attention among tokens ------------------------------------
268        let tokens = simple_attention(&tokens, &tokens, &tokens)?;
269
270        // --- Combine with dense embeddings if present -----------------------
271        let n_dense = prompt_embedding.dense_embeddings.dim().0;
272        let combined = if n_dense > 0 {
273            // Average dense embeddings and add to each token.
274            let mut avg = vec![0.0f64; embed_dim];
275            let dense_cols = prompt_embedding.dense_embeddings.dim().1.min(embed_dim);
276            for i in 0..n_dense {
277                for (j, avg_val) in avg.iter_mut().enumerate().take(dense_cols) {
278                    *avg_val += prompt_embedding.dense_embeddings[[i, j]];
279                }
280            }
281            if n_dense > 0 {
282                for v in &mut avg {
283                    *v /= n_dense as f64;
284                }
285            }
286            let mut combined = tokens.clone();
287            for i in 0..total_tokens {
288                for j in 0..embed_dim {
289                    combined[[i, j]] += avg[j];
290                }
291            }
292            combined
293        } else {
294            tokens
295        };
296
297        // --- Extract mask tokens and run IoU head ---------------------------
298        let mask_token_start = n_sparse;
299        let mut mask_tokens = Array2::zeros((n_masks, embed_dim));
300        for m in 0..n_masks {
301            let src_row = (mask_token_start + m).min(combined.dim().0.saturating_sub(1));
302            for j in 0..embed_dim {
303                mask_tokens[[m, j]] = combined[[src_row, j]];
304            }
305        }
306
307        // IoU prediction head: MLP per mask token -> scalar.
308        let iou_hidden = self.config.iou_head_hidden;
309        let iou_raw = mlp_forward(&mask_tokens, iou_hidden, 1);
310        let iou_predictions: Vec<f64> = (0..n_masks).map(|m| sigmoid(iou_raw[[m, 0]])).collect();
311
312        // --- Generate masks at encoder resolution then up-sample ------------
313        // Compute per-mask logit map from mask tokens and image features.
314        let n_feat = proj_features.dim().0;
315        let feat_side = (n_feat as f64).sqrt().ceil() as usize;
316        let feat_h = feat_side;
317        let feat_w = if feat_h > 0 {
318            n_feat.div_ceil(feat_h)
319        } else {
320            0
321        };
322
323        let mut masks = Vec::with_capacity(n_masks);
324        let mut stability_scores = Vec::with_capacity(n_masks);
325
326        for m in 0..n_masks {
327            // Dot product between mask token and each spatial feature.
328            let mut logit_map = Array2::zeros((feat_h, feat_w));
329            for fy in 0..feat_h {
330                for fx in 0..feat_w {
331                    let fi = fy * feat_w + fx;
332                    if fi < n_feat {
333                        let mut dot = 0.0f64;
334                        for d in 0..embed_dim {
335                            dot += mask_tokens[[m, d]] * proj_features[[fi, d]];
336                        }
337                        logit_map[[fy, fx]] = dot;
338                    }
339                }
340            }
341
342            // Up-sample to original image size using iterative 2x up-sampling.
343            let full_mask = upsample_to_size(&logit_map, img_h, img_w)?;
344
345            let stab = compute_stability_score(&full_mask, 0.0, -1.0);
346            stability_scores.push(stab);
347            masks.push(full_mask);
348        }
349
350        Ok(SegmentationResult {
351            masks,
352            iou_predictions,
353            stability_scores,
354        })
355    }
356}
357
358// ---------------------------------------------------------------------------
359// Utility functions
360// ---------------------------------------------------------------------------
361
362/// Sigmoid activation.
363fn sigmoid(x: f64) -> f64 {
364    1.0 / (1.0 + (-x).exp())
365}
366
367/// Linearly project features from `in_dim` channels to `out_dim`.
368fn project_features(features: &Array2<f64>, out_dim: usize) -> Array2<f64> {
369    let (n, in_dim) = features.dim();
370    let mut out = Array2::zeros((n, out_dim));
371    let copy_dim = in_dim.min(out_dim);
372    for i in 0..n {
373        for j in 0..copy_dim {
374            out[[i, j]] = features[[i, j]];
375        }
376    }
377    out
378}
379
380/// Up-sample a 2-D map to `(target_h, target_w)` via bilinear interpolation.
381fn upsample_to_size(map: &Array2<f64>, target_h: usize, target_w: usize) -> Result<Array2<f64>> {
382    let (src_h, src_w) = map.dim();
383    if src_h == 0 || src_w == 0 {
384        return Ok(Array2::zeros((target_h, target_w)));
385    }
386
387    let mut out = Array2::zeros((target_h, target_w));
388    for ty in 0..target_h {
389        for tx in 0..target_w {
390            let sy = ty as f64 * (src_h as f64) / target_h.max(1) as f64;
391            let sx = tx as f64 * (src_w as f64) / target_w.max(1) as f64;
392
393            let y0 = (sy.floor() as usize).min(src_h.saturating_sub(1));
394            let x0 = (sx.floor() as usize).min(src_w.saturating_sub(1));
395            let y1 = (y0 + 1).min(src_h.saturating_sub(1));
396            let x1 = (x0 + 1).min(src_w.saturating_sub(1));
397
398            let fy = sy - y0 as f64;
399            let fx = sx - x0 as f64;
400
401            let val = (1.0 - fy) * (1.0 - fx) * map[[y0, x0]]
402                + (1.0 - fy) * fx * map[[y0, x1]]
403                + fy * (1.0 - fx) * map[[y1, x0]]
404                + fy * fx * map[[y1, x1]];
405
406            out[[ty, tx]] = val;
407        }
408    }
409    Ok(out)
410}
411
412/// Compute the stability score of a mask.
413///
414/// Defined as the IoU between the mask binarised at a high threshold and the
415/// mask binarised at a low threshold. A score close to 1.0 indicates a stable
416/// prediction.
417pub fn compute_stability_score(mask: &Array2<f64>, threshold_high: f64, threshold_low: f64) -> f64 {
418    let (h, w) = mask.dim();
419    let mut intersection = 0usize;
420    let mut union = 0usize;
421
422    for r in 0..h {
423        for c in 0..w {
424            let v = mask[[r, c]];
425            let in_high = v > threshold_high;
426            let in_low = v > threshold_low;
427            if in_high || in_low {
428                union += 1;
429            }
430            if in_high && in_low {
431                intersection += 1;
432            }
433        }
434    }
435
436    if union == 0 {
437        1.0
438    } else {
439        intersection as f64 / union as f64
440    }
441}
442
443// ---------------------------------------------------------------------------
444// Tests
445// ---------------------------------------------------------------------------
446
447#[cfg(test)]
448mod tests {
449    use super::*;
450    use crate::prompt_segmentation::prompt_encoder::PromptEmbedding;
451    use crate::prompt_segmentation::types::SAMConfig;
452    use scirs2_core::ndarray::Array2;
453
454    fn small_config() -> SAMConfig {
455        SAMConfig {
456            image_size: 16,
457            embed_dim: 8,
458            num_mask_outputs: 3,
459            iou_head_hidden: 8,
460            encoder_stages: 2,
461        }
462    }
463
464    #[test]
465    fn test_simple_attention_identity() {
466        let q = Array2::from_shape_vec((2, 3), vec![1.0, 0.0, 0.0, 0.0, 1.0, 0.0]).expect("shape");
467        let k = q.clone();
468        let v = q.clone();
469        let out = simple_attention(&q, &k, &v).expect("attention");
470        assert_eq!(out.dim(), (2, 3));
471    }
472
473    #[test]
474    fn test_upsample_2x() {
475        let input = Array2::from_shape_vec((4, 2), vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0])
476            .expect("shape");
477        let (oh, ow, out) = upsample_2x(&input, 2, 2).expect("upsample");
478        assert_eq!(oh, 4);
479        assert_eq!(ow, 4);
480        assert_eq!(out.dim(), (16, 2));
481    }
482
483    #[test]
484    fn test_stability_score_all_above() {
485        let mask = Array2::from_elem((4, 4), 5.0);
486        let s = compute_stability_score(&mask, 0.0, -1.0);
487        assert!((s - 1.0).abs() < 1e-9);
488    }
489
490    #[test]
491    fn test_stability_score_mixed() {
492        let mut mask = Array2::zeros((4, 4));
493        // Half above high threshold, all above low threshold.
494        for r in 0..2 {
495            for c in 0..4 {
496                mask[[r, c]] = 1.0;
497            }
498        }
499        for r in 2..4 {
500            for c in 0..4 {
501                mask[[r, c]] = -0.5;
502            }
503        }
504        let s = compute_stability_score(&mask, 0.0, -1.0);
505        // intersection = 8 (top rows above both), union = 16 (all above low)
506        assert!((s - 0.5).abs() < 1e-9);
507    }
508
509    #[test]
510    fn test_mask_decoder_smoke() {
511        let cfg = small_config();
512        let decoder = MaskDecoder::new(&cfg);
513
514        let image_features = Array2::from_elem((16, 8), 0.1);
515        let prompt_emb = PromptEmbedding {
516            sparse_embeddings: Array2::from_elem((1, 8), 0.5),
517            dense_embeddings: Array2::zeros((0, 8)),
518        };
519
520        let result = decoder
521            .decode(&image_features, &prompt_emb, (16, 16))
522            .expect("decode");
523
524        assert_eq!(result.masks.len(), 3);
525        assert_eq!(result.iou_predictions.len(), 3);
526        assert_eq!(result.stability_scores.len(), 3);
527
528        for mask in &result.masks {
529            assert_eq!(mask.dim(), (16, 16));
530        }
531        for &iou in &result.iou_predictions {
532            assert!((0.0..=1.0).contains(&iou));
533        }
534    }
535
536    #[test]
537    fn test_mask_decoder_with_dense() {
538        let cfg = small_config();
539        let decoder = MaskDecoder::new(&cfg);
540
541        let image_features = Array2::from_elem((16, 8), 0.1);
542        let prompt_emb = PromptEmbedding {
543            sparse_embeddings: Array2::zeros((0, 8)),
544            dense_embeddings: Array2::from_elem((4, 8), 0.3),
545        };
546
547        let result = decoder
548            .decode(&image_features, &prompt_emb, (8, 8))
549            .expect("decode with dense");
550
551        assert_eq!(result.masks.len(), 3);
552        for mask in &result.masks {
553            assert_eq!(mask.dim(), (8, 8));
554        }
555    }
556
557    #[test]
558    fn test_mask_decoder_zero_image_err() {
559        let cfg = small_config();
560        let decoder = MaskDecoder::new(&cfg);
561        let prompt_emb = PromptEmbedding {
562            sparse_embeddings: Array2::zeros((1, 8)),
563            dense_embeddings: Array2::zeros((0, 8)),
564        };
565        assert!(decoder
566            .decode(&Array2::zeros((4, 8)), &prompt_emb, (0, 0))
567            .is_err());
568    }
569}