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;
11
12/// Marker trait for pixel element types supported by image decoding.
13///
14/// Provides conversions from decoded `u8` and `u16` pixel values into the
15/// tensor's native element type. The `from_u16` path is used for 16-bit PNG
16/// images; JPEG always decodes to `u8`.
17///
18/// ## Supported types and conversions
19///
20/// | Type | `from_u8` | `from_u16` | Notes |
21/// |------|-----------|------------|-------|
22/// | `u8` | identity | `>> 8` | |
23/// | `u16` | `* 257` | identity | 0..255 → 0..65535 |
24/// | `i8` | `XOR 0x80` | `(>> 8) XOR 0x80` | unsigned-to-signed via sign-bit flip |
25/// | `i16` | `* 257 XOR 0x8000` | `XOR 0x8000` | unsigned-to-signed via sign-bit flip |
26/// | `f32` | `/ 255.0` | `/ 65535.0` | normalised to `[0.0, 1.0]` |
27///
28/// `edgefirst_tensor::Element` is a supertrait: under the `dynamic` tensor
29/// backend, `Tensor<T>`'s own constructors need `T::DTYPE` at compile time
30/// (there is no per-variant enum to allocate through instead), so any `T`
31/// this crate builds a `Tensor<T>` over must already be one of the eleven
32/// element types `Element` is sealed to. Every `ImagePixel` impl below is
33/// one of those types, so this costs nothing real -- it just makes the
34/// existing constraint visible at the trait level instead of failing much
35/// later at a `Tensor<T>` call site.
36pub trait ImagePixel: edgefirst_tensor::Element {
37    /// Convert a `[0, 255]` byte value to this pixel type.
38    fn from_u8(v: u8) -> Self;
39
40    /// Convert a `[0, 65535]` 16-bit value to this pixel type.
41    ///
42    /// Used for 16-bit PNG images. The default implementation converts via
43    /// `from_u8(v >> 8)` which discards the low byte.
44    fn from_u16(v: u16) -> Self {
45        Self::from_u8((v >> 8) as u8)
46    }
47
48    /// The [`DType`] that corresponds to this Rust type.
49    fn dtype() -> DType;
50}
51
52impl ImagePixel for u8 {
53    #[inline]
54    fn from_u8(v: u8) -> Self {
55        v
56    }
57
58    #[inline]
59    fn from_u16(v: u16) -> Self {
60        (v >> 8) as u8
61    }
62
63    fn dtype() -> DType {
64        DType::U8
65    }
66}
67
68impl ImagePixel for u16 {
69    #[inline]
70    fn from_u8(v: u8) -> Self {
71        // Scale 0..255 → 0..65535 exactly: 0→0, 128→32896, 255→65535
72        v as u16 * 257
73    }
74
75    #[inline]
76    fn from_u16(v: u16) -> Self {
77        v
78    }
79
80    fn dtype() -> DType {
81        DType::U16
82    }
83}
84
85impl ImagePixel for i8 {
86    #[inline]
87    fn from_u8(v: u8) -> Self {
88        // XOR sign-bit flip: 0→-128, 128→0, 255→127
89        (v ^ 0x80) as i8
90    }
91
92    #[inline]
93    fn from_u16(v: u16) -> Self {
94        Self::from_u8((v >> 8) as u8)
95    }
96
97    fn dtype() -> DType {
98        DType::I8
99    }
100}
101
102impl ImagePixel for i16 {
103    #[inline]
104    fn from_u8(v: u8) -> Self {
105        // Scale to u16 range first, then XOR sign-bit flip
106        ((v as u16 * 257) ^ 0x8000) as i16
107    }
108
109    #[inline]
110    fn from_u16(v: u16) -> Self {
111        // XOR sign-bit flip: 0→-32768, 32768→0, 65535→32767
112        (v ^ 0x8000) as i16
113    }
114
115    fn dtype() -> DType {
116        DType::I16
117    }
118}
119
120impl ImagePixel for f32 {
121    #[inline]
122    fn from_u8(v: u8) -> Self {
123        v as f32 / 255.0
124    }
125
126    #[inline]
127    fn from_u16(v: u16) -> Self {
128        v as f32 / 65535.0
129    }
130
131    fn dtype() -> DType {
132        DType::F32
133    }
134}
135
136#[cfg(test)]
137mod tests {
138    use super::*;
139
140    #[test]
141    fn u8_identity() {
142        assert_eq!(u8::from_u8(0), 0);
143        assert_eq!(u8::from_u8(128), 128);
144        assert_eq!(u8::from_u8(255), 255);
145    }
146
147    #[test]
148    fn u8_from_u16() {
149        assert_eq!(u8::from_u16(0), 0);
150        assert_eq!(u8::from_u16(32768), 128);
151        assert_eq!(u8::from_u16(65535), 255);
152    }
153
154    #[test]
155    fn u16_from_u8_scaling() {
156        assert_eq!(u16::from_u8(0), 0);
157        assert_eq!(u16::from_u8(1), 257);
158        assert_eq!(u16::from_u8(128), 32896);
159        assert_eq!(u16::from_u8(255), 65535);
160    }
161
162    #[test]
163    fn u16_identity() {
164        assert_eq!(u16::from_u16(0), 0);
165        assert_eq!(u16::from_u16(32768), 32768);
166        assert_eq!(u16::from_u16(65535), 65535);
167    }
168
169    #[test]
170    fn i8_xor_trick() {
171        // XOR with 0x80 flips the sign bit: u8 midpoint (128) maps to i8 zero
172        assert_eq!(i8::from_u8(0), -128);
173        assert_eq!(i8::from_u8(128), 0);
174        assert_eq!(i8::from_u8(255), 127);
175        assert_eq!(i8::from_u8(1), -127);
176        assert_eq!(i8::from_u8(127), -1);
177    }
178
179    #[test]
180    fn i16_xor_trick() {
181        // From u8: scale to u16 first, then XOR
182        assert_eq!(i16::from_u8(0), -32768);
183        assert_eq!(i16::from_u8(128), 128); // 32896 ^ 0x8000 = 128
184        assert_eq!(i16::from_u8(255), 32767);
185
186        // From u16: direct XOR
187        assert_eq!(i16::from_u16(0), -32768);
188        assert_eq!(i16::from_u16(32768), 0);
189        assert_eq!(i16::from_u16(65535), 32767);
190    }
191
192    #[test]
193    fn f32_normalised() {
194        assert!((f32::from_u8(0) - 0.0).abs() < f32::EPSILON);
195        assert!((f32::from_u8(255) - 1.0).abs() < f32::EPSILON);
196        assert!((f32::from_u8(128) - 128.0 / 255.0).abs() < f32::EPSILON);
197    }
198
199    #[test]
200    fn f32_from_u16_normalised() {
201        assert!((f32::from_u16(0) - 0.0).abs() < f32::EPSILON);
202        assert!((f32::from_u16(65535) - 1.0).abs() < 1e-5);
203        assert!((f32::from_u16(32768) - 32768.0 / 65535.0).abs() < 1e-5);
204    }
205
206    #[test]
207    fn dtype_matches() {
208        assert_eq!(u8::dtype(), DType::U8);
209        assert_eq!(u16::dtype(), DType::U16);
210        assert_eq!(i8::dtype(), DType::I8);
211        assert_eq!(i16::dtype(), DType::I16);
212        assert_eq!(f32::dtype(), DType::F32);
213    }
214}