Compare commits

..

2 commits

Author SHA1 Message Date
Logan 2e467aca96 Changed into a CGI script 2024-08-25 01:20:52 -05:00
Logan 042055273f test 2024-06-09 01:05:23 -05:00
18 changed files with 814 additions and 617 deletions

1158
Cargo.lock generated

File diff suppressed because it is too large Load diff

View file

@ -6,19 +6,15 @@ edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies] [dependencies]
log = "0.4" image = "0.25"
image = "0.24" gif = "0.13"
gif = "0.12"
futures = "0.3" futures = "0.3"
wgpu = {version="0.18", features=["webgl"]} tokio = {version="1.37", features=["full"]}
wgpu = {version="22"}
bytemuck = {version="1.14", features=["derive"]} bytemuck = {version="1.14", features=["derive"]}
futures-intrusive = "0.5" futures-intrusive = "0.5"
pollster = "0.3" rust-cgi = "0.7"
clap = { version = "4.4.16", features = ["derive"] }
indicatif = "0.17"
[dev-dependencies]
flexi_logger = "0.27"
[profile.dev] [profile.dev]
opt-level=1 opt-level=1

View file

@ -1,11 +0,0 @@
# Fishbowl
This program turns images into animated physics simulations using
verlet integration. Image processing is done using WGPU,
a low level GPU rendering interface. It accepts most common image
formats, and can produce animations of variable quality and size.
## Web demo
Fishbowl is intended to be used as a server-side API that allows users
to submit images. However, the previous API was prone to crashing when bots
flooded the server with malformed images and requests. Fishbowl will return
soon to the new Apache server set up when I have the time to migrate it

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.7 MiB

BIN
resources/cat.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 86 KiB

BIN
resources/dog.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

BIN
resources/goback.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 326 KiB

BIN
resources/skull.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 MiB

BIN
resources/starry.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1 MiB

BIN
resources/starved.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 54 KiB

BIN
resources/stoic.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 MiB

View file

@ -0,0 +1,5 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 107.62 107.62"><defs><style>
.cls-1 {
fill: #654ff0;
}
</style></defs><title>web-assembly-icon</title><g id="Layer_2" data-name="Layer 2"><g id="Notch_-_Purple" data-name="Notch - Purple"><g id="icon"><path class="cls-1" d="M66.12,0c0,.19,0,.38,0,.58a12.34,12.34,0,1,1-24.68,0c0-.2,0-.39,0-.58H0V107.62H107.62V0ZM51.38,96.1,46.14,70.17H46L40.39,96.1H33.18L25,58h7.13L37,83.93h.09L42.94,58h6.67L54.9,84.25H55L60.55,58h7L58.46,96.1Zm39.26,0-2.43-8.48H75.4L73.53,96.1H66.36L75.59,58H86.83L98,96.1Z"/><polygon class="cls-1" points="79.87 67.39 76.76 81.37 86.44 81.37 82.87 67.39 79.87 67.39"/></g></g></g></svg>

After

Width:  |  Height:  |  Size: 685 B

BIN
resources/wimpy.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 401 KiB

View file

