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
[dependencies]
log = "0.4"
image = "0.24"
gif = "0.12"
image = "0.25"
gif = "0.13"
futures = "0.3"
wgpu = {version="0.18", features=["webgl"]}
tokio = {version="1.37", features=["full"]}
wgpu = {version="22"}
bytemuck = {version="1.14", features=["derive"]}
futures-intrusive = "0.5"
pollster = "0.3"
clap = { version = "4.4.16", features = ["derive"] }
indicatif = "0.17"
rust-cgi = "0.7"
[dev-dependencies]
flexi_logger = "0.27"
[profile.dev]
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 {
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 {
backends: wgpu::Backends::all(),
..Default::default()
@ -135,7 +135,7 @@ impl QuickDraw {
Some(a) => a,
None => {
eprintln!("Could not find WGPU adapter");
std::process::exit(1);
return None;
},
};
let (device, queue) =
@ -144,7 +144,7 @@ impl QuickDraw {
Err(e) => {
eprintln!("Could not find WGPU device:");
eprintln!("{}", e);
std::process::exit(1);
return None;
},
};
@ -244,14 +244,17 @@ impl QuickDraw {
multiview: None,
label: Some("Render Pipeline"),
layout: Some(&render_pipeline_layout),
cache: None,
vertex: wgpu::VertexState {
module: &vert,
entry_point: "vs_main",
buffers: &[Vertex::desc(), Circle::desc()],
compilation_options: PipelineCompilationOptions::default(),
},
fragment: Some(wgpu::FragmentState {
module: &frag,
entry_point: "fs_main",
compilation_options: PipelineCompilationOptions::default(),
targets: &[Some(wgpu::ColorTargetState {
format: texture_desc.format,
blend: Some(wgpu::BlendState::ALPHA_BLENDING),
@ -277,7 +280,7 @@ impl QuickDraw {
},
});
Self {
Some(Self {
device,
queue,
width,
@ -292,7 +295,7 @@ impl QuickDraw {
output_buffer,
vertex_buffer,
pipeline,
}
})
}
pub async fn resize(&mut self, width: u32, height: u32, max_circles: usize) {

View file

@ -1,23 +1,15 @@
#![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 gif::Frame;
use image::{ImageBuffer, Rgb};
use indicatif::{ProgressBar, ProgressStyle};
use log::debug;
pub mod draw;
pub mod helper;
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 HEIGHT: f32 = 512.0;
@ -27,21 +19,21 @@ pub async fn preprocess(
radius: f32,
) -> (sim::Simulation, usize, usize) {
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)
}
pub async fn simulate(
draw: &mut QuickDraw,
draw: Arc<Mutex<QuickDraw>>,
mut sim: Simulation,
it: usize,
step: usize,
max_circles: usize,
) -> Vec<Frame<'static>> {
let _draw = draw;
let mut draw = _draw.lock().await;
draw.resize(WIDTH as u32, HEIGHT as u32, max_circles).await;
let mut frames = vec![];
let progress =
make_progress("Simulating ", ((it - sim.clock) / sim.substeps) as u64);
while sim.clock < it {
let circles = &sim
.circles
@ -52,14 +44,12 @@ pub async fn simulate(
color: [c.color.0, c.color.1, c.color.2, 255],
})
.collect::<Vec<draw::Circle>>();
let bytes_future = draw.draw_circles(circles);
let steps = sim.steps(step);
let mut bytes = join!(bytes_future, steps).await.0;
let frame = gif::Frame::from_rgba(WIDTH as u16, HEIGHT as u16, &mut bytes);
sim.steps(step);
let mut bytes = draw.draw_circles(circles).await;
let frame =
gif::Frame::from_rgba_speed(WIDTH as u16, HEIGHT as u16, &mut bytes, 10);
frames.push(frame);
progress.inc(step as u64);
}
progress.finish();
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 encoder =
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 {
frame.delay = 1;
encoder.write_frame(&frame).unwrap();
progress.inc(1);
}
progress.finish();
if repeat {
encoder.set_repeat(gif::Repeat::Infinite).unwrap();
}
@ -81,96 +68,49 @@ pub async fn encode(frames: Vec<Frame<'static>>, repeat: bool) -> Vec<u8> {
buffer
}
use clap::Parser;
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)
#[arg(short = 'o', value_hint = clap::ValueHint::DirPath)]
output: Option<std::path::PathBuf>,
/// 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,
#[derive(Debug, Clone)]
pub enum Error {
CantGuessFormat,
DecodeError,
}
fn open_image<P: AsRef<Path>>(path: P) -> ImageBuffer<Rgb<u8>, Vec<u8>> {
let image = match image::io::Reader::open(path) {
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()
pub async fn make_quickdraw() -> Option<Arc<Mutex<QuickDraw>>> {
Some(Arc::new(Mutex::new(QuickDraw::new(512, 512, 1000).await?)))
}
fn main() {
let args = Args::parse();
let output = args.output.unwrap_or("output.gif".into());
let step = args.step.unwrap_or(20) as usize;
if step == 0 {
eprintln!("Physics step cannot be 0");
std::process::exit(1);
}
let radius = args.radius.unwrap_or(8.0);
if radius < 1.0 || radius > 50.0 {
eprintln!("Invalid radius {}", radius);
eprintln!("Must be between 1.0 and 50.0 inclusive");
std::process::exit(1);
}
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);
},
};
pub async fn make_gif(
input: &[u8],
draw: Arc<Mutex<QuickDraw>>,
) -> Result<Vec<u8>, Error> {
const STEP: usize = 20;
const RADIUS: f32 = 7.0;
let image = image::io::Reader::new(Cursor::new(input.to_vec().as_slice()))
.with_guessed_format()
.map_err(|_| Error::CantGuessFormat)?
.decode()
.map_err(|_| Error::DecodeError)?
.to_rgb8();
let (sim, it, max) = preprocess(image, RADIUS).await;
let frames = simulate(draw, sim, it, STEP, max).await;
let gif = encode(frames, true).await;
Ok(gif)
}
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 crate::{helper::*, make_progress};
use crate::helper::*;
use image::{ImageBuffer, Rgb};
pub struct Circle {
@ -55,10 +55,7 @@ impl Simulation {
}
#[inline]
async fn assign_colors_from_image(
&mut self,
img: ImageBuffer<Rgb<u8>, Vec<u8>>,
) {
fn assign_colors_from_image(&mut self, img: ImageBuffer<Rgb<u8>, Vec<u8>>) {
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)) {
let img_x =
@ -73,7 +70,7 @@ impl Simulation {
}
#[inline]
pub async fn simulate_image(
pub fn simulate_image(
width: f32,
height: f32,
circle_radius: f32,
@ -85,22 +82,22 @@ impl Simulation {
s.finish()
} % 1204) as usize;
let mut sim = Simulation::new(width, height, circle_radius, image_hash);
let progress = make_progress(
"Preprocessing",
(sim.max_circles + Self::POST_PROCESS) as u64,
);
// let progress = make_progress(
// "Preprocessing",
// (sim.max_circles + Self::POST_PROCESS) as u64,
// );
while sim.circles() < sim.max_circles {
sim.step().await;
progress.inc(2);
sim.step();
// progress.inc(2);
}
for _ in 0..Self::POST_PROCESS {
sim.step().await;
progress.inc(1);
sim.step();
// progress.inc(1);
}
progress.finish();
// progress.finish();
let total_iterations = sim.clock;
let max_circles = sim.circles.len();
sim.assign_colors_from_image(img).await;
sim.assign_colors_from_image(img);
sim.circles.clear();
sim.clock = sim.rand_seed;
(sim, total_iterations, max_circles)
@ -144,7 +141,7 @@ impl Simulation {
// Insertion sort
#[inline]
async fn sort(&mut self) {
fn sort(&mut self) {
if self.circles.len() == 1 {
return;
}
@ -159,7 +156,7 @@ impl Simulation {
}
#[inline]
async fn integrate(&mut self) {
fn integrate(&mut self) {
let delta = self.timescale * (1.0 / self.substeps as f32);
let gravity = Vector2::new(0.0, self.gravity) * delta.powi(2);
self.circles.iter_mut().for_each(|circle| {
@ -170,7 +167,7 @@ impl Simulation {
}
#[inline]
async fn collide(&mut self) {
fn collide(&mut self) {
for i in 0..self.circles.len() {
// Apply gravity
for j in i..self.circles.len() {
@ -218,7 +215,7 @@ impl Simulation {
}
#[inline]
pub async fn step(&mut self) {
pub fn step(&mut self) {
if self.circles.len() < self.max_circles {
self.launch();
}
@ -228,21 +225,19 @@ impl Simulation {
for _ in 0..self.substeps {
self.constrain_rect();
self.sort().await;
self.collide().await;
self.integrate().await;
self.sort();
self.collide();
self.integrate();
self.clock += 1;
}
}
#[inline]
pub async fn steps(&mut self, steps: usize) {
pub fn steps(&mut self, steps: usize) {
for _ in 0..steps {
self.step().await;
self.step();
}
}
pub fn circles(&self) -> usize {
self.circles.len()
}
pub fn circles(&self) -> usize { 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