aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/embedders.rs94
-rw-r--r--src/main.rs121
2 files changed, 215 insertions, 0 deletions
diff --git a/src/embedders.rs b/src/embedders.rs
new file mode 100644
index 0000000..a8d34b4
--- /dev/null
+++ b/src/embedders.rs
@@ -0,0 +1,94 @@
+use std::path::PathBuf;
+
+pub trait MetricElem {
+ fn dist(&self, _: &Self) -> f64;
+}
+
+impl MetricElem for f64 {
+ fn dist(&self, b: &f64) -> f64 {
+ (self - b).abs()
+ }
+}
+
+pub trait EmbedderT {
+ type Embedding: MetricElem;
+
+ fn embed(&mut self, _: &[PathBuf]) -> Result<Vec<Self::Embedding>, String>;
+}
+
+pub struct BrightnessEmbedder;
+impl EmbedderT for BrightnessEmbedder {
+ type Embedding = f64;
+
+ fn embed(&mut self, paths: &[PathBuf]) -> Result<Vec<f64>, String> {
+ paths
+ .iter()
+ .map(|p| {
+ println!("{:?}", p);
+ let im = image::open(p).map_err(|e| e.to_string())?;
+ let num_bytes = 3 * (im.height() * im.width());
+
+ if num_bytes == 0 {
+ Err("Encountered NaN brightness, due to an empty image")?;
+ }
+
+ Ok(im.to_rgb8()
+ .iter()
+ .map(|e| *e as u64)
+ .sum::<u64>() as f64 / num_bytes as f64)
+ })
+ .try_collect()
+ }
+}
+
+struct Hue (f64);
+impl MetricElem for Hue {
+ fn dist(&self, b: &Hue) -> f64 {
+ let d = self.0.dist(&b.0);
+ d.min(6.-d)
+ }
+}
+
+pub struct HueEmbedder;
+impl EmbedderT for HueEmbedder {
+ type Embedding = f64; // TODO anderes Ding was Winkel vergleicht
+
+ fn embed(&mut self, paths: &[PathBuf]) -> Result<Vec<f64>, String> {
+ paths
+ .iter()
+ .map(|p| {
+ let im = image::open(p).map_err(|e| e.to_string())?;
+ let num_pixels = im.height() * im.width();
+ let [sr, sg, sb] = im
+ .to_rgb8()
+ .pixels()
+ .fold([0, 0, 0], |[or, og, ob], n| {
+ let [nr, ng, nb] = n.0;
+ [or + nr as u64, og + ng as u64, ob + nb as u64]
+ })
+ .map(|e| e as f64 / 255. / num_pixels as f64);
+
+ let mut hue =
+ if sr >= sg && sr >= sb {
+ (sg - sb) / (sr - sg.min(sb))
+ }
+ else if sg >= sb {
+ 2. + (sb - sr) / (sg - sr.min(sb))
+ }
+ else {
+ 4. + (sr - sg) / (sb - sr.min(sg))
+ };
+
+ if hue < 0. {
+ hue += 6.;
+ }
+
+ if hue != hue {
+ Err("Encountered NaN hue, possibly because of a colorless or empty image")?;
+ }
+
+ Ok(hue)
+ })
+ .try_collect()
+ }
+}
diff --git a/src/main.rs b/src/main.rs
new file mode 100644
index 0000000..5cfc10c
--- /dev/null
+++ b/src/main.rs
@@ -0,0 +1,121 @@
+#![feature(iterator_try_collect)]
+
+use clap::Parser;
+use priority_queue::PriorityQueue;
+use std::cmp::Ordering;
+use std::collections::HashSet;
+use std::path::PathBuf;
+
+use embedders::*;
+
+mod embedders;
+
+#[derive(Debug, Clone, Copy, clap::ValueEnum)]
+enum Embedder {
+ Brightness,
+ Hue,
+}
+
+#[derive(Debug, Parser)]
+struct Args {
+ #[arg(short, long, default_value = "hue")]
+ embedder: Embedder,
+
+ images: Vec<PathBuf>,
+}
+
+//#[derive(Debug)]
+//struct Config {
+// base_dir: xdg::BaseDirectories,
+//}
+//
+//fn get_config() -> Result<Config, String> {
+// let dirs = xdg::BaseDirectories::with_prefix("embeddings-sort")
+// .map_err(|_| "oh no")?;
+//
+// Ok(Config{base_dir: dirs})
+//}
+
+fn process_embedder<E>(mut e: E, args: Args) -> Result<(), String>
+ where E: EmbedderT
+{
+ // wrapper struct to
+ // - reverse the ordering
+ // - implement Ord, even though the type is backed by an f64
+ #[repr(transparent)]
+ #[derive(Debug, PartialEq)]
+ struct DownOrd (f64);
+ impl Eq for DownOrd {}
+ impl PartialOrd for DownOrd {
+ fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
+ self.0.partial_cmp(&other.0).map(|e| e.reverse())
+ }
+ }
+ impl Ord for DownOrd {
+ fn cmp(&self, other: &Self) -> Ordering {self.partial_cmp(other).unwrap()}
+ }
+
+ let num_ims = args.images.len();
+ if num_ims == 0 {
+ return Ok(());
+ }
+
+ let embeds: Vec<_> = e
+ .embed(&args.images)?
+ .into_iter()
+ .collect();
+
+ let mut possibleEdges =
+ PriorityQueue::with_capacity((num_ims * num_ims - num_ims) / 2);
+ let mut visitedImages = HashSet::with_capacity(num_ims);
+ let mut takenEdges = HashSet::with_capacity(num_ims - 1); // TODO das muss anders
+
+ // here, we start at 0.
+ // we might get a better result in the end if we started with a vertex next
+ // to the lowest-cost edge, but we don't know which one that is (though we
+ // could compute that without changing our asymptotic complexity)
+ visitedImages.insert(0);
+ for i in 1..num_ims {
+ possibleEdges.push((0, i), DownOrd(embeds[0].dist(&embeds[i])));
+ }
+
+ // prims algorithm or something like that
+ while visitedImages.len() < num_ims {
+ // find the edge with the least cost that connects us to a new vertex
+ let (new, old) = loop {
+ let ((a, b), _) = possibleEdges.pop().unwrap();
+ if !visitedImages.contains(&a) {
+ break (a, b);
+ }
+ else if !visitedImages.contains(&b) {
+ break (b, a);
+ }
+ };
+ visitedImages.insert(new);
+
+ println!("next edge: {:?} -> {:?}", old, new);
+
+ // insert all the new edges we could take
+ takenEdges.insert((new, old));
+ for i in 0..num_ims {
+ // don't consider edges taking us to nodes we already visited
+ if visitedImages.contains(&i) {
+ continue;
+ }
+
+ possibleEdges.push((new, i), DownOrd(embeds[new].dist(&embeds[i])));
+ }
+ }
+
+ Ok(())
+}
+
+fn main() -> Result<(), String> {
+ //let cfg = get_config()?;
+ let args = Args::parse();
+
+ match args.embedder {
+ Embedder::Brightness => process_embedder(BrightnessEmbedder, args),
+ Embedder::Hue => process_embedder(HueEmbedder, args),
+ }
+}