@ -118,7 +118,7 @@ pub struct QuickDraw {
} }
impl QuickDraw { impl QuickDraw {
pub async fn new(width: u32, height: u32, max_circles: u64) -> Self { pub async fn new(width: u32, height: u32, max_circles: u64) -> Option<Self> {
let instance = wgpu::Instance::new(wgpu::InstanceDescriptor { let instance = wgpu::Instance::new(wgpu::InstanceDescriptor {
backends: wgpu::Backends::all(), backends: wgpu::Backends::all(),
..Default::default() ..Default::default()
@ -135,7 +135,7 @@ impl QuickDraw {
Some(a) => a, Some(a) => a,
None => { None => {
eprintln!("Could not find WGPU adapter"); eprintln!("Could not find WGPU adapter");
std::process::exit(1); return None;
}, },
}; };
let (device, queue) = let (device, queue) =
@ -144,7 +144,7 @@ impl QuickDraw {
Err(e) => { Err(e) => {
eprintln!("Could not find WGPU device:"); eprintln!("Could not find WGPU device:");
eprintln!("{}", e); eprintln!("{}", e);
std::process::exit(1); return None;
}, },
}; };
@ -244,14 +244,17 @@ impl QuickDraw {
multiview: None, multiview: None,
label: Some("Render Pipeline"), label: Some("Render Pipeline"),
layout: Some(&render_pipeline_layout), layout: Some(&render_pipeline_layout),
cache: None,
vertex: wgpu::VertexState { vertex: wgpu::VertexState {
module: &vert, module: &vert,
entry_point: "vs_main", entry_point: "vs_main",
buffers: &[Vertex::desc(), Circle::desc()], buffers: &[Vertex::desc(), Circle::desc()],
compilation_options: PipelineCompilationOptions::default(),
}, },
fragment: Some(wgpu::FragmentState { fragment: Some(wgpu::FragmentState {
module: &frag, module: &frag,
entry_point: "fs_main", entry_point: "fs_main",
compilation_options: PipelineCompilationOptions::default(),
targets: &[Some(wgpu::ColorTargetState { targets: &[Some(wgpu::ColorTargetState {
format: texture_desc.format, format: texture_desc.format,
blend: Some(wgpu::BlendState::ALPHA_BLENDING), blend: Some(wgpu::BlendState::ALPHA_BLENDING),
@ -277,7 +280,7 @@ impl QuickDraw {
}, },
}); });
Self { Some(Self {
device, device,
queue, queue,
width, width,
@ -292,7 +295,7 @@ impl QuickDraw {
output_buffer, output_buffer,
vertex_buffer, vertex_buffer,
pipeline, pipeline,
} })
} }
pub async fn resize(&mut self, width: u32, height: u32, max_circles: usize) { pub async fn resize(&mut self, width: u32, height: u32, max_circles: usize) {

View file

@ -1,23 +1,15 @@
#![feature(future_join)] #![feature(future_join)]
use std::{cell::RefCell, future::join, io::Write, path::Path, sync::Arc}; use std::{io::Cursor, sync::Arc};
use tokio::sync::Mutex;
use draw::QuickDraw; use draw::QuickDraw;
use gif::Frame; use gif::Frame;
use image::{ImageBuffer, Rgb}; use image::{ImageBuffer, Rgb};
use indicatif::{ProgressBar, ProgressStyle};
use log::debug;
pub mod draw; pub mod draw;
pub mod helper; pub mod helper;
pub mod sim; pub mod sim;
pub mod tests;
pub fn make_progress(msg: &'static str, max: u64) -> ProgressBar {
let progress = ProgressBar::new(max);
progress.set_style(ProgressStyle::with_template("{msg} :: {bar}").unwrap());
progress.set_message(msg);
progress
}
const WIDTH: f32 = 512.0; const WIDTH: f32 = 512.0;
const HEIGHT: f32 = 512.0; const HEIGHT: f32 = 512.0;
@ -27,21 +19,21 @@ pub async fn preprocess(
radius: f32, radius: f32,
) -> (sim::Simulation, usize, usize) { ) -> (sim::Simulation, usize, usize) {
let (sim, it, max_circles) = let (sim, it, max_circles) =
sim::Simulation::simulate_image(WIDTH, HEIGHT, radius, image).await; sim::Simulation::simulate_image(WIDTH, HEIGHT, radius, image);
(sim, it, max_circles) (sim, it, max_circles)
} }
pub async fn simulate( pub async fn simulate(
draw: &mut QuickDraw, draw: Arc<Mutex<QuickDraw>>,
mut sim: Simulation, mut sim: Simulation,
it: usize, it: usize,
step: usize, step: usize,
max_circles: usize, max_circles: usize,
) -> Vec<Frame<'static>> { ) -> Vec<Frame<'static>> {
let _draw = draw;
let mut draw = _draw.lock().await;
draw.resize(WIDTH as u32, HEIGHT as u32, max_circles).await; draw.resize(WIDTH as u32, HEIGHT as u32, max_circles).await;
let mut frames = vec![]; let mut frames = vec![];
let progress =
make_progress("Simulating ", ((it - sim.clock) / sim.substeps) as u64);
while sim.clock < it { while sim.clock < it {
let circles = &sim let circles = &sim
.circles .circles
@ -52,14 +44,12 @@ pub async fn simulate(
color: [c.color.0, c.color.1, c.color.2, 255], color: [c.color.0, c.color.1, c.color.2, 255],
}) })
.collect::<Vec<draw::Circle>>(); .collect::<Vec<draw::Circle>>();
let bytes_future = draw.draw_circles(circles); sim.steps(step);
let steps = sim.steps(step); let mut bytes = draw.draw_circles(circles).await;
let mut bytes = join!(bytes_future, steps).await.0; let frame =
let frame = gif::Frame::from_rgba(WIDTH as u16, HEIGHT as u16, &mut bytes); gif::Frame::from_rgba_speed(WIDTH as u16, HEIGHT as u16, &mut bytes, 10);
frames.push(frame); frames.push(frame);
progress.inc(step as u64);
} }
progress.finish();
frames frames
} }
@ -67,13 +57,10 @@ pub async fn encode(frames: Vec<Frame<'static>>, repeat: bool) -> Vec<u8> {
let mut buffer = Vec::<u8>::new(); let mut buffer = Vec::<u8>::new();
let mut encoder = let mut encoder =
gif::Encoder::new(&mut buffer, WIDTH as u16, HEIGHT as u16, &[]).unwrap(); gif::Encoder::new(&mut buffer, WIDTH as u16, HEIGHT as u16, &[]).unwrap();
let progress = make_progress("Encoding ", frames.len() as u64);
for mut frame in frames { for mut frame in frames {
frame.delay = 1; frame.delay = 1;
encoder.write_frame(&frame).unwrap(); encoder.write_frame(&frame).unwrap();
progress.inc(1);
} }
progress.finish();
if repeat { if repeat {
encoder.set_repeat(gif::Repeat::Infinite).unwrap(); encoder.set_repeat(gif::Repeat::Infinite).unwrap();
} }
@ -81,96 +68,49 @@ pub async fn encode(frames: Vec<Frame<'static>>, repeat: bool) -> Vec<u8> {
buffer buffer
} }
use clap::Parser;
use sim::Simulation; use sim::Simulation;
#[derive(Parser, Debug)]
#[command(author, version, about, long_about = None)]
struct Args {
/// Input file to process. All common image types are
/// supported, see the `image` crate docs for specific
/// compatibility
#[arg(short = 'i', value_hint = clap::ValueHint::DirPath)]
input: std::path::PathBuf,
/// Output file path ('./output.gif' by default) #[derive(Debug, Clone)]
#[arg(short = 'o', value_hint = clap::ValueHint::DirPath)] pub enum Error {
output: Option<std::path::PathBuf>, CantGuessFormat,
DecodeError,
/// How many physics-steps between frames. Affects the
/// speed and length of the animation
#[arg(short = 's')]
step: Option<u32>,
/// Radius of the circle, smaller means more detailed but
/// slower to compute (8.0 by default, must be between
/// 1.0 and 50.0 inclusive)
#[arg(short = 'r')]
radius: Option<f32>,
/// Loop the GIF
#[arg(short = 'l', long = "loop")]
looping: bool,
} }
fn open_image<P: AsRef<Path>>(path: P) -> ImageBuffer<Rgb<u8>, Vec<u8>> { pub async fn make_quickdraw() -> Option<Arc<Mutex<QuickDraw>>> {
let image = match image::io::Reader::open(path) { Some(Arc::new(Mutex::new(QuickDraw::new(512, 512, 1000).await?)))
Ok(i) => i,
Err(e) => {
eprintln!("Error opening file for processing: ");
eprintln!("{}", e);
std::process::exit(1);
},
};
let decoded = match image.decode() {
Ok(d) => d,
Err(e) => {
eprintln!("Error processing file: ");
eprintln!("{}", e);
std::process::exit(1);
},
};
decoded.to_rgb8()
} }
fn main() { pub async fn make_gif(
let args = Args::parse(); input: &[u8],
draw: Arc<Mutex<QuickDraw>>,
let output = args.output.unwrap_or("output.gif".into()); ) -> Result<Vec<u8>, Error> {
let step = args.step.unwrap_or(20) as usize; const STEP: usize = 20;
if step == 0 { const RADIUS: f32 = 7.0;
eprintln!("Physics step cannot be 0"); let image = image::io::Reader::new(Cursor::new(input.to_vec().as_slice()))
std::process::exit(1); .with_guessed_format()
} .map_err(|_| Error::CantGuessFormat)?
let radius = args.radius.unwrap_or(8.0); .decode()
if radius < 1.0 || radius > 50.0 { .map_err(|_| Error::DecodeError)?
eprintln!("Invalid radius {}", radius); .to_rgb8();
eprintln!("Must be between 1.0 and 50.0 inclusive"); let (sim, it, max) = preprocess(image, RADIUS).await;
std::process::exit(1); let frames = simulate(draw, sim, it, STEP, max).await;
} let gif = encode(frames, true).await;
Ok(gif)
let image = open_image(args.input);
let (sim, it, max) = pollster::block_on(preprocess(image, radius));
let mut draw = pollster::block_on(QuickDraw::new(512, 512, 1000));
let frames = pollster::block_on(simulate(&mut draw, sim, it, step, max));
let gif = pollster::block_on(encode(frames, args.looping));
let mut file = match std::fs::File::create(output.clone()) {
Ok(f) => f,
Err(e) => {
eprintln!("Error opening file for writing:");
eprintln!("{}", e);
std::process::exit(1);
},
};
match file.write(&gif) {
Ok(_) => {
println!("Successfully created file at '{}'", output.display());
},
Err(e) => {
eprintln!("Error writing output file: ");
eprintln!("{}", e);
std::process::exit(1);
},
};
} }
use rust_cgi as cgi;
const CONTENT_TYPE: &'static str = "Content-Type: image/gif";
rust_cgi::cgi_main!(|req: cgi::http::Request<Vec<u8>>| -> rust_cgi::Response {
let qd = match futures::executor::block_on(make_quickdraw()) {
Some(qd) => qd,
None => return cgi::text_response(500, "Failed to initialize WGPU"),
};
let image = req.into_body();
let gif = match futures::executor::block_on(make_gif(&image, qd)) {
Ok(gif) => gif,
Err(_) => return cgi::text_response(500, "Failed to process image"),
};
rust_cgi::binary_response(200, CONTENT_TYPE, gif)
});

View file

@ -1,6 +1,6 @@
use std::hash::{Hash, Hasher}; use std::hash::{Hash, Hasher};
use crate::{helper::*, make_progress}; use crate::helper::*;
use image::{ImageBuffer, Rgb}; use image::{ImageBuffer, Rgb};
pub struct Circle { pub struct Circle {
@ -55,10 +55,7 @@ impl Simulation {
} }
#[inline] #[inline]
async fn assign_colors_from_image( fn assign_colors_from_image(&mut self, img: ImageBuffer<Rgb<u8>, Vec<u8>>) {
&mut self,
img: ImageBuffer<Rgb<u8>, Vec<u8>>,
) {
let (width, height) = (img.width() as f32 - 1.0, img.height() as f32 - 1.0); let (width, height) = (img.width() as f32 - 1.0, img.height() as f32 - 1.0);
for (pos, index) in self.circles.iter().map(|c| (c.position, c.index)) { for (pos, index) in self.circles.iter().map(|c| (c.position, c.index)) {
let img_x = let img_x =
@ -73,7 +70,7 @@ impl Simulation {
} }
#[inline] #[inline]
pub async fn simulate_image( pub fn simulate_image(
width: f32, width: f32,
height: f32, height: f32,
circle_radius: f32, circle_radius: f32,
@ -85,22 +82,22 @@ impl Simulation {
s.finish() s.finish()
} % 1204) as usize; } % 1204) as usize;
let mut sim = Simulation::new(width, height, circle_radius, image_hash); let mut sim = Simulation::new(width, height, circle_radius, image_hash);
let progress = make_progress( // let progress = make_progress(
"Preprocessing", // "Preprocessing",
(sim.max_circles + Self::POST_PROCESS) as u64, // (sim.max_circles + Self::POST_PROCESS) as u64,
); // );
while sim.circles() < sim.max_circles { while sim.circles() < sim.max_circles {
sim.step().await; sim.step();
progress.inc(2); // progress.inc(2);
} }
for _ in 0..Self::POST_PROCESS { for _ in 0..Self::POST_PROCESS {
sim.step().await; sim.step();
progress.inc(1); // progress.inc(1);
} }
progress.finish(); // progress.finish();
let total_iterations = sim.clock; let total_iterations = sim.clock;
let max_circles = sim.circles.len(); let max_circles = sim.circles.len();
sim.assign_colors_from_image(img).await; sim.assign_colors_from_image(img);
sim.circles.clear(); sim.circles.clear();
sim.clock = sim.rand_seed; sim.clock = sim.rand_seed;
(sim, total_iterations, max_circles) (sim, total_iterations, max_circles)
@ -144,7 +141,7 @@ impl Simulation {
// Insertion sort // Insertion sort
#[inline] #[inline]
async fn sort(&mut self) { fn sort(&mut self) {
if self.circles.len() == 1 { if self.circles.len() == 1 {
return; return;
} }
@ -159,7 +156,7 @@ impl Simulation {
} }
#[inline] #[inline]
async fn integrate(&mut self) { fn integrate(&mut self) {
let delta = self.timescale * (1.0 / self.substeps as f32); let delta = self.timescale * (1.0 / self.substeps as f32);
let gravity = Vector2::new(0.0, self.gravity) * delta.powi(2); let gravity = Vector2::new(0.0, self.gravity) * delta.powi(2);
self.circles.iter_mut().for_each(|circle| { self.circles.iter_mut().for_each(|circle| {
@ -170,7 +167,7 @@ impl Simulation {
} }
#[inline] #[inline]
async fn collide(&mut self) { fn collide(&mut self) {
for i in 0..self.circles.len() { for i in 0..self.circles.len() {
// Apply gravity // Apply gravity
for j in i..self.circles.len() { for j in i..self.circles.len() {
@ -218,7 +215,7 @@ impl Simulation {
} }
#[inline] #[inline]
pub async fn step(&mut self) { pub fn step(&mut self) {
if self.circles.len() < self.max_circles { if self.circles.len() < self.max_circles {
self.launch(); self.launch();
} }
@ -228,21 +225,19 @@ impl Simulation {
for _ in 0..self.substeps { for _ in 0..self.substeps {
self.constrain_rect(); self.constrain_rect();
self.sort().await; self.sort();
self.collide().await; self.collide();
self.integrate().await; self.integrate();
self.clock += 1; self.clock += 1;
} }
} }
#[inline] #[inline]
pub async fn steps(&mut self, steps: usize) { pub fn steps(&mut self, steps: usize) {
for _ in 0..steps { for _ in 0..steps {
self.step().await; self.step();
} }
} }
pub fn circles(&self) -> usize { pub fn circles(&self) -> usize { self.circles.len() }
self.circles.len()
}
} }

View file

@ -1,19 +0,0 @@
#[cfg(test)]
#[test]
fn bench_async() {
use std::io::Write;
flexi_logger::Logger::try_with_str("debug").unwrap();
let image = image::io::Reader::open(
"resources/mona
lisa.png",
)
.unwrap()
.decode()
.unwrap()
.to_rgb8();
// let gif = pollster::block_on(crate::generate_async(image, None));
// let mut file =
// std::fs::File::create("test.gif").unwrap();
// file.write(&gif).unwrap();
}

BIN
test.gif

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 MiB