genicam/
frame.rs

1//! Frame representation combining pixel data with optional chunk metadata.
2
3use std::time::SystemTime;
4
5use bytes::Bytes;
6use pfnc::PixelFormat;
7use tracing::debug;
8
9use crate::chunks::{ChunkKind, ChunkMap, ChunkValue};
10
11/// Image frame produced by the GigE Vision stream reassembler.
12#[derive(Debug, Clone)]
13pub struct Frame {
14    /// Contiguous image payload containing pixel data.
15    pub payload: Bytes,
16    /// Width of the image in pixels.
17    pub width: u32,
18    /// Height of the image in pixels.
19    pub height: u32,
20    /// Pixel format describing how to interpret the payload bytes.
21    pub pixel_format: PixelFormat,
22    /// Optional map of chunk values decoded from the GVSP trailer.
23    pub chunks: Option<ChunkMap>,
24    /// Device timestamp reported by the camera when available.
25    pub ts_dev: Option<u64>,
26    /// Host timestamp obtained by mapping the device ticks.
27    pub ts_host: Option<SystemTime>,
28}
29
30impl Frame {
31    /// Retrieve a chunk value by kind if it exists.
32    pub fn chunk(&self, kind: ChunkKind) -> Option<&ChunkValue> {
33        self.chunks.as_ref()?.get(&kind)
34    }
35
36    /// Host-reconstructed timestamp if the camera reports a device timestamp.
37    pub fn host_time(&self) -> Option<SystemTime> {
38        self.ts_host
39    }
40
41    /// Return a borrowed slice of RGB pixels when the payload is already RGB8.
42    pub fn as_rgb8(&self) -> Option<&[u8]> {
43        match self.pixel_format {
44            PixelFormat::RGB8Packed => Some(self.payload.as_ref()),
45            _ => None,
46        }
47    }
48
49    /// Convert the frame payload into an owned RGB8 buffer.
50    pub fn to_rgb8(&self) -> Result<Vec<u8>, crate::GenicamError> {
51        if let Some(rgb) = self.as_rgb8() {
52            return Ok(rgb.to_vec());
53        }
54
55        match self.pixel_format {
56            PixelFormat::Mono8 => self.mono8_to_rgb8(),
57            PixelFormat::Mono16 => self.mono16_to_rgb8(),
58            PixelFormat::BGR8Packed => self.bgr8_to_rgb8(),
59            PixelFormat::BayerRG8
60            | PixelFormat::BayerGB8
61            | PixelFormat::BayerBG8
62            | PixelFormat::BayerGR8 => self.bayer_to_rgb8(),
63            PixelFormat::RGB8Packed => unreachable!("handled by as_rgb8 fast path"),
64            PixelFormat::Unknown(_) => Err(crate::GenicamError::UnsupportedPixelFormat(
65                self.pixel_format,
66            )),
67        }
68    }
69
70    fn total_pixels(&self) -> Result<usize, crate::GenicamError> {
71        let width = usize::try_from(self.width)
72            .map_err(|_| crate::GenicamError::Parse("frame width exceeds address space".into()))?;
73        let height = usize::try_from(self.height)
74            .map_err(|_| crate::GenicamError::Parse("frame height exceeds address space".into()))?;
75        width
76            .checked_mul(height)
77            .ok_or_else(|| crate::GenicamError::Parse("frame dimensions overflow".into()))
78    }
79
80    fn expect_payload_len(&self, expected: usize, fmt: &str) -> Result<(), crate::GenicamError> {
81        if self.payload.len() != expected {
82            return Err(crate::GenicamError::Parse(format!(
83                "payload length {} does not match {} expectation {}",
84                self.payload.len(),
85                fmt,
86                expected
87            )));
88        }
89        Ok(())
90    }
91
92    fn mono8_to_rgb8(&self) -> Result<Vec<u8>, crate::GenicamError> {
93        let pixels = self.total_pixels()?;
94        self.expect_payload_len(pixels, "Mono8")?;
95        debug!(
96            width = self.width,
97            height = self.height,
98            "converting Mono8 frame to RGB8"
99        );
100        let mut out = Vec::with_capacity(pixels * 3);
101        for &value in self.payload.as_ref() {
102            out.extend_from_slice(&[value, value, value]);
103        }
104        Ok(out)
105    }
106
107    fn mono16_to_rgb8(&self) -> Result<Vec<u8>, crate::GenicamError> {
108        let pixels = self.total_pixels()?;
109        let expected = pixels
110            .checked_mul(2)
111            .ok_or_else(|| crate::GenicamError::Parse("Mono16 payload overflow".into()))?;
112        self.expect_payload_len(expected, "Mono16")?;
113        debug!(
114            width = self.width,
115            height = self.height,
116            "converting Mono16 frame to RGB8"
117        );
118        let mut out = Vec::with_capacity(pixels * 3);
119        let data = self.payload.as_ref();
120        for idx in 0..pixels {
121            let hi = data[idx * 2 + 1];
122            out.extend_from_slice(&[hi, hi, hi]);
123        }
124        Ok(out)
125    }
126
127    fn bgr8_to_rgb8(&self) -> Result<Vec<u8>, crate::GenicamError> {
128        let pixels = self.total_pixels()?;
129        let expected = pixels
130            .checked_mul(3)
131            .ok_or_else(|| crate::GenicamError::Parse("BGR8 payload overflow".into()))?;
132        self.expect_payload_len(expected, "BGR8")?;
133        debug!(
134            width = self.width,
135            height = self.height,
136            "converting BGR8 frame to RGB8"
137        );
138        let mut out = Vec::with_capacity(expected);
139        for chunk in self.payload.chunks_exact(3) {
140            out.extend_from_slice(&[chunk[2], chunk[1], chunk[0]]);
141        }
142        Ok(out)
143    }
144
145    fn bayer_to_rgb8(&self) -> Result<Vec<u8>, crate::GenicamError> {
146        let pixels = self.total_pixels()?;
147        self.expect_payload_len(pixels, "Bayer8")?;
148        let (pattern, x_offset, y_offset) =
149            self.pixel_format
150                .cfa_pattern()
151                .ok_or(crate::GenicamError::UnsupportedPixelFormat(
152                    self.pixel_format,
153                ))?;
154        debug!(
155            width = self.width,
156            height = self.height,
157            pattern,
158            x_offset,
159            y_offset,
160            "demosaicing Bayer frame"
161        );
162
163        let width = usize::try_from(self.width)
164            .map_err(|_| crate::GenicamError::Parse("frame width exceeds address space".into()))?;
165        let height = usize::try_from(self.height)
166            .map_err(|_| crate::GenicamError::Parse("frame height exceeds address space".into()))?;
167        let src = self.payload.as_ref();
168        let mut out = vec![0u8; width * height * 3];
169
170        for y in 0..height {
171            for x in 0..width {
172                let dst_idx = (y * width + x) * 3;
173                let (r, g, b) = demosaic_pixel(src, width, height, x, y, x_offset, y_offset);
174                out[dst_idx] = r;
175                out[dst_idx + 1] = g;
176                out[dst_idx + 2] = b;
177            }
178        }
179
180        Ok(out)
181    }
182}
183
184fn demosaic_pixel(
185    src: &[u8],
186    width: usize,
187    height: usize,
188    x: usize,
189    y: usize,
190    x_offset: u8,
191    y_offset: u8,
192) -> (u8, u8, u8) {
193    use core::cmp::min;
194
195    let clamp = |value: isize, upper: usize| -> usize {
196        if value < 0 {
197            0
198        } else {
199            min(value as usize, upper.saturating_sub(1))
200        }
201    };
202
203    let sample = |sx: isize, sy: isize| -> u8 {
204        let cx = clamp(sx, width);
205        let cy = clamp(sy, height);
206        src[cy * width + cx]
207    };
208
209    let x = x as isize;
210    let y = y as isize;
211    let ox = x_offset as isize;
212    let oy = y_offset as isize;
213    let mx = ((x + ox) & 1) as i32;
214    let my = ((y + oy) & 1) as i32;
215
216    match (mx, my) {
217        (0, 0) => {
218            let r = sample(x, y);
219            let g1 = sample(x + 1, y);
220            let g2 = sample(x, y + 1);
221            let b = sample(x + 1, y + 1);
222            let g = ((g1 as u16 + g2 as u16) / 2) as u8;
223            (r, g, b)
224        }
225        (1, 1) => {
226            let r = sample(x - 1, y - 1);
227            let g1 = sample(x - 1, y);
228            let g2 = sample(x, y - 1);
229            let b = sample(x, y);
230            let g = ((g1 as u16 + g2 as u16) / 2) as u8;
231            (r, g, b)
232        }
233        (1, 0) => {
234            let r = sample(x - 1, y);
235            let g = sample(x, y);
236            let b = sample(x, y + 1);
237            (r, g, b)
238        }
239        (0, 1) => {
240            let r = sample(x, y - 1);
241            let g = sample(x, y);
242            let b = sample(x + 1, y);
243            (r, g, b)
244        }
245        _ => unreachable!(),
246    }
247}
248
249#[cfg(test)]
250mod tests {
251    use super::*;
252    use bytes::Bytes;
253
254    fn frame_with_payload(
255        payload: &[u8],
256        width: u32,
257        height: u32,
258        pixel_format: PixelFormat,
259    ) -> Frame {
260        Frame {
261            payload: Bytes::copy_from_slice(payload),
262            width,
263            height,
264            pixel_format,
265            chunks: None,
266            ts_dev: None,
267            ts_host: None,
268        }
269    }
270
271    #[test]
272    fn mono8_converts_to_rgb8() {
273        let payload = [0u8, 64, 128, 255];
274        let frame = frame_with_payload(&payload, 2, 2, PixelFormat::Mono8);
275        let rgb = frame.to_rgb8().expect("mono conversion");
276        assert_eq!(rgb.len(), 12);
277        assert_eq!(&rgb[0..3], &[0, 0, 0]);
278        assert_eq!(&rgb[3..6], &[64, 64, 64]);
279        assert_eq!(&rgb[9..12], &[255, 255, 255]);
280    }
281
282    #[test]
283    fn rgb8_fast_path_borrows_payload() {
284        let payload = vec![1u8, 2, 3, 4, 5, 6];
285        let frame = frame_with_payload(&payload, 1, 2, PixelFormat::RGB8Packed);
286        assert_eq!(frame.as_rgb8().unwrap(), payload.as_slice());
287        let owned = frame.to_rgb8().expect("rgb copy");
288        assert_eq!(owned, payload);
289    }
290
291    #[test]
292    fn bayer_rg8_demosaic_basic_pattern() {
293        // Simple 4x4 Bayer pattern with distinguishable colour quadrants.
294        let payload = [
295            255, 32, 255, 32, // R G R G row
296            32, 16, 32, 240, // G B G B row
297            255, 32, 255, 32, 32, 16, 32, 240,
298        ];
299        let frame = frame_with_payload(&payload, 4, 4, PixelFormat::BayerRG8);
300        let rgb = frame.to_rgb8().expect("bayer conversion");
301        assert_eq!(rgb.len(), 4 * 4 * 3);
302        // Top-left pixel should be red dominant.
303        assert!(rgb[0] > rgb[1] && rgb[0] > rgb[2]);
304        // Bottom-right pixel should carry the blue sample.
305        let last = &rgb[rgb.len() - 3..];
306        assert_eq!(last[2], 240);
307    }
308}