mirror of
https://github.com/italicsjenga/agb.git
synced 2024-12-23 16:21:33 +11:00
Add palette optimisation
This commit is contained in:
parent
725543912a
commit
0fe4e23758
12
agb-image-converter/src/colour.rs
Normal file
12
agb-image-converter/src/colour.rs
Normal file
|
@ -0,0 +1,12 @@
|
|||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||
pub struct Colour {
|
||||
pub r: u8,
|
||||
pub g: u8,
|
||||
pub b: u8,
|
||||
}
|
||||
|
||||
impl Colour {
|
||||
pub fn from_rgb(r: u8, g: u8, b: u8) -> Self {
|
||||
Colour { r, g, b }
|
||||
}
|
||||
}
|
37
agb-image-converter/src/image_loader.rs
Normal file
37
agb-image-converter/src/image_loader.rs
Normal file
|
@ -0,0 +1,37 @@
|
|||
use std::path;
|
||||
|
||||
use image::GenericImageView;
|
||||
|
||||
use crate::colour::Colour;
|
||||
|
||||
pub(crate) struct Image {
|
||||
pub width: usize,
|
||||
pub height: usize,
|
||||
colour_data: Vec<Colour>,
|
||||
}
|
||||
|
||||
impl Image {
|
||||
pub fn load_from_file(image_path: &path::Path) -> Self {
|
||||
let img = image::open(image_path).expect("Expected image to exist");
|
||||
let (width, height) = img.dimensions();
|
||||
|
||||
let width = width as usize;
|
||||
let height = height as usize;
|
||||
|
||||
let mut colour_data = Vec::with_capacity(width * height);
|
||||
|
||||
for (_, _, pixel) in img.pixels() {
|
||||
colour_data.push(Colour::from_rgb(pixel[0], pixel[1], pixel[2]));
|
||||
}
|
||||
|
||||
Image {
|
||||
width,
|
||||
height,
|
||||
colour_data,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn colour(&self, x: usize, y: usize) -> Colour {
|
||||
self.colour_data[x + y * self.width]
|
||||
}
|
||||
}
|
|
@ -1,6 +1,10 @@
|
|||
use std::path;
|
||||
use std::path::PathBuf;
|
||||
|
||||
mod colour;
|
||||
mod image_loader;
|
||||
mod palette16;
|
||||
|
||||
use image_loader::Image;
|
||||
|
||||
pub use colour::Colour;
|
||||
|
||||
|
@ -10,11 +14,59 @@ pub enum TileSize {
|
|||
Tile16,
|
||||
}
|
||||
|
||||
impl TileSize {
|
||||
fn to_size(&self) -> usize {
|
||||
match &self {
|
||||
TileSize::Tile8 => 8,
|
||||
TileSize::Tile16 => 16,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct ImageConverterConfig {
|
||||
pub transparent_colour: Option<Colour>,
|
||||
pub tile_size: TileSize,
|
||||
pub input_file: path::PathBuf,
|
||||
pub output_file: path::PathBuf,
|
||||
pub input_image: PathBuf,
|
||||
pub output_file: PathBuf,
|
||||
}
|
||||
|
||||
pub fn convert_image(setting: &ImageConverterConfig) {}
|
||||
pub fn convert_image(settings: &ImageConverterConfig) {
|
||||
let image = Image::load_from_file(&settings.input_image);
|
||||
|
||||
let tile_size = settings.tile_size.to_size();
|
||||
if image.width % tile_size != 0 || image.height % tile_size != 0 {
|
||||
panic!("Image size not a multiple of tile size");
|
||||
}
|
||||
|
||||
let optimiser = optimiser_for_image(&image, tile_size);
|
||||
let optimisation_results = optimiser.optimise_palettes(settings.transparent_colour);
|
||||
|
||||
println!("{:?}", optimisation_results);
|
||||
}
|
||||
|
||||
fn optimiser_for_image(image: &Image, tile_size: usize) -> palette16::Palette16Optimiser {
|
||||
let tiles_x = image.width / tile_size;
|
||||
let tiles_y = image.height / tile_size;
|
||||
|
||||
let mut palette_optimiser = palette16::Palette16Optimiser::new();
|
||||
|
||||
for y in 0..tiles_y {
|
||||
for x in 0..tiles_x {
|
||||
let mut palette = palette16::Palette16::new();
|
||||
|
||||
for j in 0..tile_size {
|
||||
for i in 0..tile_size {
|
||||
let colour = image.colour(x * tile_size + i, y * tile_size + j);
|
||||
|
||||
if !palette.add_colour(colour) {
|
||||
panic!("Tile contains more than 16 colours");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
palette_optimiser.add_palette(palette);
|
||||
}
|
||||
}
|
||||
|
||||
palette_optimiser
|
||||
}
|
||||
|
|
170
agb-image-converter/src/palette16.rs
Normal file
170
agb-image-converter/src/palette16.rs
Normal file
|
@ -0,0 +1,170 @@
|
|||
use crate::colour::Colour;
|
||||
use std::collections::HashSet;
|
||||
|
||||
const MAX_COLOURS: usize = 256;
|
||||
const MAX_COLOURS_PER_PALETTE: usize = 16;
|
||||
|
||||
#[derive(Debug, Clone, Eq, PartialEq, Hash)]
|
||||
pub(crate) struct Palette16 {
|
||||
colours: Vec<Colour>,
|
||||
}
|
||||
|
||||
impl Palette16 {
|
||||
pub fn new() -> Self {
|
||||
Palette16 {
|
||||
colours: Vec::with_capacity(MAX_COLOURS_PER_PALETTE),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn add_colour(&mut self, colour: Colour) -> bool {
|
||||
if self.colours.contains(&colour) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if self.colours.len() == MAX_COLOURS_PER_PALETTE {
|
||||
panic!("Can have at most 16 colours in a single palette");
|
||||
}
|
||||
self.colours.push(colour);
|
||||
true
|
||||
}
|
||||
|
||||
fn union_length(&self, other: &Palette16) -> usize {
|
||||
self.colours
|
||||
.iter()
|
||||
.chain(&other.colours)
|
||||
.collect::<HashSet<_>>()
|
||||
.len()
|
||||
}
|
||||
|
||||
fn is_satisfied_by(&self, other: &Palette16) -> bool {
|
||||
self.colours
|
||||
.iter()
|
||||
.collect::<HashSet<_>>()
|
||||
.is_subset(&other.colours.iter().collect::<HashSet<_>>())
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) struct Palette16Optimiser {
|
||||
palettes: Vec<Palette16>,
|
||||
colours: Vec<Colour>,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub(crate) struct Palette16OptimisationResults {
|
||||
pub optimised_palettes: Vec<Palette16>,
|
||||
pub assignments: Vec<usize>,
|
||||
}
|
||||
|
||||
impl Palette16Optimiser {
|
||||
pub fn new() -> Self {
|
||||
Palette16Optimiser {
|
||||
palettes: vec![],
|
||||
colours: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn add_palette(&mut self, palette: Palette16) {
|
||||
self.palettes.push(palette.clone());
|
||||
|
||||
for colour in palette.colours {
|
||||
if self.colours.contains(&colour) {
|
||||
continue;
|
||||
}
|
||||
|
||||
self.colours.push(colour);
|
||||
}
|
||||
|
||||
if self.colours.len() > MAX_COLOURS {
|
||||
panic!("Cannot have over 256 colours");
|
||||
}
|
||||
}
|
||||
|
||||
pub fn optimise_palettes(
|
||||
&self,
|
||||
transparent_colour: Option<Colour>,
|
||||
) -> Palette16OptimisationResults {
|
||||
let mut assignments = vec![0; self.palettes.len()];
|
||||
let mut optimised_palettes = vec![];
|
||||
|
||||
let mut unsatisfied_palettes = self
|
||||
.palettes
|
||||
.iter()
|
||||
.cloned()
|
||||
.collect::<HashSet<Palette16>>();
|
||||
|
||||
while unsatisfied_palettes.len() > 0 {
|
||||
let palette = self.find_maximal_palette_for(&unsatisfied_palettes, transparent_colour);
|
||||
|
||||
for test_palette in unsatisfied_palettes.clone() {
|
||||
if test_palette.is_satisfied_by(&palette) {
|
||||
unsatisfied_palettes.remove(&test_palette);
|
||||
}
|
||||
}
|
||||
|
||||
for (i, overall_palette) in self.palettes.iter().enumerate() {
|
||||
if overall_palette.is_satisfied_by(&palette) {
|
||||
assignments[i] = optimised_palettes.len();
|
||||
}
|
||||
}
|
||||
|
||||
optimised_palettes.push(palette);
|
||||
|
||||
if optimised_palettes.len() == MAX_COLOURS / MAX_COLOURS_PER_PALETTE {
|
||||
panic!("Failed to find covering palettes");
|
||||
}
|
||||
}
|
||||
|
||||
Palette16OptimisationResults {
|
||||
assignments,
|
||||
optimised_palettes,
|
||||
}
|
||||
}
|
||||
|
||||
fn find_maximal_palette_for(
|
||||
&self,
|
||||
unsatisfied_palettes: &HashSet<Palette16>,
|
||||
transparent_colour: Option<Colour>,
|
||||
) -> Palette16 {
|
||||
let mut palette = Palette16::new();
|
||||
|
||||
if let Some(transparent_colour) = transparent_colour {
|
||||
palette.add_colour(transparent_colour);
|
||||
}
|
||||
|
||||
loop {
|
||||
let mut colour_usage = vec![0; MAX_COLOURS];
|
||||
let mut a_colour_is_used = false;
|
||||
|
||||
for current_palette in unsatisfied_palettes {
|
||||
if palette.union_length(¤t_palette) > MAX_COLOURS_PER_PALETTE {
|
||||
continue;
|
||||
}
|
||||
|
||||
for colour in ¤t_palette.colours {
|
||||
if let Some(colour_index) = self.colours.iter().position(|c| c == colour) {
|
||||
colour_usage[colour_index] += 1;
|
||||
a_colour_is_used = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !a_colour_is_used {
|
||||
return palette;
|
||||
}
|
||||
|
||||
let best_index = colour_usage
|
||||
.iter()
|
||||
.enumerate()
|
||||
.max_by(|(_, usage1), (_, usage2)| usage1.cmp(usage2))
|
||||
.unwrap()
|
||||
.0;
|
||||
|
||||
let best_colour = self.colours[best_index];
|
||||
|
||||
palette.add_colour(best_colour);
|
||||
if palette.colours.len() == MAX_COLOURS_PER_PALETTE {
|
||||
return palette;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue