dark mode release

This commit is contained in:
Scott Pruett 2022-05-22 22:23:28 -04:00
parent 4a920b4b42
commit 34a2b61757
15 changed files with 427 additions and 342 deletions

14
car-classes.txt Normal file
View File

@ -0,0 +1,14 @@
Piccino
Superlight
Eurotruck
Muscle Car
Stock Car
Super Truck
Rally
50s GT
Touring Car
GT
Prototype
60s GP
80s GP
GP

View File

@ -4,7 +4,7 @@
"name": "lap", "name": "lap",
"x": 2300, "x": 2300,
"y": 46, "y": 46,
"width": 145, "width": 140,
"height": 90 "height": 90
}, },
{ {
@ -40,7 +40,8 @@
"x": 2325, "x": 2325,
"y": 222, "y": 222,
"width": 183, "width": 183,
"height": 43 "height": 43,
"use_ocr_cache": false
} }
], ],
"track_region": { "track_region": {

3
race_stats.csv Normal file
View File

@ -0,0 +1,3 @@
2022-05-22-20:39,whistle valley,gp,1,22.349,22.349,100,,
2022-05-22-20:39,whistle valley,gp,2,21.888,21.888,99,83,82
2022-05-22-20:39,whistle valley,gp,3,22.031,21.888,99,75,73
1 2022-05-22-20:39 whistle valley gp 1 22.349 22.349 100
2 2022-05-22-20:39 whistle valley gp 2 21.888 21.888 99 83 82
3 2022-05-22-20:39 whistle valley gp 3 22.031 21.888 99 75 73

View File

@ -4,7 +4,7 @@
"name": "lap", "name": "lap",
"x": 2300, "x": 2300,
"y": 46, "y": 46,
"width": 145, "width": 142,
"height": 90 "height": 90
}, },
{ {
@ -40,7 +40,8 @@
"x": 2325, "x": 2325,
"y": 222, "y": 222,
"width": 183, "width": 183,
"height": 43 "height": 43,
"use_ocr_cache": false
} }
], ],
"track_region": { "track_region": {

View File

@ -57,6 +57,7 @@ fn merge_frames(prev: &ParsedFrame, next: &ParsedFrame) -> ParsedFrame {
tyres: merge_with_max(&prev.tyres, &next.tyres), tyres: merge_with_max(&prev.tyres, &next.tyres),
lap_time: merge_with_max(&prev.lap_time, &next.lap_time), lap_time: merge_with_max(&prev.lap_time, &next.lap_time),
best_time: merge_with_max(&prev.best_time, &next.best_time), best_time: merge_with_max(&prev.best_time, &next.best_time),
striked: false,
} }
} }
@ -158,7 +159,7 @@ fn run_loop_once(capturer: &mut Capturer, state: &SharedAppState) -> Result<()>
} }
{ {
let mut state = state.lock().unwrap(); let mut state = state.lock().unwrap();
let parsed = ParsedFrame::parse(&ocr_results); let mut parsed = ParsedFrame::parse(&ocr_results);
handle_new_frame(&mut state, parsed, &frame); handle_new_frame(&mut state, parsed, &frame);
state.raw_data = ocr_results; state.raw_data = ocr_results;
state.saved_frames = saved_frames; state.saved_frames = saved_frames;
@ -172,6 +173,7 @@ pub fn run_control_loop(state: SharedAppState) {
if let Err(e) = run_loop_once(&mut capturer, &state) { if let Err(e) = run_loop_once(&mut capturer, &state) {
eprintln!("Error in control loop: {:?}", e) eprintln!("Error in control loop: {:?}", e)
} }
thread::sleep(Duration::from_millis(state.lock().unwrap().config.ocr_interval_ms.unwrap_or(500))); let interval = state.lock().unwrap().config.ocr_interval_ms.unwrap_or(500);
thread::sleep(Duration::from_millis(interval));
} }
} }

View File

