Media

Smart crop

Thumbnails cropped to the interesting part of a picture, as smartcrop.js does: edges, skin tones and strong colours scored, the best window of the shape you ask found, and the crop resized to the exact size.

Version 1.0.0 · 3 functions · 2 crates

#images#thumbnails#photos#social

Functions

What use smart_crop.qwer gives a program, with the types Quillo sees. To take only some, or under other names: use smart_crop_box, smart_crop from smart_crop (Quillo 0.6).

smart_crop_box(path: str, width: int, height: int, min_scale: float) → anyor an error

The best crop of the picture at path for a thumbnail width × height (only their shape counts here): { x, y, width, height, score } in the picture's own pixels. min_scale (0.5 to 1; 1 keeps as much of the picture as the shape allows) lets it zoom in on the subject.

smart_crop(path: str, out_path: str, width: int, height: int, min_scale: float) → anyor an error

The picture cropped to its interesting part and resized to exactly width × height, saved at out_path (PNG, JPEG or WebP by its extension; folders made): { path, crop: { x, y, width, height } }.

smart_crop_boxes(path: str, shapes: list) → listor an error

The best crops for several shapes at once ([[1, 1], [16, 9], [4, 5]]): a list of { shape, x, y, width, height }, for an image's social and card sizes.

Get it

$ quillo install smart_crop               # smart_crop.qwer into this folder$ quillo install smart_crop --example     # and its example, smart_crop.qwe

Or with curl:

$ curl -O https://quillolang.org/plugins/files/smart_crop.qwer$ curl -O https://quillolang.org/plugins/files/smart_crop.qwe$ curl -O https://quillolang.org/plugins/files/smart_crop_sample.png

Then, in your program, all of it or only what you call:

main.qwe
use smart_crop.qweruse smart_crop_box, smart_crop from smart_crop      // Quillo 0.6: only these

It uses these crates, which cargo fetches the first time the plugin is built:

[dependencies]
image = { version = "0.25", default-features = false, features = ["png", "jpeg", "webp", "gif"] }serde_json = { version = "1", features = ["preserve_order"] }

Example

smart_crop.qwe
// A recipe site's photos cut for every place they appear: square cards, a 16:9 banner and portrait stories, each on the interesting part and not just the middle.////   quillo run smart_crop.qwe      then open http://localhost:8080/cropsuse "smart_crop.qwer"let PHOTO = "smart_crop_sample.png"route GET /crops {    {        square: smart_crop_box(PHOTO, 200, 200, 1.0)?,        closer: smart_crop_box(PHOTO, 200, 200, 0.5)?,        shapes: smart_crop_boxes(PHOTO, [[1, 1], [16, 9], [4, 5]])?,        made: smart_crop(PHOTO, "data/smart_crop/card.jpg", 160, 160, 1.0)?,    }}route GET /card.jpg {    smart_crop(PHOTO, "data/smart_crop/card.jpg", 160, 160, 1.0)?    file("data/smart_crop/card.jpg")}route GET /face.png {    smart_crop(PHOTO, "data/smart_crop/face.png", 120, 120, 0.4)?    file("data/smart_crop/face.png")}
$ quillo run smart_crop.qwe$ curl 'localhost:8080/crops'$ curl 'localhost:8080/card.jpg'

Source

