use image::imageops::FilterType;
use image::{GenericImageView, RgbImage};
use crate::array::Array;
use crate::error::{Error, Result};
#[derive(Debug, Clone)]
pub struct ProcessedImage {
pub pixel_values: Array,
pub patch_h: i32,
pub patch_w: i32,
pub num_soft_tokens: i32,
}
pub const MIN_SOFT_TOKENS: i32 = 40;
pub fn preprocess_image_bytes(
data: &[u8],
patch_size: i32,
max_soft_tokens: i32,
pooling_kernel_size: i32,
) -> Result<ProcessedImage> {
let img = image::load_from_memory(data)
.map_err(|e| Error::Model(format!("failed to decode image: {e}")))?;
let (orig_w, orig_h) = img.dimensions();
let (target_w, target_h) = compute_target_size(
orig_h,
orig_w,
patch_size,
MIN_SOFT_TOKENS,
max_soft_tokens,
pooling_kernel_size,
);
let resized = if target_h == orig_h && target_w == orig_w {
img.to_rgb8()
} else {
img.resize_exact(target_w, target_h, FilterType::Triangle)
.to_rgb8()
};
let pixel_values = rgb_to_chw_array(&resized);
let patch_h = target_h as i32 / patch_size;
let patch_w = target_w as i32 / patch_size;
let num_soft_tokens = (patch_h * patch_w) / (pooling_kernel_size * pooling_kernel_size);
Ok(ProcessedImage {
pixel_values,
patch_h,
patch_w,
num_soft_tokens,
})
}
fn compute_target_size(
height: u32,
width: u32,
patch_size: i32,
min_soft_tokens: i32,
max_soft_tokens: i32,
pooling_kernel_size: i32,
) -> (u32, u32) {
let align = (patch_size * pooling_kernel_size) as f64;
let patch_area = align * align;
let min_pixels = min_soft_tokens as f64 * patch_area;
let max_pixels = max_soft_tokens as f64 * patch_area;
let (h, w) = (height as f64, width as f64);
let round_by = |x: f64| -> f64 { (x / align).round() * align };
let floor_by = |x: f64| -> f64 { (x / align).floor() * align };
let ceil_by = |x: f64| -> f64 { (x / align).ceil() * align };
let mut h_bar = round_by(h).max(align);
let mut w_bar = round_by(w).max(align);
if h_bar * w_bar > max_pixels {
let beta = (h * w / max_pixels).sqrt();
h_bar = floor_by(h / beta).max(align);
w_bar = floor_by(w / beta).max(align);
} else if h_bar * w_bar < min_pixels {
let beta = (min_pixels / (h * w)).sqrt();
h_bar = ceil_by(h * beta).max(align);
w_bar = ceil_by(w * beta).max(align);
}
(w_bar as u32, h_bar as u32)
}
fn rgb_to_chw_array(rgb: &RgbImage) -> Array {
let width = rgb.width() as usize;
let height = rgb.height() as usize;
let mut chw = vec![0f32; 3 * height * width];
for y in 0..height {
for x in 0..width {
let pixel = rgb.get_pixel(x as u32, y as u32);
for c in 0..3usize {
chw[c * height * width + y * width + x] = pixel[c] as f32 / 255.0;
}
}
}
Array::from_slice(&chw, &[1, 3, height as i32, width as i32])
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn compute_target_size_is_a_multiple_of_side_mult() {
let (w, h) = compute_target_size(1080, 1920, 16, 40, 280, 3);
assert_eq!(w % 48, 0);
assert_eq!(h % 48, 0);
assert!(w > 0 && h > 0);
}
#[test]
fn num_soft_tokens_never_exceeds_budget() {
let (w, h) = compute_target_size(4000, 3000, 16, 40, 280, 3);
let patch_h = h as i32 / 16;
let patch_w = w as i32 / 16;
let soft = (patch_h * patch_w) / 9;
assert!(soft <= 280, "soft={soft}");
}
#[test]
fn a_naturally_sized_photo_stays_near_native_resolution() {
let (w, h) = compute_target_size(426, 640, 16, 40, 280, 3);
assert_eq!((w, h), (624, 432));
}
}