From 3e7ed7015f728662486011076898246debf26e6e Mon Sep 17 00:00:00 2001 From: okaneco <47607823+okaneco@users.noreply.github.com> Date: Tue, 3 Jan 2023 07:56:48 -0500 Subject: [PATCH] Bump to 0.1.1, optimize SLIC index computation Pre-compute array indices in SLIC to gain slight speedup Remove height check from get_in_bounds Use iterators in slic::enforce_connectivity Update dependencies Remove unused variables in seed.rs Update CI badge --- CHANGELOG.md | 7 +++ Cargo.lock | 137 ++++++++++++++++++++++--------------------------- Cargo.toml | 2 +- LICENSE-APACHE | 2 +- LICENSE-MIT | 2 +- README.md | 2 +- src/lib.rs | 8 +-- src/seed.rs | 2 - src/slic.rs | 116 +++++++++++++++++++---------------------- 9 files changed, 127 insertions(+), 151 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f91a9a0..625926b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,11 @@ # `simple_clustering` changelog +## Version 0.1.1 - 2023-01 +Improved SLIC calculation speed by ~15-20% after refactoring calculation loop. + +[#1][1] - Bump to 0.1.1, Optimize slic index computation + ## Version 0.1.0 - 2022-04 - Initial Commit + +[1]: https://github.com/okaneco/simple_clustering/pull/1 diff --git a/Cargo.lock b/Cargo.lock index a80546f..7f22878 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -8,12 +8,6 @@ version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" -[[package]] -name = "adler32" -version = "1.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aae1277d39aeec15cb388266ecc24b11c80469deae6067e17a1a7aa9e5c1f234" - [[package]] name = "approx" version = "0.5.1" @@ -37,9 +31,9 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bytemuck" -version = "1.9.1" +version = "1.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cdead85bdec19c194affaeeb670c0e41fe23de31459efd1c174d049269cf02cc" +checksum = "aaa3a8d9a1ca92e282c96a32d6511b695d7d994d1d102ba85d279f9b2756947f" [[package]] name = "byteorder" @@ -55,24 +49,24 @@ checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" [[package]] name = "clap" -version = "3.1.18" +version = "3.2.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d2dbdf4bdacb33466e854ce889eee8dfd5729abf7ccd7664d0a2d60cd384440b" +checksum = "71655c45cb9845d3270c9d6df84ebe72b4dad3c2ba3f7023ad47c144e4e473a5" dependencies = [ "bitflags", "clap_derive", "clap_lex", "indexmap", - "lazy_static", + "once_cell", "strsim", "textwrap", ] [[package]] name = "clap_derive" -version = "3.1.18" +version = "3.2.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "25320346e922cffe59c0bbc5410c8d8784509efb321488971081313cb1e1a33c" +checksum = "ea0c8bce528c4be4da13ea6fead8965e95b6073585a2f05204bd8f4119f82a65" dependencies = [ "heck", "proc-macro-error", @@ -83,9 +77,9 @@ dependencies = [ [[package]] name = "clap_lex" -version = "0.2.0" +version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a37c35f1112dad5e6e0b1adaff798507497a18fceeb30cceb3bae7d1427b9213" +checksum = "2850f2f5a82cbf437dd5af4d49848fbdfc27c157c3d010345776f952765261c5" dependencies = [ "os_str_bytes", ] @@ -106,21 +100,22 @@ dependencies = [ ] [[package]] -name = "deflate" -version = "1.0.0" +name = "find-crate" +version = "0.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c86f7e25f518f4b81808a2cf1c50996a61f5c2eb394b2393bd87f2a4780a432f" +checksum = "59a98bbaacea1c0eb6a0876280051b892eb73594fd90cf3b20e9c817029c57d2" dependencies = [ - "adler32", + "toml", ] [[package]] -name = "find-crate" -version = "0.6.3" +name = "flate2" +version = "1.0.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "59a98bbaacea1c0eb6a0876280051b892eb73594fd90cf3b20e9c817029c57d2" +checksum = "a8a2db397cb1c8772f31494cb8917e48cd1e64f0fa7efac59fbd741a0a8ce841" dependencies = [ - "toml", + "crc32fast", + "miniz_oxide", ] [[package]] @@ -134,9 +129,9 @@ dependencies = [ [[package]] name = "hashbrown" -version = "0.11.2" +version = "0.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ab5ef0d4909ef3724cc8cce6ccc8572c5c817592e9285f5464f8e86f8bd3726e" +checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" [[package]] name = "heck" @@ -146,15 +141,14 @@ checksum = "2540771e65fc8cb83cd6e8a237f70c319bd5c29f78ed1084ba5d50eeac86f7f9" [[package]] name = "image" -version = "0.24.2" +version = "0.24.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "28edd9d7bc256be2502e325ac0628bde30b7001b9b52e0abe31a1a9dc2701212" +checksum = "69b7ea949b537b0fd0af141fff8c77690f2ce96f4f41f042ccb6c69c6c965945" dependencies = [ "bytemuck", "byteorder", "color_quant", "jpeg-decoder", - "num-iter", "num-rational", "num-traits", "png", @@ -162,9 +156,9 @@ dependencies = [ [[package]] name = "indexmap" -version = "1.8.1" +version = "1.9.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0f647032dfaa1f8b6dc29bd3edb7bbef4861b8b8007ebb118d6db284fd59f6ee" +checksum = "1885e79c1fc4b10f0e172c475f458b7f7b93061064d98c3293e98c5ba0c8b399" dependencies = [ "autocfg", "hashbrown", @@ -172,21 +166,15 @@ dependencies = [ [[package]] name = "jpeg-decoder" -version = "0.2.6" +version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9478aa10f73e7528198d75109c8be5cd7d15fb530238040148d5f9a22d4c5b3b" - -[[package]] -name = "lazy_static" -version = "1.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" +checksum = "bc0000e42512c92e31c2252315bda326620a4e034105e900c98ec492fa077b3e" [[package]] name = "miniz_oxide" -version = "0.5.1" +version = "0.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d2b29bd4bc3f33391105ebee3589c19197c4271e3e5a9ec9bfe8127eeff8f082" +checksum = "b275950c28b37e794e8c55d88aeb5e139d0ce23fdbbeda68f8d7174abdf9e8fa" dependencies = [ "adler", ] @@ -201,22 +189,11 @@ dependencies = [ "num-traits", ] -[[package]] -name = "num-iter" -version = "0.1.43" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7d03e6c028c5dc5cac6e2dec0efda81fc887605bb3d884578bb6d6bf7514e252" -dependencies = [ - "autocfg", - "num-integer", - "num-traits", -] - [[package]] name = "num-rational" -version = "0.4.0" +version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d41702bd167c2df5520b384281bc111a4b5efcf7fbc4c9c222c815b07e0a6a6a" +checksum = "0638a1c9d0a3c0914158145bc76cff373a75a627e6ecbfb71cbe6f453a5a19b0" dependencies = [ "autocfg", "num-integer", @@ -232,17 +209,23 @@ dependencies = [ "autocfg", ] +[[package]] +name = "once_cell" +version = "1.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f61fba1741ea2b3d6a1e3178721804bb716a68a6aeba1149b5d52e3d464ea66" + [[package]] name = "os_str_bytes" -version = "6.0.1" +version = "6.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "029d8d0b2f198229de29dca79676f2738ff952edf3fde542eb8bf94d8c21b435" +checksum = "9b7820b9daea5457c9f21c69448905d723fbd21136ccf521748f23fd49e723ee" [[package]] name = "palette" -version = "0.6.0" +version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f9735f7e1e51a3f740bacd5dc2724b61a7806f23597a8736e679f38ee3435d18" +checksum = "8f9cd68f7112581033f157e56c77ac4a5538ec5836a2e39284e65bd7d7275e49" dependencies = [ "approx", "num-traits", @@ -251,9 +234,9 @@ dependencies = [ [[package]] name = "palette_derive" -version = "0.6.0" +version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7799c3053ea8a6d8a1193c7ba42f534e7863cf52e378a7f90406f4a645d33bad" +checksum = "05eedf46a8e7c27f74af0c9cfcdb004ceca158cb1b918c6f68f8d7a549b3e427" dependencies = [ "find-crate", "proc-macro2", @@ -263,13 +246,13 @@ dependencies = [ [[package]] name = "png" -version = "0.17.5" +version = "0.17.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc38c0ad57efb786dd57b9864e5b18bae478c00c824dc55a38bbc9da95dde3ba" +checksum = "5d708eaf860a19b19ce538740d2b4bdeeb8337fa53f7738455e706623ad5c638" dependencies = [ "bitflags", "crc32fast", - "deflate", + "flate2", "miniz_oxide", ] @@ -299,31 +282,31 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.39" +version = "1.0.49" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c54b25569025b7fc9651de43004ae593a75ad88543b17178aa5e1b9c4f15f56f" +checksum = "57a8eca9f9c4ffde41714334dee777596264c7825420f521abc92b5b5deb63a5" dependencies = [ "unicode-ident", ] [[package]] name = "quote" -version = "1.0.18" +version = "1.0.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1feb54ed693b93a84e14094943b84b7c4eae204c512b7ccb95ab0c66d278ad1" +checksum = "8856d8364d252a14d474036ea1358d63c9e6965c8e5c1885c18f73d70bff9c7b" dependencies = [ "proc-macro2", ] [[package]] name = "serde" -version = "1.0.137" +version = "1.0.152" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "61ea8d54c77f8315140a05f4c7237403bf38b72704d031543aa1d16abbf517d1" +checksum = "bb7d1f0d3021d347a83e556fc4683dea2ea09d87bccdf88ff5c12545d89d5efb" [[package]] name = "simple_clustering" -version = "0.1.0" +version = "0.1.1" dependencies = [ "clap", "fxhash", @@ -340,9 +323,9 @@ checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" [[package]] name = "syn" -version = "1.0.95" +version = "1.0.107" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fbaf6116ab8924f39d52792136fb74fd60a80194cf1b1c6ffa6453eef1c3f942" +checksum = "1f4064b5b16e03ae50984a5a8ed5d4f8803e6bc1fd170a3cda91a1be4b18e3f5" dependencies = [ "proc-macro2", "quote", @@ -351,24 +334,24 @@ dependencies = [ [[package]] name = "textwrap" -version = "0.15.0" +version = "0.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b1141d4d61095b28419e22cb0bbf02755f5e54e0526f97f1e3d1d160e60885fb" +checksum = "222a222a5bfe1bba4a77b45ec488a741b3cb8872e5e499451fd7d0129c9c7c3d" [[package]] name = "toml" -version = "0.5.9" +version = "0.5.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8d82e1a7758622a465f8cee077614c73484dac5b836c02ff6a40d5d1010324d7" +checksum = "1333c76748e868a4d9d1017b5ab53171dfd095f70c712fdb4653a406547f598f" dependencies = [ "serde", ] [[package]] name = "unicode-ident" -version = "1.0.0" +version = "1.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d22af068fba1eb5edcb4aea19d382b2a3deb4c8f9d475c589b6ada9e0fd493ee" +checksum = "84a22b9f218b40614adcb3f4ff08b703773ad44fa9423e4e0d346d5db86e4ebc" [[package]] name = "version_check" diff --git a/Cargo.toml b/Cargo.toml index c583d5c..58d5498 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "simple_clustering" -version = "0.1.0" +version = "0.1.1" edition = "2021" exclude = ["gfx", ".github"] description = "Implementations of image clustering and segmentation algorithms such as SLIC and SNIC." diff --git a/LICENSE-APACHE b/LICENSE-APACHE index d0a4778..4dc942d 100644 --- a/LICENSE-APACHE +++ b/LICENSE-APACHE @@ -175,7 +175,7 @@ END OF TERMS AND CONDITIONS - Copyright 2022 Collyn O'Kane + Copyright 2022-2023 Collyn O'Kane Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/LICENSE-MIT b/LICENSE-MIT index 5017865..b59031b 100644 --- a/LICENSE-MIT +++ b/LICENSE-MIT @@ -1,4 +1,4 @@ -Copyright 2022 Collyn O'Kane +Copyright 2022-2023 Collyn O'Kane Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: diff --git a/README.md b/README.md index ee7d3f1..863efab 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,5 @@ # simple_clustering -[![Build Status](https://img.shields.io/github/workflow/status/okaneco/simple_clustering/Rust%20CI)](https://github.com/okaneco/simple_clustering) +[![Build Status](https://github.com/okaneco/simple_clustering/workflows/Rust%20CI/badge.svg)](https://github.com/okaneco/simple_clustering) [![Crates.io](https://img.shields.io/crates/v/simple_clustering.svg)](https://crates.io/crates/simple_clustering) [![Docs.rs](https://docs.rs/simple_clustering/badge.svg)](https://docs.rs/simple_clustering) diff --git a/src/lib.rs b/src/lib.rs index 7106aa0..082707a 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -170,8 +170,8 @@ where /// Checks if the index is in bounds and returns a reference to the data at that /// point if it exists. #[inline] -fn get_in_bounds(width: i64, height: i64, x: i64, y: i64, image: &[T]) -> Option<&T> { - if (0..width).contains(&x) && (0..height).contains(&y) { +fn get_in_bounds(width: i64, _height: i64, x: i64, y: i64, image: &[T]) -> Option<&T> { + if (0..width).contains(&x) { let i = u64::try_from(y) .ok()? .checked_mul(u64::try_from(width).ok()?)? @@ -188,12 +188,12 @@ fn get_in_bounds(width: i64, height: i64, x: i64, y: i64, image: &[T]) -> Opt #[inline] fn get_mut_in_bounds( width: i64, - height: i64, + _height: i64, x: i64, y: i64, image: &mut [T], ) -> Option<&mut T> { - if (0..width).contains(&x) && (0..height).contains(&y) { + if (0..width).contains(&x) { let i = u64::try_from(y) .ok()? .checked_mul(u64::try_from(width).ok()?)? diff --git a/src/seed.rs b/src/seed.rs index 69c86b8..3189211 100644 --- a/src/seed.rs +++ b/src/seed.rs @@ -17,8 +17,6 @@ pub fn init_seeds( seeds: &mut Vec>, ) -> Result<(), ScError> { seeds.clear(); - let width = width; - let height = height; let s = s; let half_s = div_ceil(s, 2); let mut x_seeds = div_ceil(width, s); diff --git a/src/slic.rs b/src/slic.rs index 3e338b4..5bac69a 100644 --- a/src/slic.rs +++ b/src/slic.rs @@ -186,33 +186,33 @@ where // pixels with the lowest distance measure for (center_index, center) in clusters.iter().enumerate() { for y in center.y.saturating_sub(s)..center.y.saturating_add(s).min(height) { - for x in center.x.saturating_sub(s)..center.x.saturating_add(s).min(width) { - let idx = usize::try_from( - u64::from(y) - .saturating_mul(u64::from(width)) - .saturating_add(u64::from(x)), - ) - .or(Err("Index out of bounds for finding new neighbors"))?; - let color = *image.get(idx).ok_or("Image index out of bounds")?; - - let distance = distance_s( - m_s_term, - distance_lab(color, center.data), - distance_xy( - (f64::from(x), f64::from(y)), - (f64::from(center.x), f64::from(center.y)), - ), - ); - - if idx < info.distances.len() && idx < info.labels.len() { - let info_distance = info - .distances - .get_mut(idx) - .ok_or("Distance index out of bounds")?; - if distance < *info_distance { - *info_distance = distance; - *info.labels.get_mut(idx).ok_or("Info index out of bounds")? = - center_index; + let x_start = center.x.saturating_sub(s); + let x_end = center.x.saturating_add(s).min(width); + let row_start = u64::from(y).saturating_mul(u64::from(width)); + + // (2023/01)WOULDBENICE: Try chunks_exact, attempted it here but clusters + // had worse results compared to current version indicating probable errors + // in implementation + for (x, idx) in (x_start..x_end).zip( + row_start.saturating_add(u64::from(center.x.saturating_sub(s))) + ..row_start.saturating_add(u64::from(x_end)), + ) { + let idx = usize::try_from(idx) + .or(Err("Index out of bounds for finding new neighbors"))?; + if idx < image.len() && idx < info.distances.len() && idx < info.labels.len() { + let color = image[idx]; + let distance = distance_s( + m_s_term, + distance_lab(color, center.data), + distance_xy( + (f64::from(x), f64::from(y)), + (f64::from(center.x), f64::from(center.y)), + ), + ); + + if distance < info.distances[idx] { + info.distances[idx] = distance; + info.labels[idx] = center_index; } } } @@ -220,29 +220,18 @@ where } // Compute new centers and update - for y in 0..height { - for x in 0..width { - let idx = usize::try_from( - u64::from(y) - .saturating_mul(u64::from(width)) - .saturating_add(u64::from(x)), - ) - .or(Err("Invalid update index"))?; - - if idx < image.len() && idx < info.labels.len() { - let color = *image.get(idx).ok_or("Image index out of bounds")?; - let index = *info - .labels - .get(idx) - .ok_or("Info update index out of bounds")?; - if let Some(update) = updates.get_mut(index) { - update.data += color; - update.x += f64::from(x); - update.y += f64::from(y); - update.count += 1.0; - } - } else { - return Err(ScError::General("Update index out of bounds")); + let width_usize = usize::try_from(width).or(Err("Could not convert width to usize"))?; + for (y, (row, info_labels)) in image + .chunks_exact(width_usize) + .zip(info.labels.chunks_exact(width_usize)) + .enumerate() + { + for (x, (&color, &info_label)) in row.iter().zip(info_labels).enumerate() { + if let Some(update) = updates.get_mut(info_label) { + update.data += color; + update.x += x as f64; + update.y += y as f64; + update.count += 1.0; } } } @@ -281,6 +270,7 @@ fn enforce_connectivity( let mut new_labels = Vec::new(); new_labels.try_reserve_exact(labels.len())?; new_labels.extend((0..labels.len()).map(|_| usize::MAX)); + let new_labels = new_labels.as_mut_slice(); // This will be reused for searching each superpixel cluster. // For now, the size of the queue is 8 superpixels to start. @@ -297,15 +287,12 @@ fn enforce_connectivity( let mut neighbor_label = 0; let mut new_label = 0_usize; - for y in 0..height { - for x in 0..width { - let idx_usize = usize::try_from( - u64::from(y) - .saturating_mul(u64::from(width)) - .saturating_add(u64::from(x)), - ) - .or(Err("Invalid connectivity index"))?; - let old_label = *labels.get(idx_usize).ok_or("Could not get old label")?; + let width_usize = usize::try_from(width).or(Err( + "Could not convert width to usize in enforce_connectivity", + ))?; + for (y, label_row) in labels.chunks_exact(width_usize).enumerate() { + for (x, &old_label) in label_row.iter().enumerate() { + let idx_usize = y.saturating_mul(width_usize).saturating_add(x); // If no assigned label, assign current_label if new_labels.get(idx_usize) == Some(&usize::MAX) { @@ -318,8 +305,9 @@ fn enforce_connectivity( // be used to label the cluster if the current label is too // small. for &neighbor in &neighbors { - let neighbor_x = i64::from(x) + neighbor.0; - let neighbor_y = i64::from(y) + neighbor.1; + // `x` and `y` went from u32->usize->i64 + let neighbor_x = (x as i64) + neighbor.0; + let neighbor_y = (y as i64) + neighbor.1; if let Some(l) = get_in_bounds(width_i, height_i, neighbor_x, neighbor_y, &new_labels) { @@ -333,7 +321,7 @@ fn enforce_connectivity( // same label. The members go into a queue so they can be // reassigned a neighboring label if it's a disjoint cluster. label_queue.clear(); - label_queue.push((i64::from(x), i64::from(y))); + label_queue.push(((x as i64), (y as i64))); let mut label_queue_idx = 0; let mut label_count = 1_usize; @@ -347,7 +335,7 @@ fn enforce_connectivity( if let (Some(old_visit_label), Some(new_visit_label)) = ( get_in_bounds(width_i, height_i, new_vx, new_vy, labels), - get_mut_in_bounds(width_i, height_i, new_vx, new_vy, &mut new_labels), + get_mut_in_bounds(width_i, height_i, new_vx, new_vy, new_labels), ) { // If new label is unassigned and matches old_label, assign it the current cluster if *old_visit_label == old_label && *new_visit_label == usize::MAX { @@ -369,7 +357,7 @@ fn enforce_connectivity( // superpixel size. if label_count <= cluster_threshold { for &(l_x, l_y) in &label_queue { - *get_mut_in_bounds(width_i, height_i, l_x, l_y, &mut new_labels) + *get_mut_in_bounds(width_i, height_i, l_x, l_y, new_labels) .ok_or("New label index out of bounds")? = neighbor_label; } continue;