smart_crop.qwer
//! Thumbnails cropped to the interesting part of a picture, as smartcrop.js does: edges, skin tones and saturated colours scored, the best window of the shape you ask found, and the crop resized to the exact size; for avatars, cards and social sizes from any photo.//! version: 1.0.0//!//! [dependencies]//! image = { version = "0.25", default-features = false, features = ["png", "jpeg", "webp", "gif"] }//! serde_json = { version = "1", features = ["preserve_order"] }use image::imageops::FilterType;use image::{DynamicImage, GenericImageView};use serde_json::{json, Value};const ANALYSE: u32 = 256;struct Integral { w: usize, s: Vec<f64> }impl Integral {    fn new(w: usize, h: usize, v: &[f64]) -> Integral {        let mut s = vec![0.0; (w + 1) * (h + 1)];        for y in 0..h {            let mut row = 0.0;            for x in 0..w {                row += v[y * w + x];                s[(y + 1) * (w + 1) + x + 1] = s[y * (w + 1) + x + 1] + row;            }        }        Integral { w, s }    }    fn sum(&self, x0: usize, y0: usize, x1: usize, y1: usize) -> f64 {        let w = self.w + 1;        self.s[y1 * w + x1] - self.s[y0 * w + x1] - self.s[y1 * w + x0] + self.s[y0 * w + x0]    }}// How interesting each pixel of a small copy is: edges, skin and saturated colour.fn features(img: &DynamicImage) -> (usize, usize, Vec<f64>) {    let rgb = img.to_rgb8();    let (w, h) = (rgb.width() as usize, rgb.height() as usize);    let lum: Vec<f64> = rgb.pixels().map(|p| (0.2126 * p[0] as f64 + 0.7152 * p[1] as f64 + 0.0722 * p[2] as f64) / 255.0).collect();    let mut out = vec![0.0; w * h];    for y in 0..h {        for x in 0..w {            let i = y * w + x;            let c = lum[i];            let n = |dx: i64, dy: i64| { let (nx, ny) = ((x as i64 + dx).clamp(0, w as i64 - 1) as usize, (y as i64 + dy).clamp(0, h as i64 - 1) as usize); lum[ny * w + nx] };            let edge = (4.0 * c - n(-1, 0) - n(1, 0) - n(0, -1) - n(0, 1)).abs();            let p = rgb.get_pixel(x as u32, y as u32);            let (r, g, b) = (p[0] as f64 / 255.0, p[1] as f64 / 255.0, p[2] as f64 / 255.0);            let mag = (r * r + g * g + b * b).sqrt().max(1e-6);            // Distance from a skin tone's direction (smartcrop.js's [0.78, 0.57, 0.44]).            let d = ((r / mag - 0.78).powi(2) + (g / mag - 0.57).powi(2) + (b / mag - 0.44).powi(2)).sqrt();            let skin = if c > 0.2 && c < 1.0 && d < 0.3 { (1.0 - d / 0.3) * 1.0 } else { 0.0 };            let (mx, mn) = (r.max(g).max(b), r.min(g).min(b));            let l = (mx + mn) / 2.0;            let sat = if mx == mn { 0.0 } else if l > 0.5 { (mx - mn) / (2.0 - mx - mn) } else { (mx - mn) / (mx + mn) };            let satf = if sat > 0.4 && c > 0.05 && c < 0.9 { (sat - 0.4) / 0.6 } else { 0.0 };            out[i] = edge * 4.0 + skin * 1.8 + satf * 0.3;        }    }    (w, h, out)}fn best_box(path: &str, tw: f64, th: f64, min_scale: f64) -> Result<(DynamicImage, [u32; 4], f64), String> {    if tw <= 0.0 || th <= 0.0 { return Err("the width and height are above 0".into()) }    let img = image::open(path).map_err(|e| format!("{}: {}", path, e))?;    let (ow, oh) = img.dimensions();    let small = img.thumbnail(ANALYSE, ANALYSE);    let (w, h, f) = features(&small);    let k = ow as f64 / w as f64;    let integ = Integral::new(w, h, &f);    let total = integ.sum(0, 0, w, h).max(1e-9);    // The largest window of the asked shape, then smaller ones down to `min_scale`.    let ratio = tw / th;    let (mut cw, mut ch) = (w as f64, w as f64 / ratio);    if ch > h as f64 { ch = h as f64; cw = h as f64 * ratio }    let mut best = (0.0f64, [0usize; 4]);    let mut scale = 1.0;    while scale >= min_scale - 1e-9 {        let (sw, sh) = (((cw * scale).round() as usize).max(1).min(w), ((ch * scale).round() as usize).max(1).min(h));        let step = ((sw.min(sh) / 16).max(1)).min(8);        let mut y = 0;        while y + sh <= h {            let mut x = 0;            while x + sw <= w {                let all = integ.sum(x, y, x + sw, y + sh);                let (ix, iy) = (sw / 6, sh / 6);                let inner = integ.sum(x + ix, y + iy, x + sw - ix, y + sh - iy);                // How much of what matters the crop holds for its size (as smartcrop.js scores), its middle counting most.                let score = (inner + 0.5 * (all - inner)) / (sw * sh) as f64 * (w * h) as f64 / total;                if score > best.0 { best = (score, [x, y, sw, sh]) }                x += step;            }            y += step;        }        scale -= 0.1;    }    let [x, y, sw, sh] = best.1;    let bx = ((x as f64 * k).round() as u32).min(ow - 1);    let by = ((y as f64 * k).round() as u32).min(oh - 1);    let bw = ((sw as f64 * k).round() as u32).min(ow - bx).max(1);    let bh = ((sh as f64 * k).round() as u32).min(oh - by).max(1);    Ok((img, [bx, by, bw, bh], best.0))}/// The best crop of the picture at `path` for a thumbnail `width` × `height` (only their shape counts here): `{ x, y, width, height, score }` in the picture's own pixels. `min_scale` (0.5 to 1; 1 keeps as much of the picture as the shape allows) lets it zoom in on the subject.pub fn smart_crop_box(path: &str, width: i64, height: i64, min_scale: f64) -> Result<Value, String> {    let (_, b, s) = best_box(path, width as f64, height as f64, min_scale.clamp(0.3, 1.0))?;    Ok(json!({ "x": b[0], "y": b[1], "width": b[2], "height": b[3], "score": (s * 1000.0).round() / 1000.0 }))}/// The picture cropped to its interesting part and resized to exactly `width` × `height`, saved at `out_path` (PNG, JPEG or WebP by its extension; folders made): `{ path, crop: { x, y, width, height } }`.pub fn smart_crop(path: &str, out_path: &str, width: i64, height: i64, min_scale: f64) -> Result<Value, String> {    if width <= 0 || height <= 0 || width > 10_000 || height > 10_000 { return Err("width and height are 1 to 10000 pixels".into()) }    let (img, b, _) = best_box(path, width as f64, height as f64, min_scale.clamp(0.3, 1.0))?;    let out = img.crop_imm(b[0], b[1], b[2], b[3]).resize_exact(width as u32, height as u32, FilterType::Lanczos3);    if let Some(d) = std::path::Path::new(out_path).parent().filter(|d| !d.as_os_str().is_empty()) { std::fs::create_dir_all(d).map_err(|e| e.to_string())?; }    let lower = out_path.to_lowercase();    let res = if lower.ends_with(".jpg") || lower.ends_with(".jpeg") { DynamicImage::ImageRgb8(out.to_rgb8()).save(out_path) } else { out.save(out_path) };    res.map_err(|e| format!("{}: {}", out_path, e))?;    Ok(json!({ "path": out_path, "crop": { "x": b[0], "y": b[1], "width": b[2], "height": b[3] } }))}/// The best crops for several shapes at once (`[[1, 1], [16, 9], [4, 5]]`): a list of `{ shape, x, y, width, height }`, for an image's social and card sizes.pub fn smart_crop_boxes(path: &str, shapes: Vec<Vec<i64>>) -> Result<Vec<Value>, String> {    shapes.iter().map(|s| {        if s.len() != 2 { return Err("each shape is [width, height]".to_string()) }        let (_, b, _) = best_box(path, s[0] as f64, s[1] as f64, 1.0)?;        Ok(json!({ "shape": format!("{}:{}", s[0], s[1]), "x": b[0], "y": b[1], "width": b[2], "height": b[3] }))    }).collect()}
Thread
78Useful? Like it, or add to its thread below.

Thread 0 posts

On Smart crop: questions, tips, corrections, and code that uses it.

No posts yet. The first one opens the thread.