Compare commits
10 commits
5fc6ce7636
...
16d4306763
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
16d4306763 | ||
|
|
f40db8a474 | ||
|
|
0c39058288 | ||
|
|
19e74b6dcb | ||
|
|
26744cd261 | ||
|
|
0c3ae9aa5b | ||
|
|
6a09094d54 | ||
|
|
42f2ccffac | ||
|
|
fea1db9377 | ||
|
|
40ebe619f5 |
3
.gitignore
vendored
|
|
@ -2,3 +2,6 @@ node_modules/
|
|||
target/
|
||||
pkg/
|
||||
.DS_Store
|
||||
dist/
|
||||
*.iml
|
||||
.idea
|
||||
18
README.md
|
|
@ -1,7 +1,17 @@
|
|||
**Spiegel** image filter project (Work In Progress)
|
||||
**Spiegel** image filter project
|
||||
|
||||
- rust
|
||||
- wasm
|
||||
- rust/webassembly for image processing
|
||||
- vanilla javascript for the user interface
|
||||
- no server (just static pages)
|
||||
|
||||
Live demo at https://shautvast.github.io/spiegel-demo/
|
||||
|
||||
* Sorry for the poor performance, especially on larger images.
|
||||
* It uses the median image filter from image.rs. That in itself can be pretty slow.
|
||||
(Although the implementation uses a _sliding window histogram_, which I think is pretty nifty).
|
||||
* And on top of that, I created this custom flood fill algorithm,
|
||||
that instead of filling it with with a single color, looks up a sample from the
|
||||
Spiegel book (that has a corresponding color) and takes the pixels from that.
|
||||
|
||||
sample output
|
||||

|
||||

|
||||
|
|
|
|||
BIN
dist/11dc9380193954aac8c4.module.wasm
vendored
BIN
dist/630027c604733df47374e4f5adb1b7c5.jpg
vendored
|
Before Width: | Height: | Size: 349 KiB |
BIN
dist/66d531fb4651c57454ae.module.wasm
vendored
1
dist/796.bundle.js
vendored
BIN
dist/93be161a4640fe100e05af248fa4c52e.png
vendored
|
Before Width: | Height: | Size: 2.8 KiB |
BIN
dist/95cc0985b7bd6242de9b9665261616a3.jpg
vendored
|
Before Width: | Height: | Size: 192 KiB |
523
dist/bundle.js
vendored
42
dist/crate_pkg_photon_rs_js.bundle.js
vendored
42
dist/crate_pkg_spiegel_client_js.bundle.js
vendored
BIN
dist/df3fbe72e3ad368ae5bf.module.wasm
vendored
BIN
dist/ea872833a419652a23b47d50be7fe0ef.jpg
vendored
|
Before Width: | Height: | Size: 224 KiB |
BIN
dist/f49f7893ad0a3ad10176.module.wasm
vendored
29
dist/index.html
vendored
|
|
@ -1,29 +0,0 @@
|
|||
<!doctype html>
|
||||
<html>
|
||||
|
||||
<head>
|
||||
<meta http-equiv="Content-type" content="text/html; charset=utf-8" />
|
||||
<title>Spiegel</title>
|
||||
<script defer src="bundle.js"></script></head>
|
||||
|
||||
<body>
|
||||
<div class="default">
|
||||
<div class="main">
|
||||
<label>
|
||||
<h2>Blur factor</h2< /label>
|
||||
<div class="slidecontainer">
|
||||
<input type="range" id="slider" value="0" min="0" max="100" class="slider" />
|
||||
</div>
|
||||
</label>
|
||||
<div class="main_content">
|
||||
<div class="content" id="images">
|
||||
<div id="image_container"></div>
|
||||
|
||||
<canvas id="canvas"></canvas>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
802
webclient/Cargo.lock → image-processor/Cargo.lock
generated
|
|
@ -1,19 +1,21 @@
|
|||
[package]
|
||||
edition = "2021"
|
||||
name = "spiegel-client"
|
||||
version = "0.1.0"
|
||||
name = "spiegel"
|
||||
version = "1.0.0"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
log = "0.4"
|
||||
image = "0.24.1"
|
||||
imageproc = "0.23"
|
||||
image = "0.25"
|
||||
imageproc = "0.24"
|
||||
hex = "0.4"
|
||||
anyhow = "1.0"
|
||||
photon-rs = { version = "0.3.2", default-features = false }
|
||||
console_error_panic_hook = { version = "0.1.7", optional = true }
|
||||
wasm-bindgen = "0.2.78"
|
||||
include_dir = "0.7.3"
|
||||
web-sys = "0.3.55"
|
||||
|
||||
[lib]
|
||||
crate-type = ["cdylib", "rlib"]
|
||||
|
Before Width: | Height: | Size: 6.1 MiB After Width: | Height: | Size: 6.1 MiB |
124
image-processor/src/lib.rs
Normal file
|
|
@ -0,0 +1,124 @@
|
|||
use image::{GenericImage, GenericImageView, Pixel, Rgba, RgbaImage};
|
||||
use photon_rs::PhotonImage;
|
||||
use std::collections::LinkedList;
|
||||
use image::imageops::FilterType;
|
||||
|
||||
mod samples;
|
||||
|
||||
use samples::log;
|
||||
use wasm_bindgen::prelude::*;
|
||||
|
||||
static BLACK: Rgba<u8> = Rgba([0, 0, 0, 0]);
|
||||
|
||||
#[wasm_bindgen]
|
||||
pub fn spiegel(photon_image: PhotonImage, median_kernelsize: u32, preview: bool) -> PhotonImage {
|
||||
samples::read_jpeg_bytes();
|
||||
|
||||
let width = photon_image.get_width();
|
||||
let height = photon_image.get_height();
|
||||
|
||||
let raw_pixels = photon_image.get_raw_pixels().to_vec();
|
||||
let i1 = RgbaImage::from_vec(width, height, raw_pixels).unwrap();
|
||||
|
||||
let i2 = if preview {
|
||||
image::imageops::resize(&i1, u32::min(500, width >> 1), u32::min(500, height >> 1), FilterType::Nearest)
|
||||
} else {
|
||||
image::imageops::resize(&i1, u32::min(500, width), u32::min(500, height), FilterType::Nearest)
|
||||
};
|
||||
let mut i3 = imageproc::filter::median_filter(&i2, median_kernelsize, median_kernelsize);
|
||||
let i4 = if !preview {
|
||||
apply_samples_to_image(&mut i3)
|
||||
} else {
|
||||
i3
|
||||
};
|
||||
let i5 = image::imageops::resize(&i4, width, height, FilterType::Nearest);
|
||||
PhotonImage::new(i5.into_raw(), width, height)
|
||||
}
|
||||
|
||||
fn apply_samples_to_image(src: &mut RgbaImage) -> RgbaImage {
|
||||
let mut out = RgbaImage::new(src.width(), src.height());
|
||||
unsafe {
|
||||
for y in 0..src.height() {
|
||||
log(&format!("{}", y));
|
||||
for x in 0..src.width() {
|
||||
if out.unsafe_get_pixel(x, y) == BLACK {
|
||||
let pixel = src.unsafe_get_pixel(x, y);
|
||||
if pixel != BLACK {
|
||||
let sample = samples::get_closest_color(pixel[0], pixel[1], pixel[2])
|
||||
.image
|
||||
.as_ref()
|
||||
.unwrap();
|
||||
fill(src, sample, &mut out, pixel, x, y);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
fn fill(
|
||||
src: &mut RgbaImage,
|
||||
sample: &RgbaImage,
|
||||
dest: &mut RgbaImage,
|
||||
color: Rgba<u8>,
|
||||
px: u32,
|
||||
py: u32,
|
||||
) {
|
||||
unsafe {
|
||||
let height = sample.height();
|
||||
let width = sample.width();
|
||||
let mut points = LinkedList::new();
|
||||
if is_same(src.unsafe_get_pixel(px, py), color) {
|
||||
points.push_back(Coord(px, py));
|
||||
}
|
||||
|
||||
while !points.is_empty() {
|
||||
if let Some(coord) = points.pop_back() {
|
||||
let orig_pixel = src.unsafe_get_pixel(coord.0, coord.1);
|
||||
let x = coord.0;
|
||||
let y = coord.1;
|
||||
if src.unsafe_get_pixel(x, y) != BLACK {
|
||||
if is_same(orig_pixel, color) {
|
||||
let mut xx = x;
|
||||
let mut yy = y;
|
||||
while xx >= width {
|
||||
xx -= width;
|
||||
}
|
||||
while yy >= height {
|
||||
yy -= height;
|
||||
}
|
||||
dest.unsafe_put_pixel(x, y, sample.unsafe_get_pixel(xx, yy));
|
||||
src.unsafe_put_pixel(x, y, BLACK);
|
||||
if x > 1 {
|
||||
points.push_front(Coord(x - 1, y));
|
||||
}
|
||||
if y > 1 {
|
||||
points.push_front(Coord(x, y - 1));
|
||||
}
|
||||
if x < src.width() - 1 {
|
||||
points.push_front(Coord(x + 1, y));
|
||||
}
|
||||
if y < src.height() - 1 {
|
||||
points.push_front(Coord(x, y + 1));
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
println!("break");
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn is_same(p1: Rgba<u8>, p2: Rgba<u8>) -> bool {
|
||||
let p1 = p1.channels();
|
||||
let p2 = p2.channels();
|
||||
i16::abs(p1[0] as i16 - p2[0] as i16) < 4
|
||||
&& i16::abs(p1[1] as i16 - p2[1] as i16) < 4
|
||||
&& i16::abs(p1[2] as i16 - p2[2] as i16) < 4
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
struct Coord(u32, u32);
|
||||
99
image-processor/src/samples.rs
Normal file
|
|
@ -0,0 +1,99 @@
|
|||
use image::{load_from_memory_with_format, RgbaImage};
|
||||
use std::sync::OnceLock;
|
||||
use wasm_bindgen::prelude::*;
|
||||
|
||||
use include_dir::{include_dir, Dir, DirEntry};
|
||||
|
||||
static mut SAMPLES: OnceLock<Vec<ColorSample>> = OnceLock::new();
|
||||
static SAMPLES_DIR: Dir = include_dir!("src/samples");
|
||||
|
||||
pub fn read_jpeg_bytes() {
|
||||
unsafe {
|
||||
SAMPLES.get_or_init(|| {
|
||||
log("reading image samples");
|
||||
read_color_samples().unwrap()
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_closest_color<'a>(r: u8, g: u8, b: u8) -> &'static ColorSample {
|
||||
unsafe {
|
||||
let color_samples = SAMPLES.get_mut().unwrap();
|
||||
|
||||
let mut closest = None;
|
||||
let mut min_diff: f32 = 4294967295.0; //0xFFFFFFFF
|
||||
for sample in color_samples {
|
||||
let diff = get_distance(sample.r, sample.g, sample.b, r, g, b);
|
||||
if diff < min_diff {
|
||||
closest = Some(sample);
|
||||
min_diff = diff;
|
||||
}
|
||||
}
|
||||
|
||||
let closest = closest.unwrap();
|
||||
if closest.image.is_none() {
|
||||
let sample_image =
|
||||
load_from_memory_with_format(closest.raw_bytes, image::ImageFormat::Jpeg)
|
||||
.unwrap()
|
||||
.to_rgba8();
|
||||
closest.image = Some(sample_image);
|
||||
}
|
||||
closest
|
||||
}
|
||||
}
|
||||
|
||||
/// returns squared euclidian color distance
|
||||
/// as if colors were points in 3d space
|
||||
fn get_distance(r1: u8, g1: u8, b1: u8, r2: u8, g2: u8, b2: u8) -> f32 {
|
||||
let red_dif = r1 as f32 - r2 as f32;
|
||||
let green_dif = g1 as f32 - g2 as f32;
|
||||
let blue_dif = b1 as f32 - b2 as f32;
|
||||
return red_dif * red_dif + green_dif * green_dif + blue_dif * blue_dif;
|
||||
}
|
||||
|
||||
/// read all sample jpegs into memory
|
||||
pub fn read_color_samples() -> anyhow::Result<Vec<ColorSample>> {
|
||||
let mut color_samples: Vec<ColorSample> = Vec::new();
|
||||
|
||||
for entry in SAMPLES_DIR.entries() {
|
||||
if let DirEntry::File(f) = entry {
|
||||
let filename = entry.path().file_name().unwrap().to_str().unwrap();
|
||||
let hex_r = &filename[0..2];
|
||||
let hex_g = &filename[2..4];
|
||||
let hex_b = &filename[4..6];
|
||||
|
||||
color_samples.push(ColorSample {
|
||||
filename: filename.into(),
|
||||
r: u8::from_str_radix(&hex_r, 16)?,
|
||||
g: u8::from_str_radix(&hex_g, 16)?,
|
||||
b: u8::from_str_radix(&hex_b, 16)?,
|
||||
raw_bytes: f.contents(),
|
||||
image: None,
|
||||
});
|
||||
}
|
||||
}
|
||||
log("Done reading image samples");
|
||||
Ok(color_samples)
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct ColorSample {
|
||||
pub filename: String,
|
||||
pub r: u8,
|
||||
pub g: u8,
|
||||
pub b: u8,
|
||||
pub raw_bytes: &'static [u8],
|
||||
pub image: Option<RgbaImage>,
|
||||
}
|
||||
|
||||
#[wasm_bindgen]
|
||||
extern "C" {
|
||||
#[wasm_bindgen(js_namespace = console)]
|
||||
pub fn log(s: &str);
|
||||
|
||||
#[wasm_bindgen(js_namespace = console, js_name = log)]
|
||||
fn log_u32(a: u32);
|
||||
|
||||
#[wasm_bindgen(js_namespace = console, js_name = log)]
|
||||
fn log_many(a: &str, b: &str);
|
||||
}
|
||||
|
Before Width: | Height: | Size: 20 KiB After Width: | Height: | Size: 20 KiB |
|
Before Width: | Height: | Size: 22 KiB After Width: | Height: | Size: 22 KiB |
|
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 15 KiB |
|
Before Width: | Height: | Size: 16 KiB After Width: | Height: | Size: 16 KiB |
|
Before Width: | Height: | Size: 16 KiB After Width: | Height: | Size: 16 KiB |
|
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 15 KiB |
|
Before Width: | Height: | Size: 17 KiB After Width: | Height: | Size: 17 KiB |
|
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 15 KiB |
|
Before Width: | Height: | Size: 17 KiB After Width: | Height: | Size: 17 KiB |
|
Before Width: | Height: | Size: 16 KiB After Width: | Height: | Size: 16 KiB |
|
Before Width: | Height: | Size: 14 KiB After Width: | Height: | Size: 14 KiB |
|
Before Width: | Height: | Size: 14 KiB After Width: | Height: | Size: 14 KiB |
|
Before Width: | Height: | Size: 18 KiB After Width: | Height: | Size: 18 KiB |
|
Before Width: | Height: | Size: 17 KiB After Width: | Height: | Size: 17 KiB |
|
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 44 KiB After Width: | Height: | Size: 44 KiB |
|
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 11 KiB |
|
Before Width: | Height: | Size: 27 KiB After Width: | Height: | Size: 27 KiB |
|
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 17 KiB After Width: | Height: | Size: 17 KiB |
|
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 11 KiB |
|
Before Width: | Height: | Size: 16 KiB After Width: | Height: | Size: 16 KiB |
|
Before Width: | Height: | Size: 24 KiB After Width: | Height: | Size: 24 KiB |
|
Before Width: | Height: | Size: 19 KiB After Width: | Height: | Size: 19 KiB |
|
Before Width: | Height: | Size: 13 KiB After Width: | Height: | Size: 13 KiB |
|
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 20 KiB After Width: | Height: | Size: 20 KiB |
|
Before Width: | Height: | Size: 21 KiB After Width: | Height: | Size: 21 KiB |
|
Before Width: | Height: | Size: 16 KiB After Width: | Height: | Size: 16 KiB |
|
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 11 KiB |
|
Before Width: | Height: | Size: 23 KiB After Width: | Height: | Size: 23 KiB |
|
Before Width: | Height: | Size: 20 KiB After Width: | Height: | Size: 20 KiB |
|
Before Width: | Height: | Size: 14 KiB After Width: | Height: | Size: 14 KiB |
|
Before Width: | Height: | Size: 18 KiB After Width: | Height: | Size: 18 KiB |
|
Before Width: | Height: | Size: 13 KiB After Width: | Height: | Size: 13 KiB |
|
Before Width: | Height: | Size: 13 KiB After Width: | Height: | Size: 13 KiB |
|
Before Width: | Height: | Size: 19 KiB After Width: | Height: | Size: 19 KiB |
|
Before Width: | Height: | Size: 16 KiB After Width: | Height: | Size: 16 KiB |
|
Before Width: | Height: | Size: 63 KiB After Width: | Height: | Size: 63 KiB |
|
Before Width: | Height: | Size: 24 KiB After Width: | Height: | Size: 24 KiB |
|
Before Width: | Height: | Size: 53 KiB After Width: | Height: | Size: 53 KiB |
|
Before Width: | Height: | Size: 21 KiB After Width: | Height: | Size: 21 KiB |
|
Before Width: | Height: | Size: 32 KiB After Width: | Height: | Size: 32 KiB |
|
Before Width: | Height: | Size: 18 KiB After Width: | Height: | Size: 18 KiB |
|
Before Width: | Height: | Size: 18 KiB After Width: | Height: | Size: 18 KiB |
|
Before Width: | Height: | Size: 19 KiB After Width: | Height: | Size: 19 KiB |
|
Before Width: | Height: | Size: 18 KiB After Width: | Height: | Size: 18 KiB |
|
Before Width: | Height: | Size: 21 KiB After Width: | Height: | Size: 21 KiB |
|
Before Width: | Height: | Size: 18 KiB After Width: | Height: | Size: 18 KiB |
|
Before Width: | Height: | Size: 18 KiB After Width: | Height: | Size: 18 KiB |
|
Before Width: | Height: | Size: 13 KiB After Width: | Height: | Size: 13 KiB |
|
Before Width: | Height: | Size: 17 KiB After Width: | Height: | Size: 17 KiB |
|
Before Width: | Height: | Size: 20 KiB After Width: | Height: | Size: 20 KiB |
|
Before Width: | Height: | Size: 52 KiB After Width: | Height: | Size: 52 KiB |
|
Before Width: | Height: | Size: 19 KiB After Width: | Height: | Size: 19 KiB |
|
Before Width: | Height: | Size: 23 KiB After Width: | Height: | Size: 23 KiB |
|
Before Width: | Height: | Size: 17 KiB After Width: | Height: | Size: 17 KiB |
|
Before Width: | Height: | Size: 18 KiB After Width: | Height: | Size: 18 KiB |
|
Before Width: | Height: | Size: 17 KiB After Width: | Height: | Size: 17 KiB |
|
Before Width: | Height: | Size: 19 KiB After Width: | Height: | Size: 19 KiB |
|
Before Width: | Height: | Size: 14 KiB After Width: | Height: | Size: 14 KiB |
|
Before Width: | Height: | Size: 19 KiB After Width: | Height: | Size: 19 KiB |
|
Before Width: | Height: | Size: 38 KiB After Width: | Height: | Size: 38 KiB |
|
Before Width: | Height: | Size: 23 KiB After Width: | Height: | Size: 23 KiB |
|
Before Width: | Height: | Size: 14 KiB After Width: | Height: | Size: 14 KiB |
|
Before Width: | Height: | Size: 19 KiB After Width: | Height: | Size: 19 KiB |
|
Before Width: | Height: | Size: 18 KiB After Width: | Height: | Size: 18 KiB |
|
Before Width: | Height: | Size: 18 KiB After Width: | Height: | Size: 18 KiB |
|
Before Width: | Height: | Size: 20 KiB After Width: | Height: | Size: 20 KiB |
|
Before Width: | Height: | Size: 23 KiB After Width: | Height: | Size: 23 KiB |
|
Before Width: | Height: | Size: 41 KiB After Width: | Height: | Size: 41 KiB |
|
Before Width: | Height: | Size: 21 KiB After Width: | Height: | Size: 21 KiB |
|
Before Width: | Height: | Size: 18 KiB After Width: | Height: | Size: 18 KiB |
|
Before Width: | Height: | Size: 18 KiB After Width: | Height: | Size: 18 KiB |
|
Before Width: | Height: | Size: 17 KiB After Width: | Height: | Size: 17 KiB |
|
Before Width: | Height: | Size: 18 KiB After Width: | Height: | Size: 18 KiB |
|
Before Width: | Height: | Size: 46 KiB After Width: | Height: | Size: 46 KiB |
|
Before Width: | Height: | Size: 48 KiB After Width: | Height: | Size: 48 KiB |
|
Before Width: | Height: | Size: 21 KiB After Width: | Height: | Size: 21 KiB |
|
Before Width: | Height: | Size: 48 KiB After Width: | Height: | Size: 48 KiB |