@ -11,6 +11,7 @@ pub struct Region {
width: usize, width: usize,
height: usize, height: usize,
pub threshold: Option<f64>, pub threshold: Option<f64>,
pub use_ocr_cache: Option<bool>,
} }
pub fn extract_and_filter(image: &RgbImage, region: &Region) -> RgbImage { pub fn extract_and_filter(image: &RgbImage, region: &Region) -> RgbImage {

View File

@ -1,24 +1,26 @@
// #![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] // hide console window on Windows in release // #![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] // hide console window on Windows in release
mod capture; mod capture;
mod config;
mod control_loop; mod control_loop;
mod image_processing; mod image_processing;
mod ocr; mod ocr;
mod state; mod state;
mod config;
mod stats_writer; mod stats_writer;
use std::{ use std::{
sync::{Arc, Mutex}, sync::{Arc, Mutex},
time::Duration, thread, thread,
time::{Duration, Instant},
}; };
use config::{Config, LearnedConfig}; use config::{Config, LearnedConfig};
use eframe::{ use eframe::{
egui::{self, Ui}, egui::{self, Ui, Visuals},
epaint::Color32, emath::Vec2, emath::Vec2,
epaint::Color32,
}; };
use state::{AppState, RaceState, SharedAppState}; use state::{AppState, RaceState, SharedAppState, ParsedFrame};
use stats_writer::export_race_stats; use stats_writer::export_race_stats;
fn main() -> anyhow::Result<()> { fn main() -> anyhow::Result<()> {
@ -99,11 +101,16 @@ struct MyApp {
impl MyApp { impl MyApp {
pub fn new(state: SharedAppState) -> Self { pub fn new(state: SharedAppState) -> Self {
Self { state, config_load_err: None, hash_to_learn: "".to_owned(), value_to_learn: "".to_owned() } Self {
state,
config_load_err: None,
hash_to_learn: "".to_owned(),
value_to_learn: "".to_owned(),
}
} }
} }
fn show_race_state(ui: &mut Ui, race_name: &str, race: &RaceState) { fn show_race_state(ui: &mut Ui, race_name: &str, race: &mut RaceState) {
egui::Grid::new(format!("race:{}", race_name)).show(ui, |ui| { egui::Grid::new(format!("race:{}", race_name)).show(ui, |ui| {
ui.label("Lap"); ui.label("Lap");
ui.label("Time"); ui.label("Time");
@ -113,9 +120,9 @@ fn show_race_state(ui: &mut Ui, race_name: &str, race: &RaceState) {
ui.label("Gas"); ui.label("Gas");
ui.label("Tyres"); ui.label("Tyres");
ui.end_row(); ui.end_row();
for (i, lap) in race.laps.iter().enumerate() { let mut prev_lap: Option<&ParsedFrame> = None;
for (i, lap) in race.laps.iter_mut().enumerate() {
if let Some(lap_time) = lap.lap_time { 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!("#{}", lap.lap.unwrap_or(i + 1)));
ui.label(format_time(lap_time)); ui.label(format_time(lap_time));
@ -141,14 +148,24 @@ fn show_race_state(ui: &mut Ui, race_name: &str, race: &RaceState) {
&lap.tyres, &lap.tyres,
); );
if lap.striked {
ui.colored_label(Color32::RED, "Striked from the record");
} else {
if ui.button("Strike").clicked() {
lap.striked = true;
}
}
ui.end_row(); ui.end_row();
} }
prev_lap = Some(lap);
} }
}); });
} }
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) {
ctx.set_visuals(Visuals::dark());
let mut state = self.state.lock().unwrap(); let mut state = self.state.lock().unwrap();
egui::SidePanel::left("frame").show(ctx, |ui| { egui::SidePanel::left("frame").show(ctx, |ui| {
if let Some(frame) = &state.last_frame { if let Some(frame) = &state.last_frame {
@ -175,6 +192,7 @@ impl eframe::App for MyApp {
)); ));
} }
if state.debug_frames {
ui.separator(); ui.separator();
ui.heading("Raw OCR results"); ui.heading("Raw OCR results");
let mut raw_data_sorted: Vec<_> = state.raw_data.iter().collect(); let mut raw_data_sorted: Vec<_> = state.raw_data.iter().collect();
@ -182,12 +200,14 @@ impl eframe::App for MyApp {
for (key, val) in raw_data_sorted { for (key, val) in raw_data_sorted {
ui.label(format!("{}: {:?}", key, val)); ui.label(format!("{}: {:?}", key, val));
} }
}
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 { egui::ScrollArea::vertical().show(ui, |ui| {
if let Some(race) = &mut state.current_race {
ui.heading("Current Race"); ui.heading("Current Race");
show_race_state(ui, "current", race); show_race_state(ui, "current", race);
} }
@ -204,13 +224,16 @@ impl eframe::App for MyApp {
ui.text_edit_singleline(&mut race.car); ui.text_edit_singleline(&mut race.car);
ui.label("Track:"); ui.label("Track:");
ui.text_edit_singleline(&mut race.track); ui.text_edit_singleline(&mut race.track);
ui.label("Comments:");
ui.text_edit_singleline(&mut race.comments);
if ui.button("Export").clicked() { if ui.button("Export").clicked() {
match export_race_stats(race) { match export_race_stats(race) {
Ok(_) => { Ok(_) => {
race.exported = true; race.exported = true;
} }
Err(e) => { Err(e) => {
race.export_error = Some(format!("failed to export race: {:?}", e)); race.export_error =
Some(format!("failed to export race: {:?}", e));
} }
} }
} }
@ -222,9 +245,11 @@ impl eframe::App for MyApp {
} }
} }
}); });
});
if state.debug_frames { if state.debug_frames {
egui::SidePanel::right("screenshots").show(ctx, |ui| { egui::SidePanel::right("screenshots").show(ctx, |ui| {
egui::ScrollArea::vertical().show(ui, |ui| {
let mut screenshots_sorted: Vec<_> = state.saved_frames.iter().collect(); let mut screenshots_sorted: Vec<_> = state.saved_frames.iter().collect();
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 {
@ -242,7 +267,8 @@ impl eframe::App for MyApp {
self.config_load_err = None; self.config_load_err = None;
} }
Err(e) => { Err(e) => {
self.config_load_err = Some(format!("failed to load config: {:?}", e)); self.config_load_err =
Some(format!("failed to load config: {:?}", e));
} }
} }
} }
@ -257,7 +283,9 @@ impl eframe::App for MyApp {
ui.text_edit_singleline(&mut self.value_to_learn); ui.text_edit_singleline(&mut self.value_to_learn);
if ui.button("Learn").clicked() { if ui.button("Learn").clicked() {
let mut learned_config = (*state.learned).clone(); let mut learned_config = (*state.learned).clone();
learned_config.learned_images.insert(self.hash_to_learn.clone(), self.value_to_learn.clone()); learned_config
.learned_images
.insert(self.hash_to_learn.clone(), self.value_to_learn.clone());
learned_config.save().unwrap(); learned_config.save().unwrap();
state.learned = Arc::new(learned_config); state.learned = Arc::new(learned_config);
@ -265,6 +293,7 @@ impl eframe::App for MyApp {
self.value_to_learn = "".to_owned(); self.value_to_learn = "".to_owned();
} }
}); });
});
} }
ctx.request_repaint(); ctx.request_repaint();

