Something went wrong. Try again.
iterative image reconstruction using random cubic bézier strokes, accelerated on metal
metal graphics rust
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120fn usage() -> String { [ "splined: iterative image reconstruction with random cubic bézier strokes (metal-accelerated)", "", "usage: splined <input> [args]", "", "args:", " -n, --number <u32> max splines to draw (default: (w*h)^0.7)", " -b, --batch <u32> batch size per gpu step (default: 32)", " -s, --seed <u64> rng seed (default: 0)", " --max-gpu <f32> max gpu usage in (0, 1] (default: 1.0)", " -l, --log <0..3> logging level (default: 1)", " -o, --output <path> output file or dir (default: output.png)", " -c, --current <path> current canvas image to resume from (single-file only)", " --nth <u32> save every nth accepted stroke (uses -o as dir)", " --bg <avg|r,g,b> initial canvas color (default: avg)", " -a, --alpha <f32> stroke alpha in [0, 1] (default: 1)", " --min-accept-ratio <f32> stagnant if accepted < batch*ratio (default: 0.02)", " --max-stagnant-batches <u32> stop after this many stagnant batches (default: 10)", "", "input:", " - file: writes one image to -o/--output (default: output.png)", " - dir: -o/--output must be a dir; mirrors input tree under it", " - --nth: saves frames to output dir every nth accepted stroke; also writes final.png", "", "examples:", " splined in.png -o out.png", " splined in.png -n 5000 -b 64 -s 42 -o out.png", " splined in.png --nth 50 -o frames/", " splined images/ -o results/ -n 5000 -b 64 -s 42 --nth 50", "", ].join("\n") }
#[derive(Clone, Debug)]pub struct Config { pub input : String , pub number : Option<u32> , pub batch : u32 , pub min_accept_ratio : f32 , pub seed : u64 , pub max_gpu : f32 , pub log : u8 , pub output : String , pub output_provided : bool , pub current : Option<String> , pub nth : Option<u32> , pub bg : Bg , pub alpha : f32 , pub max_stagnant_batches : u32 }
#[derive(Clone, Copy, Debug)]pub enum Bg { Avg , RgbU8 { r: u8, g: u8, b: u8 } }
impl Config { pub fn parse(argv: &[String]) -> Result<Self, String> { if argv.len() < 2 { return Err(usage()); }
let mut input : Option<String> = None; let mut number : Option<u32> = None; let mut batch : u32 = 32; let mut min_accept_ratio : Option<f32> = None; let mut seed : u64 = 0; let mut max_gpu : f32 = 1.0; let mut log : u8 = 1; let mut output : String = "output.png".to_string(); let mut output_provided : bool = false; let mut current : Option<String> = None; let mut nth : Option<u32> = None; let mut bg : Bg = Bg::Avg; let mut alpha : f32 = 1.0; let mut max_stagnant_batches : u32 = 10;
let mut i = 1usize; while i < argv.len() { let a = argv[i].as_str(); if !a.starts_with('-') && input.is_none() { input = Some(argv[i].clone()); i += 1; continue; }
match a { "-n" | "--number" => { i += 1; number = Some(parse_val(argv, i, a)?); i += 1; } "-b" | "--batch" => { i += 1; batch = parse_val(argv, i, a)?; i += 1; } "--min-accept-ratio" => { i += 1; min_accept_ratio = Some(parse_val(argv, i, a)?); i += 1; } "-s" | "--seed" => { i += 1; seed = parse_val(argv, i, a)?; i += 1; } "--max-gpu" => { i += 1; max_gpu = parse_val(argv, i, a)?; i += 1; } "-l" | "--log" => { i += 1; log = parse_val(argv, i, a)?; i += 1; } "-o" | "--output" => { i += 1; output = parse_val(argv, i, a)?; output_provided = true; i += 1; } "-c" | "--current" => { i += 1; current = Some(parse_val(argv, i, a)?); i += 1; } "--nth" => { i += 1; nth = Some(parse_val(argv, i, a)?); i += 1; } "--bg" => { i += 1; bg = parse_bg(argv, i, a)?; i += 1; } "-a" | "--alpha" => { i += 1; alpha = parse_val(argv, i, a)?; i += 1; } "--max-stagnant-batches" => { i += 1; max_stagnant_batches = parse_val(argv, i, a)?; i += 1; } "-h" | "--help" => return Err(usage()), _ => return Err(format!("unknown arg: {a}\n\n{}", usage())), } }
let Some(input) = input else { return Err(usage()); };
if batch == 0 { return Err("batch must be > 0".to_string()); }
let min_accept_ratio = min_accept_ratio.unwrap_or(0.02);
if !(0.0..=1.0).contains(&min_accept_ratio) { return Err("min-accept-ratio must be in [0, 1]".to_string()); } if log > 3 { return Err("log must be in [0, 3]".to_string()); } if !(0.0 < max_gpu && max_gpu <= 1.0) { return Err("max-gpu must be in (0, 1]".to_string()); } if let Some(nth) = nth { if nth == 0 { return Err("nth must be > 0".to_string()); } } if !(0.0..=1.0).contains(&alpha) { return Err("alpha must be in [0, 1]".to_string()); } if max_stagnant_batches == 0 { return Err("max-stagnant-batches must be > 0".to_string()); }
Ok(Self { input, number, batch, min_accept_ratio, seed, max_gpu, log, output, output_provided, current, nth, bg, alpha, max_stagnant_batches, }) } }
fn parse_val<T: std::str::FromStr>(argv: &[String], idx: usize, flag: &str) -> Result<T, String> { let s = argv.get(idx).ok_or_else(|| format!("missing value for {flag}"))?; s.parse::<T>().map_err(|_| format!("invalid value for {flag}: {s}")) }
fn parse_bg(argv: &[String], idx: usize, flag: &str) -> Result<Bg, String> { let s = argv.get(idx).ok_or_else(|| format!("missing value for {flag}"))?; if s == "avg" { return Ok(Bg::Avg); } let parts: Vec<_> = s.split(',').collect(); if parts.len() != 3 { return Err(format!("invalid bg, expected avg or r,g,b: {s}")); } let parse = |i: usize| parts[i].parse::<u8>().map_err(|_| format!("invalid bg component: {s}")); Ok(Bg::RgbU8 { r: parse(0)?, g: parse(1)?, b: parse(2)? }) }