use image::imageops::{self, FilterType};
use image::{ImageFormat, ImageReader, Limits, RgbImage};
use std::io::Cursor;
pub const IMAGENET_MEAN: [f32; 3] = [0.485, 0.456, 0.406];
pub const IMAGENET_STD: [f32; 3] = [0.229, 0.224, 0.225];
pub const MAX_IMAGE_DIMENSION: u32 = 16_384;
const MAX_DECODE_ALLOC: u64 = 128 * 1024 * 1024;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ImageKind {
Jpeg,
Png,
}
impl ImageKind {
pub fn from_media_type(essence: &str) -> Option<Self> {
if essence.eq_ignore_ascii_case("image/jpeg") || essence.eq_ignore_ascii_case("image/jpg") {
Some(Self::Jpeg)
} else if essence.eq_ignore_ascii_case("image/png") {
Some(Self::Png)
} else {
None
}
}
fn format(self) -> ImageFormat {
match self {
Self::Jpeg => ImageFormat::Jpeg,
Self::Png => ImageFormat::Png,
}
}
}
pub fn image_input_size(shape: &[usize]) -> Option<(u32, u32)> {
match *shape {
[3, h, w] if h > 0 && w > 0 => Some((u32::try_from(h).ok()?, u32::try_from(w).ok()?)),
_ => None,
}
}
pub fn preprocess_image(
bytes: &[u8],
kind: ImageKind,
height: u32,
width: u32,
) -> Result<Vec<f32>, image::ImageError> {
let mut reader = ImageReader::with_format(Cursor::new(bytes), kind.format());
let mut limits = Limits::default();
limits.max_image_width = Some(MAX_IMAGE_DIMENSION);
limits.max_image_height = Some(MAX_IMAGE_DIMENSION);
limits.max_alloc = Some(MAX_DECODE_ALLOC);
reader.limits(limits);
let image = reader.decode()?.into_rgb8();
Ok(normalize(&resize_and_crop(&image, height, width)))
}
fn resize_and_crop(image: &RgbImage, height: u32, width: u32) -> RgbImage {
let (src_w, src_h) = image.dimensions();
let (new_w, new_h) = resized_size(src_w, src_h, height, width);
let resized = if (new_w, new_h) == (src_w, src_h) {
image.clone()
} else {
imageops::resize(image, new_w, new_h, FilterType::Triangle)
};
let left = ((new_w - width) as f64 / 2.0).round() as u32;
let top = ((new_h - height) as f64 / 2.0).round() as u32;
imageops::crop_imm(&resized, left, top, width, height).to_image()
}
fn resized_size(src_w: u32, src_h: u32, height: u32, width: u32) -> (u32, u32) {
let (src_w, src_h) = (u64::from(src_w), u64::from(src_h));
let (width, height) = (u64::from(width), u64::from(height));
let short = height.min(width) * 256 / 224;
let (mut new_w, mut new_h) = if src_w <= src_h {
(short, short * src_h / src_w)
} else {
(short * src_w / src_h, short)
};
if new_w < width || new_h < height {
let scale = f64::max(width as f64 / src_w as f64, height as f64 / src_h as f64);
new_w = ((src_w as f64 * scale).ceil() as u64).max(width);
new_h = ((src_h as f64 * scale).ceil() as u64).max(height);
}
let clamp = |v: u64| u32::try_from(v).unwrap_or(u32::MAX);
(clamp(new_w), clamp(new_h))
}
fn normalize(image: &RgbImage) -> Vec<f32> {
let plane = image.width() as usize * image.height() as usize;
let mut out = vec![0.0f32; 3 * plane];
for (i, pixel) in image.pixels().enumerate() {
for c in 0..3 {
out[c * plane + i] =
(f32::from(pixel.0[c]) / 255.0 - IMAGENET_MEAN[c]) / IMAGENET_STD[c];
}
}
out
}
#[cfg(test)]
mod tests {
use super::*;
use image::{ImageEncoder, Rgb, codecs::png::PngEncoder};
fn encode_png(image: &RgbImage) -> Vec<u8> {
let mut bytes = Vec::new();
PngEncoder::new(&mut bytes)
.write_image(
image.as_raw(),
image.width(),
image.height(),
image::ExtendedColorType::Rgb8,
)
.unwrap();
bytes
}
#[test]
fn test_image_input_size() {
assert_eq!(image_input_size(&[3, 224, 224]), Some((224, 224)));
assert_eq!(image_input_size(&[3, 32, 64]), Some((32, 64)));
assert_eq!(image_input_size(&[784]), None);
assert_eq!(image_input_size(&[1, 28, 28]), None);
assert_eq!(image_input_size(&[3, 0, 224]), None);
}
#[test]
fn test_media_types() {
assert_eq!(
ImageKind::from_media_type("image/jpeg"),
Some(ImageKind::Jpeg)
);
assert_eq!(
ImageKind::from_media_type("IMAGE/PNG"),
Some(ImageKind::Png)
);
assert_eq!(ImageKind::from_media_type("image/gif"), None);
}
#[test]
fn test_solid_color_normalization() {
let image = RgbImage::from_pixel(300, 200, Rgb([255, 0, 128]));
let out = preprocess_image(&encode_png(&image), ImageKind::Png, 224, 224).unwrap();
assert_eq!(out.len(), 3 * 224 * 224);
let plane = 224 * 224;
let expected = [
(1.0 - IMAGENET_MEAN[0]) / IMAGENET_STD[0],
(0.0 - IMAGENET_MEAN[1]) / IMAGENET_STD[1],
(128.0 / 255.0 - IMAGENET_MEAN[2]) / IMAGENET_STD[2],
];
for (c, expected) in expected.iter().enumerate() {
let channel = &out[c * plane..(c + 1) * plane];
assert!(channel.iter().all(|v| (v - expected).abs() < 1e-5));
}
}
#[test]
fn test_resize_and_center_crop_geometry() {
let image = RgbImage::from_fn(512, 256, |x, _| Rgb([(x / 2) as u8, 0, 0]));
let cropped = resize_and_crop(&image, 224, 224);
assert_eq!(cropped.dimensions(), (224, 224));
assert_eq!(cropped.get_pixel(0, 0).0[0], 72);
assert_eq!(cropped.get_pixel(223, 0).0[0], 183);
assert_eq!(resized_size(1546, 1213, 224, 224), (326, 256));
assert_eq!(resized_size(1213, 1546, 224, 224), (256, 326));
assert_eq!(resized_size(100, 100, 224, 224), (256, 256));
assert_eq!(resized_size(100, 100, 32, 64), (64, 64));
}
#[test]
fn test_small_and_non_square_inputs() {
let image = RgbImage::from_pixel(10, 1000, Rgb([10, 20, 30]));
let out = preprocess_image(&encode_png(&image), ImageKind::Png, 64, 32).unwrap();
assert_eq!(out.len(), 3 * 64 * 32);
}
#[test]
fn test_rejects_garbage_and_wrong_format() {
assert!(preprocess_image(b"not an image", ImageKind::Png, 224, 224).is_err());
let png = encode_png(&RgbImage::new(8, 8));
assert!(preprocess_image(&png, ImageKind::Jpeg, 224, 224).is_err());
}
}