dark mode release
This commit is contained in:
parent
4a920b4b42
commit
34a2b61757
|
@ -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
|
|
@ -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": {
|
||||||
|
|
|
@ -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
|
|
|
@ -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": {
|
||||||
|
|
|
@ -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));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
55
src/main.rs
55
src/main.rs
|
@ -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();
|
||||||
|
|
|
@ -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) => {
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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(),
|
||||||
])?;
|
])?;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,5 @@
|
||||||
|
- Recognize
|
||||||
|
- Penalties
|
||||||
|
- Pit stops
|
||||||
|
- ComboBox for car/track
|
||||||
|
- GLobal best time not current best
|
|
@ -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
|
Loading…
Reference in New Issue