1use crate::{MediaError, Result};
4
5#[derive(Debug, Clone)]
8pub struct PixelBatch {
9 pub width: usize,
11 pub height: usize,
13 pub channels: usize,
15 pub data: Vec<f32>,
17}
18
19impl PixelBatch {
20 pub fn shape(&self) -> [usize; 4] {
22 [1, self.channels, self.height, self.width]
23 }
24}
25
26pub trait ImagePreprocessor: Send + Sync {
28 fn preprocess(&self, bytes: &[u8]) -> Result<PixelBatch>;
30}
31
32pub struct SiglipPreprocessor {
37 image_size: usize,
38 mean: [f32; 3],
39 std: [f32; 3],
40}
41
42impl SiglipPreprocessor {
43 pub fn new(image_size: usize) -> Self {
45 SiglipPreprocessor {
46 image_size,
47 mean: [0.5, 0.5, 0.5],
48 std: [0.5, 0.5, 0.5],
49 }
50 }
51}
52
53impl ImagePreprocessor for SiglipPreprocessor {
54 fn preprocess(&self, bytes: &[u8]) -> Result<PixelBatch> {
55 let img = image::load_from_memory(bytes)
56 .map_err(|e| MediaError::Decode(e.to_string()))?
57 .to_rgb8();
58 let (w, h) = (img.width() as usize, img.height() as usize);
59 if w == 0 || h == 0 {
60 return Err(MediaError::BadShape("empty image".to_string()));
61 }
62
63 let size = self.image_size as f64;
65 let scale = size / w.max(h) as f64;
66 let new_w = ((w as f64 * scale).round() as usize).max(1);
67 let new_h = ((h as f64 * scale).round() as usize).max(1);
68 let resized = image::imageops::resize(
69 &img,
70 new_w as u32,
71 new_h as u32,
72 image::imageops::FilterType::Triangle,
73 );
74
75 let pad_byte = 128u8;
77 let mut canvas = image::RgbImage::from_pixel(
78 self.image_size as u32,
79 self.image_size as u32,
80 image::Rgb([pad_byte, pad_byte, pad_byte]),
81 );
82 image::imageops::overlay(&mut canvas, &resized, 0, 0);
83
84 let n = self.image_size * self.image_size;
86 let mut data = vec![0f32; 3 * n];
87 for (x, y, pixel) in canvas.enumerate_pixels() {
88 let idx = y as usize * self.image_size + x as usize;
89 for c in 0..3 {
90 let v = pixel[c] as f32 / 255.0;
91 data[c * n + idx] = (v - self.mean[c]) / self.std[c];
92 }
93 }
94
95 Ok(PixelBatch {
96 width: self.image_size,
97 height: self.image_size,
98 channels: 3,
99 data,
100 })
101 }
102}
103
104#[cfg(test)]
105mod tests {
106 use super::*;
107
108 fn png_bytes(w: u32, h: u32, rgb: [u8; 3]) -> Vec<u8> {
109 let img = image::RgbImage::from_pixel(w, h, image::Rgb(rgb));
110 let mut buf = std::io::Cursor::new(Vec::new());
111 img.write_to(&mut buf, image::ImageFormat::Png).unwrap();
112 buf.into_inner()
113 }
114
115 #[test]
116 fn output_shape_and_range() {
117 let pp = SiglipPreprocessor::new(512);
118 let out = pp.preprocess(&png_bytes(64, 32, [255, 0, 0])).unwrap();
119 assert_eq!(out.shape(), [1, 3, 512, 512]);
120 assert_eq!(out.data.len(), 3 * 512 * 512);
121 let (mut lo, mut hi) = (f32::INFINITY, f32::NEG_INFINITY);
122 for &v in &out.data {
123 lo = lo.min(v);
124 hi = hi.max(v);
125 }
126 assert!(lo >= -1.0 && hi <= 1.0, "normalized range [{lo}, {hi}]");
127 let n = 512 * 512;
129 let r_max = out.data[..n].iter().copied().fold(f32::NEG_INFINITY, f32::max);
130 assert!(r_max > 0.9, "red channel max {r_max}");
131 }
132
133 #[test]
134 fn padding_is_zero_after_normalization() {
135 let pp = SiglipPreprocessor::new(512);
136 let out = pp.preprocess(&png_bytes(64, 16, [0, 255, 0])).unwrap();
138 let n = 512 * 512;
139 let pad_idx = n - 1;
141 for c in 0..3 {
142 assert!(
143 out.data[c * n + pad_idx].abs() < 0.02,
144 "pad should normalize to ~0, got {}",
145 out.data[c * n + pad_idx]
146 );
147 }
148 }
149
150 #[test]
151 fn rejects_garbage() {
152 let pp = SiglipPreprocessor::new(512);
153 assert!(pp.preprocess(b"not an image").is_err());
154 }
155}