Skip to main content

strand_dynamic_frame/
lib.rs

1// Copyright (C) The Strand-Braid Authors
2// SPDX-License-Identifier: MIT OR Apache-2.0
3
4//! Images from machine vision cameras used in [Strand
5//! Camera](https://strawlab.org/strand-cam).
6//!
7//! Building on the [`machine_vision_formats`] crate which provides compile-time
8//! pixel formats, this crate provides types for images whose pixel format is
9//! determined at runtime. This allows for flexibility in handling images data
10//! whose pixel format is known only dynamically, such as when reading an image
11//! from disk.
12//!
13//! There are two types here:
14//! - [DynamicFrame]: A borrowed view of an image with a dynamic pixel format.
15//! - [DynamicFrameOwned]: An owned version of `DynamicFrame` that contains its
16//!   own buffer.
17//!
18//! When compiled with the `convert-image` feature, this crate also provides
19//! conversion methods to convert the dynamic frame into a static pixel format
20//! using the [`convert_image`](https://docs.rs/convert-image) crate.
21
22#![warn(missing_docs)]
23#![cfg_attr(docsrs, feature(doc_cfg, doc_auto_cfg))]
24
25use std::borrow::Cow;
26
27use machine_vision_formats as formats;
28
29use formats::{ImageStride, PixFmt, PixelFormat, Stride, image_ref::ImageRef};
30
31#[cfg(feature = "convert-image")]
32use formats::{cow::CowImage, owned::OImage};
33
34// TODO: investigate if we can implement std::borrow::Borrow<DynamicFrame> for
35// DynamicFrameOwned. I think not due to the issues
36// [here](https://users.rust-lang.org/t/how-to-implement-borrow-for-my-own-struct/73023).
37
38#[macro_export]
39/// Macro to match all dynamic pixel formats and execute a block of code with a typed image reference.
40macro_rules! match_all_dynamic_fmts {
41    ($self:expr_2021, $x:ident, $block:expr_2021, $err:expr_2021) => {{
42        use machine_vision_formats::{
43            PixFmt,
44            pixel_format::{
45                BayerBG8, BayerBG32f, BayerGB8, BayerGB32f, BayerGR8, BayerGR32f, BayerRG8,
46                BayerRG32f, Mono8, Mono32f, NV12, RGB8, RGBA8, YUV422, YUV444,
47            },
48        };
49        match $self.pixel_format() {
50            PixFmt::Mono8 => {
51                let $x = $self.as_static::<Mono8>().unwrap();
52                $block
53            }
54            PixFmt::Mono32f => {
55                let $x = $self.as_static::<Mono32f>().unwrap();
56                $block
57            }
58            PixFmt::RGB8 => {
59                let $x = $self.as_static::<RGB8>().unwrap();
60                $block
61            }
62            PixFmt::RGBA8 => {
63                let $x = $self.as_static::<RGBA8>().unwrap();
64                $block
65            }
66            PixFmt::BayerRG8 => {
67                let $x = $self.as_static::<BayerRG8>().unwrap();
68                $block
69            }
70            PixFmt::BayerRG32f => {
71                let $x = $self.as_static::<BayerRG32f>().unwrap();
72                $block
73            }
74            PixFmt::BayerBG8 => {
75                let $x = $self.as_static::<BayerBG8>().unwrap();
76                $block
77            }
78            PixFmt::BayerBG32f => {
79                let $x = $self.as_static::<BayerBG32f>().unwrap();
80                $block
81            }
82            PixFmt::BayerGB8 => {
83                let $x = $self.as_static::<BayerGB8>().unwrap();
84                $block
85            }
86            PixFmt::BayerGB32f => {
87                let $x = $self.as_static::<BayerGB32f>().unwrap();
88                $block
89            }
90            PixFmt::BayerGR8 => {
91                let $x = $self.as_static::<BayerGR8>().unwrap();
92                $block
93            }
94            PixFmt::BayerGR32f => {
95                let $x = $self.as_static::<BayerGR32f>().unwrap();
96                $block
97            }
98            PixFmt::YUV444 => {
99                let $x = $self.as_static::<YUV444>().unwrap();
100                $block
101            }
102            PixFmt::YUV422 => {
103                let $x = $self.as_static::<YUV422>().unwrap();
104                $block
105            }
106            PixFmt::NV12 => {
107                let $x = $self.as_static::<NV12>().unwrap();
108                $block
109            }
110            _ => {
111                return Err($err);
112            }
113        }
114    }};
115}
116
117#[inline]
118const fn calc_min_stride(w: u32, pixfmt: PixFmt) -> usize {
119    w as usize * pixfmt.bits_per_pixel() as usize / 8
120}
121
122#[inline]
123const fn calc_min_buf_size(w: u32, h: u32, stride: usize, pixfmt: PixFmt) -> usize {
124    if h == 0 {
125        return 0;
126    }
127    let all_but_last = (h - 1) as usize * stride;
128    let last = calc_min_stride(w, pixfmt);
129    debug_assert!(stride >= last);
130    all_but_last + last
131}
132
133/// An image whose pixel format is determined at runtime.
134///
135/// This type is used to represent images where the pixel format is not known at
136/// compile time, allowing for flexibility in handling various image formats.
137///
138/// It can be created from raw image data and provides methods to access the
139/// image dimensions, pixel format, and raw data. It also supports conversion to
140/// static pixel formats and encoding to various formats.
141///
142/// # Type Parameters
143/// * `'a` - Lifetime of the borrowed data. If you want to own the data, use
144///   [`DynamicFrameOwned`].
145/// # Notes
146/// * This type is not `Sync` or `Send` because it contains a borrowed buffer.
147///   If you need to share it across threads, use [`DynamicFrameOwned`] instead.
148/// * The pixel format is represented by the [`PixFmt`] enum, which allows for
149///   various pixel formats like `Mono8`, `RGB8`, etc.
150/// * The buffer must be large enough to hold the image data for the specified
151///   dimensions and pixel format.
152///
153/// # Examples
154/// ```rust
155/// # use strand_dynamic_frame::DynamicFrame;
156/// # use machine_vision_formats::PixFmt;
157/// // Create a frame from raw data
158/// let data = vec![0u8; 1920 * 1080];
159/// let frame = DynamicFrame::from_buf(1920, 1080, 1920, data, PixFmt::Mono8).unwrap();
160///
161/// // Check the pixel format
162/// assert_eq!(frame.pixel_format(), PixFmt::Mono8);
163///
164/// // Get dimensions
165/// println!("Size: {}x{}", frame.width(), frame.height());
166/// ```
167#[derive(Clone)]
168pub struct DynamicFrame<'a> {
169    width: u32,
170    height: u32,
171    stride: usize,
172    buf: Cow<'a, [u8]>,
173    pixfmt: PixFmt,
174}
175
176/// An owned version of [`DynamicFrame`] that contains its own buffer.
177#[derive(Clone)]
178pub struct DynamicFrameOwned {
179    width: u32,
180    height: u32,
181    stride: usize,
182    pixfmt: PixFmt,
183    buf: Vec<u8>,
184}
185
186/// A copy-on-write dynamic frame that can be either borrowed or owned.
187pub enum CowDynamicFrame<'a> {
188    /// A borrowed dynamic frame.
189    Borrowed(DynamicFrame<'a>),
190    /// An owned dynamic frame.
191    Owned(DynamicFrameOwned),
192}
193
194impl CowDynamicFrame<'_> {
195    /// Return a borrowed view of this frame as a [`DynamicFrame`].
196    #[must_use]
197    pub fn borrow(&self) -> DynamicFrame<'_> {
198        match self {
199            CowDynamicFrame::Borrowed(f) => f.clone(),
200            CowDynamicFrame::Owned(f) => f.borrow(),
201        }
202    }
203}
204
205impl std::fmt::Debug for DynamicFrameOwned {
206    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::result::Result<(), std::fmt::Error> {
207        f.debug_struct("DynamicFrameOwned")
208            .field("width", &self.width)
209            .field("height", &self.height)
210            .field("stride", &self.stride)
211            .field("pixfmt", &self.pixfmt)
212            .finish_non_exhaustive()
213    }
214}
215
216impl Stride for DynamicFrameOwned {
217    fn stride(&self) -> usize {
218        self.stride
219    }
220}
221
222impl DynamicFrameOwned {
223    /// Return a new [`DynamicFrameOwned`] from a statically typed frame. This
224    /// moves the input data.
225    pub fn from_static<FRAME, FMT>(frame: FRAME) -> Self
226    where
227        FRAME: ImageStride<FMT> + Into<Vec<u8>>,
228        FMT: PixelFormat,
229    {
230        let pixfmt = formats::pixel_format::pixfmt::<FMT>().unwrap();
231        let width = frame.width();
232        let height = frame.height();
233        let stride = frame.stride();
234        let min_size = calc_min_buf_size(width, height, stride, pixfmt);
235        let mut buf: Vec<u8> = frame.into();
236        buf.truncate(min_size);
237        Self {
238            width,
239            height,
240            stride,
241            pixfmt,
242            buf,
243        }
244    }
245
246    /// Return a new [`DynamicFrameOwned`] from a reference to a statically
247    /// typed frame. This copies the input data.
248    pub fn from_static_ref<FMT: PixelFormat>(frame: &dyn ImageStride<FMT>) -> Self {
249        let pixfmt = formats::pixel_format::pixfmt::<FMT>().unwrap();
250        let image_data = frame.image_data();
251        let min_size = calc_min_buf_size(frame.width(), frame.height(), frame.stride(), pixfmt);
252        Self {
253            width: frame.width(),
254            height: frame.height(),
255            stride: frame.stride(),
256            buf: image_data[..min_size].to_vec(),
257            pixfmt,
258        }
259    }
260
261    /// Creates a new [`DynamicFrameOwned`] from raw image data.
262    ///
263    /// This function moves the provided buffer into the new frame without
264    /// copying. The buffer size must be appropriate for the given dimensions
265    /// and pixel format.
266    ///
267    /// # Parameters
268    /// * `w` - Image width in pixels
269    /// * `h` - Image height in pixels
270    /// * `s` - Row stride in bytes (must be >= width * `bytes_per_pixel`)
271    /// * `buf` - Raw image data buffer
272    /// * `pixfmt` - Pixel format of the image data
273    ///
274    /// # Returns
275    /// * `Some(DynamicFrameOwned)` if the buffer is valid for the given parameters
276    /// * `None` if the buffer is too small.
277    #[must_use]
278    pub fn from_buf(w: u32, h: u32, stride: usize, buf: Vec<u8>, pixfmt: PixFmt) -> Option<Self> {
279        let min_size = calc_min_buf_size(w, h, stride, pixfmt);
280        if buf.len() < min_size {
281            return None; // Buffer too small
282        }
283        Some(Self {
284            width: w,
285            height: h,
286            stride,
287            buf,
288            pixfmt,
289        })
290    }
291
292    /// Return a borrowed view of this frame as a [`DynamicFrame`].
293    #[must_use]
294    pub fn borrow(&self) -> DynamicFrame<'_> {
295        DynamicFrame {
296            width: self.width,
297            height: self.height,
298            stride: self.stride,
299            buf: Cow::Borrowed(&self.buf),
300            pixfmt: self.pixfmt,
301        }
302    }
303
304    // /// Return a mutable borrowed view of this frame as a [`DynamicFrame`].
305    // pub fn borrow_mut(&mut self) -> DynamicFrame<'_> {
306    //     DynamicFrame {
307    //         width: self.width,
308    //         height: self.height,
309    //         stride: self.stride,
310    //         buf: Cow::Borrowed(&self.buf),
311    //         pixfmt: self.pixfmt,
312    //     }
313    // }
314
315    /// Moves data into a new [`DynamicFrameOwned`] containing a region of
316    /// interest (ROI) within the image without copying.
317    ///
318    /// The ROI is defined by the specified left, top, width, and height
319    /// parameters. If the specified ROI is out of bounds or the buffer is too
320    /// small, this method returns `None`.
321    ///
322    /// # Parameters
323    /// * `left` - The left coordinate of the ROI in pixels
324    /// * `top` - The top coordinate of the ROI in pixels
325    /// * `width` - The width of the ROI in pixels
326    /// * `height` - The height of the ROI in pixels
327    ///
328    /// # Returns
329    /// * `Some(DynamicFrameOwned)` if the ROI is valid and the buffer is large
330    ///   enough
331    /// * `None` if the ROI is out of bounds or the buffer is too small
332    ///
333    /// To create a view with a ROI, use [`Self::borrow().roi()`].
334    #[must_use]
335    pub fn roi(self, left: u32, top: u32, width: u32, height: u32) -> Option<DynamicFrameOwned> {
336        if left != 0 || top != 0 {
337            todo!();
338        }
339        if left + width > self.width || top + height > self.height {
340            return None; // ROI out of bounds
341        }
342        let stride = self.stride;
343        let new_min_size = calc_min_buf_size(width, height, stride, self.pixfmt);
344        if self.buf.len() < new_min_size {
345            return None; // Buffer too small for ROI
346        }
347        Some(DynamicFrameOwned {
348            width,
349            height,
350            stride,
351            buf: self.buf,
352            pixfmt: self.pixfmt,
353        })
354    }
355
356    /// Moves the `DynamicFrameOwned` into a static pixel format.
357    ///
358    /// If the requested pixel format matches the current format, this method
359    /// returns an [`formats::owned::OImage`] that owns the data without
360    /// copying. Otherwise, it returns `None` since the data cannot be moved to
361    /// a static format.
362    ///
363    /// To convert to a static format when the target format may not match the
364    /// current format, use [`Self::into_pixel_format<FMT>()`] (requires the
365    /// `convert-image` feature).
366    ///
367    /// # Type Parameters
368    /// * `FMT` - The target pixel format type
369    ///
370    /// # Returns
371    /// * `Some(OImage<FMT>)` - If the target format matches the current format,
372    ///   returns an owned image in the specified format
373    /// * `None` - If the target format does not match the current format
374    ///
375    #[must_use]
376    pub fn as_static<FMT: PixelFormat>(self) -> Option<formats::owned::OImage<FMT>> {
377        let pixfmt = formats::pixel_format::pixfmt::<FMT>().unwrap();
378        if pixfmt == self.pixfmt {
379            // Simply return the image data as a borrowed view
380            Some(
381                formats::owned::OImage::new(self.width, self.height, self.stride, self.buf)
382                    .unwrap(),
383            )
384        } else {
385            // Cannot convert to static format
386            None
387        }
388    }
389
390    #[cfg(feature = "convert-image")]
391    /// Converts the image to the specified pixel format, returning an
392    /// [`OImage`] that owns the data.
393    ///
394    /// If the requested pixel format matches the current format, this method
395    /// moves the data without copying. Otherwise, the data is converted and a
396    /// new owned image is returned. In both cases, the original image data is
397    /// consumed.
398    ///
399    /// To move the data to a specified pixel format while excluding the
400    /// possibility of converting the format in case the requested format does
401    /// not match the current format, use [`Self::as_static<FMT>()`].
402    ///
403    /// # Type Parameters
404    /// * `FMT` - The target pixel format type
405    ///
406    /// # Returns
407    /// * `Ok(OImage<FMT>)` - If conversion is successful, returns an owned
408    ///   image in the specified format
409    /// * `Err(convert_image::Error)` - If conversion fails
410    ///
411    /// # Examples
412    /// ```rust
413    /// # use strand_dynamic_frame::DynamicFrameOwned;
414    /// # use machine_vision_formats::{PixFmt, pixel_format::Mono8};
415    /// let data = vec![64u8; 2000];
416    /// let frame = DynamicFrameOwned::from_buf(40, 50, 40, data, PixFmt::Mono8).unwrap();
417    ///
418    /// // No conversion or copying needed - returns original data
419    /// let owned_image = frame.into_pixel_format::<Mono8>().unwrap();
420    /// ```
421    pub fn into_pixel_format<FMT>(self) -> Result<OImage<FMT>, convert_image::Error>
422    where
423        FMT: PixelFormat,
424    {
425        let dest_fmt = formats::pixel_format::pixfmt::<FMT>().unwrap();
426        let self_ = self.borrow();
427        if dest_fmt == self_.pixel_format() {
428            // Fast path. Simply return the data.
429            Ok(OImage::new(self_.width(), self_.height(), self_.stride(), self.buf).unwrap())
430        } else {
431            // Conversion path. Allocate a new buffer and convert the data.
432            let width = self_.width();
433            let dest_stride = calc_min_stride(width, dest_fmt);
434            let mut dest = OImage::zeros(width, self_.height(), dest_stride).unwrap();
435            self_.into_pixel_format_dest(&mut dest)?;
436            Ok(dest)
437        }
438    }
439
440    /// Returns the width of the image in pixels.
441    ///
442    /// # Examples
443    /// ```rust
444    /// # use strand_dynamic_frame::DynamicFrame;
445    /// # use machine_vision_formats::PixFmt;
446    /// let data = vec![0u8; 1500];
447    /// let frame = DynamicFrame::from_buf(50, 10, 150, data, PixFmt::RGB8).unwrap();
448    /// assert_eq!(frame.width(), 50);
449    /// ```
450    #[must_use]
451    pub fn width(&self) -> u32 {
452        self.width
453    }
454
455    /// Returns the height of the image in pixels.
456    ///
457    /// # Examples
458    /// ```rust
459    /// # use strand_dynamic_frame::DynamicFrame;
460    /// # use machine_vision_formats::PixFmt;
461    /// let data = vec![0u8; 2000];
462    /// let frame = DynamicFrame::from_buf(40, 50, 40, data, PixFmt::Mono8).unwrap();
463    /// assert_eq!(frame.height(), 50);
464    /// ```
465    #[must_use]
466    pub fn height(&self) -> u32 {
467        self.height
468    }
469}
470
471impl<'a> DynamicFrame<'a> {
472    /// Return a new [`DynamicFrameOwned`] by copying data.
473    #[must_use]
474    pub fn copy_to_owned(&self) -> DynamicFrameOwned {
475        let pixfmt = self.pixfmt;
476        let width = self.width;
477        let height = self.height;
478        let stride = self.stride;
479        let buf = self.buf.to_vec();
480        DynamicFrameOwned {
481            width,
482            height,
483            stride,
484            pixfmt,
485            buf,
486        }
487    }
488
489    /// Return a new [`DynamicFrame`] from a reference to a statically
490    /// typed frame. This does not copy the input data.
491    pub fn from_static_ref<FMT: PixelFormat>(frame: &'a dyn ImageStride<FMT>) -> Self {
492        let pixfmt = formats::pixel_format::pixfmt::<FMT>().unwrap();
493        let image_data = frame.image_data();
494        let min_size = calc_min_buf_size(frame.width(), frame.height(), frame.stride(), pixfmt);
495        let image_data = &image_data[..min_size];
496        Self {
497            width: frame.width(),
498            height: frame.height(),
499            stride: frame.stride(),
500            buf: std::borrow::Cow::Borrowed(image_data),
501            pixfmt,
502        }
503    }
504
505    /// Creates a new [`DynamicFrame`] from raw image data.
506    ///
507    /// This function moves the provided buffer into the new frame without
508    /// copying. The buffer size must be appropriate for the given dimensions
509    /// and pixel format.
510    ///
511    /// # Parameters
512    /// * `w` - Image width in pixels
513    /// * `h` - Image height in pixels
514    /// * `s` - Row stride in bytes (must be >= width * `bytes_per_pixel`)
515    /// * `buf` - Raw image data buffer
516    /// * `pixfmt` - Pixel format of the image data
517    ///
518    /// # Returns
519    /// * `Some(DynamicFrame)` if the buffer is valid for the given parameters
520    /// * `None` if the buffer is too small.
521    ///
522    /// # Examples
523    /// ```rust
524    /// # use strand_dynamic_frame::DynamicFrame;
525    /// # use machine_vision_formats::PixFmt;
526    /// let data = vec![128u8; 640 * 480]; // Gray image data
527    /// let frame = DynamicFrame::from_buf(640, 480, 640, data, PixFmt::Mono8);
528    /// assert!(frame.is_some());
529    /// ```
530    #[must_use]
531    pub fn from_buf(w: u32, h: u32, stride: usize, buf: Vec<u8>, pixfmt: PixFmt) -> Option<Self> {
532        let min_size = calc_min_buf_size(w, h, stride, pixfmt);
533        if buf.len() < min_size {
534            return None; // Buffer too small
535        }
536        Some(Self {
537            width: w,
538            height: h,
539            stride,
540            buf: Cow::Owned(buf),
541            pixfmt,
542        })
543    }
544
545    /// Returns the width of the image in pixels.
546    ///
547    /// # Examples
548    /// ```rust
549    /// # use strand_dynamic_frame::DynamicFrame;
550    /// # use machine_vision_formats::PixFmt;
551    /// let data = vec![0u8; 1500];
552    /// let frame = DynamicFrame::from_buf(50, 10, 150, data, PixFmt::RGB8).unwrap();
553    /// assert_eq!(frame.width(), 50);
554    /// ```
555    #[must_use]
556    pub fn width(&self) -> u32 {
557        self.width
558    }
559
560    /// Returns the height of the image in pixels.
561    ///
562    /// # Examples
563    /// ```rust
564    /// # use strand_dynamic_frame::DynamicFrame;
565    /// # use machine_vision_formats::PixFmt;
566    /// let data = vec![0u8; 2000];
567    /// let frame = DynamicFrame::from_buf(40, 50, 40, data, PixFmt::Mono8).unwrap();
568    /// assert_eq!(frame.height(), 50);
569    /// ```
570    #[must_use]
571    pub fn height(&self) -> u32 {
572        self.height
573    }
574
575    /// Returns a view of the raw image data as bytes.
576    ///
577    /// This method provides access to the underlying pixel data without any
578    /// type information about the pixel format. The returned slice contains
579    /// the raw bytes that make up the image.
580    ///
581    /// The data layout depends on the pixel format and stride. Use [`pixel_format()`](Self::pixel_format)
582    /// to determine how to interpret the bytes.
583    fn minimum_image_data_without_format(&self) -> &[u8] {
584        let min_size = calc_min_buf_size(self.width, self.height, self.stride, self.pixfmt);
585        &self.buf[..min_size]
586    }
587
588    /// Creates a new [`DynamicFrame`] from an existing frame using borrowed data.
589    ///
590    /// This function copies the image data from the source frame and creates a
591    /// new [`DynamicFrame`]. The original frame remains unchanged.
592    ///
593    /// # Type Parameters
594    /// * `FMT` - The pixel format type of the source frame
595    ///
596    /// # Parameters
597    /// * `frame` - Reference to the source frame implementing [`ImageStride`]
598    ///
599    /// # Examples
600    /// ```rust
601    /// # use strand_dynamic_frame::DynamicFrame;
602    /// # use machine_vision_formats::owned::OImage;
603    /// # use machine_vision_formats::pixel_format::Mono8;
604    /// let source = OImage::<Mono8>::new(100, 100, 100, vec![0u8; 10000]).unwrap();
605    /// let dynamic_frame = DynamicFrame::copy_from(&source);
606    /// assert_eq!(dynamic_frame.width(), 100);
607    /// ```
608    pub fn copy_from<FMT: PixelFormat>(frame: &'a dyn ImageStride<FMT>) -> Self {
609        let width = frame.width();
610        let height = frame.height();
611        let stride = frame.stride();
612        let pixfmt = formats::pixel_format::pixfmt::<FMT>().unwrap();
613        let min_size = calc_min_buf_size(width, height, stride, pixfmt);
614        let data = frame.image_data();
615        debug_assert!(
616            data.len() >= min_size,
617            "Buffer too small for image dimensions and pixel format"
618        );
619        let min_data = &data[..min_size];
620        Self {
621            width,
622            height,
623            stride,
624            buf: Cow::Borrowed(min_data),
625            pixfmt,
626        }
627    }
628
629    #[cfg(feature = "convert-image")]
630    /// Converts the image to the specified pixel format, returning a [`CowImage`] that may borrow or own the data.
631    ///
632    /// If the requested pixel format matches the current format, this method returns
633    /// a borrowed view of the data without copying. Otherwise, the data is converted
634    /// and a new owned image is returned.
635    ///
636    /// # Type Parameters
637    /// * `FMT` - The target pixel format type
638    ///
639    /// # Returns
640    /// * `Ok(CowImage<FMT>)` - Either a borrowed view or owned converted image
641    /// * `Err(convert_image::Error)` - If conversion fails
642    ///
643    /// # Examples
644    /// ```rust
645    /// # use strand_dynamic_frame::DynamicFrame;
646    /// # use machine_vision_formats::{PixFmt, pixel_format::Mono8};
647    /// let data = vec![64u8; 2000];
648    /// let frame = DynamicFrame::from_buf(20, 10, 200, data, PixFmt::Mono8).unwrap();
649    ///
650    /// // No conversion needed - returns borrowed view
651    /// let cow_image = frame.into_pixel_format::<Mono8>().unwrap();
652    /// ```
653    pub fn into_pixel_format<FMT>(&self) -> Result<CowImage<'_, FMT>, convert_image::Error>
654    where
655        FMT: PixelFormat,
656    {
657        let dest_fmt = formats::pixel_format::pixfmt::<FMT>().unwrap();
658        if dest_fmt == self.pixel_format() {
659            // Fast path. Simply return the data.
660            Ok(CowImage::Borrowed(
661                ImageRef::new(
662                    self.width(),
663                    self.height(),
664                    self.stride(),
665                    self.minimum_image_data_without_format(),
666                )
667                .unwrap(),
668            ))
669        } else {
670            // Conversion path. Allocate a new buffer and convert the data.
671            let width = self.width();
672            let dest_stride = calc_min_stride(width, dest_fmt);
673            let mut dest = OImage::zeros(width, self.height(), dest_stride).unwrap();
674            self.into_pixel_format_dest(&mut dest)?;
675            Ok(CowImage::Owned(dest))
676        }
677    }
678
679    /// Return a borrowed view of the image data as a static pixel format.
680    ///
681    /// This method allows you to treat the dynamic frame as a specific pixel format
682    /// without copying the data, as long as the pixel format matches.
683    ///
684    /// # Type Parameters
685    /// * `FMT` - The target pixel format type
686    ///
687    /// # Returns
688    /// * `Some(ImageRef<FMT>)` if the pixel format matches
689    /// * `None` if the pixel format does not match
690    ///
691    /// # Examples
692    /// ```rust
693    /// # use strand_dynamic_frame::DynamicFrame;
694    /// # use machine_vision_formats::{PixFmt, pixel_format::Mono8, image_ref::ImageRef, ImageData};
695    /// // Create a dynamic frame with Mono8 pixel format.
696    /// let data = vec![128u8; 1000];
697    /// let frame = DynamicFrame::from_buf(100, 10, 100, data, PixFmt::Mono8).unwrap();
698    ///
699    /// // Convert to a static Mono8 view
700    /// let static_view: Option<ImageRef<Mono8>> = frame.as_static();
701    /// assert!(static_view.is_some());
702    /// assert_eq!(static_view.unwrap().width(), 100);
703    /// ```
704    #[must_use]
705    pub fn as_static<FMT: PixelFormat>(&'a self) -> Option<ImageRef<'a, FMT>> {
706        let pixfmt = formats::pixel_format::pixfmt::<FMT>().unwrap();
707        if pixfmt == self.pixel_format() {
708            // Simply return the image data as a borrowed view
709            Some(
710                ImageRef::new(
711                    self.width(),
712                    self.height(),
713                    self.stride(),
714                    self.minimum_image_data_without_format(),
715                )
716                .unwrap(),
717            )
718        } else {
719            // Cannot convert to static format
720            None
721        }
722    }
723
724    /// Converts the image data into a mutable destination buffer of the
725    /// specified pixel format.
726    ///
727    /// This method will convert the data in-place, modifying the destination
728    /// buffer to match the pixel format of the source image.
729    ///
730    /// # Parameters
731    /// * `dest` - A mutable reference to the destination buffer implementing
732    ///   [`machine_vision_formats::iter::HasRowChunksExactMut`] for the target
733    ///   pixel format.
734    ///
735    /// # Returns
736    /// * `Ok(())` if the conversion was successful
737    /// * `Err(convert_image::Error)` if the conversion fails
738    ///
739    /// # Examples
740    /// ```rust
741    /// # use strand_dynamic_frame::DynamicFrame;
742    /// # use machine_vision_formats::{PixFmt, pixel_format::Mono8, iter::HasRowChunksExactMut,owned::OImage, ImageData, Stride};
743    /// // Create a dynamic frame with RGB8 pixel format.
744    /// let data = vec![255u8; 3000]; // RGB8 data for 100x10 image
745    /// let frame = DynamicFrame::from_buf(100, 10, 300, data, PixFmt::RGB8).unwrap();
746    ///
747    /// // Create a destination buffer for Mono8 format
748    /// let mut dest = OImage::<Mono8>::zeros(100, 10, 100).unwrap();
749    ///
750    /// // Convert the frame into the destination buffer
751    /// frame.into_pixel_format_dest(&mut dest).unwrap();
752    /// assert_eq!(dest.width(), 100);
753    /// assert_eq!(dest.height(), 10);
754    /// assert_eq!(dest.stride(), 100);
755    /// ```
756    #[cfg(feature = "convert-image")]
757    pub fn into_pixel_format_dest<FMT>(
758        &self,
759        dest: &mut dyn machine_vision_formats::iter::HasRowChunksExactMut<FMT>,
760    ) -> Result<(), convert_image::Error>
761    where
762        FMT: PixelFormat,
763    {
764        let pixfmt = self.pixel_format();
765        match_all_dynamic_fmts!(
766            self,
767            x,
768            convert_image::convert_into(&x, dest),
769            convert_image::Error::UnimplementedPixelFormat(pixfmt)
770        )
771    }
772
773    /// Converts the image to a byte buffer encoded in the specified format.
774    ///
775    /// This method encodes the image data into a format suitable for storage or transmission.
776    /// The encoding options can be specified using [`convert_image::EncoderOptions`].
777    ///
778    /// # Parameters
779    /// * `opts` - Encoding options for the output format
780    ///
781    /// # Returns
782    /// * `Ok(Vec<u8>)` - The encoded image data as a byte vector
783    /// * `Err(convert_image::Error)` - If the encoding fails
784    ///
785    /// # Examples
786    /// ```rust
787    /// # use strand_dynamic_frame::DynamicFrame;
788    /// # use machine_vision_formats::PixFmt;
789    /// let data = vec![255u8; 3000]; // RGB8 data for 100x10 image
790    /// let frame = DynamicFrame::from_buf(100, 10, 300, data, PixFmt::RGB8).unwrap();
791    ///
792    /// // Encode the frame to PNG bytes
793    /// let encoded_buffer = frame.to_encoded_buffer(convert_image::EncoderOptions::Png).unwrap();
794    /// assert!(!encoded_buffer.is_empty());
795    /// ```
796    #[cfg(feature = "convert-image")]
797    pub fn to_encoded_buffer(
798        &self,
799        opts: convert_image::EncoderOptions,
800    ) -> Result<Vec<u8>, convert_image::Error> {
801        let pixfmt = self.pixel_format();
802        match_all_dynamic_fmts!(
803            self,
804            x,
805            convert_image::frame_to_encoded_buffer(&x, opts),
806            convert_image::Error::UnimplementedPixelFormat(pixfmt)
807        )
808    }
809
810    /// Returns the pixel format of this image.
811    ///
812    /// # Examples
813    /// ```rust
814    /// # use strand_dynamic_frame::DynamicFrame;
815    /// # use machine_vision_formats::PixFmt;
816    /// let data = vec![0u8; 300];
817    /// let frame = DynamicFrame::from_buf(10, 10, 30, data, PixFmt::RGB8).unwrap();
818    /// assert_eq!(frame.pixel_format(), PixFmt::RGB8);
819    /// ```
820    #[must_use]
821    pub fn pixel_format(&self) -> PixFmt {
822        self.pixfmt
823    }
824
825    /// Forces the image data to be interpreted as a different pixel format without converting the data.
826    ///
827    /// Use this method with caution - the resulting image may not be valid if the buffer
828    /// size is incompatible with the new pixel format requirements.
829    ///
830    /// # Parameters
831    /// * `pixfmt` - The new pixel format to interpret the data as
832    ///
833    /// # Returns
834    /// * `Some(DynamicFrame)` if the buffer size is compatible with the new format
835    /// * `None` if the buffer is too small for the new format
836    ///
837    /// # Examples
838    /// ```rust
839    /// # use strand_dynamic_frame::DynamicFrame;
840    /// # use machine_vision_formats::PixFmt;
841    /// // Create a Mono8 image
842    /// let data = vec![128u8; 1000];
843    /// let frame = DynamicFrame::from_buf(100, 10, 100, data, PixFmt::Mono8).unwrap();
844    ///
845    /// // Force it to be interpreted as a different format (if buffer size allows)
846    /// let forced_frame = frame.force_pixel_format(PixFmt::Mono8);
847    /// assert!(forced_frame.is_some());
848    /// ```
849    #[must_use]
850    pub fn force_pixel_format(self, pixfmt: PixFmt) -> Option<DynamicFrame<'a>> {
851        let new_min_size = calc_min_buf_size(self.width, self.height, self.stride, pixfmt);
852        if self.buf.len() < new_min_size {
853            None // Buffer too small for new pixel format
854        } else {
855            Some(DynamicFrame {
856                width: self.width,
857                height: self.height,
858                stride: self.stride,
859                buf: self.buf,
860                pixfmt,
861            })
862        }
863    }
864
865    /// Returns a new [`DynamicFrame`] representing a region of interest (ROI) within the image.
866    ///
867    /// The ROI is defined by the specified left, top, width, and height parameters.
868    /// If the specified ROI is out of bounds or the buffer is too small, this method returns `None`.
869    ///
870    /// # Parameters
871    /// * `left` - The left coordinate of the ROI in pixels
872    /// * `top` - The top coordinate of the ROI in pixels
873    /// * `width` - The width of the ROI in pixels
874    /// * `height` - The height of the ROI in pixels
875    ///
876    /// # Returns
877    /// * `Some(DynamicFrame)` if the ROI is valid and the buffer is large enough
878    /// * `None` if the ROI is out of bounds or the buffer is too small
879    #[must_use]
880    pub fn roi(&'a self, left: u32, top: u32, width: u32, height: u32) -> Option<DynamicFrame<'a>> {
881        if left + width > self.width || top + height > self.height {
882            return None; // ROI out of bounds
883        }
884        if left != 0 || top != 0 {
885            todo!();
886        }
887        let stride = self.stride;
888        let new_min_size = calc_min_buf_size(width, height, stride, self.pixfmt);
889        if self.buf.len() < new_min_size {
890            return None; // Buffer too small for ROI
891        }
892        Some(DynamicFrame {
893            width,
894            height,
895            stride,
896            buf: Cow::Borrowed(&self.buf[..new_min_size]),
897            pixfmt: self.pixfmt,
898        })
899    }
900}
901
902/// Compile-time test to ensure [`DynamicFrame`] implements the [`Send`] trait.
903fn _test_dynamic_frame_is_send() {
904    fn implements<T: Send>() {}
905    implements::<DynamicFrame>();
906}
907
908impl std::fmt::Debug for DynamicFrame<'_> {
909    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::result::Result<(), std::fmt::Error> {
910        f.debug_struct("DynamicFrame")
911            .field("width", &self.width)
912            .field("height", &self.height)
913            .field("stride", &self.stride)
914            .field("pixfmt", &self.pixfmt)
915            .finish_non_exhaustive()
916    }
917}
918
919impl Stride for DynamicFrame<'_> {
920    /// Returns the stride (bytes per row) of the image.
921    ///
922    /// The stride represents the number of bytes from the start of one row
923    /// to the start of the next row. This may be larger than the minimum
924    /// required by the pixel format due to alignment requirements.
925    ///
926    /// # Examples
927    /// ```rust
928    /// # use strand_dynamic_frame::DynamicFrame;
929    /// # use machine_vision_formats::{PixFmt, Stride};
930    /// let data = vec![0u8; 1000];
931    /// let frame = DynamicFrame::from_buf(10, 10, 100, data, PixFmt::Mono8).unwrap();
932    /// assert_eq!(frame.stride(), 100);
933    /// ```
934    fn stride(&self) -> usize {
935        self.stride
936    }
937}