├── pkg ├── colorcruncher_bg.wasm ├── package.json ├── colorcruncher_bg.wasm.d.ts ├── colorcruncher.d.ts └── colorcruncher.js ├── .gitignore ├── rust ├── src │ ├── lib.rs │ ├── kmeans │ │ ├── types.rs │ │ ├── gpu │ │ │ ├── common.rs │ │ │ ├── lloyd_gpu1.wgsl │ │ │ ├── buffers.rs │ │ │ └── lloyd_gpu1.rs │ │ ├── config.rs │ │ ├── utils.rs │ │ ├── gpu.rs │ │ ├── lloyd.rs │ │ ├── initializer.rs │ │ ├── distance.rs │ │ └── hamerly.rs │ ├── utils.rs │ ├── types.rs │ ├── python.rs │ ├── wasm.rs │ ├── quantize.rs │ └── kmeans.rs ├── Cargo.toml └── benches │ ├── kmeans_gpu_benchmark.rs │ ├── criterion_benchmarks.rs │ └── wasm_benchmarks.rs ├── LICENSE ├── Makefile ├── README.md ├── scripts └── compare_benchmarks.py ├── styles.css ├── index.html └── script.js /pkg/colorcruncher_bg.wasm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mattdeak/wasm-color-quantizer/HEAD/pkg/colorcruncher_bg.wasm -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /rust/target 2 | /rust/pkg 3 | /benchmark_history 4 | .venv 5 | 6 | /analysis 7 | 8 | *perf.data* 9 | *.perf 10 | flamegraph.svg 11 | 12 | *heaptrack* 13 | -------------------------------------------------------------------------------- /rust/src/lib.rs: -------------------------------------------------------------------------------- 1 | #![feature(portable_simd)] 2 | 3 | #[cfg(feature = "python")] 4 | pub mod python; 5 | 6 | pub mod kmeans; 7 | pub mod quantize; 8 | pub mod types; 9 | mod utils; 10 | pub mod wasm; 11 | -------------------------------------------------------------------------------- /rust/src/kmeans/types.rs: -------------------------------------------------------------------------------- 1 | // Some utility type aliases for readability 2 | pub type Centroids = Vec; 3 | pub type CentroidSums = Vec; 4 | pub type Assignments = Vec; 5 | pub type CentroidCounts = Vec; 6 | 7 | // Result 8 | pub type KMeansResult = Result<(Assignments, Centroids), KMeansError>; 9 | 10 | #[derive(Debug, Clone)] 11 | pub struct KMeansError(pub String); 12 | 13 | impl std::fmt::Display for KMeansError { 14 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 15 | write!(f, "{}", self.0) 16 | } 17 | } 18 | 19 | impl From<&str> for KMeansError { 20 | fn from(s: &str) -> Self { 21 | KMeansError(s.to_string()) 22 | } 23 | } 24 | 25 | impl std::error::Error for KMeansError {} 26 | -------------------------------------------------------------------------------- /rust/src/utils.rs: -------------------------------------------------------------------------------- 1 | use crate::types::{Vec4u, VectorExt}; 2 | 3 | use std::collections::HashSet; 4 | 5 | pub fn num_distinct_colors(data: &[T]) -> usize { 6 | let mut color_hashset = HashSet::new(); 7 | for pixel in data { 8 | // hacky but its fine, it only occurs once at the beginning 9 | let hash_key = pixel[0] as usize * 2 + pixel[1] as usize * 3 + pixel[2] as usize * 5; 10 | color_hashset.insert(hash_key); 11 | } 12 | color_hashset.len() 13 | } 14 | 15 | pub fn num_distinct_colors_u32(data: &[Vec4u]) -> usize { 16 | let mut color_hashset = HashSet::new(); 17 | for pixel in data { 18 | color_hashset.insert(pixel[0] * 2 + pixel[1] * 3 + pixel[2] * 5); 19 | } 20 | color_hashset.len() 21 | } 22 | -------------------------------------------------------------------------------- /pkg/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "colorcruncher", 3 | "type": "module", 4 | "collaborators": [ 5 | "Matthew Deakos " 6 | ], 7 | "description": "A fast and minimal wasm-compatible color quantization library written in Rust", 8 | "version": "0.1.2", 9 | "license": "MIT", 10 | "repository": { 11 | "type": "git", 12 | "url": "git@github.com:mattdeak/wasm-color-quantizer.git" 13 | }, 14 | "files": [ 15 | "colorcruncher_bg.wasm", 16 | "colorcruncher.js", 17 | "colorcruncher.d.ts" 18 | ], 19 | "main": "colorcruncher.js", 20 | "types": "colorcruncher.d.ts", 21 | "sideEffects": [ 22 | "./snippets/*" 23 | ], 24 | "keywords": [ 25 | "color", 26 | "image-processing", 27 | "wasm", 28 | "gpu", 29 | "k-means" 30 | ] 31 | } -------------------------------------------------------------------------------- /rust/src/kmeans/gpu/common.rs: -------------------------------------------------------------------------------- 1 | use wgpu::{Adapter, Device, DeviceDescriptor, Instance, Queue, RequestAdapterOptions}; 2 | 3 | pub async fn common_wgpu_setup() -> Result<(Instance, Adapter, Device, Queue), String> { 4 | let instance = wgpu::Instance::default(); 5 | 6 | let adapter = instance 7 | .request_adapter(&RequestAdapterOptions::default()) 8 | .await 9 | .expect("Failed to request adapter"); 10 | 11 | let (device, queue) = adapter 12 | .request_device( 13 | &DeviceDescriptor { 14 | label: None, 15 | required_features: wgpu::Features::empty(), 16 | required_limits: wgpu::Limits::default(), 17 | }, 18 | None, 19 | ) 20 | .await 21 | .expect("Failed to request device"); 22 | Ok((instance, adapter, device, queue)) 23 | } 24 | -------------------------------------------------------------------------------- /rust/src/kmeans/gpu/lloyd_gpu1.wgsl: -------------------------------------------------------------------------------- 1 | @group(0) @binding(0) var image: array>; 2 | @group(0) @binding(1) var centers: array>; 3 | @group(0) @binding(2) var assignments: array; 4 | 5 | const WORKGROUP_SIZE = 256; 6 | 7 | @compute @workgroup_size(WORKGROUP_SIZE) 8 | fn main(@builtin(global_invocation_id) global_id: vec3) { 9 | let idx = global_id.x; 10 | if (idx >= arrayLength(&image)) { 11 | return; 12 | } 13 | 14 | let pixel = image[idx]; 15 | 16 | let fpixel = vec3(pixel); 17 | var min_dist = distance(fpixel, centers[0]); 18 | var min_center = 0u; 19 | 20 | for (var i = 1u; i < arrayLength(¢ers); i++) { 21 | let dist = distance(fpixel, centers[i]); 22 | if (dist < min_dist) { 23 | min_dist = dist; 24 | min_center = i; 25 | } 26 | } 27 | 28 | assignments[idx] = min_center; 29 | } 30 | 31 | fn distance(a: vec3, b: vec3) -> f32 { 32 | let diff = a - b; 33 | return dot(diff, diff); 34 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Matthew Deakos 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /rust/src/kmeans/config.rs: -------------------------------------------------------------------------------- 1 | use crate::kmeans::initializer::Initializer; 2 | use std::fmt; 3 | 4 | #[derive(Debug, Clone)] 5 | pub enum KMeansAlgorithm { 6 | Lloyd, 7 | Hamerly, 8 | #[cfg(feature = "gpu")] 9 | LloydGpu, 10 | } 11 | 12 | impl fmt::Display for KMeansAlgorithm { 13 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 14 | write!(f, "{self:?}") 15 | } 16 | } 17 | 18 | #[derive(Debug, Clone)] 19 | pub struct KMeansConfig { 20 | pub k: usize, 21 | pub max_iterations: usize, 22 | pub tolerance: f32, 23 | pub algorithm: KMeansAlgorithm, 24 | pub initializer: Initializer, 25 | pub seed: Option, 26 | } 27 | 28 | impl Default for KMeansConfig { 29 | fn default() -> Self { 30 | Self { 31 | k: 10, 32 | max_iterations: 100, 33 | tolerance: 0.02, 34 | algorithm: KMeansAlgorithm::Lloyd, 35 | initializer: Initializer::KMeansPlusPlus, 36 | seed: None, 37 | } 38 | } 39 | } 40 | 41 | impl fmt::Display for KMeansConfig { 42 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 43 | write!(f, "{self:?}") 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /rust/src/kmeans/gpu/buffers.rs: -------------------------------------------------------------------------------- 1 | use wgpu::Buffer; 2 | use wgpu::CommandEncoder; 3 | use wgpu::Device; 4 | 5 | pub struct MappableBuffer { 6 | pub gpu_buffer: Buffer, 7 | pub staging_buffer: Buffer, 8 | pub size: u64, 9 | } 10 | 11 | // Provides some utilities to make it easier to read back a buffer from the GPU 12 | impl MappableBuffer { 13 | pub async fn read_back( 14 | &self, 15 | device: &Device, 16 | ) -> Result, &'static str> { 17 | let buffer_slice = self.staging_buffer.slice(..); 18 | let (sender, receiver) = futures::channel::oneshot::channel(); 19 | buffer_slice.map_async(wgpu::MapMode::Read, move |result| { 20 | let _ = sender.send(result); 21 | }); 22 | 23 | device.poll(wgpu::Maintain::Wait).panic_on_timeout(); 24 | 25 | if let Ok(Ok(())) = receiver.await { 26 | let buffer_data = buffer_slice.get_mapped_range(); 27 | let data: Vec = bytemuck::cast_slice(&buffer_data).to_vec(); 28 | drop(buffer_data); 29 | self.staging_buffer.unmap(); 30 | Ok(data) 31 | } else { 32 | Err("Failed to read back the buffer") 33 | } 34 | } 35 | 36 | pub fn copy_to_staging_buffer(&self, encoder: &mut CommandEncoder) { 37 | encoder.copy_buffer_to_buffer(&self.gpu_buffer, 0, &self.staging_buffer, 0, self.size); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /rust/src/kmeans/utils.rs: -------------------------------------------------------------------------------- 1 | use crate::kmeans::distance::euclidean_distance_squared; 2 | use crate::kmeans::distance::SquaredEuclideanDistance; 3 | use crate::types::VectorExt; 4 | 5 | // Return the index of closest centroid and distance to that centroid 6 | pub fn find_closest_centroid(pixel: &T, centroids: &[T]) -> usize { 7 | debug_assert!(!centroids.is_empty()); 8 | let mut min_distance = euclidean_distance_squared(pixel, ¢roids[0]); 9 | let mut min_index = 0; 10 | for (i, centroid) in centroids.iter().enumerate() { 11 | let distance = euclidean_distance_squared(pixel, centroid); 12 | if distance < min_distance { 13 | min_distance = distance; 14 | min_index = i; 15 | } 16 | } 17 | min_index 18 | } 19 | 20 | pub fn has_converged( 21 | initial_centroids: &[T], 22 | final_centroids: &[T], 23 | tolerance: f32, 24 | ) -> bool { 25 | let tolerance = SquaredEuclideanDistance(tolerance * tolerance); 26 | initial_centroids 27 | .iter() 28 | .zip(final_centroids.iter()) 29 | .all(|(a, b)| euclidean_distance_squared(a, b) < tolerance) 30 | } 31 | 32 | #[cfg(test)] 33 | mod tests { 34 | 35 | use super::*; 36 | 37 | #[test] 38 | fn test_find_closest_centroid() { 39 | let pixel = [100.0, 100.0, 100.0]; 40 | let centroids = vec![ 41 | [0.0, 0.0, 0.0], 42 | [100.0, 100.0, 100.0], 43 | [200.0, 200.0, 200.0], 44 | ]; 45 | 46 | let closest_index = find_closest_centroid(&pixel, ¢roids); 47 | assert_eq!(closest_index, 1); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: build serve bench 2 | 3 | PYTHON_ENV = .venv 4 | ROOT_DIR := $(shell pwd) 5 | 6 | build: 7 | cd rust && wasm-pack build --target web --profiling --features wasm 8 | cd .. 9 | rm -rf pkg 10 | mv rust/pkg . 11 | rm -f pkg/.gitignore 12 | 13 | build-release: 14 | cd rust && wasm-pack build --target web --release --features wasm 15 | cd .. 16 | rm -rf pkg 17 | mv rust/pkg . 18 | rm -f pkg/.gitignore 19 | 20 | analysis-env: 21 | if ! [ -d $(PYTHON_ENV) ]; then \ 22 | python3 -m venv $(PYTHON_ENV) ; \ 23 | fi 24 | $(PYTHON_ENV)/bin/python -m pip install scipy colorama 25 | 26 | bench: analysis-env 27 | $(eval PREVIOUS_BENCHMARK := $(shell ls -t benchmark_history/ | head -n 1)) 28 | $(eval NEW_BENCHMARK := bench_$(shell date +%s).txt) 29 | @echo "Benchmarking into $(NEW_BENCHMARK)" 30 | cd rust && cargo wasi bench --profile release --features gpu >> $(ROOT_DIR)/benchmark_history/$(NEW_BENCHMARK) 31 | @echo "Comparing $(PREVIOUS_BENCHMARK) to $(NEW_BENCHMARK)" 32 | cd $(ROOT_DIR) && $(PYTHON_ENV)/bin/python3 scripts/compare_benchmarks.py benchmark_history/$(PREVIOUS_BENCHMARK) benchmark_history/$(NEW_BENCHMARK) 33 | 34 | bench-compare: analysis-env 35 | $(eval MOST_RECENT_BENCHMARK := $(shell ls -t benchmark_history | head -n 1)) 36 | $(eval PREVIOUS_BENCHMARK := $(shell ls -t benchmark_history | head -n 2 | tail -n 1)) 37 | $(PYTHON_ENV)/bin/python3 scripts/compare_benchmarks.py benchmark_history/$(PREVIOUS_BENCHMARK) benchmark_history/$(MOST_RECENT_BENCHMARK) 38 | 39 | 40 | serve-profiling: build 41 | python3 -m http.server 8000 42 | 43 | serve-release: build-release 44 | python3 -m http.server 8000 45 | 46 | all: serve-release 47 | -------------------------------------------------------------------------------- /rust/src/types.rs: -------------------------------------------------------------------------------- 1 | pub type Vec3 = [f32; 3]; 2 | pub type Vec4 = [f32; 4]; 3 | pub type Vec4u = [u32; 4]; 4 | 5 | pub trait GPUVector {} 6 | pub trait VectorExt: 7 | Clone + Copy + std::ops::Index + std::ops::IndexMut + std::fmt::Debug 8 | { 9 | fn add(&self, other: &Self) -> Self; 10 | fn sub(&self, other: &Self) -> Self; 11 | fn div_scalar(&self, scalar: f32) -> Self; 12 | fn zero() -> Self; 13 | } 14 | 15 | impl VectorExt for Vec3 { 16 | fn zero() -> Self { 17 | [0.0; 3] 18 | } 19 | 20 | fn add(&self, other: &Vec3) -> Self { 21 | let mut sum = [0.0; 3]; 22 | for i in 0..3 { 23 | sum[i] = self[i] + other[i]; 24 | } 25 | sum 26 | } 27 | 28 | fn div_scalar(&self, scalar: f32) -> Self { 29 | [self[0] / scalar, self[1] / scalar, self[2] / scalar] 30 | } 31 | 32 | fn sub(&self, other: &Vec3) -> Self { 33 | let mut sum = [0.0; 3]; 34 | for i in 0..3 { 35 | sum[i] = self[i] - other[i]; 36 | } 37 | sum 38 | } 39 | } 40 | 41 | impl VectorExt for Vec4 { 42 | fn add(&self, other: &Vec4) -> Self { 43 | let mut sum = [0.0; 4]; 44 | for i in 0..4 { 45 | sum[i] = self[i] + other[i]; 46 | } 47 | sum 48 | } 49 | 50 | fn sub(&self, other: &Vec4) -> Self { 51 | let mut sum = [0.0; 4]; 52 | for i in 0..4 { 53 | sum[i] = self[i] - other[i]; 54 | } 55 | sum 56 | } 57 | 58 | fn div_scalar(&self, scalar: f32) -> Self { 59 | [ 60 | self[0] / scalar, 61 | self[1] / scalar, 62 | self[2] / scalar, 63 | self[3] / scalar, 64 | ] 65 | } 66 | 67 | fn zero() -> Self { 68 | [0.0; 4] 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /pkg/colorcruncher_bg.wasm.d.ts: -------------------------------------------------------------------------------- 1 | /* tslint:disable */ 2 | /* eslint-disable */ 3 | export const memory: WebAssembly.Memory; 4 | export function start(): void; 5 | export function __wbg_colorcruncher_free(a: number): void; 6 | export function __wbg_colorcruncherbuilder_free(a: number): void; 7 | export function colorcruncherbuilder_new(): number; 8 | export function colorcruncherbuilder_withMaxColors(a: number, b: number): number; 9 | export function colorcruncherbuilder_withSampleRate(a: number, b: number): number; 10 | export function colorcruncherbuilder_withTolerance(a: number, b: number): number; 11 | export function colorcruncherbuilder_withMaxIterations(a: number, b: number): number; 12 | export function colorcruncherbuilder_withInitializer(a: number, b: number, c: number): number; 13 | export function colorcruncherbuilder_withAlgorithm(a: number, b: number, c: number): number; 14 | export function colorcruncherbuilder_withSeed(a: number, b: number): number; 15 | export function colorcruncherbuilder_build(a: number): number; 16 | export function colorcruncher_quantizeImage(a: number, b: number, c: number): number; 17 | export function __wbindgen_malloc(a: number, b: number): number; 18 | export function __wbindgen_realloc(a: number, b: number, c: number, d: number): number; 19 | export const __wbindgen_export_2: WebAssembly.Table; 20 | export function _dyn_core__ops__function__FnMut__A____Output___R_as_wasm_bindgen__closure__WasmClosure___describe__invoke__h6e435f3bc6e3d654(a: number, b: number, c: number): void; 21 | export function _dyn_core__ops__function__FnMut__A____Output___R_as_wasm_bindgen__closure__WasmClosure___describe__invoke__h870dc3eb3e49f78a(a: number, b: number, c: number): void; 22 | export function __wbindgen_free(a: number, b: number, c: number): void; 23 | export function __wbindgen_exn_store(a: number): void; 24 | export function wasm_bindgen__convert__closures__invoke2_mut__h849a5a7e5a0f14b2(a: number, b: number, c: number, d: number): void; 25 | export function __wbindgen_start(): void; 26 | -------------------------------------------------------------------------------- /rust/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "colorcruncher" 3 | version = "0.1.2" 4 | edition = "2021" 5 | authors = ["Matthew Deakos "] 6 | license = "MIT" 7 | description = "A fast and minimal wasm-compatible color quantization library written in Rust" 8 | repository = "git@github.com:mattdeak/wasm-color-quantizer.git" 9 | keywords = ["color", "image-processing", "wasm", "gpu", "k-means"] 10 | 11 | [lib] 12 | crate-type = ["cdylib", "rlib"] 13 | 14 | [profile.bench] 15 | debug = true 16 | 17 | [features] 18 | default = ["wasm", "gpu"] 19 | python = ["pyo3", "numpy"] 20 | wasm = ["js-sys", "wasm-bindgen", "console_log", "console_error_panic_hook"] 21 | gpu = ["wgpu", "env_logger", "log", "bytemuck", "wasm-bindgen-futures"] 22 | 23 | [target.'cfg(not(target_arch = "wasm32"))'.dev-dependencies] 24 | criterion = { version = "0.5.1", features = ["async_futures"] } 25 | 26 | [[bench]] 27 | name = "wasm_benchmarks" 28 | harness = false 29 | 30 | [[bench]] 31 | name = "criterion_benchmarks" 32 | harness = false 33 | 34 | [[bench]] 35 | name = "kmeans_gpu_benchmark" 36 | harness = false 37 | 38 | [dependencies] 39 | getrandom = { version = "0.2.15", features = ["js"] } 40 | pyo3 = { version = "0.21.0", features = ["extension-module"], optional = true } 41 | numpy = { version = "0.21.0", optional = true } 42 | 43 | bytemuck = { version = "1.16.1", features = ["derive"], optional = true } 44 | env_logger = { version = "0.11.3", optional = true } 45 | futures = "0.3.30" 46 | futures-intrusive = "0.5.0" 47 | itertools = "0.13.0" 48 | js-sys = { version = "0.3.69", optional = true } 49 | log = { version = "0.4.22", optional = true } 50 | rand = "0.8.5" 51 | wasm-bindgen = { version = "0.2.92", optional = true } 52 | wgpu = { version = "0.20.1", optional = true } 53 | wasm-bindgen-futures = { version = "0.4.42", optional = true } 54 | console_log = { version = "1.0.0", optional = true } 55 | console_error_panic_hook = { version = "0.1.7", optional = true } 56 | 57 | 58 | [dev-dependencies] 59 | statrs = "0.17.1" 60 | wasm-bindgen-test = "0.3.42" 61 | -------------------------------------------------------------------------------- /rust/benches/kmeans_gpu_benchmark.rs: -------------------------------------------------------------------------------- 1 | use colorcrunch::kmeans::gpu::{GpuAlgorithm, KMeansGpu}; 2 | use colorcrunch::kmeans::{Initializer, KMeansConfig}; 3 | use colorcrunch::types::Vec4u; 4 | 5 | #[cfg(not(target_arch = "wasm32"))] 6 | use criterion::async_executor::FuturesExecutor; 7 | #[cfg(not(target_arch = "wasm32"))] 8 | use criterion::{black_box, criterion_group, criterion_main, BenchmarkId, Criterion}; 9 | use futures::executor::block_on; 10 | use rand::prelude::*; 11 | 12 | #[cfg(not(target_arch = "wasm32"))] 13 | fn benchmark_kmeans_gpu(c: &mut Criterion) { 14 | let algorithms = vec![ 15 | GpuAlgorithm::LloydAssignmentsAndCentroids, 16 | GpuAlgorithm::LloydAssignmentsOnly, 17 | ]; 18 | 19 | let mut rng = thread_rng(); 20 | let image_size = 2000; 21 | let pixels: Vec = (0..image_size * image_size) 22 | .map(|_| { 23 | [ 24 | (rng.gen::() * 255.0) as u32, 25 | (rng.gen::() * 255.0) as u32, 26 | (rng.gen::() * 255.0) as u32, 27 | (rng.gen::() * 255.0) as u32, 28 | ] 29 | }) 30 | .collect(); 31 | 32 | let mut group = c.benchmark_group("kmeans_gpu"); 33 | 34 | for algorithm in algorithms { 35 | let config = KMeansConfig { 36 | k: 10, 37 | max_iterations: 10, 38 | tolerance: 0.001, 39 | algorithm: algorithm.into(), 40 | initializer: Initializer::Random, 41 | seed: Some(42), 42 | }; 43 | let kmeans = block_on(KMeansGpu::new(config)); 44 | 45 | group.bench_with_input( 46 | BenchmarkId::from_parameter(format!("{:?}", algorithm)), 47 | &kmeans, 48 | |b, kmeans| { 49 | b.to_async(FuturesExecutor).iter_with_large_drop(|| async { 50 | kmeans 51 | .run_async(black_box(&pixels)) 52 | .await 53 | .expect("Error running kmeans"); 54 | }); 55 | }, 56 | ); 57 | } 58 | 59 | group.finish(); 60 | } 61 | 62 | #[cfg(not(target_arch = "wasm32"))] 63 | criterion_group!(benches, benchmark_kmeans_gpu); 64 | #[cfg(not(target_arch = "wasm32"))] 65 | criterion_main!(benches); 66 | 67 | #[cfg(target_arch = "wasm32")] 68 | pub fn main() { 69 | println!("Not supported"); 70 | } 71 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # WASM Color Quantizer 2 | This is a simple tool I created to share a color reducer with a pixel artist friend. It's a quick but pretty good way to reduce the number of colors in an image using the K-means++ clustering algorithm. 3 | 4 |

5 | Demo Gif 6 |

