working on config

This commit is contained in:
Scott Pruett 2022-05-22 13:19:13 -04:00
parent aa1c01a6dd
commit 3121905620
12 changed files with 562 additions and 68 deletions

100
Cargo.lock generated
View File

@ -512,7 +512,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "877bfcce06463cdbcfd7f4efd57608b1384d6d9ae03b33e503fbba1d1a899a52" checksum = "877bfcce06463cdbcfd7f4efd57608b1384d6d9ae03b33e503fbba1d1a899a52"
dependencies = [ dependencies = [
"egui", "egui",
"image", "image 0.24.2",
] ]
[[package]] [[package]]
@ -1018,6 +1018,20 @@ dependencies = [
"unicode-normalization", "unicode-normalization",
] ]
[[package]]
name = "image"
version = "0.23.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "24ffcb7e7244a9bf19d35bf2883b9c080c4ced3c07a9895572178cdb8f13f6a1"
dependencies = [
"bytemuck",
"byteorder",
"color_quant",
"num-iter",
"num-rational 0.3.2",
"num-traits",
]
[[package]] [[package]]
name = "image" name = "image"
version = "0.24.2" version = "0.24.2"
@ -1031,13 +1045,26 @@ dependencies = [
"gif", "gif",
"jpeg-decoder", "jpeg-decoder",
"num-iter", "num-iter",
"num-rational", "num-rational 0.4.0",
"num-traits", "num-traits",
"png", "png",
"scoped_threadpool", "scoped_threadpool",
"tiff", "tiff",
] ]
[[package]]
name = "img_hash"
version = "3.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5ea4eac6fc4f64ed363d5c210732b747bfa5ddd8a25ac347d887f298c3a70b49"
dependencies = [
"base64",
"image 0.23.14",
"rustdct",
"serde",
"transpose 0.2.1",
]
[[package]] [[package]]
name = "indexmap" name = "indexmap"
version = "1.8.1" version = "1.8.1"
@ -1390,6 +1417,16 @@ dependencies = [
"minimal-lexical", "minimal-lexical",
] ]
[[package]]
name = "num-complex"
version = "0.2.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b6b19411a9719e753aff12e5187b74d60d3dc449ec3f4dc21e3989c3f554bc95"
dependencies = [
"autocfg",
"num-traits",
]
[[package]] [[package]]
name = "num-integer" name = "num-integer"
version = "0.1.45" version = "0.1.45"
@ -1411,6 +1448,17 @@ dependencies = [
"num-traits", "num-traits",
] ]
[[package]]
name = "num-rational"
version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "12ac428b1cb17fce6f731001d307d351ec70a6d202fc2e60f7d4c5e42d8f4f07"
dependencies = [
"autocfg",
"num-integer",
"num-traits",
]
[[package]] [[package]]
name = "num-rational" name = "num-rational"
version = "0.4.0" version = "0.4.0"
@ -1803,6 +1851,28 @@ dependencies = [
"winapi 0.3.9", "winapi 0.3.9",
] ]
[[package]]
name = "rustdct"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ef4d167674b4cf68c2114bdbcd34c95aa9071652b73b0f43b19298f1d2780b7d"
dependencies = [
"rustfft",
]
[[package]]
name = "rustfft"
version = "3.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "77008ed59a8923c8b4ac2e5eaa6d28fbe893d3b9515098a4a5fc7767d6430fe5"
dependencies = [
"num-complex",
"num-integer",
"num-traits",
"strength_reduce",
"transpose 0.1.0",
]
[[package]] [[package]]
name = "rustls" name = "rustls"
version = "0.20.6" version = "0.20.6"
@ -2042,6 +2112,12 @@ version = "1.0.5"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d44a3643b4ff9caf57abcee9c2c621d6c03d9135e0d8b589bd9afb5992cb176a" checksum = "d44a3643b4ff9caf57abcee9c2c621d6c03d9135e0d8b589bd9afb5992cb176a"
[[package]]
name = "strength_reduce"
version = "0.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a3ff2f71c82567c565ba4b3009a9350a96a7269eaa4001ebedae926230bc2254"
[[package]] [[package]]
name = "strsim" name = "strsim"
version = "0.10.0" version = "0.10.0"
@ -2057,11 +2133,13 @@ dependencies = [
"egui_extras", "egui_extras",
"ehttp", "ehttp",
"futures", "futures",
"image", "image 0.24.2",
"img_hash",
"poll-promise", "poll-promise",
"reqwest", "reqwest",
"scrap", "scrap",
"serde", "serde",
"serde_json",
"tokio", "tokio",
] ]
@ -2247,6 +2325,22 @@ dependencies = [
"lazy_static", "lazy_static",
] ]
[[package]]
name = "transpose"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "643e21580bb0627c7bb09e5cedbb42c8705b19d012de593ed6b0309270b3cd1e"
[[package]]
name = "transpose"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "95f9c900aa98b6ea43aee227fd680550cdec726526aab8ac801549eadb25e39f"
dependencies = [
"num-integer",
"strength_reduce",
]
[[package]] [[package]]
name = "try-lock" name = "try-lock"
version = "0.2.3" version = "0.2.3"

