Skip to main content

rusty_esp_image_core/
source.rs

1//! The capture seam.
2//!
3//! An [`ImageSource`] writes one frame **into memory the caller owns** and
4//! returns a validated view of it. A DVP engine, a MIPI-CSI engine, a file on
5//! the host and the [`TestPattern`] below all fit the same two methods, so
6//! everything downstream (encoders, packetizers, the mesh) is written once.
7
8use rusty_esp_core::error::{Error, Result};
9use rusty_esp_core::frame::{Frame, Geometry, PixelFormat};
10use rusty_esp_core::time::Micros;
11
12/// Something that produces frames.
13pub trait ImageSource {
14    /// The geometry every frame from this source has right now.
15    fn geometry(&self) -> Geometry;
16
17    /// Capture one frame into `out`. The returned view borrows `out`; for a
18    /// JPEG source only the coded bytes are meaningful and `out` may be
19    /// larger than the frame.
20    fn grab<'b>(&mut self, out: &'b mut [u8]) -> Result<Frame<'b>>;
21}
22
23/// A deterministic synthetic source: SMPTE-style colour bars with a moving
24/// marker, in Gray8, RGB565 or RGB888. It exists so a pipeline can be
25/// exercised on the host, and so a firmware can prove its wiring before a
26/// sensor is attached. Frame `n` is identical on every platform.
27#[derive(Debug, Clone)]
28pub struct TestPattern {
29    geometry: Geometry,
30    frame_micros: u64,
31    sequence: u32,
32    timestamp: Micros,
33}
34
35impl TestPattern {
36    /// A pattern source at `fps` frames per second (the timestamps advance
37    /// by `1_000_000 / fps` microseconds per frame). Only uncompressed packed
38    /// formats are supported.
39    pub fn new(geometry: Geometry, fps: u32) -> Result<Self> {
40        if fps == 0 {
41            return Err(Error::InvalidGeometry);
42        }
43        match geometry.format {
44            PixelFormat::Gray8 | PixelFormat::Rgb565 | PixelFormat::Rgb888 => Ok(TestPattern {
45                geometry,
46                frame_micros: 1_000_000 / u64::from(fps),
47                sequence: 0,
48                timestamp: Micros::ZERO,
49            }),
50            _ => Err(Error::Unsupported),
51        }
52    }
53
54    /// The seven bars, as RGB888.
55    const BARS: [[u8; 3]; 7] = [
56        [0xC0, 0xC0, 0xC0], // white
57        [0xC0, 0xC0, 0x00], // yellow
58        [0x00, 0xC0, 0xC0], // cyan
59        [0x00, 0xC0, 0x00], // green
60        [0xC0, 0x00, 0xC0], // magenta
61        [0xC0, 0x00, 0x00], // red
62        [0x00, 0x00, 0xC0], // blue
63    ];
64
65    fn colour_at(&self, x: u32, y: u32) -> [u8; 3] {
66        let w = self.geometry.width;
67        let h = self.geometry.height;
68        // A marker row sweeps down one pixel per frame so consecutive frames differ.
69        let marker_y = self.sequence % h.max(1);
70        if y == marker_y {
71            return [0xFF, 0xFF, 0xFF];
72        }
73        let bar = ((u64::from(x) * 7) / u64::from(w.max(1))) as usize;
74        Self::BARS[bar.min(6)]
75    }
76}
77
78impl ImageSource for TestPattern {
79    fn geometry(&self) -> Geometry {
80        self.geometry
81    }
82
83    fn grab<'b>(&mut self, out: &'b mut [u8]) -> Result<Frame<'b>> {
84        let needed = self.geometry.byte_len().ok_or(Error::Unsupported)?;
85        if out.len() < needed {
86            return Err(Error::BufferTooSmall { needed });
87        }
88        let (w, h) = (self.geometry.width, self.geometry.height);
89        let mut i = 0usize;
90        for y in 0..h {
91            for x in 0..w {
92                let [r, g, b] = self.colour_at(x, y);
93                match self.geometry.format {
94                    PixelFormat::Gray8 => {
95                        // BT.601 luma, integer.
96                        out[i] = ((77 * u32::from(r) + 150 * u32::from(g) + 29 * u32::from(b)) >> 8)
97                            as u8;
98                        i += 1;
99                    }
100                    PixelFormat::Rgb565 => {
101                        let p = crate::ops::pack_rgb565(r, g, b);
102                        out[i..i + 2].copy_from_slice(&p.to_le_bytes());
103                        i += 2;
104                    }
105                    PixelFormat::Rgb888 => {
106                        out[i..i + 3].copy_from_slice(&[r, g, b]);
107                        i += 3;
108                    }
109                    _ => return Err(Error::Unsupported),
110                }
111            }
112        }
113        let frame = Frame::packed(self.geometry, self.timestamp, self.sequence, &out[..needed])?;
114        self.sequence = self.sequence.wrapping_add(1);
115        self.timestamp = self.timestamp.add_micros(self.frame_micros);
116        Ok(frame)
117    }
118}
119
120#[cfg(test)]
121mod tests {
122    use super::*;
123
124    #[test]
125    fn pattern_is_deterministic_and_advances() {
126        let g = Geometry::new(14, 4, PixelFormat::Rgb888).unwrap();
127        let mut a = TestPattern::new(g, 10).unwrap();
128        let mut b = TestPattern::new(g, 10).unwrap();
129        let mut ba = [0u8; 14 * 4 * 3];
130        let mut bb = [0u8; 14 * 4 * 3];
131        let fa = a.grab(&mut ba).unwrap();
132        assert_eq!(fa.sequence, 0);
133        assert_eq!(fa.timestamp, Micros::ZERO);
134        let fb = b.grab(&mut bb).unwrap();
135        assert_eq!(fa.byte_len(), fb.byte_len());
136        assert_eq!(ba, bb);
137        // second frame: marker moved, timestamp advanced by 100 ms
138        let f2 = a.grab(&mut ba).unwrap();
139        assert_eq!(f2.sequence, 1);
140        assert_eq!(f2.timestamp.as_millis(), 100);
141        assert_ne!(ba, bb);
142        // frame 0 (still in `bb`): row 0 is the marker, row 1 starts with bar 0 (0xC0)
143        let row1 = 14 * 3;
144        assert_eq!(&bb[row1..row1 + 3], &[0xC0, 0xC0, 0xC0]);
145        assert_eq!(&bb[..3], &[0xFF, 0xFF, 0xFF], "marker row of frame 0");
146        // frame 1 (in `ba`): the marker moved to row 1
147        assert_eq!(&ba[row1..row1 + 3], &[0xFF, 0xFF, 0xFF]);
148    }
149
150    #[test]
151    fn rejects_unsupported_and_small_buffers() {
152        let g = Geometry::new(8, 8, PixelFormat::Jpeg).unwrap();
153        assert_eq!(TestPattern::new(g, 10).err(), Some(Error::Unsupported));
154        let g = Geometry::new(8, 8, PixelFormat::Gray8).unwrap();
155        assert_eq!(TestPattern::new(g, 0).err(), Some(Error::InvalidGeometry));
156        let mut p = TestPattern::new(g, 30).unwrap();
157        let mut small = [0u8; 10];
158        assert_eq!(
159            p.grab(&mut small).err(),
160            Some(Error::BufferTooSmall { needed: 64 })
161        );
162    }
163}