use std::ops::Range;
use crate::board::Board;
use crate::file::{InputSquares, QueensFile};
use crate::solvestate::SquareVal;
use crate::squarecolor::{ALL_SQUARE_COLORS, SquareColor};
use anyhow::{Context, Result, anyhow, ensure};
use image::{GenericImageView, Rgb, RgbImage, SubImage};
use itertools::{Itertools, iproduct};
use log::trace;
const ANSI_COLORS: [(Rgb<u8>, SquareColor); 16] = [
(Rgb([0, 0, 0]), SquareColor::Black), (Rgb([170, 0, 0]), SquareColor::Red), (Rgb([0, 170, 0]), SquareColor::Green), (Rgb([170, 85, 0]), SquareColor::Yellow), (Rgb([0, 0, 170]), SquareColor::Blue), (Rgb([170, 0, 170]), SquareColor::Magenta), (Rgb([0, 170, 170]), SquareColor::Cyan), (Rgb([170, 170, 170]), SquareColor::White), (Rgb([85, 85, 85]), SquareColor::BrightBlack), (Rgb([255, 85, 85]), SquareColor::BrightRed), (Rgb([85, 255, 85]), SquareColor::BrightGreen), (Rgb([255, 255, 85]), SquareColor::BrightYellow), (Rgb([85, 85, 255]), SquareColor::BrightBlue), (Rgb([255, 85, 255]), SquareColor::BrightMagenta), (Rgb([85, 255, 255]), SquareColor::BrightCyan), (Rgb([255, 255, 255]), SquareColor::BrightWhite), ];
const BLACK_THRESHOLD: u8 = 50;
const BLACK_LINE_RATIO: f32 = 0.35;
const GRID_LENGTH_VARIANCE: usize = 20;
const MAX_COLORS_TO_TRACK: usize = 1000;
const MAX_LINE_THICKNESS: usize = 20;
const MAX_UNIQUE_COLORS: usize = ALL_SQUARE_COLORS.len();
const COLOR_DISTANCE_THRESHOLD: u32 = 500;
const QUEEN_OTHER_RATIO: f32 = 0.06;
const X_OTHER_RATIO: f32 = 0.01;
pub fn analyze_grid_image(img: &RgbImage) -> Result<QueensFile> {
trace!("Analyze grid image start: {:?}", img);
let width_ranges = find_grid_ranges(img, 0..img.width(), true);
ensure!(
width_ranges.len() >= 4,
"Found too few columns; must be at least 4, found {}",
width_ranges.len(),
);
let height_ranges = find_grid_ranges(img, 0..img.height(), false);
ensure!(
width_ranges.len() == height_ranges.len(),
"Grid must be a square; width was {} height was {}",
width_ranges.len(),
height_ranges.len()
);
ensure!(
width_ranges.len() <= MAX_UNIQUE_COLORS,
"Grid is too large; max is {} found {}",
MAX_UNIQUE_COLORS,
width_ranges.len()
);
let board_size = width_ranges.len();
let mut all_rgb_colors = Vec::with_capacity(board_size * board_size);
let mut square_values = Vec::with_capacity(board_size * board_size);
trace!(
"Analyze grid image ranges found: {:?} {:?}",
width_ranges, height_ranges
);
for (height_range, width_range) in iproduct!(height_ranges, width_ranges) {
let view = img.view(
width_range.start,
height_range.start,
width_range.end - width_range.start,
height_range.end - height_range.start,
);
let rgb_color = get_dominant_color(&view).with_context(|| {
format!(
"Count not find dominant color in square at offset {:?}",
view.offsets()
)
})?;
trace!(
"Analyze grid image color: for height {:?} width {:?} got dominant color {:?}",
height_range, width_range, rgb_color
);
all_rgb_colors.push(rgb_color);
let other_ratio = get_other_ratio(&view, &rgb_color);
let square_val = match other_ratio {
r if r >= QUEEN_OTHER_RATIO => Some(SquareVal::Queen),
r if r >= X_OTHER_RATIO => Some(SquareVal::X),
_ => None,
};
trace!(
"Analyze grid image ratio: For height {:?} width {:?} got ratio {:?} and value {:?}",
height_range, width_range, other_ratio, square_val
);
square_values.push(square_val);
}
let unique_rgb_colors = all_rgb_colors
.clone()
.into_iter()
.unique()
.collect::<Vec<_>>();
ensure!(
unique_rgb_colors.len() == board_size,
"Number of unique colors must be equal to the board size"
);
let color_mapping = map_image_to_square_colors(&unique_rgb_colors);
let colors = all_rgb_colors
.iter()
.map(|&rgb_color| {
let color_idx = unique_rgb_colors
.iter()
.position(|&c| c == rgb_color)
.unwrap();
color_mapping[color_idx]
})
.collect::<Vec<_>>();
let board = Board::new(board_size, colors);
let squares = InputSquares::from(square_values);
trace!("Analyze grid image done.");
trace!("Board:\n{}", board);
trace!("Squares:\n{}", squares);
Ok(QueensFile {
board,
squares: Some(squares),
})
}
fn get_other_ratio(view: &SubImage<&RgbImage>, rgb_color: &Rgb<u8>) -> f32 {
const BORDER_DENOM: u32 = 10;
let (width, height) = view.dimensions();
let center_subview = view.view(
width / BORDER_DENOM,
height / BORDER_DENOM,
width - (2 * width / BORDER_DENOM),
height - (2 * height / BORDER_DENOM),
);
let other_count = center_subview
.pixels()
.filter(|(_, _, p)| color_distance(*p, *rgb_color) > COLOR_DISTANCE_THRESHOLD)
.count();
(other_count as f32) / ((width * height) as f32)
}
fn find_grid_ranges(img: &RgbImage, range: Range<u32>, is_vertical: bool) -> Vec<Range<u32>> {
let mut grid_ranges = Vec::with_capacity(MAX_UNIQUE_COLORS);
let grid_ranges_iter = range
.map(|x| (x, black_ratio(img, x, is_vertical) > BLACK_LINE_RATIO))
.dedup_by_with_count(|(_, i), (_, j)| i == j)
.filter(|&(count, (_, is_black))| is_black && count < MAX_LINE_THICKNESS)
.tuple_windows()
.map(|((start_count, (start_idx, _)), (_, (end_idx, _)))| {
(start_idx + start_count as u32)..end_idx
});
grid_ranges.extend(grid_ranges_iter);
let grid_ranges_len = grid_ranges.len();
let median_grid_length = grid_ranges
.clone()
.select_nth_unstable_by(grid_ranges_len / 2, |a, b| a.len().cmp(&b.len()))
.1
.len();
grid_ranges
.into_iter()
.filter(|r| {
r.len() > (median_grid_length * (100 - GRID_LENGTH_VARIANCE)) / 100
&& r.len() < (median_grid_length * (100 + GRID_LENGTH_VARIANCE)) / 100
})
.collect::<Vec<_>>()
}
fn black_ratio(img: &RgbImage, pos: u32, is_vertical: bool) -> f32 {
let total = if is_vertical {
img.height()
} else {
img.width()
};
let black_count = (0..total)
.map(|i| {
if is_vertical {
img.get_pixel(pos, i)
} else {
img.get_pixel(i, pos)
}
})
.filter(|&pixel| is_black(pixel))
.count();
black_count as f32 / total as f32
}
fn is_black(pixel: &Rgb<u8>) -> bool {
pixel[0] < BLACK_THRESHOLD && pixel[1] < BLACK_THRESHOLD && pixel[2] < BLACK_THRESHOLD
}
fn get_dominant_color(img: &SubImage<&RgbImage>) -> Result<Rgb<u8>> {
let mut colors = [Rgb([0, 0, 0]); MAX_COLORS_TO_TRACK];
let mut counts = [0u32; MAX_COLORS_TO_TRACK];
let mut num_colors = 0;
for pixel in img.pixels().map(|(_, _, p)| p).filter(|&p| !is_black(&p)) {
match (
num_colors,
colors[..num_colors].iter().position(|&p| p == pixel),
) {
(_, Some(idx)) => counts[idx] += 1,
(MAX_COLORS_TO_TRACK, None) => (),
(_, None) => {
colors[num_colors] = pixel;
counts[num_colors] = 1;
num_colors += 1;
}
}
}
counts[..num_colors]
.iter()
.zip(colors[..num_colors].iter())
.max_by(|&(a, _), &(b, _)| a.cmp(b))
.map(|(_, color)| *color)
.ok_or_else(|| anyhow!("Could not find dominant color"))
}
fn color_distance(rgb1: Rgb<u8>, rgb2: Rgb<u8>) -> u32 {
((rgb1[0] as u32).abs_diff(rgb2[0] as u32)).pow(2)
+ ((rgb1[1] as u32).abs_diff(rgb2[1] as u32)).pow(2)
+ ((rgb1[2] as u32).abs_diff(rgb2[2] as u32)).pow(2)
}
fn map_image_to_square_colors(image_colors: &[Rgb<u8>]) -> [SquareColor; MAX_UNIQUE_COLORS] {
let mut image_to_square_color = [SquareColor::Black; MAX_UNIQUE_COLORS];
let mut used_square_colors = [false; MAX_UNIQUE_COLORS];
let mut used_image_colors = [false; MAX_UNIQUE_COLORS];
for _ in 0..image_colors.len() {
let mut min_distance = u32::MAX;
let mut best_square_color_idx = 0;
let mut best_image_color_idx = 0;
for (image_color_idx, image_rgb) in image_colors.iter().enumerate() {
if used_image_colors[image_color_idx] {
continue;
}
for (square_color_idx, (square_rgb, _)) in ANSI_COLORS.iter().enumerate() {
if used_square_colors[square_color_idx] {
continue;
}
let distance = color_distance(*image_rgb, *square_rgb);
if distance < min_distance {
min_distance = distance;
best_square_color_idx = square_color_idx;
best_image_color_idx = image_color_idx;
}
}
}
image_to_square_color[best_image_color_idx] = ANSI_COLORS[best_square_color_idx].1;
used_square_colors[best_square_color_idx] = true;
used_image_colors[best_image_color_idx] = true;
}
image_to_square_color
}