View File

@ -17,6 +17,9 @@ scrap = "0.5"
anyhow = "1.0" anyhow = "1.0"
futures = "0.3" futures = "0.3"
serde = { version = "1", features = ["derive"] } serde = { version = "1", features = ["derive"] }
serde_json = "1"
tokio = { version = "1", features = ["full"] } tokio = { version = "1", features = ["full"] }
reqwest = { version = "0.11", features = ["json"] } reqwest = { version = "0.11", features = ["json"] }
img_hash = "3"

47
config.json Normal file
View File

@ -0,0 +1,47 @@
{
"ocr_regions": [
{
"name": "lap",
"x": 2290,
"y": 46,
"width": 145,
"height": 90
},
{
"name": "health",
"x": 90,
"y": 1364,
"width": 52,
"height": 24
},
{
"name": "gas",
"x": 208,
"y": 1364,
"width": 52,
"height": 24
},
{
"name": "tyres",
"x": 325,
"y": 1364,
"width": 52,
"height": 24
},
{
"name": "lap_time",
"x": 2325,
"y": 169,
"width": 183,
"height": 43
},
{
"name": "best_time",
"x": 2325,
"y": 222,
"width": 183,
"height": 43
}
],
"ocr_server_endpoint": "http://localhost:3000/"
}

4
learned.json Normal file
View File

@ -0,0 +1,4 @@
{
"learned_images": {},
"learned_tracks": {}
}

56
src/config.rs Normal file
View File

@ -0,0 +1,56 @@
use std::{collections::HashMap, path::PathBuf};
use anyhow::Result;
use serde::{Serialize, Deserialize, de::DeserializeOwned};
use crate::image_processing::Region;
#[derive(Default, Serialize, Deserialize, Clone)]
pub struct Config {
pub ocr_regions: Vec<Region>,
pub track_region: Option<Region>,
pub ocr_server_endpoint: String,
}
impl Config {
pub fn load() -> Result<Self> {
load_or_make_default("config.json", include_str!("configs/config.default.json"))
}
pub fn save(&self) -> Result<()> {
save_json_config("config.json", self)
}
}
#[derive(Default, Serialize, Deserialize, Clone)]
pub struct LearnedConfig {
pub learned_images: HashMap<String, String>,
pub learned_tracks: HashMap<String, String>,
}
impl LearnedConfig {
pub fn load() -> Result<Self> {
load_or_make_default("learned.json", include_str!("configs/learned.default.json"))
}
pub fn save(&self) -> Result<()> {
save_json_config("learned.json", self)
}
}
fn load_or_make_default<T: DeserializeOwned>(path: &str, default: &str) -> Result<T> {
let file_path = PathBuf::from(path);
if !file_path.exists() {
std::fs::write(&path, default)?;
}
load_json_config(&path)
}
fn load_json_config<T: DeserializeOwned>(path: &str) -> Result<T> {
let data = std::fs::read(path)?;
let value = serde_json::from_slice(&data)?;
Ok(value)
}
fn save_json_config<T: Serialize>(path: &str, val: &T) -> Result<()> {
let serialized = serde_json::to_vec_pretty(val)?;
Ok(std::fs::write(path, &serialized)?)
}