View File

@ -76,8 +76,9 @@ pub async fn ocr_all_regions(
let locked = ocr_cache.read().unwrap(); let locked = ocr_cache.read().unwrap();
locked.get(&hash).cloned() locked.get(&hash).cloned()
}; };
if let Some(cached) = cached { let use_cache = region.use_ocr_cache.unwrap_or(true) && config.use_ocr_cache.unwrap_or(true);
cached if cached.is_some() && use_cache {
cached.unwrap()
} else { } else {
match run_ocr(&filtered_image, &config.ocr_server_endpoint).await { match run_ocr(&filtered_image, &config.ocr_server_endpoint).await {
Ok(v) => { Ok(v) => {

View File

@ -17,6 +17,8 @@ pub struct ParsedFrame {
pub best_time: Option<Duration>, pub best_time: Option<Duration>,
pub lap_time: Option<Duration>, pub lap_time: Option<Duration>,
pub striked: bool,
} }
fn parse_duration(time: &str) -> Option<Duration> { fn parse_duration(time: &str) -> Option<Duration> {
@ -54,6 +56,7 @@ impl ParsedFrame {
tyres: parse_to_0_100(raw.get("tyres")), tyres: parse_to_0_100(raw.get("tyres")),
best_time: parse_to_duration(raw.get("best")), best_time: parse_to_duration(raw.get("best")),
lap_time: parse_to_duration(raw.get("lap_time")), lap_time: parse_to_duration(raw.get("lap_time")),
striked: false,
} }
} }
} }
@ -71,6 +74,7 @@ pub struct RaceState {
pub car: String, pub car: String,
pub track: String, pub track: String,
pub comments: String,
} }
impl RaceState { impl RaceState {

View File

@ -19,6 +19,10 @@ pub fn export_race_stats(race_stats: &mut RaceState) -> Result<()> {
let mut csv_writer = csv::Writer::from_writer(writer); let mut csv_writer = csv::Writer::from_writer(writer);
for lap in &race_stats.laps { for lap in &race_stats.laps {
if lap.striked {
continue;
}
csv_writer.write_record(vec![ csv_writer.write_record(vec![
race_name.clone(), race_name.clone(),
race_stats.track.clone(), race_stats.track.clone(),
@ -45,6 +49,7 @@ pub fn export_race_stats(race_stats: &mut RaceState) -> Result<()> {
lap.tyres lap.tyres
.map(|x| x.to_string()) .map(|x| x.to_string())
.unwrap_or_else(|| "".to_owned()), .unwrap_or_else(|| "".to_owned()),
race_stats.comments.clone(),
])?; ])?;
} }

5
suggestions.md Normal file
View File

@ -0,0 +1,5 @@
- Recognize
- Penalties
- Pit stops
- ComboBox for car/track
- GLobal best time not current best

19
tracklist.txt Normal file
View File

@ -0,0 +1,19 @@
Whistle Valley
Sugar Hill
Maple Ridge
Rennvoort
Magdalena GP
Magdalena Club
Copperwood GP
Copperwood Club
Interstate
Buffalo Hill
Lost Lagoons
Bullseye Speedway
Speedopolis
Faenza
Siena
Thunder Point
Tilksport GP
Tilksport Club
Tilksport Rallycross