perf: only calculate average color once per tile
This commit is contained in:
5
.gitignore
vendored
5
.gitignore
vendored
@@ -4,5 +4,8 @@
|
|||||||
*.jpg
|
*.jpg
|
||||||
*.jpeg
|
*.jpeg
|
||||||
*.gif
|
*.gif
|
||||||
|
*.heic
|
||||||
|
*.mp4
|
||||||
*.webm
|
*.webm
|
||||||
/.mypy_cache/
|
/.mypy_cache/
|
||||||
|
tiles/
|
||||||
|
18
convert_heic.py
Normal file
18
convert_heic.py
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
from pathlib import Path
|
||||||
|
from subprocess import run
|
||||||
|
|
||||||
|
from send2trash import send2trash
|
||||||
|
from tqdm import tqdm
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> None:
|
||||||
|
files = list(Path("tiles").glob("*.heic"))
|
||||||
|
with tqdm(files) as pbar:
|
||||||
|
for file in pbar:
|
||||||
|
pbar.set_description(f"Converting {file}")
|
||||||
|
run(("magick", file, file.with_suffix(".jpg")))
|
||||||
|
send2trash(file)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
45
src/main.rs
45
src/main.rs
@@ -1,17 +1,14 @@
|
|||||||
use std::collections::*;
|
use std::collections::{HashMap, HashSet};
|
||||||
use std::fs;
|
use std::fs;
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
|
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
use image::{DynamicImage, GenericImage, GenericImageView, Pixel, Rgba};
|
use image::{DynamicImage, GenericImage, GenericImageView, Pixel, Rgba};
|
||||||
use indicatif::{ParallelProgressIterator, ProgressBar, ProgressStyle};
|
use indicatif::{ParallelProgressIterator, ProgressBar, ProgressIterator, ProgressStyle};
|
||||||
use noisy_float::prelude::*;
|
use noisy_float::prelude::*;
|
||||||
use rayon::prelude::*;
|
use rayon::prelude::*;
|
||||||
use structopt::StructOpt;
|
use structopt::StructOpt;
|
||||||
|
|
||||||
/// How big each square tile of the mosaic will be (in pixels)
|
|
||||||
const TILE_SIDE: u32 = 26;
|
|
||||||
|
|
||||||
/// Calculate the average color of a given image by averaging all of its pixels together (including alpha)
|
/// Calculate the average color of a given image by averaging all of its pixels together (including alpha)
|
||||||
fn average_color(image: &DynamicImage) -> Rgba<u8> {
|
fn average_color(image: &DynamicImage) -> Rgba<u8> {
|
||||||
let pixel_count = image.width() as f64 * image.height() as f64;
|
let pixel_count = image.width() as f64 * image.height() as f64;
|
||||||
@@ -43,25 +40,29 @@ fn distance(pixel1: Rgba<u8>, pixel2: Rgba<u8>) -> R64 {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Choose the image in the given tileset whose average color is closest to the given pixel
|
/// Choose the image in the given tileset whose average color is closest to the given pixel
|
||||||
fn pick_image_for_pixel(pixel: Rgba<u8>, possible_tiles: &[DynamicImage]) -> Option<&DynamicImage> {
|
fn pick_image_for_pixel(
|
||||||
|
pixel: Rgba<u8>,
|
||||||
|
possible_tiles: &[(DynamicImage, Rgba<u8>)],
|
||||||
|
) -> Option<&DynamicImage> {
|
||||||
possible_tiles
|
possible_tiles
|
||||||
.into_par_iter()
|
.into_par_iter()
|
||||||
.min_by_key(|&img| distance(average_color(img), pixel))
|
.min_by_key(|(_img, avg)| distance(*avg, pixel))
|
||||||
|
.map(|(img, _avg)| img)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Load the tiles from the given directory
|
/// Load the tiles from the given directory
|
||||||
fn load_images<P: AsRef<Path>>(dir: P) -> Result<Vec<DynamicImage>> {
|
fn load_images<P: AsRef<Path>>(dir: P, tile_side: u32) -> Result<Vec<(DynamicImage, Rgba<u8>)>> {
|
||||||
let dir = fs::read_dir(dir)?.collect::<Result<Vec<_>, _>>()?;
|
let dir = fs::read_dir(dir)?.collect::<Result<Vec<_>, _>>()?;
|
||||||
let len = dir.len();
|
let len = dir.len();
|
||||||
Ok(dir
|
Ok(dir
|
||||||
.into_par_iter()
|
.into_par_iter()
|
||||||
.progress_with(make_pbar("images loaded", len as _))
|
.progress_with(make_pbar("images loaded", len as _))
|
||||||
.filter_map(|entry| {
|
.filter_map(|entry| {
|
||||||
Some(
|
let img = image::open(entry.path())
|
||||||
image::open(entry.path())
|
.ok()?
|
||||||
.ok()?
|
.thumbnail_exact(tile_side, tile_side);
|
||||||
.thumbnail_exact(TILE_SIDE, TILE_SIDE),
|
let avg = average_color(&img);
|
||||||
)
|
Some((img, avg))
|
||||||
})
|
})
|
||||||
.collect::<Vec<_>>())
|
.collect::<Vec<_>>())
|
||||||
}
|
}
|
||||||
@@ -92,10 +93,14 @@ struct Opt {
|
|||||||
#[structopt(short, long, parse(from_os_str), default_value = "mosaic.png")]
|
#[structopt(short, long, parse(from_os_str), default_value = "mosaic.png")]
|
||||||
output: PathBuf,
|
output: PathBuf,
|
||||||
|
|
||||||
/// The side length that the image to turn into to a mosaic will be resized to
|
/// The side length that the target image'll be resized to.
|
||||||
#[structopt(short, long, default_value = "128")]
|
#[structopt(short, long, default_value = "128")]
|
||||||
mosaic_size: u32,
|
mosaic_size: u32,
|
||||||
|
|
||||||
|
/// The side length of each tile.
|
||||||
|
#[structopt(short, long, default_value = "26")]
|
||||||
|
tile_size: u32,
|
||||||
|
|
||||||
/// Keep the image's aspect ratio
|
/// Keep the image's aspect ratio
|
||||||
#[structopt(short, long)]
|
#[structopt(short, long)]
|
||||||
keep_aspect_ratio: bool,
|
keep_aspect_ratio: bool,
|
||||||
@@ -106,11 +111,12 @@ fn main() -> Result<()> {
|
|||||||
image,
|
image,
|
||||||
tiles_directory,
|
tiles_directory,
|
||||||
mosaic_size,
|
mosaic_size,
|
||||||
|
tile_size,
|
||||||
keep_aspect_ratio,
|
keep_aspect_ratio,
|
||||||
output,
|
output,
|
||||||
} = Opt::from_args();
|
} = Opt::from_args();
|
||||||
|
|
||||||
let possible_tiles = load_images(tiles_directory)?;
|
let possible_tiles = load_images(tiles_directory, tile_size)?;
|
||||||
let image = image::open(image)?;
|
let image = image::open(image)?;
|
||||||
let image = if keep_aspect_ratio {
|
let image = if keep_aspect_ratio {
|
||||||
image.thumbnail(mosaic_size, mosaic_size)
|
image.thumbnail(mosaic_size, mosaic_size)
|
||||||
@@ -134,9 +140,12 @@ fn main() -> Result<()> {
|
|||||||
.collect::<HashMap<_, _>>();
|
.collect::<HashMap<_, _>>();
|
||||||
|
|
||||||
// Apply the mapping previously calculated and save the mosaic
|
// Apply the mapping previously calculated and save the mosaic
|
||||||
let mut mosaic = DynamicImage::new_rgba8(image.width() * TILE_SIDE, image.height() * TILE_SIDE);
|
let mut mosaic = DynamicImage::new_rgba8(image.width() * tile_size, image.height() * tile_size);
|
||||||
for (x, y, pixel) in image.pixels() {
|
for (x, y, pixel) in image.pixels().progress_with(make_pbar(
|
||||||
mosaic.copy_from(&**tiles.get(&pixel).unwrap(), x * TILE_SIDE, y * TILE_SIDE)?;
|
"actual pixels",
|
||||||
|
u64::from(image.width() * image.height()),
|
||||||
|
)) {
|
||||||
|
mosaic.copy_from(&**tiles.get(&pixel).unwrap(), x * tile_size, y * tile_size)?;
|
||||||
}
|
}
|
||||||
mosaic.save(output)?;
|
mosaic.save(output)?;
|
||||||
|
|
||||||
|
Reference in New Issue
Block a user