View File

@ -0,0 +1,47 @@
{
"ocr_regions": [
{
"name": "lap",
"x": 2290,
"y": 46,
"width": 145,
"height": 90
},
{
"name": "health",
"x": 90,
"y": 1364,
"width": 52,
"height": 24
},
{
"name": "gas",
"x": 208,
"y": 1364,
"width": 52,
"height": 24
},
{
"name": "tyres",
"x": 325,
"y": 1364,
"width": 52,
"height": 24
},
{
"name": "lap_time",
"x": 2325,
"y": 169,
"width": 183,
"height": 43
},
{
"name": "best_time",
"x": 2325,
"y": 222,
"width": 183,
"height": 43
}
],
"ocr_server_endpoint": "http://localhost:3000/"
}

View File

@ -0,0 +1,4 @@
{
"learned_images": {},
"learned_tracks": {}
}

View File

@ -1,39 +1,145 @@
use std::{collections::HashMap, time::Duration}; use std::{
collections::HashMap,
time::{Duration, Instant},
};
use anyhow::Result; use anyhow::Result;
use egui_extras::RetainedImage; use egui_extras::RetainedImage;
use image::RgbImage;
use scrap::{Capturer, Display}; use scrap::{Capturer, Display};
use crate::{ use crate::{
capture, capture,
image_processing::{self, Region}, image_processing::{self, Region},
ocr, ocr,
state::{ParsedFrame, SharedAppState}, state::{AppState, DebugOcrFrame, ParsedFrame, RaceState, SharedAppState},
}; };
async fn run_loop_once(state: &SharedAppState, regions: &[Region]) -> Result<()> { fn is_finished_lap(state: &AppState, frame: &ParsedFrame) -> bool {
if let Some(race) = &state.current_race {
if let Some(last_finish) = &race.last_lap_record_time {
let diff = Instant::now().duration_since(*last_finish);
if diff < Duration::from_secs(5) {
return false;
}
}
}
if let Some(prev_frame) = state.buffered_frames.back() {
prev_frame.lap_time.is_some()
&& prev_frame.lap_time == frame.lap_time
&& prev_frame.lap_time.unwrap() > Duration::from_secs(5)
} else {
false
}
}
fn merge_with_max<T: Ord + Clone>(a: &Option<T>, b: &Option<T>) -> Option<T> {
match (a, b) {
(Some(a), None) => Some(a.clone()),
(None, Some(b)) => Some(b.clone()),
(None, None) => None,
(Some(a), Some(b)) => {
if a > b {
Some(a.clone())
} else {
Some(b.clone())
}
}
}
}
fn merge_frames(prev: &ParsedFrame, next: &ParsedFrame) -> ParsedFrame {
ParsedFrame {
lap: merge_with_max(&prev.lap, &next.lap),
health: merge_with_max(&prev.health, &next.health),
gas: merge_with_max(&prev.gas, &next.gas),
tyres: merge_with_max(&prev.tyres, &next.tyres),
lap_time: merge_with_max(&prev.lap_time, &next.lap_time),
best_time: merge_with_max(&prev.best_time, &next.best_time),
}
}
fn handle_new_frame(state: &mut AppState, frame: ParsedFrame, image: &RgbImage) {
if frame.lap_time.is_some() {
state.last_frame = Some(frame.clone());
state.frames_without_lap = 0;
if state.current_race.is_none() {
let mut race = RaceState::default();
race.screencap = Some(
RetainedImage::from_image_bytes(
"screencap",
&image_processing::to_png_bytes(image),
)
.expect("failed to save screenshot"),
);
state.current_race = Some(race);
}
} else {
state.frames_without_lap += 1;
}
if state.frames_without_lap >= 10 {
if let Some(race) = state.current_race.take() {
state.past_races.push_front(race);
}
}
if is_finished_lap(state, &frame) {
let mut merged = merge_frames(state.buffered_frames.back().unwrap(), &frame);
if let Some(lap) = &merged.lap {
merged.lap = Some(lap - 1);
}
if let Some(race) = state.current_race.as_mut() {
race.laps.push(merged);
race.last_lap_record_time = Some(Instant::now());
}
}
state.buffered_frames.push_back(frame);
if state.buffered_frames.len() >= 20 {
state.buffered_frames.pop_front();
}
}
async fn run_loop_once(state: &SharedAppState) -> Result<()> {
let config = state.lock().unwrap().config.clone();
let frame = capture::get_frame()?; let frame = capture::get_frame()?;
let ocr_results = ocr::ocr_all_regions(&frame, &regions).await; let ocr_results = ocr::ocr_all_regions(&frame, &config.ocr_regions).await;
let mut saved_frames = HashMap::new(); let mut saved_frames = HashMap::new();
if state.lock().unwrap().debug_frames { if state.lock().unwrap().debug_frames {
for region in regions { let hasher = img_hash::HasherConfig::new().to_hasher();
for region in &config.ocr_regions {
let mut extracted = image_processing::extract_region(&frame, region); let mut extracted = image_processing::extract_region(&frame, region);
image_processing::filter_to_white(&mut extracted, 0.95, 0.05); image_processing::filter_to_white(&mut extracted);
let retained = RetainedImage::from_image_bytes( let retained = RetainedImage::from_image_bytes(
&region.name, &region.name,
&image_processing::to_png_bytes(&extracted), &image_processing::to_png_bytes(&extracted),
) )
.unwrap(); .unwrap();
saved_frames.insert(region.name.clone(), retained); let have_to_use_other_image_library_version = img_hash::image::RgbImage::from_raw(
extracted.width(),
extracted.height(),
extracted.as_raw().to_vec(),
)
.unwrap();
let hash = hasher.hash_image(&have_to_use_other_image_library_version);
saved_frames.insert(
region.name.clone(),
DebugOcrFrame {
image: retained,
rgb_image: extracted,
img_hash: hash,
},
);
} }
} }
{ {
let mut state = state.lock().unwrap(); let mut state = state.lock().unwrap();
let parsed = ParsedFrame::parse(&ocr_results); let parsed = ParsedFrame::parse(&ocr_results);
if parsed.lap_time.is_some() { handle_new_frame(&mut state, parsed, &frame);
state.last_frame = Some(parsed);
}
state.raw_data = ocr_results; state.raw_data = ocr_results;
state.saved_frames = saved_frames; state.saved_frames = saved_frames;
} }
@ -41,17 +147,8 @@ async fn run_loop_once(state: &SharedAppState, regions: &[Region]) -> Result<()>
} }
pub async fn run_control_loop(state: SharedAppState) -> Result<()> { pub async fn run_control_loop(state: SharedAppState) -> Result<()> {
let regions = vec![
Region::parse("health 91 1364 52 24").unwrap(),
Region::parse("gas 208 1364 52 24").unwrap(),
Region::parse("tyres 325 1364 52 24").unwrap(),
Region::parse("lap 2295 46 140 87").unwrap(),
Region::parse("best 2325 169 183 43").unwrap(),
Region::parse("lap_time 2325 222 183 43").unwrap(),
];
loop { loop {
if let Err(e) = run_loop_once(&state, &regions).await { if let Err(e) = run_loop_once(&state).await {
eprintln!("Error in control loop: {:?}", e) eprintln!("Error in control loop: {:?}", e)
} }
tokio::time::sleep(Duration::from_millis(500)).await; tokio::time::sleep(Duration::from_millis(500)).await;

View File

@ -1,57 +1,46 @@
use anyhow::Result; use anyhow::Result;
use image::{RgbImage, Rgb, codecs::png::PngEncoder, ColorType, ImageEncoder}; use image::{codecs::png::PngEncoder, ColorType, ImageEncoder, Rgb, RgbImage};
use serde::{Deserialize, Serialize};
#[derive(Clone)] #[derive(Clone, Deserialize, Serialize)]
pub struct Region { pub struct Region {
pub name: String, pub name: String,
x: usize, x: usize,
y: usize, y: usize,
width: usize, width: usize,
height: usize height: usize,
} }
impl Region { pub fn extract_region(image: &RgbImage, region: &Region) -> RgbImage {
pub fn parse(encoded: &str) -> Result<Region> {
let split: Vec<_> = encoded.split(' ').collect();
Ok(Region {
name: split.get(0).ok_or(anyhow::anyhow!("expected name in region"))?.to_string(),
x: split.get(1).ok_or(anyhow::anyhow!("failed to parse x from region"))?.parse()?,
y: split.get(2).ok_or(anyhow::anyhow!("failed to parse y from region"))?.parse()?,
width: split.get(3).ok_or(anyhow::anyhow!("failed to parse width from region"))?.parse()?,
height: split.get(4).ok_or(anyhow::anyhow!("failed to parse height from region"))?.parse()?,
})
}
}
pub fn extract_region(
image: &RgbImage,
region: &Region,
) -> RgbImage {
let mut buffer = RgbImage::new(region.width as u32, region.height as u32); let mut buffer = RgbImage::new(region.width as u32, region.height as u32);
for y in 0..region.height { for y in 0..region.height {
for x in 0..region.width { for x in 0..region.width {
buffer.put_pixel( buffer.put_pixel(
x as u32, x as u32,
y as u32, y as u32,
*image.get_pixel((region.x + x) as u32, (region.y + y) as u32) *image.get_pixel((region.x + x) as u32, (region.y + y) as u32),
); );
} }
} }
buffer buffer
} }
pub fn filter_to_white( pub fn filter_to_white(image: &mut RgbImage) {
image: &mut RgbImage, let threshold = 0.98;
threshold: f64, let variance_threshold = 0.02;
variance_threshold: f64
) {
let past_threshold_color = |v: u8| v as f64 >= (u8::MAX as f64 * threshold); let past_threshold_color = |v: u8| v as f64 >= (u8::MAX as f64 * threshold);
let color_diff = |a: u8, b: u8| (a.abs_diff(b) as f64) / (u8::MAX as f64); let color_diff = |a: u8, b: u8| (a.abs_diff(b) as f64) / (u8::MAX as f64);
for y in 0..image.height() { for y in 0..image.height() {
for x in 0..image.width() { for x in 0..image.width() {
let pixel = image.get_pixel(x, y); let pixel = image.get_pixel(x, y);
let [r, g, b] = pixel.0; let [r, g, b] = pixel.0;
if past_threshold_color(r) && past_threshold_color(g) && past_threshold_color(b) && color_diff(r, g) < variance_threshold && color_diff(r, b) < variance_threshold && color_diff(b, g) < variance_threshold { if past_threshold_color(r)
&& past_threshold_color(g)
&& past_threshold_color(b)
&& color_diff(r, g) < variance_threshold
&& color_diff(r, b) < variance_threshold
&& color_diff(b, g) < variance_threshold
{
// This is a white pixel, make it black // This is a white pixel, make it black
image.put_pixel(x, y, Rgb([0, 0, 0])); image.put_pixel(x, y, Rgb([0, 0, 0]));
} else { } else {
@ -65,11 +54,13 @@ pub fn filter_to_white(
pub fn to_png_bytes(image: &RgbImage) -> Vec<u8> { pub fn to_png_bytes(image: &RgbImage) -> Vec<u8> {
let mut buffer: Vec<u8> = Vec::new(); let mut buffer: Vec<u8> = Vec::new();
let encoder = PngEncoder::new(&mut buffer); let encoder = PngEncoder::new(&mut buffer);
encoder.write_image( encoder
.write_image(
image.as_raw(), image.as_raw(),
image.width(), image.width(),
image.height(), image.height(),
ColorType::Rgb8 ColorType::Rgb8,
).expect("failed encoding image to PNG"); )
.expect("failed encoding image to PNG");
buffer buffer
} }

View File

@ -5,19 +5,26 @@ mod control_loop;
mod image_processing; mod image_processing;
mod ocr; mod ocr;
mod state; mod state;
mod config;
use std::{ use std::{
sync::{Arc, Mutex}, sync::{Arc, Mutex},
time::Duration, time::Duration,
}; };
use eframe::egui; use config::{Config, LearnedConfig};
use egui_extras::RetainedImage; use eframe::{
use state::{AppState, SharedAppState}; egui::{self, Ui},
epaint::Color32, emath::Vec2,
};
use state::{AppState, RaceState, SharedAppState};
#[tokio::main(flavor = "multi_thread", worker_threads = 8)] #[tokio::main(flavor = "multi_thread", worker_threads = 8)]
async fn main() -> anyhow::Result<()> { async fn main() -> anyhow::Result<()> {
let state = Arc::new(Mutex::new(AppState::default())); let mut app_state = AppState::default();
app_state.config = Arc::new(Config::load().unwrap());
app_state.learned = Arc::new(LearnedConfig::load().unwrap());
let state = Arc::new(Mutex::new(app_state));
{ {
let state = state.clone(); let state = state.clone();
let _ = tokio::spawn(async move { let _ = tokio::spawn(async move {
@ -35,6 +42,53 @@ async fn main() -> anyhow::Result<()> {
); );
} }
fn show_optional_usize_with_diff(
ui: &mut Ui,
color: Color32,
old: &Option<usize>,
v: &Option<usize>,
) {
let diff_string = match (old, v) {
(Some(old), Some(v)) => {
let diff = *v as isize - *old as isize;
if diff.is_positive() {
format!(" (+{})", diff)
} else {
format!(" ({})", diff)
}
}
_ => "".to_owned(),
};
let value = match v {
Some(v) => v.to_string(),
None => "???".to_owned(),
};
let text = format!("{}{}", value, diff_string);
ui.colored_label(color, text);
}
fn format_time(time: Duration) -> String {
format!("{:.3}", time.as_secs_f64())
}
fn label_time_delta(ui: &mut Ui, time: Duration, old: Option<Duration>) {
if let Some(old) = old {
if time > old {
ui.colored_label(
Color32::LIGHT_RED,
"+".to_owned() + &format_time(time - old),
);
} else {
ui.colored_label(
Color32::LIGHT_GREEN,
"-".to_owned() + &format_time(old - time),
);
}
} else {
ui.label("--");
}
}
struct MyApp { struct MyApp {
state: SharedAppState, state: SharedAppState,
} }
@ -45,6 +99,50 @@ impl MyApp {
} }
} }
fn show_race_state(ui: &mut Ui, race: &RaceState) {
egui::Grid::new("current-race").show(ui, |ui| {
ui.label("Lap");
ui.label("Time");
ui.label("Δ Previous");
ui.label("Δ Best");
ui.label("Health");
ui.label("Gas");
ui.label("Tyres");
ui.end_row();
for (i, lap) in race.laps.iter().enumerate() {
if let Some(lap_time) = *&lap.lap_time {
let prev_lap = race.laps.get(i - 1);
ui.label(format!("#{}", lap.lap.unwrap_or(i + 1)));
ui.label(format_time(lap_time));
label_time_delta(ui, lap_time, prev_lap.and_then(|p| p.lap_time));
label_time_delta(ui, lap_time, lap.best_time);
show_optional_usize_with_diff(
ui,
Color32::RED,
&prev_lap.and_then(|p| p.health),
&lap.health,
);
show_optional_usize_with_diff(
ui,
Color32::GRAY,
&prev_lap.and_then(|p| p.gas),
&lap.gas,
);
show_optional_usize_with_diff(
ui,
Color32::GREEN,
&prev_lap.and_then(|p| p.tyres),
&lap.tyres,
);
ui.end_row();
}
}
});
}
impl eframe::App for MyApp { impl eframe::App for MyApp {
fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) { fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) {
let mut state = self.state.lock().unwrap(); let mut state = self.state.lock().unwrap();
@ -84,7 +182,28 @@ impl eframe::App for MyApp {
ui.separator(); ui.separator();
ui.checkbox(&mut state.debug_frames, "Debug OCR regions"); ui.checkbox(&mut state.debug_frames, "Debug OCR regions");
}); });
egui::CentralPanel::default().show(ctx, |ui| {}); egui::CentralPanel::default().show(ctx, |ui| {
if let Some(race) = &state.current_race {
ui.heading("Current Race");
show_race_state(ui, race);
}
let len = state.past_races.len();
for (i, race) in state.past_races.iter_mut().enumerate() {
ui.separator();
ui.heading(format!("Race #{}", len - i));
show_race_state(ui, race);
if let Some(img) = &race.screencap {
img.show_max_size(ui, Vec2::new(600.0, 500.0));
}
ui.label("Car:");
ui.text_edit_singleline(&mut race.car);
ui.label("Track:");
ui.text_edit_singleline(&mut race.track);
if ui.button("Export").clicked() {
println!("EXPORT: TODO");
}
}
});
if state.debug_frames { if state.debug_frames {
egui::SidePanel::right("screenshots").show(ctx, |ui| { egui::SidePanel::right("screenshots").show(ctx, |ui| {
@ -92,7 +211,8 @@ impl eframe::App for MyApp {
screenshots_sorted.sort_by_key(|(name, _)| name.clone()); screenshots_sorted.sort_by_key(|(name, _)| name.clone());
for (name, image) in screenshots_sorted { for (name, image) in screenshots_sorted {
ui.label(name); ui.label(name);
image.show_max_size(ui, ui.available_size()); ui.label(image.img_hash.to_base64());
image.image.show_max_size(ui, ui.available_size());
} }
}); });
} }

View File

@ -43,7 +43,7 @@ pub async fn ocr_all_regions(image: &RgbImage, regions: &[Region]) -> HashMap<St
let results = results.clone(); let results = results.clone();
handles.push(tokio::spawn(async move { handles.push(tokio::spawn(async move {
let mut image = filtered_image; let mut image = filtered_image;
filter_to_white(&mut image, 0.95, 0.05); filter_to_white(&mut image);
let ocr_results = run_ocr(&image).await; let ocr_results = run_ocr(&image).await;
let value = match ocr_results { let value = match ocr_results {
Ok(ocr_regions) => { Ok(ocr_regions) => {

View File

@ -1,8 +1,12 @@
use std::{sync::{Arc, Mutex}, time::Duration, collections::HashMap}; use std::{sync::{Arc, Mutex}, time::{Duration, Instant}, collections::{HashMap, VecDeque}};
use egui_extras::RetainedImage; use egui_extras::RetainedImage;
use image::RgbImage;
use crate::config::{Config, LearnedConfig};
#[derive(Debug, Clone)]
pub struct ParsedFrame { pub struct ParsedFrame {
pub lap: Option<usize>, pub lap: Option<usize>,
@ -18,7 +22,6 @@ fn parse_duration(time: &str) -> Option<Duration> {
let sep = time.find(':')?; let sep = time.find(':')?;
let (minutes, secs) = time.split_at(sep); let (minutes, secs) = time.split_at(sep);
println!("{} {}", minutes, secs);
let minutes = minutes.parse::<f64>().ok()?; let minutes = minutes.parse::<f64>().ok()?;
let secs = secs[1..secs.len()].parse::<f64>().ok()?; let secs = secs[1..secs.len()].parse::<f64>().ok()?;
@ -54,13 +57,41 @@ impl ParsedFrame {
} }
} }
#[derive(Default)]
pub struct RaceState {
pub laps: Vec<ParsedFrame>,
pub last_lap_record_time: Option<Instant>,
pub screencap: Option<RetainedImage>,
pub exported: bool,
pub car: String,
pub track: String,
}
pub struct DebugOcrFrame {
pub image: RetainedImage,
pub rgb_image: RgbImage,
pub img_hash: img_hash::ImageHash,
}
#[derive(Default)] #[derive(Default)]
pub struct AppState { pub struct AppState {
pub raw_data: HashMap<String, Option<String>>, pub raw_data: HashMap<String, Option<String>>,
pub last_frame: Option<ParsedFrame>, pub last_frame: Option<ParsedFrame>,
pub buffered_frames: VecDeque<ParsedFrame>,
pub frames_without_lap: usize,
pub current_race: Option<RaceState>,
pub past_races: VecDeque<RaceState>,
pub debug_frames: bool, pub debug_frames: bool,
pub saved_frames: HashMap<String, RetainedImage>, pub saved_frames: HashMap<String, DebugOcrFrame>,
pub config: Arc<Config>,
pub learned: Arc<LearnedConfig>,
} }
pub type SharedAppState = Arc<Mutex<AppState>>; pub type SharedAppState = Arc<Mutex<AppState>>;