Skip to main content

edgefirst_codec/
pixel.rs

1// SPDX-FileCopyrightText: Copyright 2026 Au-Zone Technologies
2// SPDX-License-Identifier: Apache-2.0
3
4//! Pixel type trait for image decoding.
5//!
6//! Types implementing [`ImagePixel`] can be used as the element type of a
7//! [`Tensor`](edgefirst_tensor::Tensor) passed to
8//! [`ImageLoad::load_image`](crate::ImageLoad::load_image).
9
10use edgefirst_tensor::DType;
11use std::fmt;
12
13/// Marker trait for pixel element types supported by image decoding.
14///
15/// Provides conversions from decoded `u8` and `u16` pixel values into the
16/// tensor's native element type. The `from_u16` path is used for 16-bit PNG
17/// images; JPEG always decodes to `u8`.
18///
19/// ## Supported types and conversions
20///
21/// | Type | `from_u8` | `from_u16` | Notes |
22/// |------|-----------|------------|-------|
23/// | `u8` | identity | `>> 8` | |
24/// | `u16` | `* 257` | identity | 0..255 → 0..65535 |
25/// | `i8` | `XOR 0x80` | `(>> 8) XOR 0x80` | unsigned-to-signed via sign-bit flip |
26/// | `i16` | `* 257 XOR 0x8000` | `XOR 0x8000` | unsigned-to-signed via sign-bit flip |
27/// | `f32` | `/ 255.0` | `/ 65535.0` | normalised to `[0.0, 1.0]` |
28pub trait ImagePixel: num_traits::Num + Clone + fmt::Debug + Send + Sync + 'static {
29    /// Convert a `[0, 255]` byte value to this pixel type.
30    fn from_u8(v: u8) -> Self;
31
32    /// Convert a `[0, 65535]` 16-bit value to this pixel type.
33    ///
34    /// Used for 16-bit PNG images. The default implementation converts via
35    /// `from_u8(v >> 8)` which discards the low byte.
36    fn from_u16(v: u16) -> Self {
37        Self::from_u8((v >> 8) as u8)
38    }
39
40    /// The [`DType`] that corresponds to this Rust type.
41    fn dtype() -> DType;
42}
43
44impl ImagePixel for u8 {
45    #[inline]
46    fn from_u8(v: u8) -> Self {
47        v
48    }
49
50    #[inline]
51    fn from_u16(v: u16) -> Self {
52        (v >> 8) as u8
53    }
54
55    fn dtype() -> DType {
56        DType::U8
57    }
58}
59
60impl ImagePixel for u16 {
61    #[inline]
62    fn from_u8(v: u8) -> Self {
63        // Scale 0..255 → 0..65535 exactly: 0→0, 128→32896, 255→65535
64        v as u16 * 257
65    }
66
67    #[inline]
68    fn from_u16(v: u16) -> Self {
69        v
70    }
71
72    fn dtype() -> DType {
73        DType::U16
74    }
75}
76
77impl ImagePixel for i8 {
78    #[inline]
79    fn from_u8(v: u8) -> Self {
80        // XOR sign-bit flip: 0→-128, 128→0, 255→127
81        (v ^ 0x80) as i8
82    }
83
84    #[inline]
85    fn from_u16(v: u16) -> Self {
86        Self::from_u8((v >> 8) as u8)
87    }
88
89    fn dtype() -> DType {
90        DType::I8
91    }
92}
93
94impl ImagePixel for i16 {
95    #[inline]
96    fn from_u8(v: u8) -> Self {
97        // Scale to u16 range first, then XOR sign-bit flip
98        ((v as u16 * 257) ^ 0x8000) as i16
99    }
100
101    #[inline]
102    fn from_u16(v: u16) -> Self {
103        // XOR sign-bit flip: 0→-32768, 32768→0, 65535→32767
104        (v ^ 0x8000) as i16
105    }
106
107    fn dtype() -> DType {
108        DType::I16
109    }
110}
111
112impl ImagePixel for f32 {
113    #[inline]
114    fn from_u8(v: u8) -> Self {
115        v as f32 / 255.0
116    }
117
118    #[inline]
119    fn from_u16(v: u16) -> Self {
120        v as f32 / 65535.0
121    }
122
123    fn dtype() -> DType {
124        DType::F32
125    }
126}
127
128#[cfg(test)]
129mod tests {
130    use super::*;
131
132    #[test]
133    fn u8_identity() {
134        assert_eq!(u8::from_u8(0), 0);
135        assert_eq!(u8::from_u8(128), 128);
136        assert_eq!(u8::from_u8(255), 255);
137    }
138
139    #[test]
140    fn u8_from_u16() {
141        assert_eq!(u8::from_u16(0), 0);
142        assert_eq!(u8::from_u16(32768), 128);
143        assert_eq!(u8::from_u16(65535), 255);
144    }
145
146    #[test]
147    fn u16_from_u8_scaling() {
148        assert_eq!(u16::from_u8(0), 0);
149        assert_eq!(u16::from_u8(1), 257);
150        assert_eq!(u16::from_u8(128), 32896);
151        assert_eq!(u16::from_u8(255), 65535);
152    }
153
154    #[test]
155    fn u16_identity() {
156        assert_eq!(u16::from_u16(0), 0);
157        assert_eq!(u16::from_u16(32768), 32768);
158        assert_eq!(u16::from_u16(65535), 65535);
159    }
160
161    #[test]
162    fn i8_xor_trick() {
163        // XOR with 0x80 flips the sign bit: u8 midpoint (128) maps to i8 zero
164        assert_eq!(i8::from_u8(0), -128);
165        assert_eq!(i8::from_u8(128), 0);
166        assert_eq!(i8::from_u8(255), 127);
167        assert_eq!(i8::from_u8(1), -127);
168        assert_eq!(i8::from_u8(127), -1);
169    }
170
171    #[test]
172    fn i16_xor_trick() {
173        // From u8: scale to u16 first, then XOR
174        assert_eq!(i16::from_u8(0), -32768);
175        assert_eq!(i16::from_u8(128), 128); // 32896 ^ 0x8000 = 128
176        assert_eq!(i16::from_u8(255), 32767);
177
178        // From u16: direct XOR
179        assert_eq!(i16::from_u16(0), -32768);
180        assert_eq!(i16::from_u16(32768), 0);
181        assert_eq!(i16::from_u16(65535), 32767);
182    }
183
184    #[test]
185    fn f32_normalised() {
186        assert!((f32::from_u8(0) - 0.0).abs() < f32::EPSILON);
187        assert!((f32::from_u8(255) - 1.0).abs() < f32::EPSILON);
188        assert!((f32::from_u8(128) - 128.0 / 255.0).abs() < f32::EPSILON);
189    }
190
191    #[test]
192    fn f32_from_u16_normalised() {
193        assert!((f32::from_u16(0) - 0.0).abs() < f32::EPSILON);
194        assert!((f32::from_u16(65535) - 1.0).abs() < 1e-5);
195        assert!((f32::from_u16(32768) - 32768.0 / 65535.0).abs() < 1e-5);
196    }
197
198    #[test]
199    fn dtype_matches() {
200        assert_eq!(u8::dtype(), DType::U8);
201        assert_eq!(u16::dtype(), DType::U16);
202        assert_eq!(i8::dtype(), DType::I8);
203        assert_eq!(i16::dtype(), DType::I16);
204        assert_eq!(f32::dtype(), DType::F32);
205    }
206}