use crate::{MediaError, Result};
#[derive(Debug, Clone)]
pub struct PixelBatch {
pub width: usize,
pub height: usize,
pub channels: usize,
pub data: Vec<f32>,
}
impl PixelBatch {
pub fn shape(&self) -> [usize; 4] {
[1, self.channels, self.height, self.width]
}
}
pub trait ImagePreprocessor: Send + Sync {
fn preprocess(&self, bytes: &[u8]) -> Result<PixelBatch>;
}
pub struct SiglipPreprocessor {
image_size: usize,
mean: [f32; 3],
std: [f32; 3],
}
impl SiglipPreprocessor {
pub fn new(image_size: usize) -> Self {
SiglipPreprocessor {
image_size,
mean: [0.5, 0.5, 0.5],
std: [0.5, 0.5, 0.5],
}
}
}
impl ImagePreprocessor for SiglipPreprocessor {
fn preprocess(&self, bytes: &[u8]) -> Result<PixelBatch> {
let img = image::load_from_memory(bytes)
.map_err(|e| MediaError::Decode(e.to_string()))?
.to_rgb8();
let (w, h) = (img.width() as usize, img.height() as usize);
if w == 0 || h == 0 {
return Err(MediaError::BadShape("empty image".to_string()));
}
let size = self.image_size as f64;
let scale = size / w.max(h) as f64;
let new_w = ((w as f64 * scale).round() as usize).max(1);
let new_h = ((h as f64 * scale).round() as usize).max(1);
let resized = image::imageops::resize(
&img,
new_w as u32,
new_h as u32,
image::imageops::FilterType::Triangle,
);
let pad_byte = 128u8;
let mut canvas = image::RgbImage::from_pixel(
self.image_size as u32,
self.image_size as u32,
image::Rgb([pad_byte, pad_byte, pad_byte]),
);
image::imageops::overlay(&mut canvas, &resized, 0, 0);
let n = self.image_size * self.image_size;
let mut data = vec![0f32; 3 * n];
for (x, y, pixel) in canvas.enumerate_pixels() {
let idx = y as usize * self.image_size + x as usize;
for c in 0..3 {
let v = pixel[c] as f32 / 255.0;
data[c * n + idx] = (v - self.mean[c]) / self.std[c];
}
}
Ok(PixelBatch {
width: self.image_size,
height: self.image_size,
channels: 3,
data,
})
}
}
#[cfg(test)]
mod tests {
use super::*;
fn png_bytes(w: u32, h: u32, rgb: [u8; 3]) -> Vec<u8> {
let img = image::RgbImage::from_pixel(w, h, image::Rgb(rgb));
let mut buf = std::io::Cursor::new(Vec::new());
img.write_to(&mut buf, image::ImageFormat::Png).unwrap();
buf.into_inner()
}
#[test]
fn output_shape_and_range() {
let pp = SiglipPreprocessor::new(512);
let out = pp.preprocess(&png_bytes(64, 32, [255, 0, 0])).unwrap();
assert_eq!(out.shape(), [1, 3, 512, 512]);
assert_eq!(out.data.len(), 3 * 512 * 512);
let (mut lo, mut hi) = (f32::INFINITY, f32::NEG_INFINITY);
for &v in &out.data {
lo = lo.min(v);
hi = hi.max(v);
}
assert!(lo >= -1.0 && hi <= 1.0, "normalized range [{lo}, {hi}]");
let n = 512 * 512;
let r_max = out.data[..n].iter().copied().fold(f32::NEG_INFINITY, f32::max);
assert!(r_max > 0.9, "red channel max {r_max}");
}
#[test]
fn padding_is_zero_after_normalization() {
let pp = SiglipPreprocessor::new(512);
let out = pp.preprocess(&png_bytes(64, 16, [0, 255, 0])).unwrap();
let n = 512 * 512;
let pad_idx = n - 1;
for c in 0..3 {
assert!(
out.data[c * n + pad_idx].abs() < 0.02,
"pad should normalize to ~0, got {}",
out.data[c * n + pad_idx]
);
}
}
#[test]
fn rejects_garbage() {
let pp = SiglipPreprocessor::new(512);
assert!(pp.preprocess(b"not an image").is_err());
}
}