2024-09-17 02:43:18 +02:00
|
|
|
// {{{ Imports
|
2024-09-09 18:06:07 +02:00
|
|
|
use std::fs;
|
2024-06-22 23:07:11 +02:00
|
|
|
|
2024-09-09 18:06:07 +02:00
|
|
|
use anyhow::Context;
|
2024-10-05 00:44:54 +02:00
|
|
|
use faer::{Mat, MatRef};
|
|
|
|
use image::{GenericImageView, Pixel};
|
|
|
|
use num::{Integer, ToPrimitive};
|
2024-09-09 18:06:07 +02:00
|
|
|
use serde::{Deserialize, Serialize};
|
2024-06-22 23:07:11 +02:00
|
|
|
|
2024-09-17 02:43:18 +02:00
|
|
|
use crate::arcaea::chart::{Difficulty, Jacket, SongCache};
|
2024-11-09 12:22:35 +01:00
|
|
|
use crate::context::paths::ShimmeringPaths;
|
2024-09-17 02:43:18 +02:00
|
|
|
use crate::context::Error;
|
|
|
|
// }}}
|
2024-06-22 23:07:11 +02:00
|
|
|
|
|
|
|
/// How many sub-segments to split each side into
|
2024-07-01 18:00:03 +02:00
|
|
|
pub const SPLIT_FACTOR: u32 = 8;
|
|
|
|
pub const IMAGE_VEC_DIM: usize = (SPLIT_FACTOR * SPLIT_FACTOR * 3) as usize;
|
2024-07-20 04:52:24 +02:00
|
|
|
pub const BITMAP_IMAGE_SIZE: u32 = 174;
|
2024-10-28 21:57:22 +01:00
|
|
|
pub const JACKET_RECOGNITITION_DIMENSIONS: usize = 20;
|
2024-10-05 00:44:54 +02:00
|
|
|
|
|
|
|
// {{{ (Image => vector) encoding
|
|
|
|
#[allow(clippy::identity_op)]
|
|
|
|
pub fn image_to_vec(image: &impl GenericImageView) -> MVec<f32> {
|
|
|
|
let mut colors = MVec::zeros(IMAGE_VEC_DIM, 1);
|
|
|
|
let chunk_width = image.width() / SPLIT_FACTOR;
|
|
|
|
let chunk_height = image.height() / SPLIT_FACTOR;
|
|
|
|
for i in 0..(SPLIT_FACTOR * SPLIT_FACTOR) {
|
|
|
|
let (iy, ix) = i.div_rem(&SPLIT_FACTOR);
|
|
|
|
let cropped = image.view(
|
|
|
|
chunk_width * ix,
|
|
|
|
chunk_height * iy,
|
|
|
|
chunk_width,
|
|
|
|
chunk_height,
|
|
|
|
);
|
|
|
|
|
|
|
|
let mut r = 0;
|
|
|
|
let mut g = 0;
|
|
|
|
let mut b = 0;
|
|
|
|
let mut count = 0;
|
|
|
|
|
|
|
|
for (_, _, pixel) in cropped.pixels() {
|
|
|
|
let channels = pixel.channels();
|
|
|
|
|
|
|
|
// I'm not sure this does what it's supposed to do for non rgb(a) pixels...
|
|
|
|
r += channels[0].to_u64().unwrap().pow(2);
|
|
|
|
g += channels[1].to_u64().unwrap().pow(2);
|
|
|
|
b += channels[2].to_u64().unwrap().pow(2);
|
|
|
|
|
|
|
|
count += 1;
|
2024-06-22 23:07:11 +02:00
|
|
|
}
|
|
|
|
|
2024-10-05 00:44:54 +02:00
|
|
|
let count = count as f64;
|
|
|
|
let r = (r as f64 / count).sqrt();
|
|
|
|
let g = (g as f64 / count).sqrt();
|
|
|
|
let b = (b as f64 / count).sqrt();
|
|
|
|
colors[(i as usize * 3 + 0, 0)] = r as f32;
|
|
|
|
colors[(i as usize * 3 + 1, 0)] = g as f32;
|
|
|
|
colors[(i as usize * 3 + 2, 0)] = b as f32;
|
2024-06-22 23:07:11 +02:00
|
|
|
}
|
|
|
|
|
2024-10-05 00:44:54 +02:00
|
|
|
colors
|
2024-06-22 23:07:11 +02:00
|
|
|
}
|
2024-10-05 00:44:54 +02:00
|
|
|
// }}}
|
|
|
|
|
|
|
|
/// A column vector
|
|
|
|
pub type MVec<T> = Mat<T>;
|
2024-06-22 23:07:11 +02:00
|
|
|
|
2024-10-05 00:44:54 +02:00
|
|
|
/// This struct holds:
|
|
|
|
/// - a set of (song_id, vec) pairs of different images projected through the
|
|
|
|
/// aforementioned transform.
|
|
|
|
/// - an projection matrix for dimensionality reduction
|
|
|
|
#[derive(Clone, Serialize, Deserialize)]
|
2024-06-22 23:07:11 +02:00
|
|
|
pub struct JacketCache {
|
2024-10-05 00:44:54 +02:00
|
|
|
/// A matrix with each column corresponding to the result of passing a jacket
|
|
|
|
/// through [[image_to_vec]], and then projecting it through `transform_matrix`
|
|
|
|
pub jacket_matrix: Mat<f32>,
|
|
|
|
|
|
|
|
/// Assigns each column of `jacket_matrix` a song id.
|
|
|
|
pub jacket_ids: Vec<u32>,
|
|
|
|
|
|
|
|
/// A projection matrix for dimensionality reduction.
|
|
|
|
pub transform_matrix: Mat<f32>,
|
2024-06-22 23:07:11 +02:00
|
|
|
}
|
|
|
|
|
2024-10-05 00:44:54 +02:00
|
|
|
// {{{ Read jackets
|
2024-11-09 12:22:35 +01:00
|
|
|
pub fn read_jackets(paths: &ShimmeringPaths, song_cache: &mut SongCache) -> Result<(), Error> {
|
2024-10-05 00:44:54 +02:00
|
|
|
let suffix = format!("_{BITMAP_IMAGE_SIZE}.jpg");
|
2024-11-09 12:22:35 +01:00
|
|
|
let songs_dir = paths.jackets_path();
|
2024-10-05 00:44:54 +02:00
|
|
|
let entries = fs::read_dir(songs_dir).with_context(|| "Couldn't read songs directory")?;
|
|
|
|
|
|
|
|
for entry in entries {
|
|
|
|
let dir = entry?;
|
|
|
|
let raw_dir_name = dir.file_name();
|
|
|
|
let dir_name = raw_dir_name.to_str().unwrap();
|
|
|
|
let song_id = dir_name
|
|
|
|
.parse()
|
|
|
|
.with_context(|| format!("Dir name {dir_name} could not be parsed as `u32` song id"))?;
|
|
|
|
|
|
|
|
let entries = fs::read_dir(dir.path()).with_context(|| "Couldn't read song directory")?;
|
|
|
|
for entry in entries {
|
|
|
|
let file = entry?;
|
|
|
|
let raw_name = file.file_name();
|
|
|
|
let name = raw_name.to_str().unwrap();
|
|
|
|
if !name.ends_with(&suffix) {
|
|
|
|
continue;
|
2024-06-23 02:51:50 +02:00
|
|
|
}
|
2024-07-01 18:00:03 +02:00
|
|
|
|
2024-10-05 00:44:54 +02:00
|
|
|
let name = name.strip_suffix(&suffix).unwrap();
|
2024-10-04 15:17:51 +02:00
|
|
|
|
2024-10-05 00:44:54 +02:00
|
|
|
let difficulty = Difficulty::DIFFICULTY_SHORTHANDS
|
|
|
|
.iter()
|
|
|
|
.zip(Difficulty::DIFFICULTIES)
|
|
|
|
.find_map(|(s, d)| Some(d).filter(|_| name == s.to_lowercase()));
|
2024-07-19 00:02:17 +02:00
|
|
|
|
2024-10-05 00:44:54 +02:00
|
|
|
let contents: &'static _ = fs::read(file.path())
|
|
|
|
.with_context(|| "Coult not read prepared jacket image")?
|
|
|
|
.leak();
|
2024-08-10 03:08:38 +02:00
|
|
|
|
2024-10-05 00:44:54 +02:00
|
|
|
let image = image::load_from_memory(contents)
|
|
|
|
.with_context(|| "Could not load jacket image from prepared bytes")?;
|
|
|
|
let bitmap: &'static _ = Box::leak(Box::new(image.into_rgb8()));
|
2024-08-12 03:13:41 +02:00
|
|
|
|
2024-10-05 00:44:54 +02:00
|
|
|
if let Some(difficulty) = difficulty {
|
|
|
|
let chart = song_cache
|
|
|
|
.lookup_by_difficulty_mut(song_id, difficulty)
|
|
|
|
.unwrap();
|
|
|
|
chart.jacket_source = Some(difficulty);
|
|
|
|
chart.cached_jacket = Some(Jacket {
|
|
|
|
raw: contents,
|
|
|
|
bitmap,
|
|
|
|
});
|
|
|
|
} else {
|
|
|
|
for (_, chart_id) in song_cache.lookup_song(song_id)?.charts() {
|
|
|
|
let chart = song_cache.lookup_chart_mut(chart_id)?;
|
|
|
|
if chart.jacket_source.is_none() {
|
2024-09-09 18:06:07 +02:00
|
|
|
chart.cached_jacket = Some(Jacket {
|
|
|
|
raw: contents,
|
|
|
|
bitmap,
|
|
|
|
});
|
2024-10-05 00:44:54 +02:00
|
|
|
chart.jacket_source = None;
|
2024-07-19 00:02:17 +02:00
|
|
|
}
|
2024-07-01 18:00:03 +02:00
|
|
|
}
|
|
|
|
}
|
2024-10-05 00:44:54 +02:00
|
|
|
}
|
|
|
|
}
|
2024-06-22 23:07:11 +02:00
|
|
|
|
2024-10-05 00:44:54 +02:00
|
|
|
Ok(())
|
|
|
|
}
|
|
|
|
// }}}
|
2024-07-18 20:17:39 +02:00
|
|
|
|
2024-10-05 00:44:54 +02:00
|
|
|
impl JacketCache {
|
|
|
|
// {{{ Generate
|
2024-11-09 12:22:35 +01:00
|
|
|
pub fn new(paths: &ShimmeringPaths) -> Result<Self, Error> {
|
|
|
|
let bytes = fs::read(paths.recognition_matrix_path())
|
2024-10-05 00:44:54 +02:00
|
|
|
.with_context(|| "Could not read jacket recognition matrix")?;
|
|
|
|
|
|
|
|
let result = postcard::from_bytes(&bytes)?;
|
|
|
|
// .with_context(|| "Could not decode jacket recognition matrix")?;
|
2024-06-22 23:07:11 +02:00
|
|
|
|
|
|
|
Ok(result)
|
|
|
|
}
|
|
|
|
// }}}
|
|
|
|
// {{{ Recognise
|
2024-10-05 00:44:54 +02:00
|
|
|
/// Transforms a vector from image space to recognition space.
|
|
|
|
#[inline]
|
|
|
|
pub fn transform_vec(&self, vec: MatRef<f32>) -> MVec<f32> {
|
|
|
|
&self.transform_matrix * vec
|
|
|
|
}
|
|
|
|
|
2024-06-22 23:07:11 +02:00
|
|
|
#[inline]
|
2024-10-05 00:44:54 +02:00
|
|
|
pub fn recognise(&self, image: &impl GenericImageView) -> Option<(f32, u32)> {
|
|
|
|
let vec = self.transform_vec(image_to_vec(image).as_ref());
|
|
|
|
self.jacket_ids
|
2024-08-08 15:59:36 +02:00
|
|
|
.iter()
|
2024-10-05 00:44:54 +02:00
|
|
|
.enumerate()
|
|
|
|
.map(|(idx, id)| {
|
|
|
|
(id, {
|
|
|
|
(self.jacket_matrix.subcols(idx, 1) - &vec).squared_norm_l2()
|
|
|
|
})
|
|
|
|
})
|
|
|
|
.min_by(|(_, d1), (_, d2)| d1.partial_cmp(d2).expect("NaN distance encountered"))
|
|
|
|
.map(|(i, d)| (d.sqrt(), *i))
|
2024-06-22 23:07:11 +02:00
|
|
|
}
|
|
|
|
// }}}
|
|
|
|
}
|