timelapse_merger/src/main.rs

298 lines
9.8 KiB
Rust

use image;
use std::cmp::max;
use std::cmp::min;
use std::env;
use std::fmt;
use std::fs;
use std::path;
use std::process;
fn print_usage() {
let args: Vec<String> = env::args().collect();
eprintln!("Usage:");
eprintln!(
" {} <png_out> <folder_in> <swipe_factor> <effect> <image_samples>",
args[0]
);
eprintln!(" swipe_factor {{1~-1}}");
eprintln!(" effect {{harsh|dissolve|dissolve:<exponent>}}");
eprintln!(" image_samples {{1~...}} 1=all");
}
#[derive(PartialEq)]
enum EffectEnum {
HARSH,
DISSOLVE,
}
impl fmt::Display for EffectEnum {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match *self {
EffectEnum::HARSH => write!(f, "HARSH"),
EffectEnum::DISSOLVE => write!(f, "DISSOLVE"),
}
}
}
trait Interpolator {
fn interpolate(&self, d1: f64, d2: f64, v1: u8, v2: u8) -> u8;
}
struct IntegerInterpolator {}
struct PowerInterpolator {
exponent: f64,
}
impl Interpolator for IntegerInterpolator {
fn interpolate(&self, d1: f64, d2: f64, v1: u8, v2: u8) -> u8 {
return interpolate_nearest(d1, d2, v1, v2);
}
}
impl Interpolator for PowerInterpolator {
fn interpolate(&self, d1: f64, d2: f64, v1: u8, v2: u8) -> u8 {
return interpolate_power(d1, d2, v1, v2, self.exponent);
}
}
impl From<&str> for IntegerInterpolator {
fn from(_: &str) -> IntegerInterpolator {
IntegerInterpolator {}
}
}
impl From<&str> for PowerInterpolator {
fn from(arg: &str) -> PowerInterpolator {
let power: f64 = if arg == "" {
1.0
} else {
match arg.parse() {
Ok(n) => n,
Err(_) => {
eprintln!("error: \"dissolve:<exponent>\" argument not a number (error 10: input_should_be_a_float_number)");
process::exit(10);
}
}
};
PowerInterpolator { exponent: power }
}
}
fn interpolate_nearest(d1: f64, d2: f64, v1: u8, v2: u8) -> u8 {
if d1 >= d2 {
return v1;
} else {
return v2;
}
}
fn interpolate_power(d1: f64, d2: f64, v1: u8, v2: u8, power: f64) -> u8 {
if v1 == v2 {
return v1;
}
let ds: f64 = d1.abs() + d2.abs();
let a1: f64 = (d1.abs() / ds).powf(power);
let a2: f64 = (d2.abs() / ds).powf(power);
let bs: f64 = a1.abs() + a2.abs();
let b1: f64 = a1.abs() / bs;
let b2: f64 = a2.abs() / bs;
let px: u8 = ((v1 as f64) * b1 + (v2 as f64) * b2).round() as u8;
return px;
}
fn main() {
let args: Vec<String> = env::args().collect();
match args.len() {
6 => {
let png_out: &path::Path = path::Path::new(&args[1]);
let folder_in: &path::Path = path::Path::new(&args[2]);
if !folder_in.exists() {
eprintln!(
"error: {:?} does not exist (error 2: input_path_should_exist)",
folder_in
);
eprintln!("error: <folder_in> argument should exist");
print_usage();
process::exit(2);
};
let swipe_factor_untreated: f64 = match args[3].parse() {
Ok(n) => n,
Err(_) => {
eprintln!("error: <swipe_factor> argument not a number (error 3: input_should_be_a_float_number)");
print_usage();
process::exit(3);
}
};
let swipe_factor: f64 = if swipe_factor_untreated > 1.0 {
1.0
} else if swipe_factor_untreated < -1.0 {
-1.0
} else {
swipe_factor_untreated
};
let arg_effect_raw: &str = args[4].as_str();
let arg_effect_raw_split: (&str, &str) = match arg_effect_raw.find(":") {
Some(n) => arg_effect_raw.split_at(n),
None => (arg_effect_raw, ""),
};
let effect: (EffectEnum, &str) = match arg_effect_raw_split.0 {
"harsh" => (EffectEnum::HARSH, &arg_effect_raw_split.1[1..]),
"dissolve" => (EffectEnum::DISSOLVE, &arg_effect_raw_split.1[1..]),
_ => {
eprintln!("error: <effect> argument not supported (error 4: input_should_be_already_enumerated)");
print_usage();
process::exit(4);
}
};
let mut reversed_image_order = false;
let image_samples: i64 = match args[5].parse() {
Ok(n) => {
if n == 0 {
eprintln!("warning: <image_samples> was increased to 1 (error 5: input_out_of_range)");
1
} else if n < 0 {
reversed_image_order = true;
(n as i64).abs()
} else {
n
}
}
Err(_) => {
eprintln!("error: <image_samples> argument not a number (error 6: input_should_be_an_integer_number)");
print_usage();
process::exit(6);
}
};
process::exit(main_parsed(
png_out,
folder_in,
swipe_factor,
effect,
image_samples,
reversed_image_order,
));
}
_ => {
print_usage();
process::exit(1);
}
}
}
fn main_parsed(
png_out: &path::Path,
folder_in: &path::Path,
swipe_factor: f64,
effect: (EffectEnum, &str),
mut image_samples: i64,
reversed_image_order: bool,
) -> i32 {
let mut folder_in_files_: Vec<path::PathBuf> = folder_in
.read_dir()
.unwrap()
.map(|it| it.unwrap().path())
.filter(|it| fs::metadata(it).unwrap().is_file())
.collect();
folder_in_files_.sort();
let folder_in_files: Vec<&path::PathBuf> = folder_in_files_.iter().collect();
if image_samples == 1 {
image_samples = folder_in_files.len() as i64;
}
if folder_in_files.len() == 0 {
eprintln!("error: There are no images in specified folder. (error 7: empty_input)");
return 7;
}
let mut sample_spacing_: i64 = (folder_in_files.len() as i64) / image_samples;
if sample_spacing_ == 0 {
eprintln!("warning: There are less images available than the requested sample; reducing sample size (error 8: image_samples__lt__files_len)");
image_samples = folder_in_files.len() as i64;
sample_spacing_ = 1;
}
let sample_spacing: i64 = max(1, sample_spacing_);
let sample_padding: i64 =
((folder_in_files.len() as i64) - (sample_spacing * image_samples)) / 2;
let samples: Vec<&&path::PathBuf> = folder_in_files
.iter()
.enumerate()
.filter(|e| ((e.0 as i64) % sample_spacing) == sample_padding)
.map(|e| e.1)
.collect();
let mut dimensions: Vec<(u32, u32)> = samples
.iter()
.map(|sample_path| image::image_dimensions(sample_path).unwrap())
.collect();
dimensions.dedup();
if dimensions.len() != 1 {
eprintln!("error: Found images with different sizes (error 9: image_size_consistency)");
return 9;
}
let dimension: (u32, u32) = dimensions[0];
let output_canvas: image::RgbaImage = image::RgbaImage::new(dimension.0, dimension.1);
let mut sample_images: Vec<image::RgbaImage> = samples
.iter()
.map(|path| image::open(path).unwrap().to_rgba())
.collect();
if reversed_image_order {
sample_images.reverse();
}
let effect_arg: &str = effect.1;
let final_func = |effect_fn| {
fill_image(
png_out,
output_canvas,
sample_images,
swipe_factor,
effect_fn,
)
};
return match effect.0 {
EffectEnum::HARSH => final_func(&IntegerInterpolator::from(effect_arg)),
EffectEnum::DISSOLVE => final_func(&PowerInterpolator::from(effect_arg)),
};
}
fn fill_image(
png_out: &path::Path,
mut output_canvas: image::RgbaImage,
sample_images: Vec<image::RgbaImage>,
swipe_factor: f64,
effect: &dyn Interpolator,
) -> i32 {
let max_dimens: (u32, u32) = output_canvas.dimensions();
let dx = max_dimens.0 as i64;
let dy = max_dimens.1 as i64;
let y_pixels_spacing: i64 = dy / ((sample_images.len() - 1) as i64);
for x in 0..max_dimens.0 {
let y_displacement: i64 = ((((x as i64) - (dx / 2)) as f64) * swipe_factor).round() as i64;
for y in 0..max_dimens.1 {
let y_displaced: i64 = y as i64 + y_displacement;
let positioning: f64 = (y_displaced as f64) / (y_pixels_spacing as f64);
let positioning_max_: usize = positioning.ceil() as usize;
let positioning_min_: usize = positioning.floor() as usize;
let bounded = |val| limit_bound(val, 0, sample_images.len() - 1);
let positioning_min: usize = bounded(positioning_min_);
let positioning_max: usize = bounded(positioning_max_);
let spxu = sample_images[positioning_max].get_pixel(x, y);
let spxl = sample_images[positioning_min].get_pixel(x, y);
let this_px = output_canvas.get_pixel_mut(x, y);
for i in 0..4 {
this_px[i] = effect.interpolate(
(positioning_max_ as f64 - positioning) as f64,
(positioning - positioning_min_ as f64) as f64,
spxl[i],
spxu[i],
)
}
}
}
output_canvas
.save_with_format(png_out, image::ImageFormat::Png)
.unwrap();
0
}
fn limit_bound<T: Ord>(value: T, lower: T, upper: T) -> T {
min(upper, max(value, lower))
}