7 | 8 | ## Access 9 | You can access and use this tool [right here](https://mattdeak.github.io/wasm-color-quantizer/). It just runs in the browser. 10 | 11 | ## Installation 12 | This package is published as `colorcruncher` on npm. You can install it with `npm install colorcruncher`. 13 | ```javascript 14 | import init, { ColorCruncher } from 'colorcruncher'; 15 | 16 | // initialization (required for wasm) 17 | async function initWasm() { 18 | await init(); 19 | } 20 | 21 | 22 | async function quantizeImage(imageData) { 23 | let cruncherBuilder = new ColorCruncher(numColors, sampleRate) 24 | .withAlgorithm(selectedAlgorithm) 25 | .withMaxIterations(maxIter) 26 | .withTolerance(tol); 27 | 28 | if (seedValue !== undefined) { 29 | cruncherBuilder = cruncherBuilder.withSeed(seedValue); 30 | } 31 | 32 | const cruncher = await cruncherBuilder.build(); 33 | const processedData = await cruncher.quantizeImage(imageData.data); 34 | 35 | const processedImageData = new ImageData(new Uint8ClampedArray(processedData), canvas.width, canvas.height, { 36 | colorSpace: 'srgb' 37 | }); 38 | ctx.putImageData(processedImageData, 0, 0); 39 | } 40 | ``` 41 | 42 | ## How it Works 43 | This quantizer uses Rust compiled to WebAssembly (WASM) to perform the K-means calculation quickly and efficiently in the browser. 44 | Will update soon with better sampling to handle very large N-color requests or humongous images (bigger than any reasonable image would be). Maybe gifs/video too. 45 | 46 | I might eventually get around to splitting out the Rust package if I add enough functionality (other clustering methods, maybe), but feel free to clone this and rip it all out if you want. 47 | 48 | Feel free to use this tool for whatever you like. 49 | 50 | ### WebGPU 51 | If you have a browser that supports WebGPU, you can run the quantizer on GPU (under Advanced, select a GPU algorithm). This can be faster, particularly for big images, but Hamerly on CPU will be quicker on most images smaller than 1000x1000 if the desired number of colors is reasonably low. 52 | -------------------------------------------------------------------------------- /rust/src/kmeans/gpu.rs: -------------------------------------------------------------------------------- 1 | #![cfg(feature = "gpu")] 2 | 3 | mod buffers; 4 | mod common; 5 | mod lloyd_gpu1; 6 | 7 | use crate::kmeans::config::KMeansConfig; 8 | use crate::types::{Vec4, Vec4u}; 9 | 10 | use self::lloyd_gpu1::LloydAssignmentsOnly; 11 | 12 | use super::types::KMeansError; 13 | 14 | pub async fn run_lloyd_gpu( 15 | config: KMeansConfig, 16 | data: &[Vec4u], 17 | ) -> Result<(Vec, Vec), KMeansError> { 18 | let lloyd_gpu = LloydAssignmentsOnly::from_config(config).await; 19 | lloyd_gpu.run_async(data).await 20 | } 21 | 22 | #[cfg(test)] 23 | mod tests { 24 | use super::*; 25 | use crate::kmeans::config::KMeansConfig; 26 | use crate::types::Vec4u; 27 | use futures::executor::block_on; 28 | use rand::rngs::StdRng; 29 | use rand::Rng; 30 | use rand::SeedableRng; 31 | 32 | #[test] 33 | fn test_gpu_algorithms_convergence() { 34 | const K: usize = 5; 35 | const N: usize = 1000; 36 | const SEED: u64 = 42; 37 | 38 | // Generate random data 39 | let mut rng = StdRng::seed_from_u64(SEED); 40 | let data: Vec = (0..N) 41 | .map(|_| { 42 | [ 43 | rng.gen_range(0..255), 44 | rng.gen_range(0..255), 45 | rng.gen_range(0..255), 46 | rng.gen_range(0..255), 47 | ] 48 | }) 49 | .collect(); 50 | 51 | // Create configuration 52 | let config = KMeansConfig { 53 | seed: Some(SEED), 54 | k: K, 55 | max_iterations: 100, 56 | tolerance: 1.0, 57 | initializer: crate::kmeans::Initializer::Random, 58 | ..Default::default() 59 | }; 60 | 61 | // Run the GPU algorithm 62 | let (assignments, centroids) = block_on(run_lloyd_gpu(config, &data)).unwrap(); 63 | 64 | // Basic sanity checks 65 | assert_eq!(assignments.len(), N); 66 | assert_eq!(centroids.len(), K); 67 | 68 | // Check if all assignments are within the valid range 69 | for assignment in &assignments { 70 | assert!(*assignment < K); 71 | } 72 | 73 | // Check if all centroid components are within the valid range 74 | for centroid in ¢roids { 75 | for component in centroid.iter() { 76 | assert!(*component >= 0.0 && *component <= 255.0); 77 | } 78 | } 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /rust/src/kmeans/lloyd.rs: -------------------------------------------------------------------------------- 1 | use crate::kmeans::config::KMeansConfig; 2 | use crate::kmeans::utils::{find_closest_centroid, has_converged}; 3 | use crate::types::VectorExt; 4 | 5 | pub fn kmeans_lloyd(data: &[T], config: &KMeansConfig) -> (Vec, Vec) { 6 | let mut centroids = config 7 | .initializer 8 | .initialize_centroids(data, config.k, config.seed); 9 | let mut new_centroids: Vec = centroids.clone(); 10 | 11 | let mut clusters = vec![Vec::new(); config.k]; 12 | let mut assignments = vec![0; data.len()]; 13 | 14 | // Define the convergence criterion percentage (e.g., 2%) 15 | let mut iterations = 0; 16 | let mut converged = false; 17 | while iterations < config.max_iterations && !converged { 18 | // Assign points to clusters 19 | for (i, pixel) in data.iter().enumerate() { 20 | let closest_centroid = find_closest_centroid(pixel, ¢roids); 21 | if assignments[i] != closest_centroid { 22 | assignments[i] = closest_centroid; 23 | } 24 | } 25 | 26 | clusters.iter_mut().for_each(|cluster| cluster.clear()); 27 | assignments.iter().enumerate().for_each(|(i, &cluster)| { 28 | clusters[cluster].push(i); 29 | }); 30 | 31 | // Update centroids and check for convergence 32 | clusters 33 | .iter() 34 | .zip(new_centroids.iter_mut()) 35 | .for_each(|(cluster, new_centroid)| { 36 | if cluster.is_empty() { 37 | return; // centroid can't move if there are no points 38 | } 39 | 40 | let mut sum_r = 0.0; 41 | let mut sum_g = 0.0; 42 | let mut sum_b = 0.0; 43 | let num_pixels = cluster.len() as f32; 44 | 45 | for &idx in cluster { 46 | let pixel = &data[idx]; 47 | sum_r += pixel[0]; 48 | sum_g += pixel[1]; 49 | sum_b += pixel[2]; 50 | } 51 | 52 | new_centroid[0] = sum_r / num_pixels; 53 | new_centroid[1] = sum_g / num_pixels; 54 | new_centroid[2] = sum_b / num_pixels; 55 | }); 56 | converged = has_converged(¢roids, &new_centroids, config.tolerance); 57 | // Swap the centroids and new_centroid. We'll update the new centroids again before 58 | // we check for convergence. 59 | std::mem::swap(&mut centroids, &mut new_centroids); 60 | iterations += 1; 61 | } 62 | 63 | (assignments, centroids) 64 | } 65 | -------------------------------------------------------------------------------- /scripts/compare_benchmarks.py: -------------------------------------------------------------------------------- 1 | import argparse 2 | import re 3 | from typing import Literal 4 | 5 | from colorama import Fore, Style, init 6 | from scipy import stats 7 | 8 | 9 | def parse_benchmark_file( 10 | file_path: str, 11 | ) -> dict[tuple[int, int], tuple[float, float, float]]: 12 | results: dict[tuple[int, int], tuple[float, float, float]] = {} 13 | with open(file_path, "r") as f: 14 | for line in f: 15 | match = re.search( 16 | r"Size: (\d+), K: (\d+), Mean Time: ([\d.]+)s, CI: ([\d.]+)s - ([\d.]+)s", 17 | line, 18 | ) 19 | if match: 20 | size, k = int(match.group(1)), int(match.group(2)) 21 | mean_time = float(match.group(3)) 22 | ci_lower, ci_upper = float(match.group(4)), float(match.group(5)) 23 | results[(size, k)] = (mean_time, ci_lower, ci_upper) 24 | return results 25 | 26 | 27 | def compare_benchmarks(old_file: str, new_file: str) -> None: 28 | old_results = parse_benchmark_file(old_file) 29 | new_results = parse_benchmark_file(new_file) 30 | 31 | init() # Initialize colorama 32 | print( 33 | f"{'Size':>5} {'K':>3} {'Old (ms)':>10} {'New (ms)':>10} {'Change':>10} {'p-value':>10}" 34 | ) 35 | print("-" * 55) 36 | 37 | for (size, k), (old_mean, old_lower, old_upper) in old_results.items(): 38 | if (size, k) in new_results: 39 | new_mean, new_lower, new_upper = new_results[(size, k)] 40 | 41 | # Convert to milliseconds 42 | old_mean_ms = old_mean * 1000 43 | new_mean_ms = new_mean * 1000 44 | 45 | percent_change = (new_mean - old_mean) / old_mean * 100 46 | old_std = (old_upper - old_lower) / (2 * 1.96) 47 | new_std = (new_upper - new_lower) / (2 * 1.96) 48 | 49 | # Calculate degrees of freedom using Welch–Satterthwaite equation 50 | df = ((old_std**2 + new_std**2) ** 2) / ( 51 | (old_std**4 / (30 - 1)) + (new_std**4 / (30 - 1)) 52 | ) 53 | 54 | t_statistic = (new_mean - old_mean) / ((old_std**2 + new_std**2) ** 0.5) 55 | p_value = stats.t.sf(abs(t_statistic), df) * 2 56 | 57 | color: Literal[Fore.GREEN, Fore.RED, ""] = "" 58 | if p_value < 0.05: 59 | color = Fore.GREEN if new_mean < old_mean else Fore.RED 60 | 61 | print( 62 | f"{size:5d} {k:3d} {old_mean_ms:10.2f} {new_mean_ms:10.2f} " 63 | f"{color}{percent_change:+10.2f}%{Style.RESET_ALL} {p_value:10.4f}" 64 | ) 65 | 66 | 67 | def main() -> None: 68 | parser = argparse.ArgumentParser(description="Compare two benchmark result files.") 69 | parser.add_argument("old_file", help="Path to the old benchmark results file") 70 | parser.add_argument("new_file", help="Path to the new benchmark results file") 71 | args = parser.parse_args() 72 | 73 | compare_benchmarks(args.old_file, args.new_file) 74 | 75 | 76 | if __name__ == "__main__": 77 | main() 78 | -------------------------------------------------------------------------------- /rust/src/kmeans/initializer.rs: -------------------------------------------------------------------------------- 1 | use crate::kmeans::distance::euclidean_distance_squared; 2 | use crate::kmeans::distance::SquaredEuclideanDistance; 3 | use crate::types::VectorExt; 4 | use rand::prelude::*; 5 | use rand::SeedableRng; 6 | 7 | #[derive(Debug, Clone)] 8 | pub enum Initializer { 9 | KMeansPlusPlus, 10 | Random, 11 | } 12 | 13 | impl Initializer { 14 | pub fn initialize_centroids( 15 | &self, 16 | data: &[T], 17 | k: usize, 18 | seed: Option, 19 | ) -> Vec { 20 | match self { 21 | Initializer::KMeansPlusPlus => kmeans_plus_plus(data, k, seed), 22 | Initializer::Random => initialize_random(data, k, seed), 23 | } 24 | } 25 | } 26 | 27 | fn get_seedable_rng(seed: Option) -> StdRng { 28 | if let Some(seed) = seed { 29 | rand::rngs::StdRng::seed_from_u64(seed) 30 | } else { 31 | rand::rngs::StdRng::from_entropy() 32 | } 33 | } 34 | 35 | // Ok we're using the K-Means++ initialization 36 | // I think this is right? Seems to work 37 | fn kmeans_plus_plus(data: &[T], k: usize, seed: Option) -> Vec { 38 | let mut centroids = Vec::with_capacity(k); 39 | 40 | // Seed the RNG if provided, otherwise use the current time 41 | let mut rng = get_seedable_rng(seed); 42 | 43 | // Choose the first centroid randomly 44 | if let Some(first_centroid) = data.choose(&mut rng) { 45 | centroids.push(*first_centroid); 46 | } else { 47 | return centroids; 48 | } 49 | 50 | // K-Means++ 51 | while centroids.len() < k { 52 | let distances: Vec = data 53 | .iter() 54 | .map(|pixel| { 55 | centroids 56 | .iter() 57 | .map(|centroid| euclidean_distance_squared(pixel, centroid)) 58 | .min_by(|a, b| a.partial_cmp(b).unwrap()) 59 | .unwrap() 60 | }) 61 | .collect(); 62 | 63 | let total_distance: SquaredEuclideanDistance = distances.iter().sum(); 64 | let threshold = rng.gen::() * total_distance.0; 65 | 66 | let mut cumulative_distance = 0.0; 67 | for (i, distance) in distances.iter().enumerate() { 68 | cumulative_distance += distance.0; 69 | if cumulative_distance >= threshold { 70 | let pixel = &data[i]; 71 | centroids.push(*pixel); 72 | break; 73 | } 74 | } 75 | } 76 | centroids 77 | } 78 | 79 | pub fn initialize_random(data: &[T], k: usize, seed: Option) -> Vec { 80 | // Seed the RNG if provided, otherwise use the current time 81 | let mut rng = { 82 | if let Some(seed) = seed { 83 | rand::rngs::StdRng::seed_from_u64(seed) 84 | } else { 85 | rand::rngs::StdRng::from_entropy() 86 | } 87 | }; 88 | 89 | let mut centroids = Vec::with_capacity(k); 90 | for centroid in data.choose_multiple(&mut rng, k) { 91 | centroids.push(*centroid); 92 | } 93 | 94 | centroids 95 | } 96 | -------------------------------------------------------------------------------- /styles.css: -------------------------------------------------------------------------------- 1 | body { 2 | font-family: Arial, sans-serif; 3 | background: linear-gradient(to right, #e0f2fe, #f3e8ff); 4 | margin: 0; 5 | padding: 20px; 6 | min-height: 100vh; 7 | display: flex; 8 | justify-content: center; 9 | align-items: center; 10 | } 11 | .container { 12 | background-color: white; 13 | padding: 2rem; 14 | border-radius: 12px; 15 | box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); 16 | max-width: 800px; 17 | width: 100%; 18 | } 19 | h1 { 20 | text-align: center; 21 | color: #333; 22 | margin-bottom: 1.5rem; 23 | } 24 | .grid { 25 | display: grid; 26 | gap: 2rem; 27 | grid-template-columns: 1fr 1fr 1fr; 28 | } 29 | 30 | @media (max-width: 1024px) { 31 | .grid { 32 | grid-template-columns: 1fr 1fr; 33 | } 34 | } 35 | 36 | @media (max-width: 768px) { 37 | .grid { 38 | grid-template-columns: 1fr; 39 | } 40 | } 41 | .input-group { 42 | margin-bottom: 1rem; 43 | } 44 | label { 45 | display: block; 46 | margin-bottom: 0.5rem; 47 | color: #4b5563; 48 | } 49 | input[type="file"], input[type="number"] { 50 | width: 100%; 51 | padding: 0.5rem; 52 | border: 1px solid #d1d5db; 53 | border-radius: 4px; 54 | font-size: 1rem; 55 | } 56 | button { 57 | width: 100%; 58 | padding: 0.75rem; 59 | border: none; 60 | border-radius: 4px; 61 | color: white; 62 | font-size: 1rem; 63 | cursor: pointer; 64 | transition: background-color 0.15s; 65 | } 66 | #processBtn { 67 | background-color: #4f46e5; 68 | margin-bottom: 0.5rem; 69 | } 70 | #processBtn:hover { 71 | background-color: #4338ca; 72 | } 73 | #downloadBtn { 74 | background-color: #16a34a; 75 | } 76 | #downloadBtn:hover { 77 | background-color: #15803d; 78 | } 79 | #downloadBtn:disabled { 80 | background-color: #9ca3af; 81 | cursor: not-allowed; 82 | } 83 | .image-preview { 84 | border: 2px dashed #d1d5db; 85 | border-radius: 8px; 86 | height: 200px; 87 | display: flex; 88 | align-items: center; 89 | justify-content: center; 90 | color: #6b7280; 91 | overflow: hidden; 92 | } 93 | #previewImage, #processedImage { 94 | max-width: 100%; 95 | max-height: 200px; 96 | object-fit: contain; 97 | } 98 | .spinner { 99 | border: 4px solid #f3f3f3; 100 | border-top: 4px solid #3498db; 101 | border-radius: 50%; 102 | width: 40px; 103 | height: 40px; 104 | animation: spin 1s linear infinite; 105 | margin: 20px auto; 106 | } 107 | 108 | @keyframes spin { 109 | 0% { transform: rotate(0deg); } 110 | 100% { transform: rotate(360deg); } 111 | } 112 | 113 | #processingIndicator { 114 | text-align: center; 115 | } 116 | 117 | details { 118 | margin-bottom: 1rem; 119 | } 120 | 121 | summary { 122 | cursor: pointer; 123 | color: #4b5563; 124 | font-weight: bold; 125 | margin-bottom: 0.5rem; 126 | } 127 | 128 | select { 129 | width: 100%; 130 | padding: 0.5rem; 131 | border: 1px solid #d1d5db; 132 | border-radius: 4px; 133 | font-size: 1rem; 134 | background-color: white; 135 | } -------------------------------------------------------------------------------- /rust/src/python.rs: -------------------------------------------------------------------------------- 1 | use numpy::ndarray::Axis; 2 | use numpy::PyReadonlyArray3; 3 | use pyo3::prelude::*; 4 | use pyo3::wrap_pyfunction; 5 | 6 | use crate::{kmeans::kmeans, kmeans::KMeansConfig, quantize::ColorCruncher}; 7 | use numpy::{PyArray1, PyArray2, PyArray3}; 8 | 9 | #[pyfunction(name = "kmeans_3chan")] 10 | #[doc = "Perform k-means clustering on a 3-channel dataset. Expects nx3 array of floats, returns nxk array of labels and kx3 array of centroids"] 11 | fn py_kmeans_3chan( 12 | data: Vec<[f64; 3]>, 13 | k: usize, 14 | ) -> PyResult<(Py>, Py>)> { 15 | let array = match numpy::ndarray::Array2::from_shape_vec((data.len(), 3), data) { 16 | Ok(array) => array, 17 | Err(e) => { 18 | return Err(pyo3::exceptions::PyValueError::new_err(format!( 19 | "Failed to create array: {}", 20 | e 21 | ))) 22 | } 23 | }; 24 | let shape = array.shape(); 25 | if shape[1] != 3 { 26 | return Err(pyo3::exceptions::PyValueError::new_err(format!( 27 | "Expected 3-channel data, got {} channels", 28 | shape[1] 29 | ))); 30 | } 31 | 32 | let array: Vec<[f32; 3]> = array 33 | .into_raw_vec() 34 | .into_iter() 35 | .map(|row| [row[0] as f32, row[1] as f32, row[2] as f32]) 36 | .collect(); 37 | 38 | let config = KMeansConfig { 39 | k, 40 | ..Default::default() 41 | }; 42 | 43 | let (clusters, centroids) = kmeans(&array, &config).unwrap(); 44 | let centroids: Vec> = centroids 45 | .into_iter() 46 | .map(|c| vec![c[0], c[1], c[2]]) 47 | .collect(); 48 | 49 | Python::with_gil(|py| { 50 | let clusters = PyArray1::from_vec_bound(py, clusters); 51 | let centroids = PyArray2::from_vec2_bound(py, ¢roids) 52 | .expect("Failed to convert centroids to PyArray2"); 53 | Ok((clusters.to_owned().into(), centroids.to_owned().into())) 54 | }) 55 | } 56 | 57 | #[pyfunction(name = "reduce_colorspace")] 58 | #[doc = "Reduce the colorspace of a 3-channel dataset. Expects nxm x 3 array of bytes, returns nxm x k array of bytes"] 59 | fn py_reduce_colorspace( 60 | data: PyReadonlyArray3, 61 | num_colors: i32, 62 | sample_rate: i32, 63 | ) -> PyResult>> { 64 | let array = data.as_array(); 65 | let shape = array.shape(); 66 | if shape[2] != 3 { 67 | return Err(pyo3::exceptions::PyValueError::new_err(format!( 68 | "Expected 3-channel data, got {} channels", 69 | shape[2] 70 | ))); 71 | } 72 | 73 | let flattened: Vec = array 74 | .lanes(Axis(2)) 75 | .into_iter() 76 | .map(|lane| { 77 | let slice = lane.as_slice().unwrap(); 78 | [slice[0], slice[1], slice[2]] 79 | }) 80 | .flatten() 81 | .collect(); 82 | 83 | let quantizer = ColorCruncher::new(num_colors as usize, sample_rate as usize, 3); 84 | let data = quantizer.quantize_image(&flattened); 85 | 86 | let reshaped = match numpy::ndarray::Array3::from_shape_vec((shape[0], shape[1], 3), data) { 87 | Ok(reshaped) => reshaped, 88 | Err(e) => { 89 | return Err(pyo3::exceptions::PyValueError::new_err(format!( 90 | "Failed to reshape array: {}", 91 | e 92 | ))) 93 | } 94 | }; 95 | 96 | Python::with_gil(|py| { 97 | Ok(PyArray3::from_owned_array_bound(py, reshaped) 98 | .to_owned() 99 | .into()) 100 | }) 101 | } 102 | 103 | #[pymodule] 104 | fn colorcrunch(_py: Python<'_>, m: &PyModule) -> PyResult<()> { 105 | m.add_function(wrap_pyfunction!(py_kmeans_3chan, m)?)?; 106 | m.add_function(wrap_pyfunction!(py_reduce_colorspace, m)?)?; 107 | Ok(()) 108 | } 109 | -------------------------------------------------------------------------------- /rust/src/kmeans/distance.rs: -------------------------------------------------------------------------------- 1 | use crate::types::VectorExt; 2 | use std::iter::Sum; 3 | use std::ops::Add; 4 | use std::ops::AddAssign; 5 | use std::ops::Div; 6 | use std::ops::DivAssign; 7 | use std::ops::Mul; 8 | use std::ops::MulAssign; 9 | use std::ops::Sub; 10 | use std::ops::SubAssign; 11 | 12 | #[inline] 13 | pub fn euclidean_distance_squared(a: &T, b: &T) -> SquaredEuclideanDistance { 14 | SquaredEuclideanDistance( 15 | f32::powi(a[0] - b[0], 2) + f32::powi(a[1] - b[1], 2) + f32::powi(a[2] - b[2], 2), 16 | ) 17 | } 18 | 19 | #[derive(Debug, Clone, Copy, PartialEq, PartialOrd)] 20 | #[repr(transparent)] 21 | pub struct EuclideanDistance(pub f32); 22 | #[derive(Debug, Clone, Copy, PartialEq, PartialOrd)] 23 | #[repr(transparent)] 24 | pub struct SquaredEuclideanDistance(pub f32); 25 | 26 | macro_rules! impl_distance { 27 | ($name:ident) => { 28 | impl $name { 29 | pub fn min(self, other: Self) -> Self { 30 | Self(self.0.min(other.0)) 31 | } 32 | 33 | pub fn max(self, other: Self) -> Self { 34 | Self(self.0.max(other.0)) 35 | } 36 | 37 | pub fn max_f32(self, other: f32) -> Self { 38 | Self(self.0.max(other)) 39 | } 40 | } 41 | 42 | impl Sum for $name { 43 | fn sum>(iter: I) -> Self { 44 | iter.fold(Self(0.0), |acc, x| acc + x) 45 | } 46 | } 47 | 48 | impl<'a> Sum<&'a Self> for $name { 49 | fn sum>(iter: I) -> Self { 50 | iter.fold(Self(0.0), |acc, &x| acc + x) 51 | } 52 | } 53 | 54 | impl Add for $name { 55 | type Output = Self; 56 | fn add(self, other: Self) -> Self { 57 | Self(self.0 + other.0) 58 | } 59 | } 60 | 61 | impl AddAssign for $name { 62 | fn add_assign(&mut self, other: Self) { 63 | self.0 += other.0; 64 | } 65 | } 66 | 67 | impl Sub for $name { 68 | type Output = Self; 69 | fn sub(self, other: Self) -> Self { 70 | Self(self.0 - other.0) 71 | } 72 | } 73 | 74 | impl SubAssign for $name { 75 | fn sub_assign(&mut self, other: Self) { 76 | self.0 -= other.0; 77 | } 78 | } 79 | 80 | impl Div for $name { 81 | type Output = Self; 82 | fn div(self, other: Self) -> Self { 83 | Self(self.0 / other.0) 84 | } 85 | } 86 | 87 | impl DivAssign for $name { 88 | fn div_assign(&mut self, other: Self) { 89 | self.0 /= other.0; 90 | } 91 | } 92 | 93 | impl Mul for $name { 94 | type Output = Self; 95 | fn mul(self, other: Self) -> Self { 96 | Self(self.0 * other.0) 97 | } 98 | } 99 | 100 | impl MulAssign for $name { 101 | fn mul_assign(&mut self, other: Self) { 102 | self.0 *= other.0; 103 | } 104 | } 105 | 106 | impl From for $name { 107 | fn from(value: f32) -> Self { 108 | Self(value) 109 | } 110 | } 111 | }; 112 | } 113 | 114 | impl_distance!(EuclideanDistance); 115 | impl_distance!(SquaredEuclideanDistance); 116 | 117 | impl SquaredEuclideanDistance { 118 | pub fn sqrt(self) -> EuclideanDistance { 119 | EuclideanDistance(self.0.sqrt()) 120 | } 121 | } 122 | 123 | impl From for SquaredEuclideanDistance { 124 | fn from(value: EuclideanDistance) -> Self { 125 | Self(value.0 * value.0) 126 | } 127 | } 128 | 129 | impl From for EuclideanDistance { 130 | fn from(value: SquaredEuclideanDistance) -> Self { 131 | Self(value.0.sqrt()) 132 | } 133 | } 134 | -------------------------------------------------------------------------------- /pkg/colorcruncher.d.ts: -------------------------------------------------------------------------------- 1 | /* tslint:disable */ 2 | /* eslint-disable */ 3 | /** 4 | */ 5 | export function start(): void; 6 | 7 | export type Algorithm = "lloyd" | "hamerly" | "lloyd-gpu" 8 | export type Initializer = "kmeans++" | "random"; 9 | 10 | 11 | /** 12 | */ 13 | export class ColorCruncher { 14 | free(): void; 15 | /** 16 | * @param {Uint8Array} data 17 | * @returns {Promise} 18 | */ 19 | quantizeImage(data: Uint8Array): Promise; 20 | } 21 | /** 22 | */ 23 | export class ColorCruncherBuilder { 24 | free(): void; 25 | /** 26 | */ 27 | constructor(); 28 | /** 29 | * @param {number} max_colors 30 | * @returns {ColorCruncherBuilder} 31 | */ 32 | withMaxColors(max_colors: number): ColorCruncherBuilder; 33 | /** 34 | * @param {number} sample_rate 35 | * @returns {ColorCruncherBuilder} 36 | */ 37 | withSampleRate(sample_rate: number): ColorCruncherBuilder; 38 | /** 39 | * @param {number} tolerance 40 | * @returns {ColorCruncherBuilder} 41 | */ 42 | withTolerance(tolerance: number): ColorCruncherBuilder; 43 | /** 44 | * @param {number} max_iterations 45 | * @returns {ColorCruncherBuilder} 46 | */ 47 | withMaxIterations(max_iterations: number): ColorCruncherBuilder; 48 | /** 49 | * @param {string} initializer 50 | * @returns {ColorCruncherBuilder} 51 | */ 52 | withInitializer(initializer: string): ColorCruncherBuilder; 53 | /** 54 | * @param {string} algorithm 55 | * @returns {ColorCruncherBuilder} 56 | */ 57 | withAlgorithm(algorithm: string): ColorCruncherBuilder; 58 | /** 59 | * @param {bigint} seed 60 | * @returns {ColorCruncherBuilder} 61 | */ 62 | withSeed(seed: bigint): ColorCruncherBuilder; 63 | /** 64 | * @returns {Promise} 65 | */ 66 | build(): Promise; 67 | } 68 | 69 | export type InitInput = RequestInfo | URL | Response | BufferSource | WebAssembly.Module; 70 | 71 | export interface InitOutput { 72 | readonly memory: WebAssembly.Memory; 73 | readonly start: () => void; 74 | readonly __wbg_colorcruncher_free: (a: number) => void; 75 | readonly __wbg_colorcruncherbuilder_free: (a: number) => void; 76 | readonly colorcruncherbuilder_new: () => number; 77 | readonly colorcruncherbuilder_withMaxColors: (a: number, b: number) => number; 78 | readonly colorcruncherbuilder_withSampleRate: (a: number, b: number) => number; 79 | readonly colorcruncherbuilder_withTolerance: (a: number, b: number) => number; 80 | readonly colorcruncherbuilder_withMaxIterations: (a: number, b: number) => number; 81 | readonly colorcruncherbuilder_withInitializer: (a: number, b: number, c: number) => number; 82 | readonly colorcruncherbuilder_withAlgorithm: (a: number, b: number, c: number) => number; 83 | readonly colorcruncherbuilder_withSeed: (a: number, b: number) => number; 84 | readonly colorcruncherbuilder_build: (a: number) => number; 85 | readonly colorcruncher_quantizeImage: (a: number, b: number, c: number) => number; 86 | readonly __wbindgen_malloc: (a: number, b: number) => number; 87 | readonly __wbindgen_realloc: (a: number, b: number, c: number, d: number) => number; 88 | readonly __wbindgen_export_2: WebAssembly.Table; 89 | readonly _dyn_core__ops__function__FnMut__A____Output___R_as_wasm_bindgen__closure__WasmClosure___describe__invoke__h6e435f3bc6e3d654: (a: number, b: number, c: number) => void; 90 | readonly _dyn_core__ops__function__FnMut__A____Output___R_as_wasm_bindgen__closure__WasmClosure___describe__invoke__h870dc3eb3e49f78a: (a: number, b: number, c: number) => void; 91 | readonly __wbindgen_free: (a: number, b: number, c: number) => void; 92 | readonly __wbindgen_exn_store: (a: number) => void; 93 | readonly wasm_bindgen__convert__closures__invoke2_mut__h849a5a7e5a0f14b2: (a: number, b: number, c: number, d: number) => void; 94 | readonly __wbindgen_start: () => void; 95 | } 96 | 97 | export type SyncInitInput = BufferSource | WebAssembly.Module; 98 | /** 99 | * Instantiates the given `module`, which can either be bytes or 100 | * a precompiled `WebAssembly.Module`. 101 | * 102 | * @param {SyncInitInput} module 103 | * 104 | * @returns {InitOutput} 105 | */ 106 | export function initSync(module: SyncInitInput): InitOutput; 107 | 108 | /** 109 | * If `module_or_path` is {RequestInfo} or {URL}, makes a request and 110 | * for everything else, calls `WebAssembly.instantiate` directly. 111 | * 112 | * @param {InitInput | Promise} module_or_path 113 | * 114 | * @returns {Promise} 115 | */ 116 | export default function __wbg_init (module_or_path?: InitInput | Promise): Promise; 117 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Image Color Quantizer 7 | 8 | 9 | 10 |
11 |

