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
|
109
config.json
109
config.json
|
@ -1,55 +1,56 @@
|
||||||
{
|
{
|
||||||
"ocr_regions": [
|
"ocr_regions": [
|
||||||
{
|
{
|
||||||
"name": "lap",
|
"name": "lap",
|
||||||
"x": 2300,
|
"x": 2300,
|
||||||
"y": 46,
|
"y": 46,
|
||||||
"width": 145,
|
"width": 140,
|
||||||
"height": 90
|
"height": 90
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "health",
|
"name": "health",
|
||||||
"x": 90,
|
"x": 90,
|
||||||
"y": 1364,
|
"y": 1364,
|
||||||
"width": 52,
|
"width": 52,
|
||||||
"height": 24
|
"height": 24
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "gas",
|
"name": "gas",
|
||||||
"x": 208,
|
"x": 208,
|
||||||
"y": 1364,
|
"y": 1364,
|
||||||
"width": 52,
|
"width": 52,
|
||||||
"height": 24
|
"height": 24
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "tyres",
|
"name": "tyres",
|
||||||
"x": 325,
|
"x": 325,
|
||||||
"y": 1364,
|
"y": 1364,
|
||||||
"width": 52,
|
"width": 52,
|
||||||
"height": 24
|
"height": 24
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "best",
|
"name": "best",
|
||||||
"x": 2325,
|
"x": 2325,
|
||||||
"y": 169,
|
"y": 169,
|
||||||
"width": 183,
|
"width": 183,
|
||||||
"height": 43
|
"height": 43
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "lap_time",
|
"name": "lap_time",
|
||||||
"x": 2325,
|
"x": 2325,
|
||||||
"y": 222,
|
"y": 222,
|
||||||
"width": 183,
|
"width": 183,
|
||||||
"height": 43
|
"height": 43,
|
||||||
}
|
"use_ocr_cache": false
|
||||||
],
|
}
|
||||||
"track_region": {
|
],
|
||||||
"name": "track",
|
"track_region": {
|
||||||
"x": 2020,
|
"name": "track",
|
||||||
"y": 1030,
|
"x": 2020,
|
||||||
"width": 540,
|
"y": 1030,
|
||||||
"height": 410,
|
"width": 540,
|
||||||
"threshold": 0.85
|
"height": 410,
|
||||||
},
|
"threshold": 0.85
|
||||||
"ocr_server_endpoint": "https://tesserver.spruett.dev/"
|
},
|
||||||
|
"ocr_server_endpoint": "https://tesserver.spruett.dev/"
|
||||||
}
|
}
|
|
@ -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,42 +1,42 @@
|
||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
|
|
||||||
import argparse
|
import argparse
|
||||||
import json
|
import json
|
||||||
from typing import Tuple
|
from typing import Tuple
|
||||||
|
|
||||||
|
|
||||||
def parse_resolution(resolution: str) -> Tuple[int, int]:
|
def parse_resolution(resolution: str) -> Tuple[int, int]:
|
||||||
a, b = resolution.split('x')
|
a, b = resolution.split('x')
|
||||||
return int(a), int(b)
|
return int(a), int(b)
|
||||||
|
|
||||||
def scale_x_y(x, y, from_resolution, to_resolution):
|
def scale_x_y(x, y, from_resolution, to_resolution):
|
||||||
return (x * to_resolution[0] / from_resolution[0], y * to_resolution[1] / from_resolution[1])
|
return (x * to_resolution[0] / from_resolution[0], y * to_resolution[1] / from_resolution[1])
|
||||||
|
|
||||||
def scale_region(region, from_resolution, to_resolution):
|
def scale_region(region, from_resolution, to_resolution):
|
||||||
x, y = scale_x_y(region['x'], region['y'], from_resolution, to_resolution)
|
x, y = scale_x_y(region['x'], region['y'], from_resolution, to_resolution)
|
||||||
width, height = scale_x_y(region['width'], region['height'], from_resolution, to_resolution)
|
width, height = scale_x_y(region['width'], region['height'], from_resolution, to_resolution)
|
||||||
region['x'] = round(x)
|
region['x'] = round(x)
|
||||||
region['y'] = round(y)
|
region['y'] = round(y)
|
||||||
region['width'] = round(width)
|
region['width'] = round(width)
|
||||||
region['height'] = round(height)
|
region['height'] = round(height)
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
argparser = argparse.ArgumentParser()
|
argparser = argparse.ArgumentParser()
|
||||||
argparser.add_argument("--from_res", help="From resolution", default="2560x1440")
|
argparser.add_argument("--from_res", help="From resolution", default="2560x1440")
|
||||||
argparser.add_argument("--to_res", help="To resolution (e.g. 1920x1080)")
|
argparser.add_argument("--to_res", help="To resolution (e.g. 1920x1080)")
|
||||||
argparser.add_argument("--config", help="Config file", default="config.json")
|
argparser.add_argument("--config", help="Config file", default="config.json")
|
||||||
|
|
||||||
args = argparser.parse_args()
|
args = argparser.parse_args()
|
||||||
|
|
||||||
from_resolution = parse_resolution(args.from_res)
|
from_resolution = parse_resolution(args.from_res)
|
||||||
to_resolution = parse_resolution(args.to_res)
|
to_resolution = parse_resolution(args.to_res)
|
||||||
config = json.load(open(args.config, 'r'))
|
config = json.load(open(args.config, 'r'))
|
||||||
for region in config['ocr_regions']:
|
for region in config['ocr_regions']:
|
||||||
scale_region(region, from_resolution, to_resolution)
|
scale_region(region, from_resolution, to_resolution)
|
||||||
scale_region(config['track_region'], from_resolution, to_resolution)
|
scale_region(config['track_region'], from_resolution, to_resolution)
|
||||||
print(json.dumps(config, indent=4))
|
print(json.dumps(config, indent=4))
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
main()
|
main()
|
110
src/config.rs
110
src/config.rs
|
@ -1,56 +1,56 @@
|
||||||
use std::{collections::HashMap, path::PathBuf};
|
use std::{collections::HashMap, path::PathBuf};
|
||||||
|
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
use serde::{Serialize, Deserialize, de::DeserializeOwned};
|
use serde::{Serialize, Deserialize, de::DeserializeOwned};
|
||||||
|
|
||||||
use crate::image_processing::Region;
|
use crate::image_processing::Region;
|
||||||
|
|
||||||
#[derive(Default, Serialize, Deserialize, Clone)]
|
#[derive(Default, Serialize, Deserialize, Clone)]
|
||||||
pub struct Config {
|
pub struct Config {
|
||||||
pub ocr_regions: Vec<Region>,
|
pub ocr_regions: Vec<Region>,
|
||||||
pub track_region: Option<Region>,
|
pub track_region: Option<Region>,
|
||||||
pub ocr_server_endpoint: String,
|
pub ocr_server_endpoint: String,
|
||||||
pub filter_threshold: Option<f64>,
|
pub filter_threshold: Option<f64>,
|
||||||
pub use_ocr_cache: Option<bool>,
|
pub use_ocr_cache: Option<bool>,
|
||||||
pub ocr_interval_ms: Option<u64>,
|
pub ocr_interval_ms: Option<u64>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Config {
|
impl Config {
|
||||||
pub fn load() -> Result<Self> {
|
pub fn load() -> Result<Self> {
|
||||||
load_or_make_default("config.json", include_str!("configs/config.default.json"))
|
load_or_make_default("config.json", include_str!("configs/config.default.json"))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Default, Serialize, Deserialize, Clone)]
|
#[derive(Default, Serialize, Deserialize, Clone)]
|
||||||
pub struct LearnedConfig {
|
pub struct LearnedConfig {
|
||||||
pub learned_images: HashMap<String, String>,
|
pub learned_images: HashMap<String, String>,
|
||||||
pub learned_tracks: HashMap<String, String>,
|
pub learned_tracks: HashMap<String, String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl LearnedConfig {
|
impl LearnedConfig {
|
||||||
pub fn load() -> Result<Self> {
|
pub fn load() -> Result<Self> {
|
||||||
load_or_make_default("learned.json", include_str!("configs/learned.default.json"))
|
load_or_make_default("learned.json", include_str!("configs/learned.default.json"))
|
||||||
}
|
}
|
||||||
pub fn save(&self) -> Result<()> {
|
pub fn save(&self) -> Result<()> {
|
||||||
save_json_config("learned.json", self)
|
save_json_config("learned.json", self)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn load_or_make_default<T: DeserializeOwned>(path: &str, default: &str) -> Result<T> {
|
fn load_or_make_default<T: DeserializeOwned>(path: &str, default: &str) -> Result<T> {
|
||||||
let file_path = PathBuf::from(path);
|
let file_path = PathBuf::from(path);
|
||||||
if !file_path.exists() {
|
if !file_path.exists() {
|
||||||
std::fs::write(&path, default)?;
|
std::fs::write(&path, default)?;
|
||||||
}
|
}
|
||||||
load_json_config(path)
|
load_json_config(path)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn load_json_config<T: DeserializeOwned>(path: &str) -> Result<T> {
|
fn load_json_config<T: DeserializeOwned>(path: &str) -> Result<T> {
|
||||||
let data = std::fs::read(path)?;
|
let data = std::fs::read(path)?;
|
||||||
let value = serde_json::from_slice(&data)?;
|
let value = serde_json::from_slice(&data)?;
|
||||||
Ok(value)
|
Ok(value)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn save_json_config<T: Serialize>(path: &str, val: &T) -> Result<()> {
|
fn save_json_config<T: Serialize>(path: &str, val: &T) -> Result<()> {
|
||||||
let serialized = serde_json::to_vec_pretty(val)?;
|
let serialized = serde_json::to_vec_pretty(val)?;
|
||||||
Ok(std::fs::write(path, &serialized)?)
|
Ok(std::fs::write(path, &serialized)?)
|
||||||
}
|
}
|
|
@ -1,55 +1,56 @@
|
||||||
{
|
{
|
||||||
"ocr_regions": [
|
"ocr_regions": [
|
||||||
{
|
{
|
||||||
"name": "lap",
|
"name": "lap",
|
||||||
"x": 2300,
|
"x": 2300,
|
||||||
"y": 46,
|
"y": 46,
|
||||||
"width": 145,
|
"width": 142,
|
||||||
"height": 90
|
"height": 90
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "health",
|
"name": "health",
|
||||||
"x": 90,
|
"x": 90,
|
||||||
"y": 1364,
|
"y": 1364,
|
||||||
"width": 52,
|
"width": 52,
|
||||||
"height": 24
|
"height": 24
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "gas",
|
"name": "gas",
|
||||||
"x": 208,
|
"x": 208,
|
||||||
"y": 1364,
|
"y": 1364,
|
||||||
"width": 52,
|
"width": 52,
|
||||||
"height": 24
|
"height": 24
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "tyres",
|
"name": "tyres",
|
||||||
"x": 325,
|
"x": 325,
|
||||||
"y": 1364,
|
"y": 1364,
|
||||||
"width": 52,
|
"width": 52,
|
||||||
"height": 24
|
"height": 24
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "best",
|
"name": "best",
|
||||||
"x": 2325,
|
"x": 2325,
|
||||||
"y": 169,
|
"y": 169,
|
||||||
"width": 183,
|
"width": 183,
|
||||||
"height": 43
|
"height": 43
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "lap_time",
|
"name": "lap_time",
|
||||||
"x": 2325,
|
"x": 2325,
|
||||||
"y": 222,
|
"y": 222,
|
||||||
"width": 183,
|
"width": 183,
|
||||||
"height": 43
|
"height": 43,
|
||||||
}
|
"use_ocr_cache": false
|
||||||
],
|
}
|
||||||
"track_region": {
|
],
|
||||||
"name": "track",
|
"track_region": {
|
||||||
"x": 2020,
|
"name": "track",
|
||||||
"y": 1030,
|
"x": 2020,
|
||||||
"width": 540,
|
"y": 1030,
|
||||||
"height": 410,
|
"width": 540,
|
||||||
"threshold": 0.85
|
"height": 410,
|
||||||
},
|
"threshold": 0.85
|
||||||
"ocr_server_endpoint": "https://tesserver.spruett.dev/"
|
},
|
||||||
|
"ocr_server_endpoint": "https://tesserver.spruett.dev/"
|
||||||
}
|
}
|
|
@ -1,4 +1,4 @@
|
||||||
{
|
{
|
||||||
"learned_images": {},
|
"learned_images": {},
|
||||||
"learned_tracks": {}
|
"learned_tracks": {}
|
||||||
}
|
}
|
|
@ -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 {
|
||||||
|
|
187
src/main.rs
187
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<()> {
|
||||||
|
@ -92,18 +94,23 @@ struct MyApp {
|
||||||
state: SharedAppState,
|
state: SharedAppState,
|
||||||
|
|
||||||
config_load_err: Option<String>,
|
config_load_err: Option<String>,
|
||||||
|
|
||||||
hash_to_learn: String,
|
hash_to_learn: String,
|
||||||
value_to_learn: String,
|
value_to_learn: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
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,95 +192,107 @@ impl eframe::App for MyApp {
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
ui.separator();
|
if state.debug_frames {
|
||||||
ui.heading("Raw OCR results");
|
ui.separator();
|
||||||
let mut raw_data_sorted: Vec<_> = state.raw_data.iter().collect();
|
ui.heading("Raw OCR results");
|
||||||
raw_data_sorted.sort();
|
let mut raw_data_sorted: Vec<_> = state.raw_data.iter().collect();
|
||||||
for (key, val) in raw_data_sorted {
|
raw_data_sorted.sort();
|
||||||
ui.label(format!("{}: {:?}", key, val));
|
for (key, val) in raw_data_sorted {
|
||||||
|
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| {
|
||||||
ui.heading("Current Race");
|
if let Some(race) = &mut state.current_race {
|
||||||
show_race_state(ui, "current", race);
|
ui.heading("Current Race");
|
||||||
}
|
show_race_state(ui, "current", 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, &format!("{}", i), race);
|
|
||||||
if let Some(img) = &race.screencap {
|
|
||||||
img.show_max_size(ui, Vec2::new(600.0, 500.0));
|
|
||||||
}
|
}
|
||||||
if !race.exported {
|
let len = state.past_races.len();
|
||||||
ui.label("Car:");
|
for (i, race) in state.past_races.iter_mut().enumerate() {
|
||||||
ui.text_edit_singleline(&mut race.car);
|
ui.separator();
|
||||||
ui.label("Track:");
|
ui.heading(format!("Race #{}", len - i));
|
||||||
ui.text_edit_singleline(&mut race.track);
|
show_race_state(ui, &format!("{}", i), race);
|
||||||
if ui.button("Export").clicked() {
|
if let Some(img) = &race.screencap {
|
||||||
match export_race_stats(race) {
|
img.show_max_size(ui, Vec2::new(600.0, 500.0));
|
||||||
Ok(_) => {
|
}
|
||||||
race.exported = true;
|
if !race.exported {
|
||||||
}
|
ui.label("Car:");
|
||||||
Err(e) => {
|
ui.text_edit_singleline(&mut race.car);
|
||||||
race.export_error = Some(format!("failed to export race: {:?}", e));
|
ui.label("Track:");
|
||||||
|
ui.text_edit_singleline(&mut race.track);
|
||||||
|
ui.label("Comments:");
|
||||||
|
ui.text_edit_singleline(&mut race.comments);
|
||||||
|
if ui.button("Export").clicked() {
|
||||||
|
match export_race_stats(race) {
|
||||||
|
Ok(_) => {
|
||||||
|
race.exported = true;
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
race.export_error =
|
||||||
|
Some(format!("failed to export race: {:?}", e));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if let Some(e) = &race.export_error {
|
||||||
|
ui.colored_label(Color32::RED, e);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
ui.label("Exported ✅");
|
||||||
}
|
}
|
||||||
if let Some(e) = &race.export_error {
|
|
||||||
ui.colored_label(Color32::RED, e);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
ui.label("Exported ✅");
|
|
||||||
}
|
}
|
||||||
}
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
if state.debug_frames {
|
if state.debug_frames {
|
||||||
egui::SidePanel::right("screenshots").show(ctx, |ui| {
|
egui::SidePanel::right("screenshots").show(ctx, |ui| {
|
||||||
let mut screenshots_sorted: Vec<_> = state.saved_frames.iter().collect();
|
egui::ScrollArea::vertical().show(ui, |ui| {
|
||||||
screenshots_sorted.sort_by_key(|(name, _)| name.clone());
|
let mut screenshots_sorted: Vec<_> = state.saved_frames.iter().collect();
|
||||||
for (name, image) in screenshots_sorted {
|
screenshots_sorted.sort_by_key(|(name, _)| name.clone());
|
||||||
ui.label(name);
|
for (name, image) in screenshots_sorted {
|
||||||
if ui.button(&image.img_hash).on_hover_text("Copy").clicked() {
|
ui.label(name);
|
||||||
ui.output().copied_text = image.img_hash.clone();
|
if ui.button(&image.img_hash).on_hover_text("Copy").clicked() {
|
||||||
}
|
ui.output().copied_text = image.img_hash.clone();
|
||||||
image.image.show_max_size(ui, ui.available_size());
|
|
||||||
}
|
|
||||||
|
|
||||||
if ui.button("Reload config").clicked() {
|
|
||||||
match Config::load() {
|
|
||||||
Ok(c) => {
|
|
||||||
state.config = Arc::new(c);
|
|
||||||
self.config_load_err = None;
|
|
||||||
}
|
}
|
||||||
Err(e) => {
|
image.image.show_max_size(ui, ui.available_size());
|
||||||
self.config_load_err = Some(format!("failed to load config: {:?}", e));
|
}
|
||||||
|
|
||||||
|
if ui.button("Reload config").clicked() {
|
||||||
|
match Config::load() {
|
||||||
|
Ok(c) => {
|
||||||
|
state.config = Arc::new(c);
|
||||||
|
self.config_load_err = None;
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
self.config_load_err =
|
||||||
|
Some(format!("failed to load config: {:?}", e));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
if let Some(e) = &self.config_load_err {
|
||||||
if let Some(e) = &self.config_load_err {
|
ui.colored_label(Color32::RED, e);
|
||||||
ui.colored_label(Color32::RED, e);
|
}
|
||||||
}
|
|
||||||
|
|
||||||
ui.separator();
|
ui.separator();
|
||||||
ui.label("Hash");
|
ui.label("Hash");
|
||||||
ui.text_edit_singleline(&mut self.hash_to_learn);
|
ui.text_edit_singleline(&mut self.hash_to_learn);
|
||||||
ui.label("Value");
|
ui.label("Value");
|
||||||
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_config.save().unwrap();
|
.learned_images
|
||||||
state.learned = Arc::new(learned_config);
|
.insert(self.hash_to_learn.clone(), self.value_to_learn.clone());
|
||||||
|
learned_config.save().unwrap();
|
||||||
|
state.learned = Arc::new(learned_config);
|
||||||
|
|
||||||
self.hash_to_learn = "".to_owned();
|
self.hash_to_learn = "".to_owned();
|
||||||
self.value_to_learn = "".to_owned();
|
self.value_to_learn = "".to_owned();
|
||||||
}
|
}
|
||||||
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -1,52 +1,57 @@
|
||||||
use std::{
|
use std::{
|
||||||
io::BufWriter,
|
io::BufWriter,
|
||||||
time::{Duration},
|
time::{Duration},
|
||||||
};
|
};
|
||||||
|
|
||||||
use crate::state::RaceState;
|
use crate::state::RaceState;
|
||||||
|
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
|
|
||||||
|
|
||||||
pub fn export_race_stats(race_stats: &mut RaceState) -> Result<()> {
|
pub fn export_race_stats(race_stats: &mut RaceState) -> Result<()> {
|
||||||
let race_name = race_stats.name();
|
let race_name = race_stats.name();
|
||||||
|
|
||||||
let file = std::fs::OpenOptions::new()
|
let file = std::fs::OpenOptions::new()
|
||||||
.create(true)
|
.create(true)
|
||||||
.append(true)
|
.append(true)
|
||||||
.open("race_stats.csv")?;
|
.open("race_stats.csv")?;
|
||||||
let writer = BufWriter::new(file);
|
let writer = BufWriter::new(file);
|
||||||
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 {
|
||||||
csv_writer.write_record(vec![
|
if lap.striked {
|
||||||
race_name.clone(),
|
continue;
|
||||||
race_stats.track.clone(),
|
}
|
||||||
race_stats.car.clone(),
|
|
||||||
lap.lap
|
csv_writer.write_record(vec![
|
||||||
.map(|x| x.to_string())
|
race_name.clone(),
|
||||||
.unwrap_or_else(|| "?".to_owned()),
|
race_stats.track.clone(),
|
||||||
format!(
|
race_stats.car.clone(),
|
||||||
"{:.3}",
|
lap.lap
|
||||||
lap.lap_time.unwrap_or(Duration::from_secs(0)).as_secs_f64()
|
.map(|x| x.to_string())
|
||||||
),
|
.unwrap_or_else(|| "?".to_owned()),
|
||||||
format!(
|
format!(
|
||||||
"{:.3}",
|
"{:.3}",
|
||||||
lap.best_time
|
lap.lap_time.unwrap_or(Duration::from_secs(0)).as_secs_f64()
|
||||||
.unwrap_or(Duration::from_secs(0))
|
),
|
||||||
.as_secs_f64()
|
format!(
|
||||||
),
|
"{:.3}",
|
||||||
lap.health
|
lap.best_time
|
||||||
.map(|x| x.to_string())
|
.unwrap_or(Duration::from_secs(0))
|
||||||
.unwrap_or_else(|| "".to_owned()),
|
.as_secs_f64()
|
||||||
lap.gas
|
),
|
||||||
.map(|x| x.to_string())
|
lap.health
|
||||||
.unwrap_or_else(|| "".to_owned()),
|
.map(|x| x.to_string())
|
||||||
lap.tyres
|
.unwrap_or_else(|| "".to_owned()),
|
||||||
.map(|x| x.to_string())
|
lap.gas
|
||||||
.unwrap_or_else(|| "".to_owned()),
|
.map(|x| x.to_string())
|
||||||
])?;
|
.unwrap_or_else(|| "".to_owned()),
|
||||||
}
|
lap.tyres
|
||||||
|
.map(|x| x.to_string())
|
||||||
Ok(())
|
.unwrap_or_else(|| "".to_owned()),
|
||||||
}
|
race_stats.comments.clone(),
|
||||||
|
])?;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
|
@ -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