feagi_data_structures/data/
image_descriptors.rs

1//! Vision processing descriptors and parameter structures for FEAGI.
2//!
3//! This module provides data structures and enums for describing image properties
4
5use std::cmp;
6use std::fmt::Display;
7use std::ops::RangeInclusive;
8use crate::FeagiDataError;
9use crate::basic_components::{CartesianResolution, FlatCoordinateU32};
10use crate::data::{ImageFrame, SegmentedImageFrame};
11
12//region Image XY
13
14/// Represents a coordinate on an image. +x goes tot he right, +y goes downward. (0,0) is in the top_left
15#[repr(transparent)]
16#[derive(Debug, Clone, PartialEq, Eq, Hash, Copy)]
17pub struct ImageXYPoint(FlatCoordinateU32);
18
19impl ImageXYPoint {
20    pub fn new(x: u32, y: u32) -> Self {
21        ImageXYPoint(FlatCoordinateU32::new(x, y))
22    }
23}
24
25impl std::ops::Deref for ImageXYPoint {
26    type Target = FlatCoordinateU32;
27    fn deref(&self) -> &Self::Target {
28        &self.0
29    }
30}
31
32impl From<FlatCoordinateU32> for ImageXYPoint {
33    fn from(x: FlatCoordinateU32) -> Self {
34        ImageXYPoint(x)
35    }
36}
37
38impl From<ImageXYPoint> for FlatCoordinateU32 {
39    fn from(x: ImageXYPoint) -> Self {
40        x.0
41    }
42}
43
44impl Display for ImageXYPoint {
45    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
46        write!(f, "[{}, {}]", self.0, self.0)
47    }
48}
49
50
51
52/// Describes the resolution of the image (width and height)
53#[repr(transparent)]
54#[derive(Debug, Clone, PartialEq, Eq, Hash, Copy)]
55pub struct ImageXYResolution(CartesianResolution);
56
57impl ImageXYResolution {
58    pub fn new(x_width: usize, y_height: usize,) -> Result<Self,FeagiDataError> {
59        Ok(ImageXYResolution(CartesianResolution::new(x_width, y_height)?))
60    }
61}
62
63impl std::ops::Deref for ImageXYResolution {
64    type Target = CartesianResolution;
65    fn deref(&self) -> &Self::Target {
66        &self.0
67    }
68}
69
70impl From<CartesianResolution> for ImageXYResolution {
71    fn from(coord: CartesianResolution) -> Self {
72        ImageXYResolution(coord)
73    }
74}
75
76impl From<ImageXYResolution> for CartesianResolution {
77    fn from(coord: ImageXYResolution) -> Self {
78        coord.0
79    }
80}
81
82impl Display for ImageXYResolution {
83    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
84        write!(f, "<{}w by {}h>", self.0, self.0)
85    }
86}
87
88
89//endregion
90
91//region Segmented Image XY Resolutions
92/// Target resolutions for each of the nine segments in a segmented vision frame
93///
94/// This structure stores the desired output resolution for each of the segments
95/// in a grid arrangement (3x3): corners, edges, and center.
96#[derive(PartialEq, Clone, Copy, Debug, Eq, Hash)]
97pub struct SegmentedXYImageResolutions {
98    pub lower_left: ImageXYResolution,
99    pub lower_middle: ImageXYResolution,
100    pub lower_right: ImageXYResolution,
101    pub middle_left: ImageXYResolution,
102    pub center: ImageXYResolution,
103    pub middle_right: ImageXYResolution,
104    pub upper_left: ImageXYResolution,
105    pub upper_middle: ImageXYResolution,
106    pub upper_right: ImageXYResolution,
107}
108
109impl SegmentedXYImageResolutions {
110
111    pub fn new(
112        lower_left: ImageXYResolution,
113        lower_middle: ImageXYResolution,
114        lower_right: ImageXYResolution,
115        middle_left: ImageXYResolution,
116        center: ImageXYResolution,
117        middle_right: ImageXYResolution,
118        upper_left: ImageXYResolution,
119        upper_middle: ImageXYResolution,
120        upper_right: ImageXYResolution,
121    ) -> SegmentedXYImageResolutions {
122        SegmentedXYImageResolutions {
123            lower_left,
124            lower_middle,
125            lower_right,
126            middle_left,
127            center,
128            middle_right,
129            upper_left,
130            upper_middle,
131            upper_right,
132        }
133    }
134
135    /// Creates a SegmentedVisionTargetResolutions with uniform peripheral segment sizes.
136    ///
137    /// This convenience method creates a configuration where all eight peripheral segments
138    /// have the same resolution, while the center segment can have a different resolution.
139    ///
140    /// # Arguments
141    ///
142    /// * `center_width_height` - Resolution for the center segment as (width, height)
143    /// * `peripheral_width_height` - Resolution for all peripheral segments as (width, height)
144    ///
145    /// # Returns
146    ///
147    /// A Result containing either:
148    /// - Ok(SegmentedVisionTargetResolutions) if all resolutions are valid (non-zero)
149    /// - Err(DataProcessingError) if any resolution has zero width or height
150    pub fn create_with_same_sized_peripheral(center_resolution: ImageXYResolution, peripheral_resolutions: ImageXYResolution) -> SegmentedXYImageResolutions {
151
152        SegmentedXYImageResolutions::new(peripheral_resolutions, peripheral_resolutions,
153                                         peripheral_resolutions, peripheral_resolutions,
154                                         center_resolution, peripheral_resolutions,
155                                         peripheral_resolutions, peripheral_resolutions,
156                                         peripheral_resolutions)
157    }
158
159    pub fn as_ordered_array(&self) ->[&ImageXYResolution; 9] {
160        [
161            &self.lower_left,
162            &self.lower_middle,
163            &self.lower_right,
164            &self.middle_left,
165            &self.center,
166            &self.middle_right,
167            &self.upper_left,
168            &self.upper_middle,
169            &self.upper_right,
170        ]
171    }
172}
173
174impl Display for SegmentedXYImageResolutions {
175    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
176        write!(f, "LowerLeft:{}, LowerMiddle:{}, LowerRight:{}, MiddleLeft:{}, Center:{}, MiddleRight:{}, TopLeft:{}, TopMiddle:{}, TopRight:{}",
177               self.lower_left, self.lower_middle, self.lower_right, self.middle_left, self.center, self.middle_right, self.upper_left, self.upper_middle, self.upper_right)
178    }
179}
180
181//endregion
182
183//region Enums
184
185/// Represents the color space of an image.
186///
187/// This enum defines the possible color spaces:
188/// - Linear: Linear color space
189/// - Gamma: Gamma-corrected color space
190#[derive(Debug, PartialEq, Clone, Copy, Eq, Hash)]
191pub enum ColorSpace {
192    Linear,
193    Gamma
194}
195
196impl std::fmt::Display for ColorSpace {
197    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
198        match self {
199            ColorSpace::Linear => write!(f, "Linear"),
200            ColorSpace::Gamma => write!(f, "Gamma"),
201        }
202    }
203}
204
205/// Represents the color channel format of an image.
206///
207/// This enum defines the possible color channel configurations for an image:
208/// - GrayScale: Single channel (grayscale, or red)
209/// - RG: Two channels (red, green)
210/// - RGB: Three channels (red, green, blue)
211/// - RGBA: Four channels (red, green, blue, alpha)
212#[derive(Debug, PartialEq, Clone, Copy, Eq, Hash)]
213pub enum ColorChannelLayout {
214    GrayScale = 1, // R
215    RG = 2,
216    RGB = 3,
217    RGBA = 4,
218}
219
220impl Display for ColorChannelLayout {
221    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
222        match self {
223            ColorChannelLayout::GrayScale => write!(f, "ChannelLayout(GrayScale)"),
224            ColorChannelLayout::RG => write!(f, "ChannelLayout(RedGreen)"),
225            ColorChannelLayout::RGB => write!(f, "ChannelLayout(RedGreenBlue)"),
226            ColorChannelLayout::RGBA => write!(f, "ChannelLayout(RedGreenBlueAlpha)"),
227        }
228    }
229}
230
231impl TryFrom<usize> for ColorChannelLayout {
232    type Error = FeagiDataError;
233    fn try_from(value: usize) -> Result<Self, Self::Error> {
234        match value {
235            1 => Ok(ColorChannelLayout::GrayScale),
236            2 => Ok(ColorChannelLayout::RG),
237            3 => Ok(ColorChannelLayout::RGB),
238            4 => Ok(ColorChannelLayout::RGBA),
239            _ => Err(FeagiDataError::BadParameters(format!("No Channel Layout has {} channels! Acceptable values are 1,2,3,4!", value)).into())
240        }
241    }
242}
243
244impl TryFrom<image::ColorType> for ColorChannelLayout {
245    type Error = FeagiDataError;
246    fn try_from(value: image::ColorType) -> Result<Self, Self::Error> {
247        match value {
248            image::ColorType::L8 => Ok(ColorChannelLayout::GrayScale),
249            image::ColorType::La8 => Ok(ColorChannelLayout::RG),
250            image::ColorType::Rgb8 => Ok(ColorChannelLayout::RGB),
251            image::ColorType::Rgba8 => Ok(ColorChannelLayout::RGBA),
252            _ => Err(FeagiDataError::BadParameters("Unsupported image color!".to_string()))
253        }
254    }
255}
256
257impl From<ColorChannelLayout> for usize {
258    fn from(value: ColorChannelLayout) -> usize {
259        value as usize
260    }
261}
262
263/// Represents the memory layout of an image array.
264///
265/// This enum defines the possible memory layouts for image data:
266/// - HeightsWidthsChannels: Row-major format (default)
267/// - ChannelsHeightsWidths: Common in machine learning
268/// - WidthsHeightsChannels: Cartesian format
269/// - HeightsChannelsWidths: Alternative format
270/// - ChannelsWidthsHeights: Alternative format
271/// - WidthsChannelsHeights: Alternative format
272#[derive(Debug, PartialEq, Clone, Copy)]
273pub enum MemoryOrderLayout {
274    HeightsWidthsChannels, // default, also called row major
275    ChannelsHeightsWidths, // common in machine learning
276    WidthsHeightsChannels, // cartesian, the best one
277    HeightsChannelsWidths,
278    ChannelsWidthsHeights,
279    WidthsChannelsHeights,
280}
281
282impl Display for MemoryOrderLayout {
283    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
284        match self {
285            MemoryOrderLayout::HeightsWidthsChannels => write!(f, "HeightsWidthsChannels"),
286            MemoryOrderLayout::ChannelsHeightsWidths => write!(f, "ChannelsHeightsWidths"),
287            MemoryOrderLayout::WidthsHeightsChannels => write!(f, "WidthsHeightsChannels"),
288            MemoryOrderLayout::HeightsChannelsWidths => write!(f, "HeightsChannelsWidths"),
289            MemoryOrderLayout::ChannelsWidthsHeights => write!(f, "ChannelsWidthsHeights"),
290            MemoryOrderLayout::WidthsChannelsHeights => write!(f, "WidthsChannelsHeights"),
291        }
292    }
293}
294//endregion
295
296//region Image Frame Properties
297
298#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
299pub struct ImageFrameProperties {
300    image_resolution: ImageXYResolution,
301    color_space: ColorSpace,
302    color_channel_layout: ColorChannelLayout,
303}
304
305impl ImageFrameProperties {
306    /// Creates a new ImageFrameProperties instance.
307    ///
308    /// # Arguments
309    ///
310    /// * `image_resolution` - The image dimensions
311    /// * `color_space` - The color space (Linear or Gamma-corrected)
312    /// * `color_channel_layout` - The channel configuration (Grayscale, RGB, RGBA, etc.)
313    ///
314    /// # Returns
315    ///
316    /// A new ImageFrameProperties instance with the specified configuration.
317    pub fn new(image_resolution: ImageXYResolution, color_space: ColorSpace, color_channel_layout: ColorChannelLayout) -> Result<Self, FeagiDataError> {
318        Ok(ImageFrameProperties{
319            image_resolution,
320            color_space,
321            color_channel_layout,
322        })
323    }
324
325    /// Verifies that an image frame matches these properties.
326    ///
327    /// Checks if the given image frame has the same resolution, color space,
328    /// and channel layout as specified in these properties.
329    ///
330    /// # Arguments
331    ///
332    /// * `image` - The image frame to verify against these properties
333    ///
334    /// # Returns
335    ///
336    /// * `Ok(())` if the image frame matches these properties
337    /// * `Err(FeagiDataError)` if any property doesn't match
338    ///
339    /// # Errors
340    ///
341    /// Returns an error with a descriptive message if:
342    /// - The resolution doesn't match
343    /// - The color space doesn't match  
344    /// - The channel layout doesn't match
345    pub fn verify_image_frame_matches_properties(&self, image_frame: &ImageFrame) -> Result<(), FeagiDataError> {
346        if image_frame.get_xy_resolution() != self.image_resolution {
347            return Err(FeagiDataError::BadParameters(format!{"Expected resolution of {} but received an image with resolution of {}!",
348                                                             self.image_resolution, image_frame.get_xy_resolution()}).into())
349        }
350        if image_frame.get_color_space() != &self.color_space {
351            return Err(FeagiDataError::BadParameters(format!("Expected color space of {}, but got image with color space of {}!", self.color_space.to_string(), self.color_space.to_string())).into())
352        }
353        if image_frame.get_channel_layout() != &self.color_channel_layout {
354            return Err(FeagiDataError::BadParameters(format!("Expected color channel layout of {}, but got image with color channel layout of {}!", self.color_channel_layout.to_string(), self.color_channel_layout.to_string())).into())
355        }
356        Ok(())
357    }
358
359    /// Returns the XY resolution.
360    ///
361    /// # Returns
362    ///
363    /// An ImageXYResolution
364    pub fn get_image_resolution(&self) -> ImageXYResolution {
365        self.image_resolution
366    }
367
368    /// Returns the color space.
369    ///
370    /// # Returns
371    ///
372    /// The ColorSpace enum value (Linear or Gamma).
373    pub fn get_color_space(&self) -> ColorSpace {
374        self.color_space
375    }
376
377    /// Returns the color channel layout.
378    ///
379    /// # Returns
380    ///
381    /// The ChannelLayout enum value (Grayscale, RGB, RGBA, etc.).
382    pub fn get_color_channel_layout(&self) -> ColorChannelLayout {
383        self.color_channel_layout
384    }
385
386    pub fn get_number_of_channels(&self) -> usize {
387        self.color_channel_layout.into()
388    }
389
390    pub fn get_number_of_samples(&self) -> usize {
391        self.image_resolution.width * self.image_resolution.height * self.get_number_of_channels()
392    }
393}
394
395impl Display for ImageFrameProperties {
396    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
397        let s = format!("ImageFrameProperties({}, {}, {})", self.image_resolution, self.color_space.to_string(), self.color_channel_layout.to_string());
398        write!(f, "{}", s)
399    }
400}
401
402//endregion
403
404//region Segmented Image Frame Properties
405
406#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
407pub struct SegmentedImageFrameProperties {
408    segment_xy_resolutions: SegmentedXYImageResolutions,
409    center_color_channel: ColorChannelLayout,
410    peripheral_color_channels: ColorChannelLayout,
411    color_space: ColorSpace,
412}
413impl SegmentedImageFrameProperties {
414    pub fn new( // TODO why take references if we are going to clone anyways?
415        segment_xy_resolutions: &SegmentedXYImageResolutions,
416        center_color_channels: &ColorChannelLayout,
417        peripheral_color_channels: &ColorChannelLayout,
418        color_space: &ColorSpace,
419    ) -> SegmentedImageFrameProperties {
420        SegmentedImageFrameProperties {
421            segment_xy_resolutions: segment_xy_resolutions.clone(),
422            center_color_channel: center_color_channels.clone(),
423            peripheral_color_channels: peripheral_color_channels.clone(),
424            color_space: *color_space,
425        }
426    }
427
428    pub fn get_resolutions(&self) -> &SegmentedXYImageResolutions {
429        &self.segment_xy_resolutions
430    }
431
432    pub fn get_center_color_channel(&self) -> &ColorChannelLayout {
433        &self.center_color_channel
434    }
435
436    pub fn get_peripheral_color_channels(&self) -> &ColorChannelLayout {
437        &self.peripheral_color_channels
438    }
439
440    pub fn get_color_space(&self) -> &ColorSpace {
441        &self.color_space
442    }
443
444    pub fn verify_segmented_image_frame_matches_properties(&self, segmented_image_frame: &SegmentedImageFrame) -> Result<(), FeagiDataError> {
445        if self != &segmented_image_frame.get_segmented_image_frame_properties() {
446            return Err(FeagiDataError::BadParameters("Segmented image frame does not match the expected segmented frame properties!".into()).into())
447        }
448        Ok(())
449    }
450
451
452}
453
454impl Display for SegmentedImageFrameProperties {
455    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
456        write!(f, "SegmentedImageFrameProperties(TODO)") // TODO
457    }
458}
459
460//endregion
461
462//region Corner Points
463/// Holds pixel coordinates for cropping
464#[derive(Debug, PartialEq, Clone, Copy)]
465pub struct CornerPoints {
466    pub upper_left: ImageXYPoint,
467    pub lower_right: ImageXYPoint,
468
469}
470
471impl CornerPoints {
472
473    pub fn new(upper_left: ImageXYPoint, lower_right: ImageXYPoint) -> Result<Self, FeagiDataError> {
474        if lower_right.x <= upper_left.x || lower_right.y <= upper_left.y {
475            return Err(FeagiDataError::BadParameters("Given Points are not forming a proper rectangle!".into()).into())
476        }
477        Ok(CornerPoints {
478            upper_left, lower_right
479        })
480    }
481    pub fn get_upper_right(&self) -> ImageXYPoint {
482        ImageXYPoint::new(self.lower_right.x, self.upper_left.y)
483    }
484
485    pub fn get_lower_left(&self) -> ImageXYPoint {
486        ImageXYPoint::new(self.upper_left.x, self.lower_right.y)
487    }
488
489    pub fn get_width(&self) -> u32 {
490        self.lower_right.x - self.upper_left.x
491    }
492
493    pub fn get_height(&self) -> u32 {
494        self.lower_right.y - self.upper_left.y
495    }
496    
497    pub fn enclosed_area_width_height(&self) -> ImageXYResolution {
498        ImageXYResolution::new(self.get_width() as usize, self.get_height() as usize).unwrap()
499    }
500
501    pub fn verify_fits_in_resolution(&self, resolution: ImageXYResolution) -> Result<(), FeagiDataError> {
502        if self.lower_right.x > resolution.width as u32 || self.lower_right.y > resolution.height as u32 {
503            return Err(FeagiDataError::BadParameters(format!("Corner Points {} do not fit in given resolution {}!", self, resolution)).into())
504        }
505        Ok(())
506    }
507}
508
509impl Display for CornerPoints {
510    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
511        write!(f, "CornerPoints(Upper Left: {}, Lower Right: {})", self.upper_left.to_string(), self.lower_right.to_string())
512    }
513}
514
515//endregion
516
517//region Gaze Properties
518/// Properties defining the center region of a segmented vision frame
519///
520/// This structure defines the coordinates and size of the central region
521/// in a normalized coordinate space (0.0 to 1.0).
522#[derive(PartialEq, Clone, Copy, Debug)]
523pub struct GazeProperties {
524    /// Center point coordinates in normalized space (0.0-1.0), from the top left
525    pub(crate) eccentricity_normalized_xy: (f32, f32), // Scaled from 0 to 1
526    /// Size of the center region in normalized space (0.0-1.0)
527    pub(crate) modularity_normalized_xy: (f32, f32), // Scaled from 0 to 1
528}
529
530impl GazeProperties {
531
532    pub fn new(eccentricity_center_xy: (f32, f32), modularity_size_xy: (f32, f32)) -> Self {
533        GazeProperties {
534            eccentricity_normalized_xy: (eccentricity_center_xy.0.clamp(0.0, 1.0), eccentricity_center_xy.1.clamp(0.0, 1.0)),
535            modularity_normalized_xy: (modularity_size_xy.0.clamp(0.0, 1.0), modularity_size_xy.1.clamp(0.0, 1.0)),
536        }
537    }
538
539    /// Creates a default centered SegmentedFrameCenterProperties.
540    ///
541    /// This convenience method creates center properties with the center region
542    /// positioned at the middle of the image with a moderate size.
543    ///
544    /// # Returns
545    ///
546    /// A SegmentedFrameCenterProperties with default centered configuration.
547    pub fn create_default_centered() -> GazeProperties {
548        GazeProperties::new((0.5, 0.5), (0.5, 0.5))
549    }
550
551    pub fn calculate_source_corner_points_for_segmented_video_frame(&self, source_frame_resolution: ImageXYResolution) -> Result<[CornerPoints; 9], FeagiDataError> {
552        if source_frame_resolution.width < 3 || source_frame_resolution.height < 3 {
553            return Err(FeagiDataError::BadParameters("Source frame width and height must be at least 3!".into()).into())
554        }
555
556
557        let center_corner_points = self.calculate_pixel_coordinates_of_center_corners(source_frame_resolution)?;
558        Ok([
559            CornerPoints::new(ImageXYPoint::new(0, center_corner_points.lower_right.y), ImageXYPoint::new(center_corner_points.upper_left.x, source_frame_resolution.height as u32))?,
560            CornerPoints::new(center_corner_points.get_lower_left(), ImageXYPoint::new(center_corner_points.lower_right.x, source_frame_resolution.height as u32))?,
561            CornerPoints::new(center_corner_points.lower_right, ImageXYPoint::new(source_frame_resolution.width as u32, source_frame_resolution.height as u32))?,
562            CornerPoints::new(ImageXYPoint::new(0, center_corner_points.upper_left.y), center_corner_points.get_lower_left())?,
563            center_corner_points,
564            CornerPoints::new(center_corner_points.get_upper_right(), ImageXYPoint::new(source_frame_resolution.width as u32, center_corner_points.lower_right.y))?,
565            CornerPoints::new(ImageXYPoint::new(0,0), center_corner_points.upper_left)?,
566            CornerPoints::new(ImageXYPoint::new(center_corner_points.upper_left.x, 0), center_corner_points.get_upper_right())?,
567            CornerPoints::new(ImageXYPoint::new(center_corner_points.lower_right.x, 0), ImageXYPoint::new(source_frame_resolution.width as u32, center_corner_points.upper_left.y))?,
568        ])
569    }
570
571    fn calculate_pixel_coordinates_of_center_corners(&self, source_frame_resolution: ImageXYResolution) -> Result<CornerPoints, FeagiDataError> {
572        let source_frame_width_height_f: (f32, f32) = (source_frame_resolution.width as f32, source_frame_resolution.height as f32);
573        let center_size_normalized_half_xy: (f32, f32) = (self.modularity_normalized_xy.0 / 2.0, self.modularity_normalized_xy.1 / 2.0);
574
575        // We use max / min to ensure that there is always a 1 pixel buffer along all edges for use in peripheral vision (since we cannot use a resolution of 0)
576        let bottom_pixel: usize = cmp::min(source_frame_resolution.height - 1,
577                                           ((self.eccentricity_normalized_xy.1 + center_size_normalized_half_xy.1) * source_frame_width_height_f.1).floor() as usize);
578        let top_pixel: usize = cmp::max(1,
579                                        (( self.eccentricity_normalized_xy.1 - center_size_normalized_half_xy.1) * source_frame_width_height_f.1).floor() as usize);
580        let left_pixel: usize = cmp::max(1,
581                                         ((self.eccentricity_normalized_xy.0 - center_size_normalized_half_xy.0) * source_frame_width_height_f.0).floor() as usize);
582        let right_pixel: usize = cmp::min(source_frame_resolution.width - 1,
583                                          (( self.eccentricity_normalized_xy.0 + center_size_normalized_half_xy.0) * source_frame_width_height_f.0).floor() as usize);
584
585        let top_left = ImageXYPoint::new(left_pixel as u32, top_pixel as u32);
586        let bottom_right = ImageXYPoint::new(right_pixel as u32, bottom_pixel as u32);
587
588        let corner_points: CornerPoints = CornerPoints::new(top_left, bottom_right)?;
589        Ok(corner_points)
590    }
591}
592
593impl std::fmt::Display for GazeProperties {
594    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
595        write!(f, "GazeProperties(TODO)") // TODO
596    }
597}
598//endregion
599
600