Image Color Quantizer

12 |
13 |
14 |
15 | 16 | 17 |
18 |
19 | 20 | 21 | 25 |
26 |
27 | Advanced Options 28 |
29 | 30 | 38 |
39 | 42 |
43 | 44 | 45 |
46 |
47 | 48 | 56 |
57 |
58 | 59 | 64 |
65 |
66 | 67 | 68 |
69 |
70 |

Original Image

71 |
72 |

No image uploaded

73 | 74 |
75 |
76 |
77 |

Processed Image

78 |
79 |

No image processed yet

80 | 85 | 89 |
90 |
91 |
92 |
93 | 94 | 95 | 131 | 132 | 133 | -------------------------------------------------------------------------------- /rust/benches/criterion_benchmarks.rs: -------------------------------------------------------------------------------- 1 | #[cfg(not(target_arch = "wasm32"))] 2 | #[cfg(feature = "gpu")] 3 | use criterion::async_executor::FuturesExecutor; 4 | #[cfg(not(target_arch = "wasm32"))] 5 | use criterion::{black_box, criterion_group, criterion_main, Criterion}; 6 | 7 | use colorcrunch::{ 8 | kmeans::{KMeans, KMeansAlgorithm}, 9 | types::{Vec3, Vec4, Vec4u}, 10 | }; 11 | use rand::rngs::StdRng; 12 | use rand::{Rng, SeedableRng}; 13 | 14 | #[cfg(not(target_arch = "wasm32"))] 15 | fn generate_random_pixels(count: usize, seed: u64) -> Vec { 16 | let mut rng = StdRng::seed_from_u64(seed ^ (count as u64)); 17 | (0..count) 18 | .map(|_| { 19 | [ 20 | rng.gen::() * 255.0, 21 | rng.gen::() * 255.0, 22 | rng.gen::() * 255.0, 23 | ] 24 | }) 25 | .collect() 26 | } 27 | 28 | #[cfg(not(target_arch = "wasm32"))] 29 | fn generate_random_pixels_vec4u(count: usize, seed: u64) -> Vec { 30 | let mut rng = StdRng::seed_from_u64(seed ^ (count as u64)); 31 | (0..count) 32 | .map(|_| { 33 | [ 34 | rng.gen_range(0..=255), 35 | rng.gen_range(0..=255), 36 | rng.gen_range(0..=255), 37 | 0, 38 | ] 39 | }) 40 | .collect() 41 | } 42 | 43 | #[cfg(not(target_arch = "wasm32"))] 44 | fn benchmark_kmeans_comparison(c: &mut Criterion) { 45 | use colorcrunch::kmeans::KMeansConfig; 46 | use futures::executor::block_on; 47 | 48 | let k_values = [2, 4, 8, 16]; 49 | let data_sizes = [1000, 10000, 100000, 500000]; 50 | let seed = 42; // Fixed seed for reproducibility 51 | 52 | for &size in &data_sizes { 53 | let data = generate_random_pixels(size, seed); 54 | let data_vec4u = generate_random_pixels_vec4u(size, seed); 55 | 56 | for &k in &k_values { 57 | let mut group = c.benchmark_group(format!("kmeans_size_{}_k_{}", size, k)); 58 | 59 | group.bench_function("Hamerly", |b| { 60 | b.iter(|| { 61 | black_box( 62 | block_on(KMeans::new(KMeansConfig { 63 | algorithm: KMeansAlgorithm::Hamerly, 64 | k: k as usize, 65 | ..Default::default() 66 | })) 67 | .run_vec3(black_box(&data)), 68 | ) 69 | }) 70 | }); 71 | 72 | group.bench_function("Lloyd", |b| { 73 | b.iter(|| { 74 | black_box( 75 | block_on(KMeans::new(KMeansConfig { 76 | algorithm: KMeansAlgorithm::Lloyd, 77 | k: k as usize, 78 | ..Default::default() 79 | })) 80 | .run_vec3(black_box(&data)), 81 | ) 82 | }) 83 | }); 84 | 85 | // Initialize outside because it takes a while 86 | let gpu_kmeans = block_on(KMeans::new(KMeansConfig { 87 | algorithm: KMeansAlgorithm::Lloyd, 88 | k: k as usize, 89 | ..Default::default() 90 | })); 91 | 92 | #[cfg(feature = "gpu")] 93 | group.bench_function("Lloyd (GPU)", |b| { 94 | b.to_async(FuturesExecutor).iter_with_large_drop(|| async { 95 | gpu_kmeans.run_async(black_box(&data_vec4u)).await 96 | }) 97 | }); 98 | 99 | group.finish(); 100 | } 101 | } 102 | } 103 | 104 | #[cfg(not(target_arch = "wasm32"))] 105 | fn benchmark_euclidean_distance(c: &mut Criterion) { 106 | use colorcrunch::kmeans; 107 | 108 | let mut rng = rand::thread_rng(); 109 | let a: Vec3 = [rng.gen(), rng.gen(), rng.gen()]; 110 | let b: Vec3 = [rng.gen(), rng.gen(), rng.gen()]; 111 | let mut group = c.benchmark_group("euclidean_distance"); 112 | 113 | group.bench_function("euclidean_distance_arr", |bencher| { 114 | bencher.iter(|| kmeans::distance::euclidean_distance_squared(black_box(&a), black_box(&b))) 115 | }); 116 | 117 | group.finish(); 118 | } 119 | 120 | #[cfg(not(target_arch = "wasm32"))] 121 | fn benchmark_find_closest_centroid(c: &mut Criterion) { 122 | use colorcrunch::kmeans; 123 | let mut rng = rand::thread_rng(); 124 | let pixel = [rng.gen(), rng.gen(), rng.gen()]; 125 | let centroids: Vec = (0..100) 126 | .map(|_| [rng.gen(), rng.gen(), rng.gen()]) 127 | .collect(); 128 | 129 | c.bench_function("find_closest_centroid", |bencher| { 130 | bencher.iter(|| kmeans::find_closest_centroid(black_box(&pixel), black_box(¢roids))) 131 | }); 132 | } 133 | 134 | #[cfg(not(target_arch = "wasm32"))] 135 | criterion_group!( 136 | benches, 137 | benchmark_kmeans_comparison, 138 | benchmark_euclidean_distance, 139 | benchmark_find_closest_centroid 140 | ); 141 | 142 | #[cfg(not(target_arch = "wasm32"))] 143 | criterion_main!(benches); 144 | 145 | #[cfg(target_arch = "wasm32")] 146 | pub fn main() { 147 | println!("This benchmark is only supported on non-wasm32 targets."); 148 | } 149 | -------------------------------------------------------------------------------- /rust/benches/wasm_benchmarks.rs: -------------------------------------------------------------------------------- 1 | // #![cfg(target_arch = "wasm32")] 2 | 3 | use colorcrunch::{ 4 | kmeans::{KMeans, KMeansAlgorithm, KMeansConfig}, 5 | types::{Vec3, Vec4, Vec4u}, 6 | }; 7 | use futures::executor::block_on; 8 | use rand::Rng; 9 | use statrs::{self, statistics::Statistics}; 10 | use std::time::{Duration, Instant}; 11 | 12 | fn generate_random_pixels_u32(count: usize) -> Vec { 13 | let mut rng = rand::thread_rng(); 14 | (0..count) 15 | .map(|_| { 16 | [ 17 | rng.gen_range(0..=255), 18 | rng.gen_range(0..=255), 19 | rng.gen_range(0..=255), 20 | rng.gen_range(0..=255), 21 | ] 22 | }) 23 | .collect() 24 | } 25 | 26 | fn generate_random_pixels(count: usize) -> Vec { 27 | let mut rng = rand::thread_rng(); 28 | (0..count) 29 | .map(|_| { 30 | [ 31 | rng.gen::() * 255.0, 32 | rng.gen::() * 255.0, 33 | rng.gen::() * 255.0, 34 | 0.0, 35 | ] 36 | }) 37 | .collect() 38 | } 39 | 40 | #[no_mangle] 41 | pub extern "C" fn benchmark() -> f64 { 42 | let k_values = [2, 4, 8, 16]; 43 | let data_sizes = [1000, 10000, 100000]; 44 | let iterations = 100; 45 | let warmup_duration = Duration::from_secs(3); 46 | let mut total_time = 0.0; 47 | let algorithms = vec![ 48 | colorcrunch::kmeans::KMeansAlgorithm::Hamerly, 49 | colorcrunch::kmeans::KMeansAlgorithm::Lloyd, 50 | ]; 51 | 52 | for &size in &data_sizes { 53 | for &k in &k_values { 54 | for algorithm in &algorithms { 55 | let kmeans_cpu = block_on(KMeans::new(KMeansConfig { 56 | algorithm: algorithm.clone(), 57 | k: k as usize, 58 | max_iterations: 1000, 59 | tolerance: 0.02, 60 | seed: Some(0), 61 | ..Default::default() 62 | })); 63 | 64 | let kmeans_gpu = block_on(KMeans::new_gpu(KMeansConfig { 65 | algorithm: algorithm.clone(), 66 | k: k as usize, 67 | max_iterations: 1000, 68 | tolerance: 0.02, 69 | seed: Some(0), 70 | ..Default::default() 71 | })); 72 | 73 | // Warmup with new data each time 74 | let warmup_start = Instant::now(); 75 | while warmup_start.elapsed() < warmup_duration { 76 | let warmup_data = generate_random_pixels(size); 77 | let warmup_u32_data = generate_random_pixels_u32(size); 78 | kmeans_cpu.run_vec4(&warmup_data).unwrap(); 79 | block_on(kmeans_gpu.run_async(&warmup_u32_data)).unwrap(); 80 | } 81 | 82 | let mut times_cpu = Vec::with_capacity(iterations); 83 | let mut times_gpu = Vec::with_capacity(iterations); 84 | 85 | for _ in 0..iterations { 86 | let data = generate_random_pixels(size); 87 | let u32_data = generate_random_pixels_u32(size); 88 | 89 | let start = Instant::now(); 90 | kmeans_cpu.run_vec4(&data).unwrap(); 91 | let duration = start.elapsed(); 92 | times_cpu.push(duration.as_secs_f64()); 93 | 94 | let start = Instant::now(); 95 | block_on(kmeans_gpu.run_async(&u32_data)).unwrap(); 96 | let duration = start.elapsed(); 97 | times_gpu.push(duration.as_secs_f64()); 98 | } 99 | 100 | let mean_time_cpu: f64 = (×_cpu).mean(); 101 | let std_dev_cpu: f64 = (×_cpu).std_dev(); 102 | let mean_time_gpu: f64 = (×_gpu).mean(); 103 | let std_dev_gpu: f64 = (×_gpu).std_dev(); 104 | 105 | total_time += mean_time_cpu * iterations as f64; 106 | total_time += mean_time_gpu * iterations as f64; 107 | 108 | let ci_lower_cpu = 109 | mean_time_cpu - (1.96 * std_dev_cpu / (iterations as f64).sqrt()); 110 | let ci_upper_cpu = 111 | mean_time_cpu + (1.96 * std_dev_cpu / (iterations as f64).sqrt()); 112 | let ci_lower_gpu = 113 | mean_time_gpu - (1.96 * std_dev_gpu / (iterations as f64).sqrt()); 114 | let ci_upper_gpu = 115 | mean_time_gpu + (1.96 * std_dev_gpu / (iterations as f64).sqrt()); 116 | 117 | println!( 118 | "Size: {}, K: {}, Algorithm: {:?}, CPU Mean Time: {:.6}s, CI: {:.6}s - {:.6}s", 119 | size, k, algorithm, mean_time_cpu, ci_lower_cpu, ci_upper_cpu 120 | ); 121 | println!( 122 | "Size: {}, K: {}, Algorithm: {:?}, GPU Mean Time: {:.6}s, CI: {:.6}s - {:.6}s", 123 | size, k, algorithm, mean_time_gpu, ci_lower_gpu, ci_upper_gpu 124 | ); 125 | } 126 | } 127 | } 128 | total_time 129 | } 130 | 131 | #[cfg(target_arch = "wasm32")] 132 | pub fn main() { 133 | let total_time = benchmark(); 134 | println!("Total benchmark time: {:.6}s", total_time); 135 | } 136 | 137 | #[cfg(not(target_arch = "wasm32"))] 138 | pub fn main() { 139 | println!("This benchmark is only supported on wasm32 targets."); 140 | } 141 | -------------------------------------------------------------------------------- /rust/src/wasm.rs: -------------------------------------------------------------------------------- 1 | #![cfg(target_arch = "wasm32")] 2 | #![cfg(feature = "wasm")] 3 | #![cfg(feature = "gpu")] 4 | 5 | const RGBA_CHANNELS: usize = 4; 6 | use js_sys::Uint8Array; 7 | 8 | use crate::quantize::{ColorCruncher, ColorCruncherBuilder}; 9 | use console_error_panic_hook; 10 | use console_log; 11 | use log::Level; 12 | use std; 13 | use wasm_bindgen::prelude::*; 14 | 15 | // On start, load the wasm stuff we need 16 | #[wasm_bindgen(start)] 17 | fn start() { 18 | // Load the wasm stuff we need 19 | std::panic::set_hook(Box::new(console_error_panic_hook::hook)); 20 | console_log::init_with_level(Level::Warn).expect("Failed to initialize console log"); 21 | } 22 | 23 | #[wasm_bindgen(js_name = ColorCruncher)] 24 | pub struct WasmColorCruncher(ColorCruncher); 25 | 26 | #[wasm_bindgen(js_name = ColorCruncherBuilder)] 27 | pub struct WasmColorCruncherBuilder(ColorCruncherBuilder); 28 | 29 | #[wasm_bindgen(typescript_custom_section)] 30 | const TS_APPEND_CONTENT: &'static str = r#" 31 | export type Algorithm = "lloyd" | "hamerly" | "lloyd-gpu" 32 | export type Initializer = "kmeans++" | "random"; 33 | "#; 34 | 35 | type Algorithm = String; 36 | type Initializer = String; 37 | 38 | #[wasm_bindgen(js_class = ColorCruncherBuilder)] 39 | impl WasmColorCruncherBuilder { 40 | #[wasm_bindgen(constructor)] 41 | pub fn new() -> Self { 42 | Self(ColorCruncherBuilder::new().with_channels(RGBA_CHANNELS)) 43 | } 44 | 45 | #[wasm_bindgen(js_name = withMaxColors)] 46 | pub fn with_max_colors(self, max_colors: u32) -> Self { 47 | Self(self.0.with_max_colors(max_colors as usize)) 48 | } 49 | 50 | #[wasm_bindgen(js_name = withSampleRate)] 51 | pub fn with_sample_rate(self, sample_rate: u32) -> Self { 52 | Self(self.0.with_sample_rate(sample_rate as usize)) 53 | } 54 | 55 | #[wasm_bindgen(js_name = withTolerance)] 56 | pub fn with_tolerance(self, tolerance: f32) -> Self { 57 | Self(self.0.with_tolerance(tolerance)) 58 | } 59 | 60 | #[wasm_bindgen(js_name = withMaxIterations)] 61 | pub fn with_max_iterations(self, max_iterations: u32) -> Self { 62 | Self(self.0.with_max_iterations(max_iterations as usize)) 63 | } 64 | 65 | #[wasm_bindgen(js_name = withInitializer)] 66 | pub fn with_initializer(self, initializer: Initializer) -> Self { 67 | let init = match initializer.as_str() { 68 | "kmeans++" => crate::kmeans::Initializer::KMeansPlusPlus, 69 | "random" => crate::kmeans::Initializer::Random, 70 | _ => panic!("Invalid initializer: {}", initializer), 71 | }; 72 | Self(self.0.with_initializer(init)) 73 | } 74 | 75 | #[wasm_bindgen(js_name = withAlgorithm)] 76 | pub fn with_algorithm(self, algorithm: Algorithm) -> Self { 77 | let algo = match algorithm.as_str() { 78 | "lloyd" => crate::kmeans::KMeansAlgorithm::Lloyd, 79 | "hamerly" => crate::kmeans::KMeansAlgorithm::Hamerly, 80 | "lloyd-gpu" => crate::kmeans::KMeansAlgorithm::LloydGpu, 81 | _ => panic!("Invalid algorithm: {}", algorithm), 82 | }; 83 | Self(self.0.with_algorithm(algo)) 84 | } 85 | 86 | #[wasm_bindgen(js_name = withSeed)] 87 | pub fn with_seed(self, seed: u64) -> Self { 88 | Self(self.0.with_seed(seed)) 89 | } 90 | 91 | #[wasm_bindgen(js_name = build)] 92 | pub async fn build(&self) -> WasmColorCruncher { 93 | WasmColorCruncher(self.0.build().await) 94 | } 95 | } 96 | 97 | #[wasm_bindgen(js_class = ColorCruncher)] 98 | impl WasmColorCruncher { 99 | #[wasm_bindgen(js_name = quantizeImage)] 100 | pub async fn quantize_image(&self, data: &[u8]) -> Result { 101 | let result = self.0.quantize_image(data).await; 102 | Ok(Uint8Array::from(result.as_slice())) 103 | } 104 | } 105 | 106 | #[cfg(test)] 107 | mod tests { 108 | use super::*; 109 | use js_sys::Uint8Array; 110 | use wasm_bindgen_test::*; 111 | 112 | wasm_bindgen_test_configure!(run_in_browser); 113 | 114 | #[wasm_bindgen_test] 115 | async fn test_color_cruncher_builder() { 116 | let builder = WasmColorCruncherBuilder::new(); 117 | let builder = builder 118 | .with_max_colors(16) 119 | .with_sample_rate(2) 120 | .with_tolerance(0.01) 121 | .with_max_iterations(100) 122 | .with_initializer("kmeans++".to_string()) 123 | .with_algorithm("lloyd".to_string()) 124 | .with_seed(42); 125 | 126 | let cruncher = builder.build().await; 127 | assert!(true); // If we got here, the test passed 128 | } 129 | 130 | #[wasm_bindgen_test] 131 | async fn test_quantize_image() { 132 | let builder = WasmColorCruncherBuilder::new() 133 | .with_max_colors(2) 134 | .with_sample_rate(1); 135 | let cruncher = builder.build().await; 136 | 137 | let input_data = vec![ 138 | 255, 0, 0, 255, 0, 255, 0, 255, 0, 0, 255, 255, 255, 255, 255, 255, 139 | ]; 140 | 141 | let result = cruncher.quantize_image(&input_data).await.unwrap(); 142 | assert_eq!(result.length(), input_data.len() as u32); 143 | 144 | // Convert Uint8Array back to Vec for easier assertions 145 | let result_vec: Vec = result.to_vec(); 146 | 147 | // Check that we have only two colors (plus alpha) 148 | let unique_colors: std::collections::HashSet<_> = result_vec 149 | .chunks_exact(4) 150 | .map(|chunk| (chunk[0], chunk[1], chunk[2])) 151 | .collect(); 152 | assert!(unique_colors.len() <= 2); 153 | } 154 | } 155 | -------------------------------------------------------------------------------- /script.js: -------------------------------------------------------------------------------- 1 | document.addEventListener("DOMContentLoaded", () => { 2 | const imageUpload = document.getElementById("imageUpload"); 3 | const colorCount = document.getElementById("colorCount"); 4 | const processBtn = document.getElementById("processBtn"); 5 | const downloadBtn = document.getElementById("downloadBtn"); 6 | const imagePreview = document.getElementById("imagePreview"); 7 | const previewImage = document.getElementById("previewImage"); 8 | const dropText = document.getElementById("dropText"); 9 | const processedImagePreview = document.getElementById( 10 | "processedImagePreview", 11 | ); 12 | const processedImage = document.getElementById("processedImage"); 13 | const processingIndicator = document.getElementById("processingIndicator"); 14 | let uploadedImage = null; 15 | const algorithm = document.getElementById("algorithm"); 16 | const maxIterations = document.getElementById("maxIterations"); 17 | const tolerance = document.getElementById("tolerance"); 18 | const seed = document.getElementById("seed"); 19 | const algorithmWarning = document.getElementById("algorithmWarning"); 20 | 21 | imageUpload.addEventListener("change", handleImageUpload); 22 | imagePreview.addEventListener("dragover", handleDragOver); 23 | imagePreview.addEventListener("drop", handleDrop); 24 | 25 | function handleImageUpload(e) { 26 | const file = e.target.files[0]; 27 | if (file) { 28 | uploadedImage = file; 29 | displayImage(file); 30 | downloadBtn.disabled = true; 31 | } 32 | } 33 | 34 | function handleDragOver(e) { 35 | e.preventDefault(); 36 | e.stopPropagation(); 37 | } 38 | 39 | function handleDrop(e) { 40 | e.preventDefault(); 41 | e.stopPropagation(); 42 | const file = e.dataTransfer.files[0]; 43 | if (file && file.type.startsWith("image/")) { 44 | imageUpload.files = e.dataTransfer.files; 45 | handleImageUpload({ target: { files: [file] } }); 46 | } 47 | } 48 | 49 | function displayImage(file) { 50 | const reader = new FileReader(); 51 | reader.onload = function (e) { 52 | previewImage.src = e.target.result; 53 | previewImage.style.display = "block"; 54 | dropText.style.display = "none"; 55 | }; 56 | reader.readAsDataURL(file); 57 | } 58 | 59 | function getBestSampleRate(totalPixels, maxColors) { 60 | // TODO: Implement a better sample rate calculation 61 | return 1; 62 | } 63 | 64 | processBtn.addEventListener("click", async () => { 65 | if (uploadedImage) { 66 | const before = performance.now(); 67 | const img = new Image(); 68 | img.onload = async function () { 69 | const canvas = document.createElement("canvas"); 70 | const ctx = canvas.getContext("2d"); 71 | canvas.width = img.width; 72 | canvas.height = img.height; 73 | ctx.drawImage(img, 0, 0); 74 | const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height, { 75 | colorSpace: "srgb", 76 | }); 77 | 78 | const numColors = parseInt(colorCount.value); 79 | const selectedAlgorithm = algorithm.value; 80 | const maxIter = parseInt(maxIterations.value); 81 | const tol = parseFloat(tolerance.value); 82 | const seedValue = seed.value ? BigInt(seed.value) : undefined; 83 | 84 | console.log(`Image data length: ${imageData.data.length}`); 85 | const sampleRate = getBestSampleRate( 86 | imageData.data.length / 4, 87 | numColors, 88 | ); 89 | console.log(`Selected Sample rate: ${sampleRate}`); 90 | 91 | try { 92 | let cruncherBuilder = new ColorCruncherBuilder() 93 | .withMaxColors(numColors) 94 | .withSampleRate(sampleRate) 95 | .withAlgorithm(selectedAlgorithm) 96 | .withMaxIterations(maxIter) 97 | .withTolerance(tol); 98 | 99 | if (seedValue !== undefined) { 100 | cruncherBuilder = cruncherBuilder.withSeed(seedValue); 101 | } 102 | 103 | const cruncher = await cruncherBuilder.build(); 104 | const processedData = await cruncher.quantizeImage(imageData.data); 105 | 106 | const processedImageData = new ImageData( 107 | new Uint8ClampedArray(processedData), 108 | canvas.width, 109 | canvas.height, 110 | { 111 | colorSpace: "srgb", 112 | }, 113 | ); 114 | ctx.putImageData(processedImageData, 0, 0); 115 | 116 | const processedImage = document.getElementById("processedImage"); 117 | processedImage.src = canvas.toDataURL(); 118 | processedImage.style.display = "block"; 119 | document.querySelector("#processedImagePreview p").style.display = 120 | "none"; 121 | downloadBtn.disabled = false; 122 | 123 | const after = performance.now(); 124 | console.log(`Time taken: ${after - before}ms`); 125 | } catch (error) { 126 | console.error("Error processing image:", error); 127 | alert( 128 | "An error occurred while processing the image. Please try again.", 129 | ); 130 | } 131 | }; 132 | img.src = URL.createObjectURL(uploadedImage); 133 | } else { 134 | alert("Please upload an image first"); 135 | } 136 | }); 137 | 138 | downloadBtn.addEventListener("click", () => { 139 | const processedImage = document.getElementById("processedImage"); 140 | if (processedImage.src) { 141 | const a = document.createElement("a"); 142 | a.href = processedImage.src; 143 | a.download = "processed_image.png"; 144 | document.body.appendChild(a); 145 | a.click(); 146 | document.body.removeChild(a); 147 | } 148 | }); 149 | 150 | algorithm.addEventListener("change", function () { 151 | algorithmWarning.style.display = 152 | this.value === "lloyd-all-gpu" ? "block" : "none"; 153 | }); 154 | }); 155 | -------------------------------------------------------------------------------- /rust/src/quantize.rs: -------------------------------------------------------------------------------- 1 | use crate::kmeans::find_closest_centroid; 2 | use crate::kmeans::Initializer; 3 | use crate::kmeans::KMeans; 4 | use crate::kmeans::KMeansAlgorithm; 5 | use crate::kmeans::KMeansConfig; 6 | use crate::types::Vec4u; 7 | use crate::utils::num_distinct_colors_u32; 8 | 9 | #[derive(Debug)] 10 | pub struct ColorCruncher { 11 | kmeans: KMeans, 12 | max_colors: usize, 13 | pub sample_rate: usize, 14 | pub channels: usize, 15 | } 16 | 17 | #[derive(Clone, Debug, Default)] 18 | pub struct ColorCruncherBuilder { 19 | pub max_colors: Option, 20 | pub channels: Option, 21 | pub sample_rate: Option, 22 | pub tolerance: Option, 23 | pub max_iterations: Option, 24 | pub initializer: Option, 25 | pub algorithm: Option, 26 | pub seed: Option, 27 | } 28 | 29 | impl ColorCruncherBuilder { 30 | pub fn new() -> Self { 31 | Self::default() 32 | } 33 | 34 | pub fn with_max_colors(mut self, max_colors: usize) -> Self { 35 | self.max_colors = Some(max_colors); 36 | self 37 | } 38 | 39 | pub fn with_channels(mut self, channels: usize) -> Self { 40 | self.channels = Some(channels); 41 | self 42 | } 43 | 44 | pub fn with_sample_rate(mut self, sample_rate: usize) -> Self { 45 | self.sample_rate = Some(sample_rate); 46 | self 47 | } 48 | 49 | pub fn with_tolerance(mut self, tolerance: f32) -> Self { 50 | self.tolerance = Some(tolerance); 51 | self 52 | } 53 | 54 | pub fn with_max_iterations(mut self, max_iterations: usize) -> Self { 55 | self.max_iterations = Some(max_iterations); 56 | self 57 | } 58 | 59 | pub fn with_initializer(mut self, initializer: Initializer) -> Self { 60 | self.initializer = Some(initializer); 61 | self 62 | } 63 | 64 | pub fn with_algorithm(mut self, algorithm: KMeansAlgorithm) -> Self { 65 | self.algorithm = Some(algorithm); 66 | self 67 | } 68 | 69 | pub fn with_seed(mut self, seed: u64) -> Self { 70 | self.seed = Some(seed); 71 | self 72 | } 73 | 74 | pub async fn build(&self) -> ColorCruncher { 75 | let kmeans_config = self.build_config(); 76 | let kmeans = KMeans::new(kmeans_config.clone()).await; 77 | 78 | ColorCruncher { 79 | kmeans, 80 | max_colors: kmeans_config.k, 81 | sample_rate: self.sample_rate.unwrap_or(1), 82 | channels: self.channels.unwrap_or(3), 83 | } 84 | } 85 | 86 | fn build_config(&self) -> KMeansConfig { 87 | let default_config = KMeansConfig::default(); 88 | let mut config = KMeansConfig::default(); 89 | config.k = self.max_colors.unwrap_or_else(|| default_config.k); 90 | config.max_iterations = self 91 | .max_iterations 92 | .unwrap_or_else(|| default_config.max_iterations); 93 | config.tolerance = self.tolerance.unwrap_or_else(|| default_config.tolerance); 94 | config.algorithm = self 95 | .algorithm 96 | .clone() 97 | .unwrap_or_else(|| default_config.algorithm); 98 | config.initializer = self 99 | .initializer 100 | .clone() 101 | .unwrap_or_else(|| default_config.initializer); 102 | config.seed = self.seed; 103 | config 104 | } 105 | } 106 | 107 | impl ColorCruncher { 108 | fn chunk_pixels_vec4u(&self, pixels: &[u8]) -> Vec { 109 | pixels 110 | .chunks_exact(self.channels) 111 | .step_by(self.sample_rate) 112 | .map(|chunk| { 113 | [ 114 | chunk[0] as u32, 115 | chunk[1] as u32, 116 | chunk[2] as u32, 117 | chunk[3] as u32, 118 | ] 119 | }) 120 | .collect() 121 | } 122 | 123 | pub async fn quantize_image(&self, pixels: &[u8]) -> Vec { 124 | let image_data = self.chunk_pixels_vec4u(pixels); 125 | 126 | // If there's already less than or equal to the max number of colors, return the original pixels 127 | if num_distinct_colors_u32(&image_data) <= self.max_colors { 128 | return pixels.to_vec(); 129 | } 130 | 131 | let (_, centroids) = self.kmeans.run_async(&image_data).await.unwrap(); 132 | 133 | let mut new_image = Vec::with_capacity(pixels.len()); 134 | for pixel in pixels.chunks_exact(self.channels) { 135 | let px_vec = [ 136 | pixel[0] as f32, 137 | pixel[1] as f32, 138 | pixel[2] as f32, 139 | pixel[3] as f32, 140 | ]; 141 | let closest_centroid = find_closest_centroid(&px_vec, ¢roids); 142 | let new_color = ¢roids[closest_centroid]; 143 | 144 | if self.channels == 3 { 145 | new_image.extend_from_slice(&[ 146 | new_color[0] as u8, 147 | new_color[1] as u8, 148 | new_color[2] as u8, 149 | ]); 150 | } else { 151 | new_image.extend_from_slice(&[ 152 | new_color[0] as u8, 153 | new_color[1] as u8, 154 | new_color[2] as u8, 155 | pixel[3], 156 | ]); 157 | } 158 | } 159 | 160 | new_image 161 | } 162 | 163 | pub async fn create_palette(&self, pixels: &[u8]) -> Vec<[u8; 3]> { 164 | let image_data = self.chunk_pixels_vec4u(pixels); 165 | 166 | // If there's already less than or equal to the max number of colors, return the original pixels 167 | if num_distinct_colors_u32(&image_data) < self.max_colors { 168 | // todo 169 | todo!() 170 | } 171 | 172 | let (_, centroids) = self.kmeans.run_async(&image_data).await.unwrap(); 173 | centroids 174 | .iter() 175 | .map(|color| [color[0] as u8, color[1] as u8, color[2] as u8]) 176 | .collect() 177 | } 178 | } 179 | 180 | #[cfg(test)] 181 | mod tests { 182 | use futures::executor::block_on; 183 | 184 | use super::*; 185 | 186 | #[test] 187 | fn test_reduce_colorspace() { 188 | let data = vec![ 189 | 255, 0, 0, 255, 0, 0, 255, 255, 0, 0, 255, 255, 0, 0, 255, 255, 190 | ]; 191 | let max_colors = 2; 192 | let sample_rate = 1; 193 | let channels = 4; 194 | 195 | let quantizer = block_on( 196 | ColorCruncherBuilder::default() 197 | .with_max_colors(max_colors) 198 | .with_sample_rate(sample_rate) 199 | .with_channels(channels) 200 | .build(), 201 | ); 202 | 203 | let result = block_on(quantizer.quantize_image(&data)); 204 | assert_eq!(result.len(), data.len()); 205 | } 206 | } 207 | -------------------------------------------------------------------------------- /rust/src/kmeans/hamerly.rs: -------------------------------------------------------------------------------- 1 | use crate::kmeans::config::KMeansConfig; 2 | use crate::kmeans::distance::{ 3 | euclidean_distance_squared, EuclideanDistance, SquaredEuclideanDistance, 4 | }; 5 | use crate::kmeans::types::{Assignments, CentroidCounts, CentroidSums, Centroids}; 6 | use crate::kmeans::utils::has_converged; 7 | use crate::types::VectorExt; 8 | use itertools::izip; 9 | 10 | type UpperBounds = Vec; 11 | type LowerBounds = Vec; 12 | 13 | pub fn kmeans_hamerly( 14 | data: &[T], 15 | config: &KMeansConfig, 16 | ) -> (Assignments, Centroids) { 17 | let ( 18 | mut centroids, 19 | mut centroid_sums, 20 | mut centroid_counts, 21 | mut upper_bounds, 22 | mut lower_bounds, 23 | mut clusters, 24 | ) = initialize_hamerly(data, config); 25 | 26 | let mut centroid_move_distances = vec![EuclideanDistance(0.0); config.k]; 27 | let mut centroid_neighbor_distances = vec![EuclideanDistance(f32::MAX); config.k]; 28 | let mut new_centroids = centroids.clone(); 29 | 30 | let num_pixels = data.len(); 31 | let k = config.k; 32 | 33 | // I need to check this but I think it should ensure 34 | // we don't get any bounds checks? 35 | // If the compiler is smart enough, that is. 36 | assert!(num_pixels >= k); 37 | 38 | for _ in 0..config.max_iterations { 39 | compute_neighbor_distances(¢roids, &mut centroid_neighbor_distances); 40 | 41 | for (pixel, assigned_cluster, upper_bound, lower_bound) in 42 | izip!(data, &mut clusters, &mut upper_bounds, &mut lower_bounds) 43 | { 44 | let m = lower_bound.max(centroid_neighbor_distances[*assigned_cluster] / (2.).into()); 45 | if *upper_bound <= m { 46 | continue; 47 | } 48 | 49 | *upper_bound = euclidean_distance_squared(¢roids[*assigned_cluster], pixel).sqrt(); 50 | 51 | if *upper_bound <= m { 52 | continue; 53 | } 54 | 55 | let (best_distance, second_best_distance, best_index) = 56 | find_best_and_second_best(¢roids, pixel); 57 | *upper_bound = best_distance; 58 | *lower_bound = second_best_distance; 59 | if best_index != *assigned_cluster { 60 | centroid_sums[*assigned_cluster] = centroid_sums[*assigned_cluster].sub(pixel); 61 | centroid_counts[*assigned_cluster] -= 1; 62 | centroid_sums[best_index] = centroid_sums[best_index].add(pixel); 63 | centroid_counts[best_index] += 1; 64 | 65 | *assigned_cluster = best_index; 66 | } 67 | } 68 | 69 | // Move centroids into new_centroids (we swap later) 70 | move_centroids( 71 | &mut centroids, 72 | &mut new_centroids, 73 | &mut centroid_sums, 74 | &mut centroid_counts, 75 | &mut centroid_move_distances, 76 | ); 77 | 78 | // We can optimize this by keeping a running total, but I doubt it's a bottleneck so 79 | // TODO maybe look into it 80 | if has_converged(¢roids, &new_centroids, config.tolerance) { 81 | std::mem::swap(&mut centroids, &mut new_centroids); 82 | break; 83 | } 84 | std::mem::swap(&mut centroids, &mut new_centroids); 85 | 86 | update_bounds( 87 | &mut upper_bounds, 88 | &mut lower_bounds, 89 | ¢roid_move_distances, 90 | &clusters, 91 | ) 92 | } 93 | (clusters.to_vec(), centroids.to_vec()) 94 | } 95 | 96 | fn initialize_hamerly( 97 | data: &[T], 98 | config: &KMeansConfig, 99 | ) -> ( 100 | Centroids, 101 | CentroidSums, 102 | CentroidCounts, 103 | UpperBounds, 104 | LowerBounds, 105 | Assignments, 106 | ) { 107 | // indicex of the cluster each pixel belongs to 108 | let centroids = config 109 | .initializer 110 | .initialize_centroids(data, config.k, config.seed); 111 | 112 | let num_pixels = data.len(); 113 | let mut clusters = vec![0; num_pixels]; 114 | let mut upper_bounds = vec![EuclideanDistance(0.0); num_pixels]; 115 | let mut lower_bounds = vec![EuclideanDistance(0.0); num_pixels]; 116 | 117 | let mut centroid_sums = vec![T::zero(); config.k]; 118 | let mut centroid_counts = vec![0; config.k]; 119 | 120 | assert!(data.len() >= config.k); 121 | assert!(centroid_sums.len() == config.k); 122 | 123 | for i in 0..num_pixels { 124 | let (best_distance, second_best_distance, best_index) = 125 | find_best_and_second_best(¢roids, &data[i]); 126 | 127 | upper_bounds[i] = best_distance; 128 | lower_bounds[i] = second_best_distance; 129 | clusters[i] = best_index; 130 | centroid_sums[best_index] = centroid_sums[best_index].add(&data[i]); 131 | centroid_counts[best_index] += 1; 132 | } 133 | ( 134 | centroids, 135 | centroid_sums, 136 | centroid_counts, 137 | upper_bounds, 138 | lower_bounds, 139 | clusters, 140 | ) 141 | } 142 | 143 | #[inline] 144 | fn find_best_and_second_best( 145 | centroids: &[T], 146 | point: &T, 147 | ) -> (EuclideanDistance, EuclideanDistance, usize) { 148 | let mut best_distance = SquaredEuclideanDistance(f32::MAX); 149 | let mut second_best_distance = SquaredEuclideanDistance(f32::MAX); 150 | let mut best_index = 0; 151 | 152 | for (j, centroid) in centroids.iter().enumerate() { 153 | let distance = euclidean_distance_squared(centroid, point); 154 | if distance < best_distance { 155 | second_best_distance = best_distance; 156 | best_distance = distance; 157 | best_index = j; 158 | } else if distance < second_best_distance { 159 | second_best_distance = distance; 160 | } 161 | } 162 | 163 | // We need to square root before returning as we're comparing squared distances. 164 | ( 165 | best_distance.sqrt(), 166 | second_best_distance.sqrt(), 167 | best_index, 168 | ) 169 | } 170 | 171 | #[inline] 172 | fn update_bounds( 173 | upper_bounds: &mut [EuclideanDistance], 174 | lower_bounds: &mut [EuclideanDistance], 175 | distances: &[EuclideanDistance], 176 | clusters: &[usize], 177 | ) { 178 | let (r1, r1_distance, _, r2_distance) = { 179 | let mut r1_distance = EuclideanDistance(f32::MIN); 180 | let mut r2_distance = EuclideanDistance(f32::MIN); 181 | 182 | let mut r1_index = 0; 183 | let mut r2_index = 0; 184 | 185 | for (i, distance) in distances.iter().enumerate() { 186 | if *distance > r1_distance { 187 | r2_distance = r1_distance; 188 | r1_distance = *distance; 189 | r2_index = r1_index; 190 | r1_index = i; 191 | } else if *distance > r2_distance { 192 | r2_distance = *distance; 193 | r2_index = i; 194 | } 195 | } 196 | (r1_index, r1_distance, r2_index, r2_distance) 197 | }; 198 | 199 | for i in 0..clusters.len() { 200 | upper_bounds[i] += distances[clusters[i]]; 201 | if clusters[i] == r1 { 202 | lower_bounds[i] -= r2_distance; 203 | } else { 204 | lower_bounds[i] -= r1_distance; 205 | } 206 | } 207 | } 208 | 209 | fn compute_neighbor_distances(centroids: &[T], distances: &mut [EuclideanDistance]) { 210 | for (i, centroid) in centroids.iter().enumerate() { 211 | distances[i] = EuclideanDistance(f32::MAX); 212 | for (j, other_centroid) in centroids.iter().enumerate() { 213 | if i == j { 214 | continue; 215 | } 216 | // We need to square root here because the bounds check assumes true distances. 217 | distances[i] = 218 | distances[i].min(euclidean_distance_squared(centroid, other_centroid).sqrt()); 219 | } 220 | } 221 | } 222 | 223 | fn move_centroids( 224 | centroids: &mut [T], 225 | new_centroids: &mut [T], 226 | centroid_sums: &mut [T], 227 | centroid_counts: &mut [usize], 228 | centroid_move_distances: &mut [EuclideanDistance], 229 | ) { 230 | for (j, (current_centroid, new_centroid)) in 231 | centroids.iter().zip(new_centroids.iter_mut()).enumerate() 232 | { 233 | *new_centroid = centroid_sums[j].div_scalar(centroid_counts[j] as f32); 234 | // We need to square root here because the bounds check assumes true distances. 235 | centroid_move_distances[j] = 236 | euclidean_distance_squared(current_centroid, new_centroid).sqrt(); 237 | } 238 | } 239 | -------------------------------------------------------------------------------- /rust/src/kmeans.rs: -------------------------------------------------------------------------------- 1 | mod config; 2 | pub mod distance; 3 | pub mod hamerly; 4 | pub mod initializer; 5 | pub mod lloyd; 6 | mod types; 7 | mod utils; 8 | 9 | pub mod gpu; 10 | 11 | #[cfg(feature = "gpu")] 12 | use self::gpu::run_lloyd_gpu; 13 | 14 | pub use crate::kmeans::config::{KMeansAlgorithm, KMeansConfig}; 15 | pub use crate::kmeans::initializer::Initializer; 16 | pub use crate::kmeans::utils::find_closest_centroid; 17 | use crate::utils::num_distinct_colors; 18 | 19 | use crate::types::{Vec3, Vec4, Vec4u, VectorExt}; 20 | 21 | use self::types::{KMeansError, KMeansResult}; 22 | 23 | const DEFAULT_INITIALIZER: Initializer = Initializer::KMeansPlusPlus; 24 | 25 | // A wrapper for easier usage 26 | #[derive(Debug, Clone)] 27 | pub struct KMeans(KMeansConfig); 28 | 29 | impl KMeans { 30 | pub fn from_config(config: KMeansConfig) -> Self { 31 | Self(config) 32 | } 33 | 34 | pub fn with_k(mut self, k: usize) -> Self { 35 | self.0.k = k; 36 | self 37 | } 38 | 39 | // Builders 40 | pub fn with_max_iterations(mut self, max_iterations: usize) -> Self { 41 | self.0.max_iterations = max_iterations; 42 | self 43 | } 44 | 45 | pub fn with_tolerance(mut self, tolerance: f64) -> Self { 46 | self.0.tolerance = tolerance as f32; 47 | self 48 | } 49 | 50 | pub fn with_algorithm(mut self, algorithm: KMeansAlgorithm) -> Self { 51 | self.0.algorithm = algorithm; 52 | self 53 | } 54 | 55 | pub fn with_seed(mut self, seed: u64) -> Self { 56 | self.0.seed = Some(seed); 57 | self 58 | } 59 | } 60 | 61 | impl Default for KMeans { 62 | fn default() -> Self { 63 | KMeans(KMeansConfig { 64 | k: 3, 65 | max_iterations: 100, 66 | tolerance: 1e-4, 67 | algorithm: KMeansAlgorithm::Lloyd, 68 | initializer: DEFAULT_INITIALIZER, 69 | seed: None, 70 | }) 71 | } 72 | } 73 | 74 | impl KMeans { 75 | pub fn run(&self, data: &[T]) -> KMeansResult { 76 | let unique_colors = num_distinct_colors(data); 77 | if unique_colors < self.0.k { 78 | return Err(KMeansError(format!( 79 | "Number of unique colors is less than k: {}", 80 | unique_colors 81 | ))); 82 | } 83 | 84 | match self.0.algorithm { 85 | KMeansAlgorithm::Lloyd => Ok(lloyd::kmeans_lloyd(data, &self.0)), 86 | KMeansAlgorithm::Hamerly => Ok(hamerly::kmeans_hamerly(data, &self.0)), 87 | #[cfg(feature = "gpu")] 88 | _ => Err(KMeansError(format!( 89 | "Algorithm not supported on cpu: {}", 90 | self.0.algorithm 91 | ))), 92 | } 93 | } 94 | pub async fn run_async(&self, data: &[Vec4u]) -> KMeansResult { 95 | match &self.0.algorithm { 96 | KMeansAlgorithm::Lloyd | KMeansAlgorithm::Hamerly => { 97 | let data = data 98 | .iter() 99 | .map(|v| [v[0] as f32, v[1] as f32, v[2] as f32, v[3] as f32]) 100 | .collect::>(); 101 | self.run(&data) 102 | } 103 | #[cfg(feature = "gpu")] 104 | _ => run_lloyd_gpu(self.0.clone(), data) 105 | .await 106 | .map_err(|e| KMeansError(e.to_string())), 107 | } 108 | } 109 | } 110 | 111 | impl KMeans { 112 | pub async fn new(config: KMeansConfig) -> Self { 113 | KMeans(config) 114 | } 115 | 116 | pub fn run_vec4(&self, data: &[Vec4]) -> KMeansResult { 117 | match &self.0.algorithm { 118 | KMeansAlgorithm::Lloyd => self.run(data), 119 | KMeansAlgorithm::Hamerly => self.run(data), 120 | #[cfg(feature = "gpu")] 121 | _ => Err(KMeansError( 122 | "GPU not supported for vec4 float data. Convert to u8 data first.".to_string(), 123 | )), 124 | } 125 | } 126 | 127 | pub fn run_vec3(&self, data: &[Vec3]) -> KMeansResult { 128 | match &self.0.algorithm { 129 | KMeansAlgorithm::Lloyd => self.run(data), 130 | KMeansAlgorithm::Hamerly => self.run(data), 131 | #[cfg(feature = "gpu")] 132 | _ => Err(KMeansError( 133 | "GPU not supported for 3 channel data. Convert to 4 channel data first." 134 | .to_string(), 135 | )), 136 | } 137 | } 138 | } 139 | 140 | #[cfg(test)] 141 | mod tests { 142 | use super::*; 143 | use crate::kmeans::config::KMeansAlgorithm; 144 | use futures::executor::block_on; 145 | use rand::rngs::StdRng; 146 | use rand::Rng; 147 | use rand::SeedableRng; 148 | 149 | trait TestExt { 150 | fn assert_almost_eq(&self, other: &[T], tolerance: f64); 151 | } 152 | 153 | impl TestExt for Vec { 154 | fn assert_almost_eq(&self, other: &[T], tolerance: f64) { 155 | assert_eq!(self.len(), other.len()); 156 | 157 | for i in 0..self.len() { 158 | let a_matches = (self[i][0] as f64 - other[i][0] as f64).abs() < tolerance; 159 | let b_matches = (self[i][1] as f64 - other[i][1] as f64).abs() < tolerance; 160 | let c_matches = (self[i][2] as f64 - other[i][2] as f64).abs() < tolerance; 161 | assert!( 162 | a_matches && b_matches && c_matches, 163 | "{:?} does not match {:?}", 164 | self[i], 165 | other[i] 166 | ); 167 | } 168 | } 169 | } 170 | 171 | fn run_kmeans_test(data: &[Vec3], k: usize, expected_non_empty_clusters: usize) { 172 | let algorithms = vec![KMeansAlgorithm::Lloyd, KMeansAlgorithm::Hamerly]; 173 | 174 | for algorithm in algorithms { 175 | let config = KMeansConfig { 176 | k, 177 | max_iterations: 100, 178 | tolerance: 1e-4, 179 | algorithm, 180 | initializer: DEFAULT_INITIALIZER, 181 | seed: None, 182 | }; 183 | 184 | let kmeans = KMeans::from_config(config.clone()); 185 | let (clusters, centroids) = kmeans.run(data).unwrap(); 186 | 187 | assert_eq!( 188 | clusters.len(), 189 | data.len(), 190 | "clusters.len() == data.len() with algorithm {}", 191 | &config.algorithm 192 | ); 193 | assert_eq!( 194 | centroids.len(), 195 | k, 196 | "centroids.len() == k with algorithm {}", 197 | config.algorithm 198 | ); 199 | assert_eq!( 200 | clusters.iter().filter(|&&c| c < k).count(), 201 | data.len(), 202 | "clusters.iter().filter(|&&c| c < k).count() == data.len() with algorithm {}", 203 | config.algorithm 204 | ); 205 | assert!(expected_non_empty_clusters >= 1); 206 | } 207 | } 208 | 209 | #[test] 210 | fn test_kmeans_basic() { 211 | let data = vec![[255.0, 0.0, 0.0], [0.0, 255.0, 0.0], [0.0, 0.0, 255.0]]; 212 | run_kmeans_test(&data, 3, 3); 213 | } 214 | 215 | #[test] 216 | fn test_kmeans_single_color() { 217 | let data = vec![ 218 | [100.0, 100.0, 100.0], 219 | [100.0, 100.0, 100.0], 220 | [100.0, 100.0, 100.0], 221 | ]; 222 | run_kmeans_test(&data, 1, 1); 223 | } 224 | 225 | #[test] 226 | fn test_kmeans_two_distinct_colors() { 227 | let data = vec![[255.0, 0.0, 0.0], [0.0, 0.0, 255.0]]; 228 | run_kmeans_test(&data, 2, 2); 229 | } 230 | 231 | #[test] 232 | fn test_kmeans_more_clusters_than_colors() { 233 | let data = vec![[255.0, 0.0, 0.0], [0.0, 255.0, 0.0]]; 234 | let config = KMeansConfig { 235 | k: 3, 236 | max_iterations: 100, 237 | tolerance: 1e-4, 238 | algorithm: KMeansAlgorithm::Lloyd, 239 | initializer: DEFAULT_INITIALIZER, 240 | seed: None, 241 | }; 242 | let kmeans = KMeans::from_config(config); 243 | let result = kmeans.run(&data); 244 | assert_eq!( 245 | result.err().unwrap().to_string(), 246 | "Number of unique colors is less than k: 2" 247 | ); 248 | } 249 | 250 | #[test] 251 | fn test_algorithms_converge_to_the_same_result_for_same_initial_conditions() { 252 | let seed = 42; 253 | let data_size = 100; 254 | 255 | let mut rng = StdRng::seed_from_u64(seed); 256 | let data = (0..data_size) 257 | .map(|_| { 258 | [ 259 | rng.gen::() * 255.0, 260 | rng.gen::() * 255.0, 261 | rng.gen::() * 255.0, 262 | 0.0, 263 | ] 264 | }) 265 | .collect::>(); 266 | 267 | let config_lloyd = KMeansConfig { 268 | k: 3, 269 | max_iterations: 500, 270 | tolerance: 1e-6, 271 | algorithm: KMeansAlgorithm::Lloyd, 272 | initializer: DEFAULT_INITIALIZER, 273 | seed: Some(seed), 274 | }; 275 | 276 | let config_hamerly = KMeansConfig { 277 | k: 3, 278 | max_iterations: 500, 279 | tolerance: 1e-6, 280 | algorithm: KMeansAlgorithm::Hamerly, 281 | initializer: DEFAULT_INITIALIZER, 282 | seed: Some(seed), 283 | }; 284 | 285 | #[cfg(feature = "gpu")] 286 | let config_gpu = KMeansConfig { 287 | k: 3, 288 | max_iterations: 500, 289 | tolerance: 1e-6, 290 | algorithm: KMeansAlgorithm::LloydGpu, 291 | initializer: DEFAULT_INITIALIZER, 292 | seed: Some(seed), 293 | }; 294 | 295 | let kmeans_lloyd = KMeans::from_config(config_lloyd); 296 | let kmeans_hamerly = KMeans::from_config(config_hamerly); 297 | 298 | let (clusters1, centroids1) = kmeans_lloyd.run(&data).unwrap(); 299 | let (clusters2, centroids2) = kmeans_hamerly.run(&data).unwrap(); 300 | 301 | #[cfg(feature = "gpu")] 302 | { 303 | let kmeans_gpu = KMeans::from_config(config_gpu); 304 | let u32_data: Vec = data 305 | .iter() 306 | .map(|p| [p[0] as u32, p[1] as u32, p[2] as u32, p[3] as u32]) 307 | .collect(); 308 | let (clusters3, centroids3) = block_on(kmeans_gpu.run_async(&u32_data)).unwrap(); 309 | 310 | dbg!(¢roids1); 311 | dbg!(¢roids3); 312 | 313 | centroids1.assert_almost_eq(¢roids3, 1.0); 314 | } 315 | 316 | centroids1.assert_almost_eq(¢roids2, 1.0); 317 | assert_eq!(clusters1, clusters2); 318 | } 319 | } 320 | -------------------------------------------------------------------------------- /rust/src/kmeans/gpu/lloyd_gpu1.rs: -------------------------------------------------------------------------------- 1 | use super::buffers::MappableBuffer; 2 | use super::common::common_wgpu_setup; 3 | use crate::kmeans::types::KMeansResult; 4 | use crate::kmeans::utils::has_converged; 5 | use crate::kmeans::KMeansConfig; 6 | use crate::types::VectorExt; 7 | use crate::types::{Vec4, Vec4u}; 8 | use futures::executor::block_on; 9 | use wgpu::{ 10 | BindGroup, BindGroupDescriptor, BindGroupEntry, BindGroupLayout, BindGroupLayoutDescriptor, 11 | BindGroupLayoutEntry, BindingType, Buffer, BufferBindingType, BufferDescriptor, BufferUsages, 12 | CommandEncoderDescriptor, ComputePassDescriptor, ComputePipeline, ComputePipelineDescriptor, 13 | Device, PipelineCompilationOptions, PipelineLayout, PipelineLayoutDescriptor, Queue, 14 | ShaderModuleDescriptor, ShaderSource, ShaderStages, 15 | }; 16 | 17 | const WORKGROUP_SIZE: u32 = 256; 18 | 19 | struct ProcessBuffers { 20 | pixel_buffer: Buffer, 21 | centroid_buffer: Buffer, 22 | assignment_buffer: MappableBuffer, 23 | bind_group: BindGroup, 24 | } 25 | 26 | #[derive(Debug)] 27 | pub struct LloydAssignmentsOnly { 28 | device: Device, 29 | queue: Queue, 30 | compute_pipeline: ComputePipeline, 31 | bind_group_layout: BindGroupLayout, 32 | pipeline_layout: PipelineLayout, 33 | config: KMeansConfig, 34 | } 35 | 36 | impl LloydAssignmentsOnly { 37 | // useful because the initialization function is big 38 | // and we don't want to recompile the shader 39 | // every time we change the number of clusters 40 | pub fn set_k(&mut self, k: usize) { 41 | self.config.k = k; 42 | } 43 | 44 | fn make_bind_group_layout(device: &Device) -> BindGroupLayout { 45 | let entries = [ 46 | // Pixel Group 47 | BindGroupLayoutEntry { 48 | binding: 0, 49 | visibility: ShaderStages::COMPUTE, 50 | ty: BindingType::Buffer { 51 | ty: BufferBindingType::Storage { read_only: true }, 52 | has_dynamic_offset: false, 53 | min_binding_size: None, 54 | }, 55 | count: None, 56 | }, 57 | // Centroids 58 | BindGroupLayoutEntry { 59 | binding: 1, 60 | visibility: ShaderStages::COMPUTE, 61 | ty: BindingType::Buffer { 62 | ty: BufferBindingType::Storage { read_only: true }, 63 | has_dynamic_offset: false, 64 | min_binding_size: None, 65 | }, 66 | count: None, 67 | }, 68 | // Assignments 69 | BindGroupLayoutEntry { 70 | binding: 2, 71 | visibility: ShaderStages::COMPUTE, 72 | ty: BindingType::Buffer { 73 | ty: BufferBindingType::Storage { read_only: false }, 74 | has_dynamic_offset: false, 75 | min_binding_size: None, 76 | }, 77 | count: None, 78 | }, 79 | ] 80 | .to_vec(); 81 | 82 | device.create_bind_group_layout(&BindGroupLayoutDescriptor { 83 | label: Some("kmeans_bind_group_layout".into()), 84 | entries: &entries, 85 | }) 86 | } 87 | 88 | pub async fn from_config(config: KMeansConfig) -> Self { 89 | let (_, _, device, queue) = common_wgpu_setup().await.unwrap(); 90 | let bind_group_layout = Self::make_bind_group_layout(&device); 91 | 92 | let shader_module = device.create_shader_module(ShaderModuleDescriptor { 93 | label: Some("kmeans_shader".into()), 94 | source: ShaderSource::Wgsl(include_str!("lloyd_gpu1.wgsl").into()), 95 | }); 96 | 97 | let pipeline_layout = device.create_pipeline_layout(&PipelineLayoutDescriptor { 98 | label: Some("kmeans_pipeline_layout".into()), 99 | bind_group_layouts: &[&bind_group_layout], 100 | push_constant_ranges: &[], 101 | }); 102 | 103 | let compute_pipeline = device.create_compute_pipeline(&ComputePipelineDescriptor { 104 | label: Some("kmeans_compute_pipeline".into()), 105 | layout: Some(&pipeline_layout), 106 | module: &shader_module, 107 | entry_point: "main", 108 | compilation_options: PipelineCompilationOptions::default(), 109 | }); 110 | 111 | Self { 112 | device, 113 | queue, 114 | compute_pipeline, 115 | bind_group_layout, 116 | pipeline_layout, 117 | config, 118 | } 119 | } 120 | 121 | fn prepare_buffers( 122 | &self, 123 | pixels: &[Vec4u], 124 | centroids: &[Vec4], 125 | assignments: &[u32], 126 | ) -> Result { 127 | // Pixel Buffer (in shader these are vec4) 128 | let pixel_buffer = self.device.create_buffer(&BufferDescriptor { 129 | label: None, 130 | size: std::mem::size_of_val(pixels) as u64, 131 | usage: BufferUsages::STORAGE | BufferUsages::COPY_DST, 132 | mapped_at_creation: false, 133 | }); 134 | 135 | self.queue 136 | .write_buffer(&pixel_buffer, 0, bytemuck::cast_slice(pixels)); 137 | 138 | // Centroid Buffer (in shader these are vec3) 139 | let centroid_buffer = self.device.create_buffer(&BufferDescriptor { 140 | label: None, 141 | // 3 floats per centroid, 4 bytes per float (as they are f32), but we have to align 142 | // to 16 bytes to match the alignment of the pixel buffer 143 | size: std::mem::size_of_val(centroids) as u64, 144 | usage: BufferUsages::STORAGE | BufferUsages::COPY_DST | BufferUsages::COPY_SRC, 145 | mapped_at_creation: false, 146 | }); 147 | 148 | self.queue 149 | .write_buffer(¢roid_buffer, 0, bytemuck::cast_slice(centroids)); 150 | 151 | let assignment_size: u64 = (pixels.len() * std::mem::size_of::()) as u64; 152 | let assignment_buffer = self.device.create_buffer(&BufferDescriptor { 153 | label: None, 154 | // 1 int per assignment 155 | // technically I think we can get away with a u8 here 156 | // since our color space is limited to 256 colors 157 | // but alignment. we'll maybe do this later 158 | size: assignment_size, 159 | usage: BufferUsages::STORAGE | BufferUsages::COPY_SRC | BufferUsages::COPY_DST, 160 | mapped_at_creation: false, 161 | }); 162 | 163 | let assignment_staging_buffer = self.device.create_buffer(&BufferDescriptor { 164 | label: None, 165 | size: assignment_size, 166 | usage: BufferUsages::COPY_DST | BufferUsages::MAP_READ, 167 | mapped_at_creation: false, 168 | }); 169 | 170 | // this should just be zeros but that's fine 171 | self.queue 172 | .write_buffer(&assignment_buffer, 0, bytemuck::cast_slice(assignments)); 173 | 174 | let buffers = vec![ 175 | BindGroupEntry { 176 | binding: 0, 177 | resource: pixel_buffer.as_entire_binding(), 178 | }, 179 | BindGroupEntry { 180 | binding: 1, 181 | resource: centroid_buffer.as_entire_binding(), 182 | }, 183 | BindGroupEntry { 184 | binding: 2, 185 | resource: assignment_buffer.as_entire_binding(), 186 | }, 187 | ]; 188 | 189 | let bind_group = self.device.create_bind_group(&BindGroupDescriptor { 190 | label: None, 191 | layout: &self.bind_group_layout, 192 | entries: &buffers, 193 | }); 194 | 195 | Ok(ProcessBuffers { 196 | pixel_buffer, 197 | centroid_buffer, 198 | assignment_buffer: MappableBuffer { 199 | gpu_buffer: assignment_buffer, 200 | staging_buffer: assignment_staging_buffer, 201 | size: assignment_size, 202 | }, 203 | bind_group, 204 | }) 205 | } 206 | 207 | pub fn run(&self, pixels: &[Vec4u]) -> KMeansResult { 208 | block_on(self.run_async(pixels)) 209 | } 210 | 211 | pub async fn run_async(&self, pixels: &[Vec4u]) -> KMeansResult { 212 | let vec4_pixels: Vec = pixels 213 | .iter() 214 | .map(|v| [v[0] as f32, v[1] as f32, v[2] as f32, v[3] as f32]) 215 | .collect(); 216 | let mut centroids: Vec = self.config.initializer.initialize_centroids( 217 | &vec4_pixels, 218 | self.config.k, 219 | self.config.seed, 220 | ); 221 | 222 | let mut assignments: Vec = vec![0; pixels.len()]; 223 | 224 | let process_buffers = self 225 | .prepare_buffers(pixels, ¢roids, &assignments) 226 | .unwrap(); 227 | 228 | let mut iterations = 0; 229 | 230 | while iterations < self.config.max_iterations { 231 | let (new_assignments, new_centroids) = 232 | self.run_iteration(&pixels, &process_buffers).await?; 233 | 234 | if has_converged(¢roids, &new_centroids, self.config.tolerance) { 235 | centroids = new_centroids; 236 | break; 237 | } 238 | 239 | self.queue.write_buffer( 240 | &process_buffers.centroid_buffer, 241 | 0, 242 | bytemuck::cast_slice(&new_centroids), 243 | ); 244 | centroids = new_centroids; 245 | assignments = new_assignments; 246 | 247 | iterations += 1; 248 | } 249 | 250 | Ok(( 251 | assignments.into_iter().map(|a| a as usize).collect(), 252 | centroids, 253 | )) 254 | } 255 | 256 | async fn run_iteration( 257 | &self, 258 | pixels: &[Vec4u], 259 | process_buffers: &ProcessBuffers, 260 | ) -> Result<(Vec, Vec), &'static str> { 261 | let mut encoder = self 262 | .device 263 | .create_command_encoder(&CommandEncoderDescriptor { label: None }); 264 | 265 | // This should probably be a variable we can configure 266 | // but it requires templating the shader, which I don't want to do yet. 267 | let num_workgroups = (pixels.len() as u32 + WORKGROUP_SIZE - 1) / WORKGROUP_SIZE; 268 | 269 | { 270 | let mut pass = encoder.begin_compute_pass(&ComputePassDescriptor { 271 | label: None, 272 | timestamp_writes: None, 273 | }); 274 | 275 | pass.set_pipeline(&self.compute_pipeline); 276 | pass.set_bind_group(0, &process_buffers.bind_group, &[]); 277 | pass.insert_debug_marker("kmeans_iteration"); 278 | pass.dispatch_workgroups(num_workgroups, 1, 1); 279 | } 280 | 281 | process_buffers 282 | .assignment_buffer 283 | .copy_to_staging_buffer(&mut encoder); 284 | self.queue.submit(Some(encoder.finish())); 285 | 286 | let assignments: Vec = process_buffers 287 | .assignment_buffer 288 | .read_back(&self.device) 289 | .await?; 290 | let new_centroids = self.get_new_centroids(pixels, &assignments); 291 | 292 | Ok((assignments, new_centroids)) 293 | } 294 | 295 | fn get_new_centroids(&self, pixels: &[Vec4u], assignments: &[u32]) -> Vec { 296 | let mut new_centroids: Vec = vec![]; 297 | 298 | let mut centroid_sums: Vec = vec![[0.0; 4]; self.config.k]; 299 | let mut centroid_counts: Vec = vec![0; self.config.k]; 300 | for (pixel, assignment) in pixels.iter().zip(assignments.iter()) { 301 | centroid_sums[*assignment as usize] = centroid_sums[*assignment as usize].add(&[ 302 | pixel[0] as f32, 303 | pixel[1] as f32, 304 | pixel[2] as f32, 305 | pixel[3] as f32, 306 | ]); 307 | centroid_counts[*assignment as usize] += 1; 308 | } 309 | for (centroid_sum, centroid_count) in centroid_sums.iter().zip(centroid_counts.iter()) { 310 | new_centroids.push(centroid_sum.div_scalar(*centroid_count as f32)); 311 | } 312 | new_centroids 313 | } 314 | } 315 | 316 | #[cfg(test)] 317 | mod tests { 318 | use super::*; 319 | use crate::kmeans::initializer::Initializer; 320 | use crate::kmeans::KMeansAlgorithm; 321 | use futures::executor::block_on; 322 | use rand::prelude::*; 323 | use rand::thread_rng; 324 | 325 | fn create_test_config() -> KMeansConfig { 326 | KMeansConfig { 327 | k: 3, 328 | max_iterations: 10, 329 | tolerance: 0.001, 330 | algorithm: KMeansAlgorithm::LloydGpu, 331 | initializer: Initializer::Random, 332 | seed: Some(42), 333 | } 334 | } 335 | 336 | #[test] 337 | fn test_kmeans_gpu_basic() { 338 | let config = create_test_config(); 339 | let pixels: Vec = vec![ 340 | [0, 0, 0, 0], 341 | [1, 1, 1, 1], 342 | [2, 2, 2, 2], 343 | [10, 10, 10, 10], 344 | [11, 11, 11, 11], 345 | [12, 12, 12, 12], 346 | [20, 20, 20, 20], 347 | [21, 21, 21, 21], 348 | [22, 22, 22, 22], 349 | ]; 350 | 351 | let kmeans = block_on(LloydAssignmentsOnly::from_config(config)); 352 | let (assignments, centroids) = block_on(kmeans.run_async(&pixels)).unwrap(); 353 | 354 | assert_eq!(assignments.len(), pixels.len()); 355 | 356 | // Check that pixels close to each other are in the same cluster 357 | assert_eq!(assignments[0], assignments[1]); 358 | assert_eq!(assignments[1], assignments[2]); 359 | assert_eq!(assignments[3], assignments[4]); 360 | assert_eq!(assignments[4], assignments[5]); 361 | assert_eq!(assignments[6], assignments[7]); 362 | assert_eq!(assignments[7], assignments[8]); 363 | 364 | // Check that pixels far from each other are in different clusters 365 | assert_ne!(assignments[0], assignments[3]); 366 | assert_ne!(assignments[3], assignments[6]); 367 | assert_ne!(assignments[0], assignments[6]); 368 | } 369 | 370 | #[test] 371 | fn test_kmeans_gpu_convergence() { 372 | let config = KMeansConfig { 373 | k: 2, 374 | max_iterations: 100, 375 | tolerance: 0.001, 376 | algorithm: KMeansAlgorithm::LloydGpu, 377 | initializer: Initializer::Random, 378 | seed: Some(42), 379 | }; 380 | 381 | let pixels: Vec = vec![ 382 | [0, 0, 0, 0], 383 | [1, 1, 1, 1], 384 | [2, 2, 2, 2], 385 | [10, 10, 10, 10], 386 | [11, 11, 11, 11], 387 | [12, 12, 12, 12], 388 | ]; 389 | 390 | let kmeans = block_on(LloydAssignmentsOnly::from_config(config)); 391 | let (assignments, centroids) = block_on(kmeans.run_async(&pixels)).unwrap(); 392 | 393 | assert_eq!(assignments.len(), pixels.len()); 394 | 395 | // Check that the algorithm converged to two clusters 396 | assert_eq!(assignments[0], assignments[1]); 397 | assert_eq!(assignments[1], assignments[2]); 398 | assert_eq!(assignments[3], assignments[4]); 399 | assert_eq!(assignments[4], assignments[5]); 400 | assert_ne!(assignments[0], assignments[3]); 401 | } 402 | 403 | #[test] 404 | fn test_kmeans_gpu_empty_input() { 405 | let config = create_test_config(); 406 | let pixels: Vec = vec![]; 407 | 408 | let kmeans = block_on(LloydAssignmentsOnly::from_config(config)); 409 | let (assignments, centroids) = block_on(kmeans.run_async(&pixels)).unwrap(); 410 | 411 | assert_eq!(assignments.len(), 0); 412 | assert_eq!(centroids.len(), 0); 413 | } 414 | 415 | #[test] 416 | fn test_big_and_varied_input() { 417 | let mut config = create_test_config(); 418 | config.k = 15; 419 | 420 | let kmeans = block_on(LloydAssignmentsOnly::from_config(config.clone())); 421 | let mut rng = thread_rng(); 422 | 423 | let image_size = 2000; 424 | let pixels: Vec = (0..image_size * image_size) 425 | .map(|_| { 426 | [ 427 | (rng.gen::() * 255.) as u32, 428 | (rng.gen::() * 255.) as u32, 429 | (rng.gen::() * 255.) as u32, 430 | 0, 431 | ] 432 | }) 433 | .collect(); 434 | 435 | let (assignments, centroids) = block_on(kmeans.run_async(&pixels)).unwrap(); 436 | 437 | assert_eq!(assignments.len(), pixels.len()); 438 | assert_eq!(centroids.len(), config.k); 439 | } 440 | } 441 | -------------------------------------------------------------------------------- /pkg/colorcruncher.js: -------------------------------------------------------------------------------- 1 | let wasm; 2 | 3 | const heap = new Array(128).fill(undefined); 4 | 5 | heap.push(undefined, null, true, false); 6 | 7 | function getObject(idx) { return heap[idx]; } 8 | 9 | let heap_next = heap.length; 10 | 11 | function dropObject(idx) { 12 | if (idx < 132) return; 13 | heap[idx] = heap_next; 14 | heap_next = idx; 15 | } 16 | 17 | function takeObject(idx) { 18 | const ret = getObject(idx); 19 | dropObject(idx); 20 | return ret; 21 | } 22 | 23 | const cachedTextDecoder = (typeof TextDecoder !== 'undefined' ? new TextDecoder('utf-8', { ignoreBOM: true, fatal: true }) : { decode: () => { throw Error('TextDecoder not available') } } ); 24 | 25 | if (typeof TextDecoder !== 'undefined') { cachedTextDecoder.decode(); }; 26 | 27 | let cachedUint8Memory0 = null; 28 | 29 | function getUint8Memory0() { 30 | if (cachedUint8Memory0 === null || cachedUint8Memory0.byteLength === 0) { 31 | cachedUint8Memory0 = new Uint8Array(wasm.memory.buffer); 32 | } 33 | return cachedUint8Memory0; 34 | } 35 | 36 | function getStringFromWasm0(ptr, len) { 37 | ptr = ptr >>> 0; 38 | return cachedTextDecoder.decode(getUint8Memory0().subarray(ptr, ptr + len)); 39 | } 40 | 41 | function addHeapObject(obj) { 42 | if (heap_next === heap.length) heap.push(heap.length + 1); 43 | const idx = heap_next; 44 | heap_next = heap[idx]; 45 | 46 | heap[idx] = obj; 47 | return idx; 48 | } 49 | 50 | let WASM_VECTOR_LEN = 0; 51 | 52 | const cachedTextEncoder = (typeof TextEncoder !== 'undefined' ? new TextEncoder('utf-8') : { encode: () => { throw Error('TextEncoder not available') } } ); 53 | 54 | const encodeString = (typeof cachedTextEncoder.encodeInto === 'function' 55 | ? function (arg, view) { 56 | return cachedTextEncoder.encodeInto(arg, view); 57 | } 58 | : function (arg, view) { 59 | const buf = cachedTextEncoder.encode(arg); 60 | view.set(buf); 61 | return { 62 | read: arg.length, 63 | written: buf.length 64 | }; 65 | }); 66 | 67 | function passStringToWasm0(arg, malloc, realloc) { 68 | 69 | if (realloc === undefined) { 70 | const buf = cachedTextEncoder.encode(arg); 71 | const ptr = malloc(buf.length, 1) >>> 0; 72 | getUint8Memory0().subarray(ptr, ptr + buf.length).set(buf); 73 | WASM_VECTOR_LEN = buf.length; 74 | return ptr; 75 | } 76 | 77 | let len = arg.length; 78 | let ptr = malloc(len, 1) >>> 0; 79 | 80 | const mem = getUint8Memory0(); 81 | 82 | let offset = 0; 83 | 84 | for (; offset < len; offset++) { 85 | const code = arg.charCodeAt(offset); 86 | if (code > 0x7F) break; 87 | mem[ptr + offset] = code; 88 | } 89 | 90 | if (offset !== len) { 91 | if (offset !== 0) { 92 | arg = arg.slice(offset); 93 | } 94 | ptr = realloc(ptr, len, len = offset + arg.length * 3, 1) >>> 0; 95 | const view = getUint8Memory0().subarray(ptr + offset, ptr + len); 96 | const ret = encodeString(arg, view); 97 | 98 | offset += ret.written; 99 | ptr = realloc(ptr, len, offset, 1) >>> 0; 100 | } 101 | 102 | WASM_VECTOR_LEN = offset; 103 | return ptr; 104 | } 105 | 106 | function isLikeNone(x) { 107 | return x === undefined || x === null; 108 | } 109 | 110 | let cachedInt32Memory0 = null; 111 | 112 | function getInt32Memory0() { 113 | if (cachedInt32Memory0 === null || cachedInt32Memory0.byteLength === 0) { 114 | cachedInt32Memory0 = new Int32Array(wasm.memory.buffer); 115 | } 116 | return cachedInt32Memory0; 117 | } 118 | 119 | function debugString(val) { 120 | // primitive types 121 | const type = typeof val; 122 | if (type == 'number' || type == 'boolean' || val == null) { 123 | return `${val}`; 124 | } 125 | if (type == 'string') { 126 | return `"${val}"`; 127 | } 128 | if (type == 'symbol') { 129 | const description = val.description; 130 | if (description == null) { 131 | return 'Symbol'; 132 | } else { 133 | return `Symbol(${description})`; 134 | } 135 | } 136 | if (type == 'function') { 137 | const name = val.name; 138 | if (typeof name == 'string' && name.length > 0) { 139 | return `Function(${name})`; 140 | } else { 141 | return 'Function'; 142 | } 143 | } 144 | // objects 145 | if (Array.isArray(val)) { 146 | const length = val.length; 147 | let debug = '['; 148 | if (length > 0) { 149 | debug += debugString(val[0]); 150 | } 151 | for(let i = 1; i < length; i++) { 152 | debug += ', ' + debugString(val[i]); 153 | } 154 | debug += ']'; 155 | return debug; 156 | } 157 | // Test for built-in 158 | const builtInMatches = /\[object ([^\]]+)\]/.exec(toString.call(val)); 159 | let className; 160 | if (builtInMatches.length > 1) { 161 | className = builtInMatches[1]; 162 | } else { 163 | // Failed to match the standard '[object ClassName]' 164 | return toString.call(val); 165 | } 166 | if (className == 'Object') { 167 | // we're a user defined class or Object 168 | // JSON.stringify avoids problems with cycles, and is generally much 169 | // easier than looping through ownProperties of `val`. 170 | try { 171 | return 'Object(' + JSON.stringify(val) + ')'; 172 | } catch (_) { 173 | return 'Object'; 174 | } 175 | } 176 | // errors 177 | if (val instanceof Error) { 178 | return `${val.name}: ${val.message}\n${val.stack}`; 179 | } 180 | // TODO we could test for more things here, like `Set`s and `Map`s. 181 | return className; 182 | } 183 | 184 | const CLOSURE_DTORS = (typeof FinalizationRegistry === 'undefined') 185 | ? { register: () => {}, unregister: () => {} } 186 | : new FinalizationRegistry(state => { 187 | wasm.__wbindgen_export_2.get(state.dtor)(state.a, state.b) 188 | }); 189 | 190 | function makeMutClosure(arg0, arg1, dtor, f) { 191 | const state = { a: arg0, b: arg1, cnt: 1, dtor }; 192 | const real = (...args) => { 193 | // First up with a closure we increment the internal reference 194 | // count. This ensures that the Rust closure environment won't 195 | // be deallocated while we're invoking it. 196 | state.cnt++; 197 | const a = state.a; 198 | state.a = 0; 199 | try { 200 | return f(a, state.b, ...args); 201 | } finally { 202 | if (--state.cnt === 0) { 203 | wasm.__wbindgen_export_2.get(state.dtor)(a, state.b); 204 | CLOSURE_DTORS.unregister(state); 205 | } else { 206 | state.a = a; 207 | } 208 | } 209 | }; 210 | real.original = state; 211 | CLOSURE_DTORS.register(real, state, state); 212 | return real; 213 | } 214 | function __wbg_adapter_28(arg0, arg1, arg2) { 215 | wasm._dyn_core__ops__function__FnMut__A____Output___R_as_wasm_bindgen__closure__WasmClosure___describe__invoke__h6e435f3bc6e3d654(arg0, arg1, addHeapObject(arg2)); 216 | } 217 | 218 | function __wbg_adapter_33(arg0, arg1, arg2) { 219 | wasm._dyn_core__ops__function__FnMut__A____Output___R_as_wasm_bindgen__closure__WasmClosure___describe__invoke__h870dc3eb3e49f78a(arg0, arg1, addHeapObject(arg2)); 220 | } 221 | 222 | /** 223 | */ 224 | export function start() { 225 | wasm.start(); 226 | } 227 | 228 | function passArray8ToWasm0(arg, malloc) { 229 | const ptr = malloc(arg.length * 1, 1) >>> 0; 230 | getUint8Memory0().set(arg, ptr / 1); 231 | WASM_VECTOR_LEN = arg.length; 232 | return ptr; 233 | } 234 | 235 | let cachedUint32Memory0 = null; 236 | 237 | function getUint32Memory0() { 238 | if (cachedUint32Memory0 === null || cachedUint32Memory0.byteLength === 0) { 239 | cachedUint32Memory0 = new Uint32Array(wasm.memory.buffer); 240 | } 241 | return cachedUint32Memory0; 242 | } 243 | 244 | function getArrayU32FromWasm0(ptr, len) { 245 | ptr = ptr >>> 0; 246 | return getUint32Memory0().subarray(ptr / 4, ptr / 4 + len); 247 | } 248 | 249 | function handleError(f, args) { 250 | try { 251 | return f.apply(this, args); 252 | } catch (e) { 253 | wasm.__wbindgen_exn_store(addHeapObject(e)); 254 | } 255 | } 256 | function __wbg_adapter_387(arg0, arg1, arg2, arg3) { 257 | wasm.wasm_bindgen__convert__closures__invoke2_mut__h849a5a7e5a0f14b2(arg0, arg1, addHeapObject(arg2), addHeapObject(arg3)); 258 | } 259 | 260 | const ColorCruncherFinalization = (typeof FinalizationRegistry === 'undefined') 261 | ? { register: () => {}, unregister: () => {} } 262 | : new FinalizationRegistry(ptr => wasm.__wbg_colorcruncher_free(ptr >>> 0)); 263 | /** 264 | */ 265 | export class ColorCruncher { 266 | 267 | static __wrap(ptr) { 268 | ptr = ptr >>> 0; 269 | const obj = Object.create(ColorCruncher.prototype); 270 | obj.__wbg_ptr = ptr; 271 | ColorCruncherFinalization.register(obj, obj.__wbg_ptr, obj); 272 | return obj; 273 | } 274 | 275 | __destroy_into_raw() { 276 | const ptr = this.__wbg_ptr; 277 | this.__wbg_ptr = 0; 278 | ColorCruncherFinalization.unregister(this); 279 | return ptr; 280 | } 281 | 282 | free() { 283 | const ptr = this.__destroy_into_raw(); 284 | wasm.__wbg_colorcruncher_free(ptr); 285 | } 286 | /** 287 | * @param {Uint8Array} data 288 | * @returns {Promise} 289 | */ 290 | quantizeImage(data) { 291 | const ptr0 = passArray8ToWasm0(data, wasm.__wbindgen_malloc); 292 | const len0 = WASM_VECTOR_LEN; 293 | const ret = wasm.colorcruncher_quantizeImage(this.__wbg_ptr, ptr0, len0); 294 | return takeObject(ret); 295 | } 296 | } 297 | 298 | const ColorCruncherBuilderFinalization = (typeof FinalizationRegistry === 'undefined') 299 | ? { register: () => {}, unregister: () => {} } 300 | : new FinalizationRegistry(ptr => wasm.__wbg_colorcruncherbuilder_free(ptr >>> 0)); 301 | /** 302 | */ 303 | export class ColorCruncherBuilder { 304 | 305 | static __wrap(ptr) { 306 | ptr = ptr >>> 0; 307 | const obj = Object.create(ColorCruncherBuilder.prototype); 308 | obj.__wbg_ptr = ptr; 309 | ColorCruncherBuilderFinalization.register(obj, obj.__wbg_ptr, obj); 310 | return obj; 311 | } 312 | 313 | __destroy_into_raw() { 314 | const ptr = this.__wbg_ptr; 315 | this.__wbg_ptr = 0; 316 | ColorCruncherBuilderFinalization.unregister(this); 317 | return ptr; 318 | } 319 | 320 | free() { 321 | const ptr = this.__destroy_into_raw(); 322 | wasm.__wbg_colorcruncherbuilder_free(ptr); 323 | } 324 | /** 325 | */ 326 | constructor() { 327 | const ret = wasm.colorcruncherbuilder_new(); 328 | this.__wbg_ptr = ret >>> 0; 329 | return this; 330 | } 331 | /** 332 | * @param {number} max_colors 333 | * @returns {ColorCruncherBuilder} 334 | */ 335 | withMaxColors(max_colors) { 336 | const ptr = this.__destroy_into_raw(); 337 | const ret = wasm.colorcruncherbuilder_withMaxColors(ptr, max_colors); 338 | return ColorCruncherBuilder.__wrap(ret); 339 | } 340 | /** 341 | * @param {number} sample_rate 342 | * @returns {ColorCruncherBuilder} 343 | */ 344 | withSampleRate(sample_rate) { 345 | const ptr = this.__destroy_into_raw(); 346 | const ret = wasm.colorcruncherbuilder_withSampleRate(ptr, sample_rate); 347 | return ColorCruncherBuilder.__wrap(ret); 348 | } 349 | /** 350 | * @param {number} tolerance 351 | * @returns {ColorCruncherBuilder} 352 | */ 353 | withTolerance(tolerance) { 354 | const ptr = this.__destroy_into_raw(); 355 | const ret = wasm.colorcruncherbuilder_withTolerance(ptr, tolerance); 356 | return ColorCruncherBuilder.__wrap(ret); 357 | } 358 | /** 359 | * @param {number} max_iterations 360 | * @returns {ColorCruncherBuilder} 361 | */ 362 | withMaxIterations(max_iterations) { 363 | const ptr = this.__destroy_into_raw(); 364 | const ret = wasm.colorcruncherbuilder_withMaxIterations(ptr, max_iterations); 365 | return ColorCruncherBuilder.__wrap(ret); 366 | } 367 | /** 368 | * @param {string} initializer 369 | * @returns {ColorCruncherBuilder} 370 | */ 371 | withInitializer(initializer) { 372 | const ptr = this.__destroy_into_raw(); 373 | const ptr0 = passStringToWasm0(initializer, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc); 374 | const len0 = WASM_VECTOR_LEN; 375 | const ret = wasm.colorcruncherbuilder_withInitializer(ptr, ptr0, len0); 376 | return ColorCruncherBuilder.__wrap(ret); 377 | } 378 | /** 379 | * @param {string} algorithm 380 | * @returns {ColorCruncherBuilder} 381 | */ 382 | withAlgorithm(algorithm) { 383 | const ptr = this.__destroy_into_raw(); 384 | const ptr0 = passStringToWasm0(algorithm, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc); 385 | const len0 = WASM_VECTOR_LEN; 386 | const ret = wasm.colorcruncherbuilder_withAlgorithm(ptr, ptr0, len0); 387 | return ColorCruncherBuilder.__wrap(ret); 388 | } 389 | /** 390 | * @param {bigint} seed 391 | * @returns {ColorCruncherBuilder} 392 | */ 393 | withSeed(seed) { 394 | const ptr = this.__destroy_into_raw(); 395 | const ret = wasm.colorcruncherbuilder_withSeed(ptr, seed); 396 | return ColorCruncherBuilder.__wrap(ret); 397 | } 398 | /** 399 | * @returns {Promise} 400 | */ 401 | build() { 402 | const ret = wasm.colorcruncherbuilder_build(this.__wbg_ptr); 403 | return takeObject(ret); 404 | } 405 | } 406 | 407 | async function __wbg_load(module, imports) { 408 | if (typeof Response === 'function' && module instanceof Response) { 409 | if (typeof WebAssembly.instantiateStreaming === 'function') { 410 | try { 411 | return await WebAssembly.instantiateStreaming(module, imports); 412 | 413 | } catch (e) { 414 | if (module.headers.get('Content-Type') != 'application/wasm') { 415 | console.warn("`WebAssembly.instantiateStreaming` failed because your server does not serve wasm with `application/wasm` MIME type. Falling back to `WebAssembly.instantiate` which is slower. Original error:\n", e); 416 | 417 | } else { 418 | throw e; 419 | } 420 | } 421 | } 422 | 423 | const bytes = await module.arrayBuffer(); 424 | return await WebAssembly.instantiate(bytes, imports); 425 | 426 | } else { 427 | const instance = await WebAssembly.instantiate(module, imports); 428 | 429 | if (instance instanceof WebAssembly.Instance) { 430 | return { instance, module }; 431 | 432 | } else { 433 | return instance; 434 | } 435 | } 436 | } 437 | 438 | function __wbg_get_imports() { 439 | const imports = {}; 440 | imports.wbg = {}; 441 | imports.wbg.__wbg_colorcruncher_new = function(arg0) { 442 | const ret = ColorCruncher.__wrap(arg0); 443 | return addHeapObject(ret); 444 | }; 445 | imports.wbg.__wbindgen_object_drop_ref = function(arg0) { 446 | takeObject(arg0); 447 | }; 448 | imports.wbg.__wbindgen_string_new = function(arg0, arg1) { 449 | const ret = getStringFromWasm0(arg0, arg1); 450 | return addHeapObject(ret); 451 | }; 452 | imports.wbg.__wbg_new_abda76e883ba8a5f = function() { 453 | const ret = new Error(); 454 | return addHeapObject(ret); 455 | }; 456 | imports.wbg.__wbg_stack_658279fe44541cf6 = function(arg0, arg1) { 457 | const ret = getObject(arg1).stack; 458 | const ptr1 = passStringToWasm0(ret, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc); 459 | const len1 = WASM_VECTOR_LEN; 460 | getInt32Memory0()[arg0 / 4 + 1] = len1; 461 | getInt32Memory0()[arg0 / 4 + 0] = ptr1; 462 | }; 463 | imports.wbg.__wbg_error_f851667af71bcfc6 = function(arg0, arg1) { 464 | let deferred0_0; 465 | let deferred0_1; 466 | try { 467 | deferred0_0 = arg0; 468 | deferred0_1 = arg1; 469 | console.error(getStringFromWasm0(arg0, arg1)); 470 | } finally { 471 | wasm.__wbindgen_free(deferred0_0, deferred0_1, 1); 472 | } 473 | }; 474 | imports.wbg.__wbg_instanceof_GpuValidationError_776dc042f9752ecb = function(arg0) { 475 | let result; 476 | try { 477 | result = getObject(arg0) instanceof GPUValidationError; 478 | } catch (_) { 479 | result = false; 480 | } 481 | const ret = result; 482 | return ret; 483 | }; 484 | imports.wbg.__wbg_message_e73620d927b54373 = function(arg0, arg1) { 485 | const ret = getObject(arg1).message; 486 | const ptr1 = passStringToWasm0(ret, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc); 487 | const len1 = WASM_VECTOR_LEN; 488 | getInt32Memory0()[arg0 / 4 + 1] = len1; 489 | getInt32Memory0()[arg0 / 4 + 0] = ptr1; 490 | }; 491 | imports.wbg.__wbg_instanceof_GpuOutOfMemoryError_3621d9e8ec05691e = function(arg0) { 492 | let result; 493 | try { 494 | result = getObject(arg0) instanceof GPUOutOfMemoryError; 495 | } catch (_) { 496 | result = false; 497 | } 498 | const ret = result; 499 | return ret; 500 | }; 501 | imports.wbg.__wbindgen_object_clone_ref = function(arg0) { 502 | const ret = getObject(arg0); 503 | return addHeapObject(ret); 504 | }; 505 | imports.wbg.__wbg_error_c4453561fa6c2209 = function(arg0) { 506 | const ret = getObject(arg0).error; 507 | return addHeapObject(ret); 508 | }; 509 | imports.wbg.__wbindgen_cb_drop = function(arg0) { 510 | const obj = takeObject(arg0).original; 511 | if (obj.cnt-- == 1) { 512 | obj.a = 0; 513 | return true; 514 | } 515 | const ret = false; 516 | return ret; 517 | }; 518 | imports.wbg.__wbg_instanceof_GpuDeviceLostInfo_22f963b61044b3b1 = function(arg0) { 519 | let result; 520 | try { 521 | result = getObject(arg0) instanceof GPUDeviceLostInfo; 522 | } catch (_) { 523 | result = false; 524 | } 525 | const ret = result; 526 | return ret; 527 | }; 528 | imports.wbg.__wbg_message_3bef8c43f84eab9c = function(arg0, arg1) { 529 | const ret = getObject(arg1).message; 530 | const ptr1 = passStringToWasm0(ret, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc); 531 | const len1 = WASM_VECTOR_LEN; 532 | getInt32Memory0()[arg0 / 4 + 1] = len1; 533 | getInt32Memory0()[arg0 / 4 + 0] = ptr1; 534 | }; 535 | imports.wbg.__wbindgen_string_get = function(arg0, arg1) { 536 | const obj = getObject(arg1); 537 | const ret = typeof(obj) === 'string' ? obj : undefined; 538 | var ptr1 = isLikeNone(ret) ? 0 : passStringToWasm0(ret, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc); 539 | var len1 = WASM_VECTOR_LEN; 540 | getInt32Memory0()[arg0 / 4 + 1] = len1; 541 | getInt32Memory0()[arg0 / 4 + 0] = ptr1; 542 | }; 543 | imports.wbg.__wbg_has_1509b2ce6759dc2a = function(arg0, arg1, arg2) { 544 | const ret = getObject(arg0).has(getStringFromWasm0(arg1, arg2)); 545 | return ret; 546 | }; 547 | imports.wbg.__wbg_maxTextureDimension1D_ea59b0f0cc2e29cd = function(arg0) { 548 | const ret = getObject(arg0).maxTextureDimension1D; 549 | return ret; 550 | }; 551 | imports.wbg.__wbg_maxTextureDimension2D_00984ba245729ced = function(arg0) { 552 | const ret = getObject(arg0).maxTextureDimension2D; 553 | return ret; 554 | }; 555 | imports.wbg.__wbg_maxTextureDimension3D_95c3d3adb6d66ec5 = function(arg0) { 556 | const ret = getObject(arg0).maxTextureDimension3D; 557 | return ret; 558 | }; 559 | imports.wbg.__wbg_maxTextureArrayLayers_68f4a1218a54fa93 = function(arg0) { 560 | const ret = getObject(arg0).maxTextureArrayLayers; 561 | return ret; 562 | }; 563 | imports.wbg.__wbg_maxBindGroups_e76fb8650a4459d7 = function(arg0) { 564 | const ret = getObject(arg0).maxBindGroups; 565 | return ret; 566 | }; 567 | imports.wbg.__wbg_maxBindingsPerBindGroup_2af20f39aef3fd86 = function(arg0) { 568 | const ret = getObject(arg0).maxBindingsPerBindGroup; 569 | return ret; 570 | }; 571 | imports.wbg.__wbg_maxDynamicUniformBuffersPerPipelineLayout_074c891075b375b7 = function(arg0) { 572 | const ret = getObject(arg0).maxDynamicUniformBuffersPerPipelineLayout; 573 | return ret; 574 | }; 575 | imports.wbg.__wbg_maxDynamicStorageBuffersPerPipelineLayout_b91e3e6efb7b7a8c = function(arg0) { 576 | const ret = getObject(arg0).maxDynamicStorageBuffersPerPipelineLayout; 577 | return ret; 578 | }; 579 | imports.wbg.__wbg_maxSampledTexturesPerShaderStage_76354979d03a2b27 = function(arg0) { 580 | const ret = getObject(arg0).maxSampledTexturesPerShaderStage; 581 | return ret; 582 | }; 583 | imports.wbg.__wbg_maxSamplersPerShaderStage_fe8d223de90e5459 = function(arg0) { 584 | const ret = getObject(arg0).maxSamplersPerShaderStage; 585 | return ret; 586 | }; 587 | imports.wbg.__wbg_maxStorageBuffersPerShaderStage_bced69629145d26d = function(arg0) { 588 | const ret = getObject(arg0).maxStorageBuffersPerShaderStage; 589 | return ret; 590 | }; 591 | imports.wbg.__wbg_maxStorageTexturesPerShaderStage_fcf51f22620c0092 = function(arg0) { 592 | const ret = getObject(arg0).maxStorageTexturesPerShaderStage; 593 | return ret; 594 | }; 595 | imports.wbg.__wbg_maxUniformBuffersPerShaderStage_b3b013238400f0c0 = function(arg0) { 596 | const ret = getObject(arg0).maxUniformBuffersPerShaderStage; 597 | return ret; 598 | }; 599 | imports.wbg.__wbg_maxUniformBufferBindingSize_194fd7147cf2e95a = function(arg0) { 600 | const ret = getObject(arg0).maxUniformBufferBindingSize; 601 | return ret; 602 | }; 603 | imports.wbg.__wbg_maxStorageBufferBindingSize_78504383af63ac53 = function(arg0) { 604 | const ret = getObject(arg0).maxStorageBufferBindingSize; 605 | return ret; 606 | }; 607 | imports.wbg.__wbg_maxVertexBuffers_78c71ff19beac74b = function(arg0) { 608 | const ret = getObject(arg0).maxVertexBuffers; 609 | return ret; 610 | }; 611 | imports.wbg.__wbg_maxBufferSize_0c7ed57407582d40 = function(arg0) { 612 | const ret = getObject(arg0).maxBufferSize; 613 | return ret; 614 | }; 615 | imports.wbg.__wbg_maxVertexAttributes_c11cb018a9c5a224 = function(arg0) { 616 | const ret = getObject(arg0).maxVertexAttributes; 617 | return ret; 618 | }; 619 | imports.wbg.__wbg_maxVertexBufferArrayStride_c53560cc036cb477 = function(arg0) { 620 | const ret = getObject(arg0).maxVertexBufferArrayStride; 621 | return ret; 622 | }; 623 | imports.wbg.__wbg_minUniformBufferOffsetAlignment_4880e6786cb7ec5d = function(arg0) { 624 | const ret = getObject(arg0).minUniformBufferOffsetAlignment; 625 | return ret; 626 | }; 627 | imports.wbg.__wbg_minStorageBufferOffsetAlignment_9913f200aee2c749 = function(arg0) { 628 | const ret = getObject(arg0).minStorageBufferOffsetAlignment; 629 | return ret; 630 | }; 631 | imports.wbg.__wbg_maxInterStageShaderComponents_f9243ac86242eb18 = function(arg0) { 632 | const ret = getObject(arg0).maxInterStageShaderComponents; 633 | return ret; 634 | }; 635 | imports.wbg.__wbg_maxColorAttachments_d33b1d22c06a6fc5 = function(arg0) { 636 | const ret = getObject(arg0).maxColorAttachments; 637 | return ret; 638 | }; 639 | imports.wbg.__wbg_maxColorAttachmentBytesPerSample_637fd3ac394c14ee = function(arg0) { 640 | const ret = getObject(arg0).maxColorAttachmentBytesPerSample; 641 | return ret; 642 | }; 643 | imports.wbg.__wbg_maxComputeWorkgroupStorageSize_7e5bc378e5a62367 = function(arg0) { 644 | const ret = getObject(arg0).maxComputeWorkgroupStorageSize; 645 | return ret; 646 | }; 647 | imports.wbg.__wbg_maxComputeInvocationsPerWorkgroup_1ed5b24d52720f8a = function(arg0) { 648 | const ret = getObject(arg0).maxComputeInvocationsPerWorkgroup; 649 | return ret; 650 | }; 651 | imports.wbg.__wbg_maxComputeWorkgroupSizeX_56b713fb17f8c261 = function(arg0) { 652 | const ret = getObject(arg0).maxComputeWorkgroupSizeX; 653 | return ret; 654 | }; 655 | imports.wbg.__wbg_maxComputeWorkgroupSizeY_13040bdf12fd4e65 = function(arg0) { 656 | const ret = getObject(arg0).maxComputeWorkgroupSizeY; 657 | return ret; 658 | }; 659 | imports.wbg.__wbg_maxComputeWorkgroupSizeZ_8c8594730967472d = function(arg0) { 660 | const ret = getObject(arg0).maxComputeWorkgroupSizeZ; 661 | return ret; 662 | }; 663 | imports.wbg.__wbg_maxComputeWorkgroupsPerDimension_4094c8501eea36ce = function(arg0) { 664 | const ret = getObject(arg0).maxComputeWorkgroupsPerDimension; 665 | return ret; 666 | }; 667 | imports.wbg.__wbg_instanceof_GpuAdapter_32bc80c8c30adaa0 = function(arg0) { 668 | let result; 669 | try { 670 | result = getObject(arg0) instanceof GPUAdapter; 671 | } catch (_) { 672 | result = false; 673 | } 674 | const ret = result; 675 | return ret; 676 | }; 677 | imports.wbg.__wbg_queue_2bddd1700cb0bec2 = function(arg0) { 678 | const ret = getObject(arg0).queue; 679 | return addHeapObject(ret); 680 | }; 681 | imports.wbg.__wbindgen_is_object = function(arg0) { 682 | const val = getObject(arg0); 683 | const ret = typeof(val) === 'object' && val !== null; 684 | return ret; 685 | }; 686 | imports.wbg.__wbg_instanceof_GpuCanvasContext_b3bff0de75efe6fd = function(arg0) { 687 | let result; 688 | try { 689 | result = getObject(arg0) instanceof GPUCanvasContext; 690 | } catch (_) { 691 | result = false; 692 | } 693 | const ret = result; 694 | return ret; 695 | }; 696 | imports.wbg.__wbg_getMappedRange_1216b00d6d7803de = function(arg0, arg1, arg2) { 697 | const ret = getObject(arg0).getMappedRange(arg1, arg2); 698 | return addHeapObject(ret); 699 | }; 700 | imports.wbg.__wbg_Window_94d759f1f207a15b = function(arg0) { 701 | const ret = getObject(arg0).Window; 702 | return addHeapObject(ret); 703 | }; 704 | imports.wbg.__wbindgen_is_undefined = function(arg0) { 705 | const ret = getObject(arg0) === undefined; 706 | return ret; 707 | }; 708 | imports.wbg.__wbg_WorkerGlobalScope_b13c8cef62388de9 = function(arg0) { 709 | const ret = getObject(arg0).WorkerGlobalScope; 710 | return addHeapObject(ret); 711 | }; 712 | imports.wbg.__wbg_gpu_1f3675e2d4aa88f4 = function(arg0) { 713 | const ret = getObject(arg0).gpu; 714 | return addHeapObject(ret); 715 | }; 716 | imports.wbg.__wbg_requestAdapter_e6f12701c7a38391 = function(arg0, arg1) { 717 | const ret = getObject(arg0).requestAdapter(getObject(arg1)); 718 | return addHeapObject(ret); 719 | }; 720 | imports.wbg.__wbindgen_number_new = function(arg0) { 721 | const ret = arg0; 722 | return addHeapObject(ret); 723 | }; 724 | imports.wbg.__wbg_requestDevice_727ad8687b0d6553 = function(arg0, arg1) { 725 | const ret = getObject(arg0).requestDevice(getObject(arg1)); 726 | return addHeapObject(ret); 727 | }; 728 | imports.wbg.__wbg_features_b56ebab8f515839e = function(arg0) { 729 | const ret = getObject(arg0).features; 730 | return addHeapObject(ret); 731 | }; 732 | imports.wbg.__wbg_limits_be2f592b5e154a3d = function(arg0) { 733 | const ret = getObject(arg0).limits; 734 | return addHeapObject(ret); 735 | }; 736 | imports.wbg.__wbg_getPreferredCanvasFormat_012ef9f3b0238ffa = function(arg0) { 737 | const ret = getObject(arg0).getPreferredCanvasFormat(); 738 | return addHeapObject(ret); 739 | }; 740 | imports.wbg.__wbg_configure_6cde48f0c99a3497 = function(arg0, arg1) { 741 | getObject(arg0).configure(getObject(arg1)); 742 | }; 743 | imports.wbg.__wbg_getCurrentTexture_95b5b88416fdb0c2 = function(arg0) { 744 | const ret = getObject(arg0).getCurrentTexture(); 745 | return addHeapObject(ret); 746 | }; 747 | imports.wbg.__wbg_features_4991b2a28904a253 = function(arg0) { 748 | const ret = getObject(arg0).features; 749 | return addHeapObject(ret); 750 | }; 751 | imports.wbg.__wbg_limits_1aa8a49e0a8442cc = function(arg0) { 752 | const ret = getObject(arg0).limits; 753 | return addHeapObject(ret); 754 | }; 755 | imports.wbg.__wbg_createShaderModule_036b780a18124d9e = function(arg0, arg1) { 756 | const ret = getObject(arg0).createShaderModule(getObject(arg1)); 757 | return addHeapObject(ret); 758 | }; 759 | imports.wbg.__wbg_createBindGroupLayout_313b4151e718ff1f = function(arg0, arg1) { 760 | const ret = getObject(arg0).createBindGroupLayout(getObject(arg1)); 761 | return addHeapObject(ret); 762 | }; 763 | imports.wbg.__wbg_createBindGroup_2d6778f92445c8bf = function(arg0, arg1) { 764 | const ret = getObject(arg0).createBindGroup(getObject(arg1)); 765 | return addHeapObject(ret); 766 | }; 767 | imports.wbg.__wbg_createPipelineLayout_9134c6c32c505ec8 = function(arg0, arg1) { 768 | const ret = getObject(arg0).createPipelineLayout(getObject(arg1)); 769 | return addHeapObject(ret); 770 | }; 771 | imports.wbg.__wbg_createRenderPipeline_2bfc852ce09914fc = function(arg0, arg1) { 772 | const ret = getObject(arg0).createRenderPipeline(getObject(arg1)); 773 | return addHeapObject(ret); 774 | }; 775 | imports.wbg.__wbg_createComputePipeline_02674342979c6288 = function(arg0, arg1) { 776 | const ret = getObject(arg0).createComputePipeline(getObject(arg1)); 777 | return addHeapObject(ret); 778 | }; 779 | imports.wbg.__wbg_createBuffer_65c2fc555c46aa07 = function(arg0, arg1) { 780 | const ret = getObject(arg0).createBuffer(getObject(arg1)); 781 | return addHeapObject(ret); 782 | }; 783 | imports.wbg.__wbg_createTexture_5adbcf0db3fd41b4 = function(arg0, arg1) { 784 | const ret = getObject(arg0).createTexture(getObject(arg1)); 785 | return addHeapObject(ret); 786 | }; 787 | imports.wbg.__wbg_createSampler_942022241ecf4277 = function(arg0, arg1) { 788 | const ret = getObject(arg0).createSampler(getObject(arg1)); 789 | return addHeapObject(ret); 790 | }; 791 | imports.wbg.__wbg_createQuerySet_424dbf8130140914 = function(arg0, arg1) { 792 | const ret = getObject(arg0).createQuerySet(getObject(arg1)); 793 | return addHeapObject(ret); 794 | }; 795 | imports.wbg.__wbg_createCommandEncoder_1db1770ea9eab9af = function(arg0, arg1) { 796 | const ret = getObject(arg0).createCommandEncoder(getObject(arg1)); 797 | return addHeapObject(ret); 798 | }; 799 | imports.wbg.__wbg_createRenderBundleEncoder_32896e68340fabc6 = function(arg0, arg1) { 800 | const ret = getObject(arg0).createRenderBundleEncoder(getObject(arg1)); 801 | return addHeapObject(ret); 802 | }; 803 | imports.wbg.__wbg_destroy_4f7ed2bbb4742899 = function(arg0) { 804 | getObject(arg0).destroy(); 805 | }; 806 | imports.wbg.__wbg_lost_42410660a8cd8819 = function(arg0) { 807 | const ret = getObject(arg0).lost; 808 | return addHeapObject(ret); 809 | }; 810 | imports.wbg.__wbg_setonuncapturederror_4e4946a65c61f3ef = function(arg0, arg1) { 811 | getObject(arg0).onuncapturederror = getObject(arg1); 812 | }; 813 | imports.wbg.__wbg_pushErrorScope_a09c8b037ab27e15 = function(arg0, arg1) { 814 | getObject(arg0).pushErrorScope(takeObject(arg1)); 815 | }; 816 | imports.wbg.__wbg_popErrorScope_f8f0d4b6d5c635f9 = function(arg0) { 817 | const ret = getObject(arg0).popErrorScope(); 818 | return addHeapObject(ret); 819 | }; 820 | imports.wbg.__wbg_mapAsync_3b0a03a892fb22b3 = function(arg0, arg1, arg2, arg3) { 821 | const ret = getObject(arg0).mapAsync(arg1 >>> 0, arg2, arg3); 822 | return addHeapObject(ret); 823 | }; 824 | imports.wbg.__wbg_unmap_7a0dddee82ac6ed3 = function(arg0) { 825 | getObject(arg0).unmap(); 826 | }; 827 | imports.wbg.__wbg_createView_0ab0576f1665c9ad = function(arg0, arg1) { 828 | const ret = getObject(arg0).createView(getObject(arg1)); 829 | return addHeapObject(ret); 830 | }; 831 | imports.wbg.__wbg_destroy_199808599201ee27 = function(arg0) { 832 | getObject(arg0).destroy(); 833 | }; 834 | imports.wbg.__wbg_destroy_57694ff5aabbf32d = function(arg0) { 835 | getObject(arg0).destroy(); 836 | }; 837 | imports.wbg.__wbg_getBindGroupLayout_a0d36a72bd39bb04 = function(arg0, arg1) { 838 | const ret = getObject(arg0).getBindGroupLayout(arg1 >>> 0); 839 | return addHeapObject(ret); 840 | }; 841 | imports.wbg.__wbg_getBindGroupLayout_abc654a192f85d5e = function(arg0, arg1) { 842 | const ret = getObject(arg0).getBindGroupLayout(arg1 >>> 0); 843 | return addHeapObject(ret); 844 | }; 845 | imports.wbg.__wbg_copyBufferToBuffer_667953bc6dccb6b4 = function(arg0, arg1, arg2, arg3, arg4, arg5) { 846 | getObject(arg0).copyBufferToBuffer(getObject(arg1), arg2, getObject(arg3), arg4, arg5); 847 | }; 848 | imports.wbg.__wbg_copyBufferToTexture_ca5b298687bed60a = function(arg0, arg1, arg2, arg3) { 849 | getObject(arg0).copyBufferToTexture(getObject(arg1), getObject(arg2), getObject(arg3)); 850 | }; 851 | imports.wbg.__wbg_copyTextureToBuffer_cdf8118386295eb4 = function(arg0, arg1, arg2, arg3) { 852 | getObject(arg0).copyTextureToBuffer(getObject(arg1), getObject(arg2), getObject(arg3)); 853 | }; 854 | imports.wbg.__wbg_copyTextureToTexture_67678f03fd20bd23 = function(arg0, arg1, arg2, arg3) { 855 | getObject(arg0).copyTextureToTexture(getObject(arg1), getObject(arg2), getObject(arg3)); 856 | }; 857 | imports.wbg.__wbg_beginComputePass_a148b983810f6795 = function(arg0, arg1) { 858 | const ret = getObject(arg0).beginComputePass(getObject(arg1)); 859 | return addHeapObject(ret); 860 | }; 861 | imports.wbg.__wbg_end_28d311f5d435aa6d = function(arg0) { 862 | getObject(arg0).end(); 863 | }; 864 | imports.wbg.__wbg_beginRenderPass_0b83360fd99b5810 = function(arg0, arg1) { 865 | const ret = getObject(arg0).beginRenderPass(getObject(arg1)); 866 | return addHeapObject(ret); 867 | }; 868 | imports.wbg.__wbg_end_e3cea1776c95d64f = function(arg0) { 869 | getObject(arg0).end(); 870 | }; 871 | imports.wbg.__wbg_label_175c4f59b3eca611 = function(arg0, arg1) { 872 | const ret = getObject(arg1).label; 873 | const ptr1 = passStringToWasm0(ret, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc); 874 | const len1 = WASM_VECTOR_LEN; 875 | getInt32Memory0()[arg0 / 4 + 1] = len1; 876 | getInt32Memory0()[arg0 / 4 + 0] = ptr1; 877 | }; 878 | imports.wbg.__wbg_finish_d1d9eb9915c96a79 = function(arg0, arg1) { 879 | const ret = getObject(arg0).finish(getObject(arg1)); 880 | return addHeapObject(ret); 881 | }; 882 | imports.wbg.__wbg_finish_ce7d5c15fce975aa = function(arg0) { 883 | const ret = getObject(arg0).finish(); 884 | return addHeapObject(ret); 885 | }; 886 | imports.wbg.__wbg_clearBuffer_2cc723ab6b818737 = function(arg0, arg1, arg2) { 887 | getObject(arg0).clearBuffer(getObject(arg1), arg2); 888 | }; 889 | imports.wbg.__wbg_clearBuffer_78a94a2eda97eb5a = function(arg0, arg1, arg2, arg3) { 890 | getObject(arg0).clearBuffer(getObject(arg1), arg2, arg3); 891 | }; 892 | imports.wbg.__wbg_resolveQuerySet_22e31015a36a09d5 = function(arg0, arg1, arg2, arg3, arg4, arg5) { 893 | getObject(arg0).resolveQuerySet(getObject(arg1), arg2 >>> 0, arg3 >>> 0, getObject(arg4), arg5 >>> 0); 894 | }; 895 | imports.wbg.__wbg_finish_2115db9e679c5aae = function(arg0) { 896 | const ret = getObject(arg0).finish(); 897 | return addHeapObject(ret); 898 | }; 899 | imports.wbg.__wbg_finish_4a754149a60eddc0 = function(arg0, arg1) { 900 | const ret = getObject(arg0).finish(getObject(arg1)); 901 | return addHeapObject(ret); 902 | }; 903 | imports.wbg.__wbg_writeBuffer_4245ce84e6d772c9 = function(arg0, arg1, arg2, arg3, arg4, arg5) { 904 | getObject(arg0).writeBuffer(getObject(arg1), arg2, getObject(arg3), arg4, arg5); 905 | }; 906 | imports.wbg.__wbg_usage_5e9a3548afbc3ebb = function(arg0) { 907 | const ret = getObject(arg0).usage; 908 | return ret; 909 | }; 910 | imports.wbg.__wbg_size_fc880d60ff425a47 = function(arg0) { 911 | const ret = getObject(arg0).size; 912 | return ret; 913 | }; 914 | imports.wbg.__wbg_writeTexture_686a8160c3c5ddbb = function(arg0, arg1, arg2, arg3, arg4) { 915 | getObject(arg0).writeTexture(getObject(arg1), getObject(arg2), getObject(arg3), getObject(arg4)); 916 | }; 917 | imports.wbg.__wbg_copyExternalImageToTexture_87bdcc3260c6efba = function(arg0, arg1, arg2, arg3) { 918 | getObject(arg0).copyExternalImageToTexture(getObject(arg1), getObject(arg2), getObject(arg3)); 919 | }; 920 | imports.wbg.__wbg_setPipeline_8630b264a9c4ec4b = function(arg0, arg1) { 921 | getObject(arg0).setPipeline(getObject(arg1)); 922 | }; 923 | imports.wbg.__wbg_setBindGroup_17e73587d3c1be08 = function(arg0, arg1, arg2) { 924 | getObject(arg0).setBindGroup(arg1 >>> 0, getObject(arg2)); 925 | }; 926 | imports.wbg.__wbg_setBindGroup_5a450a0e97199c15 = function(arg0, arg1, arg2, arg3, arg4, arg5, arg6) { 927 | getObject(arg0).setBindGroup(arg1 >>> 0, getObject(arg2), getArrayU32FromWasm0(arg3, arg4), arg5, arg6 >>> 0); 928 | }; 929 | imports.wbg.__wbg_dispatchWorkgroups_4bc133944e89d5e0 = function(arg0, arg1, arg2, arg3) { 930 | getObject(arg0).dispatchWorkgroups(arg1 >>> 0, arg2 >>> 0, arg3 >>> 0); 931 | }; 932 | imports.wbg.__wbg_dispatchWorkgroupsIndirect_8050acb60dd74a34 = function(arg0, arg1, arg2) { 933 | getObject(arg0).dispatchWorkgroupsIndirect(getObject(arg1), arg2); 934 | }; 935 | imports.wbg.__wbg_setPipeline_a95b89d99620ba34 = function(arg0, arg1) { 936 | getObject(arg0).setPipeline(getObject(arg1)); 937 | }; 938 | imports.wbg.__wbg_setBindGroup_58e27d4cd266f187 = function(arg0, arg1, arg2) { 939 | getObject(arg0).setBindGroup(arg1 >>> 0, getObject(arg2)); 940 | }; 941 | imports.wbg.__wbg_setBindGroup_f70bb0d0a5ace56d = function(arg0, arg1, arg2, arg3, arg4, arg5, arg6) { 942 | getObject(arg0).setBindGroup(arg1 >>> 0, getObject(arg2), getArrayU32FromWasm0(arg3, arg4), arg5, arg6 >>> 0); 943 | }; 944 | imports.wbg.__wbg_setIndexBuffer_747e1ba3f58d7227 = function(arg0, arg1, arg2, arg3) { 945 | getObject(arg0).setIndexBuffer(getObject(arg1), takeObject(arg2), arg3); 946 | }; 947 | imports.wbg.__wbg_setIndexBuffer_3f1635c89f72d661 = function(arg0, arg1, arg2, arg3, arg4) { 948 | getObject(arg0).setIndexBuffer(getObject(arg1), takeObject(arg2), arg3, arg4); 949 | }; 950 | imports.wbg.__wbg_setVertexBuffer_94a88edbfb4b07f8 = function(arg0, arg1, arg2, arg3) { 951 | getObject(arg0).setVertexBuffer(arg1 >>> 0, getObject(arg2), arg3); 952 | }; 953 | imports.wbg.__wbg_setVertexBuffer_407067a9522118df = function(arg0, arg1, arg2, arg3, arg4) { 954 | getObject(arg0).setVertexBuffer(arg1 >>> 0, getObject(arg2), arg3, arg4); 955 | }; 956 | imports.wbg.__wbg_draw_60508d893ce4e012 = function(arg0, arg1, arg2, arg3, arg4) { 957 | getObject(arg0).draw(arg1 >>> 0, arg2 >>> 0, arg3 >>> 0, arg4 >>> 0); 958 | }; 959 | imports.wbg.__wbg_drawIndexed_d5c5dff02437a4f0 = function(arg0, arg1, arg2, arg3, arg4, arg5) { 960 | getObject(arg0).drawIndexed(arg1 >>> 0, arg2 >>> 0, arg3 >>> 0, arg4, arg5 >>> 0); 961 | }; 962 | imports.wbg.__wbg_drawIndirect_54f93ae4ccc85358 = function(arg0, arg1, arg2) { 963 | getObject(arg0).drawIndirect(getObject(arg1), arg2); 964 | }; 965 | imports.wbg.__wbg_drawIndexedIndirect_bf668464170261b3 = function(arg0, arg1, arg2) { 966 | getObject(arg0).drawIndexedIndirect(getObject(arg1), arg2); 967 | }; 968 | imports.wbg.__wbg_setPipeline_d7c9c55035f118a6 = function(arg0, arg1) { 969 | getObject(arg0).setPipeline(getObject(arg1)); 970 | }; 971 | imports.wbg.__wbg_setBindGroup_c6ab2e9583489b58 = function(arg0, arg1, arg2) { 972 | getObject(arg0).setBindGroup(arg1 >>> 0, getObject(arg2)); 973 | }; 974 | imports.wbg.__wbg_setBindGroup_0bf976b9657f99bd = function(arg0, arg1, arg2, arg3, arg4, arg5, arg6) { 975 | getObject(arg0).setBindGroup(arg1 >>> 0, getObject(arg2), getArrayU32FromWasm0(arg3, arg4), arg5, arg6 >>> 0); 976 | }; 977 | imports.wbg.__wbg_setIndexBuffer_ea39707d8842fe03 = function(arg0, arg1, arg2, arg3) { 978 | getObject(arg0).setIndexBuffer(getObject(arg1), takeObject(arg2), arg3); 979 | }; 980 | imports.wbg.__wbg_setIndexBuffer_04ba4ea48c8f80be = function(arg0, arg1, arg2, arg3, arg4) { 981 | getObject(arg0).setIndexBuffer(getObject(arg1), takeObject(arg2), arg3, arg4); 982 | }; 983 | imports.wbg.__wbg_setVertexBuffer_907c60acf6dca161 = function(arg0, arg1, arg2, arg3) { 984 | getObject(arg0).setVertexBuffer(arg1 >>> 0, getObject(arg2), arg3); 985 | }; 986 | imports.wbg.__wbg_setVertexBuffer_9a336bb112a33317 = function(arg0, arg1, arg2, arg3, arg4) { 987 | getObject(arg0).setVertexBuffer(arg1 >>> 0, getObject(arg2), arg3, arg4); 988 | }; 989 | imports.wbg.__wbg_draw_540a514f996a5d0d = function(arg0, arg1, arg2, arg3, arg4) { 990 | getObject(arg0).draw(arg1 >>> 0, arg2 >>> 0, arg3 >>> 0, arg4 >>> 0); 991 | }; 992 | imports.wbg.__wbg_drawIndexed_f717a07602ee2d18 = function(arg0, arg1, arg2, arg3, arg4, arg5) { 993 | getObject(arg0).drawIndexed(arg1 >>> 0, arg2 >>> 0, arg3 >>> 0, arg4, arg5 >>> 0); 994 | }; 995 | imports.wbg.__wbg_drawIndirect_c588ff54fb149aee = function(arg0, arg1, arg2) { 996 | getObject(arg0).drawIndirect(getObject(arg1), arg2); 997 | }; 998 | imports.wbg.__wbg_drawIndexedIndirect_bb5585ec7f45d269 = function(arg0, arg1, arg2) { 999 | getObject(arg0).drawIndexedIndirect(getObject(arg1), arg2); 1000 | }; 1001 | imports.wbg.__wbg_setBlendConstant_496a0b5cc772c236 = function(arg0, arg1) { 1002 | getObject(arg0).setBlendConstant(getObject(arg1)); 1003 | }; 1004 | imports.wbg.__wbg_setScissorRect_9b7e673d03036c37 = function(arg0, arg1, arg2, arg3, arg4) { 1005 | getObject(arg0).setScissorRect(arg1 >>> 0, arg2 >>> 0, arg3 >>> 0, arg4 >>> 0); 1006 | }; 1007 | imports.wbg.__wbg_setViewport_85d18ceefd5180eb = function(arg0, arg1, arg2, arg3, arg4, arg5, arg6) { 1008 | getObject(arg0).setViewport(arg1, arg2, arg3, arg4, arg5, arg6); 1009 | }; 1010 | imports.wbg.__wbg_setStencilReference_b4b1f7e586967a4d = function(arg0, arg1) { 1011 | getObject(arg0).setStencilReference(arg1 >>> 0); 1012 | }; 1013 | imports.wbg.__wbg_executeBundles_16985086317c358a = function(arg0, arg1) { 1014 | getObject(arg0).executeBundles(getObject(arg1)); 1015 | }; 1016 | imports.wbg.__wbg_submit_afbd82b0d5056194 = function(arg0, arg1) { 1017 | getObject(arg0).submit(getObject(arg1)); 1018 | }; 1019 | imports.wbg.__wbg_reason_3af8e4afbe0efdd8 = function(arg0) { 1020 | const ret = getObject(arg0).reason; 1021 | return addHeapObject(ret); 1022 | }; 1023 | imports.wbg.__wbg_queueMicrotask_3cbae2ec6b6cd3d6 = function(arg0) { 1024 | const ret = getObject(arg0).queueMicrotask; 1025 | return addHeapObject(ret); 1026 | }; 1027 | imports.wbg.__wbindgen_is_function = function(arg0) { 1028 | const ret = typeof(getObject(arg0)) === 'function'; 1029 | return ret; 1030 | }; 1031 | imports.wbg.__wbg_queueMicrotask_481971b0d87f3dd4 = function(arg0) { 1032 | queueMicrotask(getObject(arg0)); 1033 | }; 1034 | imports.wbg.__wbg_instanceof_Window_f401953a2cf86220 = function(arg0) { 1035 | let result; 1036 | try { 1037 | result = getObject(arg0) instanceof Window; 1038 | } catch (_) { 1039 | result = false; 1040 | } 1041 | const ret = result; 1042 | return ret; 1043 | }; 1044 | imports.wbg.__wbg_document_5100775d18896c16 = function(arg0) { 1045 | const ret = getObject(arg0).document; 1046 | return isLikeNone(ret) ? 0 : addHeapObject(ret); 1047 | }; 1048 | imports.wbg.__wbg_navigator_6c8fa55c5cc8796e = function(arg0) { 1049 | const ret = getObject(arg0).navigator; 1050 | return addHeapObject(ret); 1051 | }; 1052 | imports.wbg.__wbg_querySelectorAll_4e0fcdb64cda2cd5 = function() { return handleError(function (arg0, arg1, arg2) { 1053 | const ret = getObject(arg0).querySelectorAll(getStringFromWasm0(arg1, arg2)); 1054 | return addHeapObject(ret); 1055 | }, arguments) }; 1056 | imports.wbg.__wbg_navigator_56803b85352a0575 = function(arg0) { 1057 | const ret = getObject(arg0).navigator; 1058 | return addHeapObject(ret); 1059 | }; 1060 | imports.wbg.__wbg_setwidth_83d936c4b04dcbec = function(arg0, arg1) { 1061 | getObject(arg0).width = arg1 >>> 0; 1062 | }; 1063 | imports.wbg.__wbg_setheight_6025ba0d58e6cc8c = function(arg0, arg1) { 1064 | getObject(arg0).height = arg1 >>> 0; 1065 | }; 1066 | imports.wbg.__wbg_getContext_c102f659d540d068 = function() { return handleError(function (arg0, arg1, arg2) { 1067 | const ret = getObject(arg0).getContext(getStringFromWasm0(arg1, arg2)); 1068 | return isLikeNone(ret) ? 0 : addHeapObject(ret); 1069 | }, arguments) }; 1070 | imports.wbg.__wbg_debug_5fb96680aecf5dc8 = function(arg0) { 1071 | console.debug(getObject(arg0)); 1072 | }; 1073 | imports.wbg.__wbg_error_8e3928cfb8a43e2b = function(arg0) { 1074 | console.error(getObject(arg0)); 1075 | }; 1076 | imports.wbg.__wbg_info_530a29cb2e4e3304 = function(arg0) { 1077 | console.info(getObject(arg0)); 1078 | }; 1079 | imports.wbg.__wbg_log_5bb5f88f245d7762 = function(arg0) { 1080 | console.log(getObject(arg0)); 1081 | }; 1082 | imports.wbg.__wbg_warn_63bbae1730aead09 = function(arg0) { 1083 | console.warn(getObject(arg0)); 1084 | }; 1085 | imports.wbg.__wbg_setwidth_080107476e633963 = function(arg0, arg1) { 1086 | getObject(arg0).width = arg1 >>> 0; 1087 | }; 1088 | imports.wbg.__wbg_setheight_dc240617639f1f51 = function(arg0, arg1) { 1089 | getObject(arg0).height = arg1 >>> 0; 1090 | }; 1091 | imports.wbg.__wbg_getContext_df50fa48a8876636 = function() { return handleError(function (arg0, arg1, arg2) { 1092 | const ret = getObject(arg0).getContext(getStringFromWasm0(arg1, arg2)); 1093 | return isLikeNone(ret) ? 0 : addHeapObject(ret); 1094 | }, arguments) }; 1095 | imports.wbg.__wbg_get_8cd5eba00ab6304f = function(arg0, arg1) { 1096 | const ret = getObject(arg0)[arg1 >>> 0]; 1097 | return isLikeNone(ret) ? 0 : addHeapObject(ret); 1098 | }; 1099 | imports.wbg.__wbg_crypto_1d1f22824a6a080c = function(arg0) { 1100 | const ret = getObject(arg0).crypto; 1101 | return addHeapObject(ret); 1102 | }; 1103 | imports.wbg.__wbg_process_4a72847cc503995b = function(arg0) { 1104 | const ret = getObject(arg0).process; 1105 | return addHeapObject(ret); 1106 | }; 1107 | imports.wbg.__wbg_versions_f686565e586dd935 = function(arg0) { 1108 | const ret = getObject(arg0).versions; 1109 | return addHeapObject(ret); 1110 | }; 1111 | imports.wbg.__wbg_node_104a2ff8d6ea03a2 = function(arg0) { 1112 | const ret = getObject(arg0).node; 1113 | return addHeapObject(ret); 1114 | }; 1115 | imports.wbg.__wbindgen_is_string = function(arg0) { 1116 | const ret = typeof(getObject(arg0)) === 'string'; 1117 | return ret; 1118 | }; 1119 | imports.wbg.__wbg_require_cca90b1a94a0255b = function() { return handleError(function () { 1120 | const ret = module.require; 1121 | return addHeapObject(ret); 1122 | }, arguments) }; 1123 | imports.wbg.__wbg_msCrypto_eb05e62b530a1508 = function(arg0) { 1124 | const ret = getObject(arg0).msCrypto; 1125 | return addHeapObject(ret); 1126 | }; 1127 | imports.wbg.__wbg_randomFillSync_5c9c955aa56b6049 = function() { return handleError(function (arg0, arg1) { 1128 | getObject(arg0).randomFillSync(takeObject(arg1)); 1129 | }, arguments) }; 1130 | imports.wbg.__wbg_getRandomValues_3aa56aa6edec874c = function() { return handleError(function (arg0, arg1) { 1131 | getObject(arg0).getRandomValues(getObject(arg1)); 1132 | }, arguments) }; 1133 | imports.wbg.__wbg_new_16b304a2cfa7ff4a = function() { 1134 | const ret = new Array(); 1135 | return addHeapObject(ret); 1136 | }; 1137 | imports.wbg.__wbg_newnoargs_e258087cd0daa0ea = function(arg0, arg1) { 1138 | const ret = new Function(getStringFromWasm0(arg0, arg1)); 1139 | return addHeapObject(ret); 1140 | }; 1141 | imports.wbg.__wbg_call_27c0f87801dedf93 = function() { return handleError(function (arg0, arg1) { 1142 | const ret = getObject(arg0).call(getObject(arg1)); 1143 | return addHeapObject(ret); 1144 | }, arguments) }; 1145 | imports.wbg.__wbg_new_72fb9a18b5ae2624 = function() { 1146 | const ret = new Object(); 1147 | return addHeapObject(ret); 1148 | }; 1149 | imports.wbg.__wbg_self_ce0dbfc45cf2f5be = function() { return handleError(function () { 1150 | const ret = self.self; 1151 | return addHeapObject(ret); 1152 | }, arguments) }; 1153 | imports.wbg.__wbg_window_c6fb939a7f436783 = function() { return handleError(function () { 1154 | const ret = window.window; 1155 | return addHeapObject(ret); 1156 | }, arguments) }; 1157 | imports.wbg.__wbg_globalThis_d1e6af4856ba331b = function() { return handleError(function () { 1158 | const ret = globalThis.globalThis; 1159 | return addHeapObject(ret); 1160 | }, arguments) }; 1161 | imports.wbg.__wbg_global_207b558942527489 = function() { return handleError(function () { 1162 | const ret = global.global; 1163 | return addHeapObject(ret); 1164 | }, arguments) }; 1165 | imports.wbg.__wbg_push_a5b05aedc7234f9f = function(arg0, arg1) { 1166 | const ret = getObject(arg0).push(getObject(arg1)); 1167 | return ret; 1168 | }; 1169 | imports.wbg.__wbg_call_b3ca7c6051f9bec1 = function() { return handleError(function (arg0, arg1, arg2) { 1170 | const ret = getObject(arg0).call(getObject(arg1), getObject(arg2)); 1171 | return addHeapObject(ret); 1172 | }, arguments) }; 1173 | imports.wbg.__wbg_instanceof_Object_71ca3c0a59266746 = function(arg0) { 1174 | let result; 1175 | try { 1176 | result = getObject(arg0) instanceof Object; 1177 | } catch (_) { 1178 | result = false; 1179 | } 1180 | const ret = result; 1181 | return ret; 1182 | }; 1183 | imports.wbg.__wbg_valueOf_a0b7c836f68a054b = function(arg0) { 1184 | const ret = getObject(arg0).valueOf(); 1185 | return addHeapObject(ret); 1186 | }; 1187 | imports.wbg.__wbg_new_81740750da40724f = function(arg0, arg1) { 1188 | try { 1189 | var state0 = {a: arg0, b: arg1}; 1190 | var cb0 = (arg0, arg1) => { 1191 | const a = state0.a; 1192 | state0.a = 0; 1193 | try { 1194 | return __wbg_adapter_387(a, state0.b, arg0, arg1); 1195 | } finally { 1196 | state0.a = a; 1197 | } 1198 | }; 1199 | const ret = new Promise(cb0); 1200 | return addHeapObject(ret); 1201 | } finally { 1202 | state0.a = state0.b = 0; 1203 | } 1204 | }; 1205 | imports.wbg.__wbg_resolve_b0083a7967828ec8 = function(arg0) { 1206 | const ret = Promise.resolve(getObject(arg0)); 1207 | return addHeapObject(ret); 1208 | }; 1209 | imports.wbg.__wbg_then_0c86a60e8fcfe9f6 = function(arg0, arg1) { 1210 | const ret = getObject(arg0).then(getObject(arg1)); 1211 | return addHeapObject(ret); 1212 | }; 1213 | imports.wbg.__wbg_then_a73caa9a87991566 = function(arg0, arg1, arg2) { 1214 | const ret = getObject(arg0).then(getObject(arg1), getObject(arg2)); 1215 | return addHeapObject(ret); 1216 | }; 1217 | imports.wbg.__wbg_buffer_12d079cc21e14bdb = function(arg0) { 1218 | const ret = getObject(arg0).buffer; 1219 | return addHeapObject(ret); 1220 | }; 1221 | imports.wbg.__wbg_newwithbyteoffsetandlength_aa4a17c33a06e5cb = function(arg0, arg1, arg2) { 1222 | const ret = new Uint8Array(getObject(arg0), arg1 >>> 0, arg2 >>> 0); 1223 | return addHeapObject(ret); 1224 | }; 1225 | imports.wbg.__wbg_new_63b92bc8671ed464 = function(arg0) { 1226 | const ret = new Uint8Array(getObject(arg0)); 1227 | return addHeapObject(ret); 1228 | }; 1229 | imports.wbg.__wbg_set_a47bac70306a19a7 = function(arg0, arg1, arg2) { 1230 | getObject(arg0).set(getObject(arg1), arg2 >>> 0); 1231 | }; 1232 | imports.wbg.__wbg_length_c20a40f15020d68a = function(arg0) { 1233 | const ret = getObject(arg0).length; 1234 | return ret; 1235 | }; 1236 | imports.wbg.__wbg_newwithlength_e9b4878cebadb3d3 = function(arg0) { 1237 | const ret = new Uint8Array(arg0 >>> 0); 1238 | return addHeapObject(ret); 1239 | }; 1240 | imports.wbg.__wbg_buffer_dd7f74bc60f1faab = function(arg0) { 1241 | const ret = getObject(arg0).buffer; 1242 | return addHeapObject(ret); 1243 | }; 1244 | imports.wbg.__wbg_subarray_a1f73cd4b5b42fe1 = function(arg0, arg1, arg2) { 1245 | const ret = getObject(arg0).subarray(arg1 >>> 0, arg2 >>> 0); 1246 | return addHeapObject(ret); 1247 | }; 1248 | imports.wbg.__wbg_set_1f9b04f170055d33 = function() { return handleError(function (arg0, arg1, arg2) { 1249 | const ret = Reflect.set(getObject(arg0), getObject(arg1), getObject(arg2)); 1250 | return ret; 1251 | }, arguments) }; 1252 | imports.wbg.__wbindgen_debug_string = function(arg0, arg1) { 1253 | const ret = debugString(getObject(arg1)); 1254 | const ptr1 = passStringToWasm0(ret, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc); 1255 | const len1 = WASM_VECTOR_LEN; 1256 | getInt32Memory0()[arg0 / 4 + 1] = len1; 1257 | getInt32Memory0()[arg0 / 4 + 0] = ptr1; 1258 | }; 1259 | imports.wbg.__wbindgen_throw = function(arg0, arg1) { 1260 | throw new Error(getStringFromWasm0(arg0, arg1)); 1261 | }; 1262 | imports.wbg.__wbindgen_memory = function() { 1263 | const ret = wasm.memory; 1264 | return addHeapObject(ret); 1265 | }; 1266 | imports.wbg.__wbindgen_closure_wrapper1008 = function(arg0, arg1, arg2) { 1267 | const ret = makeMutClosure(arg0, arg1, 269, __wbg_adapter_28); 1268 | return addHeapObject(ret); 1269 | }; 1270 | imports.wbg.__wbindgen_closure_wrapper1010 = function(arg0, arg1, arg2) { 1271 | const ret = makeMutClosure(arg0, arg1, 269, __wbg_adapter_28); 1272 | return addHeapObject(ret); 1273 | }; 1274 | imports.wbg.__wbindgen_closure_wrapper1096 = function(arg0, arg1, arg2) { 1275 | const ret = makeMutClosure(arg0, arg1, 276, __wbg_adapter_33); 1276 | return addHeapObject(ret); 1277 | }; 1278 | 1279 | return imports; 1280 | } 1281 | 1282 | function __wbg_init_memory(imports, maybe_memory) { 1283 | 1284 | } 1285 | 1286 | function __wbg_finalize_init(instance, module) { 1287 | wasm = instance.exports; 1288 | __wbg_init.__wbindgen_wasm_module = module; 1289 | cachedInt32Memory0 = null; 1290 | cachedUint32Memory0 = null; 1291 | cachedUint8Memory0 = null; 1292 | 1293 | wasm.__wbindgen_start(); 1294 | return wasm; 1295 | } 1296 | 1297 | function initSync(module) { 1298 | if (wasm !== undefined) return wasm; 1299 | 1300 | const imports = __wbg_get_imports(); 1301 | 1302 | __wbg_init_memory(imports); 1303 | 1304 | if (!(module instanceof WebAssembly.Module)) { 1305 | module = new WebAssembly.Module(module); 1306 | } 1307 | 1308 | const instance = new WebAssembly.Instance(module, imports); 1309 | 1310 | return __wbg_finalize_init(instance, module); 1311 | } 1312 | 1313 | async function __wbg_init(input) { 1314 | if (wasm !== undefined) return wasm; 1315 | 1316 | if (typeof input === 'undefined') { 1317 | input = new URL('colorcruncher_bg.wasm', import.meta.url); 1318 | } 1319 | const imports = __wbg_get_imports(); 1320 | 1321 | if (typeof input === 'string' || (typeof Request === 'function' && input instanceof Request) || (typeof URL === 'function' && input instanceof URL)) { 1322 | input = fetch(input); 1323 | } 1324 | 1325 | __wbg_init_memory(imports); 1326 | 1327 | const { instance, module } = await __wbg_load(await input, imports); 1328 | 1329 | return __wbg_finalize_init(instance, module); 1330 | } 1331 | 1332 | export { initSync } 1333 | export default __wbg_init; 1334 | --------------------------------------------------------------------------------