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": [
|
||||
{
|
||||
"name": "lap",
|
||||
"x": 2300,
|
||||
"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": "best",
|
||||
"x": 2325,
|
||||
"y": 169,
|
||||
"width": 183,
|
||||
"height": 43
|
||||
},
|
||||
{
|
||||
"name": "lap_time",
|
||||
"x": 2325,
|
||||
"y": 222,
|
||||
"width": 183,
|
||||
"height": 43
|
||||
}
|
||||
],
|
||||
"track_region": {
|
||||
"name": "track",
|
||||
"x": 2020,
|
||||
"y": 1030,
|
||||
"width": 540,
|
||||
"height": 410,
|
||||
"threshold": 0.85
|
||||
},
|
||||
"ocr_server_endpoint": "https://tesserver.spruett.dev/"
|
||||
{
|
||||
"ocr_regions": [
|
||||
{
|
||||
"name": "lap",
|
||||
"x": 2300,
|
||||
"y": 46,
|
||||
"width": 140,
|
||||
"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": "best",
|
||||
"x": 2325,
|
||||
"y": 169,
|
||||
"width": 183,
|
||||
"height": 43
|
||||
},
|
||||
{
|
||||
"name": "lap_time",
|
||||
"x": 2325,
|
||||
"y": 222,
|
||||
"width": 183,
|
||||
"height": 43,
|
||||
"use_ocr_cache": false
|
||||
}
|
||||
],
|
||||
"track_region": {
|
||||
"name": "track",
|
||||
"x": 2020,
|
||||
"y": 1030,
|
||||
"width": 540,
|
||||
"height": 410,
|
||||
"threshold": 0.85
|
||||
},
|
||||
"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
|
||||
|
||||
import argparse
|
||||
import json
|
||||
from typing import Tuple
|
||||
|
||||
|
||||
def parse_resolution(resolution: str) -> Tuple[int, int]:
|
||||
a, b = resolution.split('x')
|
||||
return int(a), int(b)
|
||||
|
||||
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])
|
||||
|
||||
def scale_region(region, 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)
|
||||
region['x'] = round(x)
|
||||
region['y'] = round(y)
|
||||
region['width'] = round(width)
|
||||
region['height'] = round(height)
|
||||
|
||||
def main():
|
||||
argparser = argparse.ArgumentParser()
|
||||
argparser.add_argument("--from_res", help="From resolution", default="2560x1440")
|
||||
argparser.add_argument("--to_res", help="To resolution (e.g. 1920x1080)")
|
||||
argparser.add_argument("--config", help="Config file", default="config.json")
|
||||
|
||||
args = argparser.parse_args()
|
||||
|
||||
from_resolution = parse_resolution(args.from_res)
|
||||
to_resolution = parse_resolution(args.to_res)
|
||||
config = json.load(open(args.config, 'r'))
|
||||
for region in config['ocr_regions']:
|
||||
scale_region(region, from_resolution, to_resolution)
|
||||
scale_region(config['track_region'], from_resolution, to_resolution)
|
||||
print(json.dumps(config, indent=4))
|
||||
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
#!/usr/bin/env python3
|
||||
|
||||
import argparse
|
||||
import json
|
||||
from typing import Tuple
|
||||
|
||||
|
||||
def parse_resolution(resolution: str) -> Tuple[int, int]:
|
||||
a, b = resolution.split('x')
|
||||
return int(a), int(b)
|
||||
|
||||
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])
|
||||
|
||||
def scale_region(region, 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)
|
||||
region['x'] = round(x)
|
||||
region['y'] = round(y)
|
||||
region['width'] = round(width)
|
||||
region['height'] = round(height)
|
||||
|
||||
def main():
|
||||
argparser = argparse.ArgumentParser()
|
||||
argparser.add_argument("--from_res", help="From resolution", default="2560x1440")
|
||||
argparser.add_argument("--to_res", help="To resolution (e.g. 1920x1080)")
|
||||
argparser.add_argument("--config", help="Config file", default="config.json")
|
||||
|
||||
args = argparser.parse_args()
|
||||
|
||||
from_resolution = parse_resolution(args.from_res)
|
||||
to_resolution = parse_resolution(args.to_res)
|
||||
config = json.load(open(args.config, 'r'))
|
||||
for region in config['ocr_regions']:
|
||||
scale_region(region, from_resolution, to_resolution)
|
||||
scale_region(config['track_region'], from_resolution, to_resolution)
|
||||
print(json.dumps(config, indent=4))
|
||||
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
110
src/config.rs
110
src/config.rs
|
@ -1,56 +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,
|
||||
pub filter_threshold: Option<f64>,
|
||||
pub use_ocr_cache: Option<bool>,
|
||||
pub ocr_interval_ms: Option<u64>,
|
||||
}
|
||||
|
||||
impl Config {
|
||||
pub fn load() -> Result<Self> {
|
||||
load_or_make_default("config.json", include_str!("configs/config.default.json"))
|
||||
}
|
||||
}
|
||||
|
||||
#[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)?)
|
||||
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,
|
||||
pub filter_threshold: Option<f64>,
|
||||
pub use_ocr_cache: Option<bool>,
|
||||
pub ocr_interval_ms: Option<u64>,
|
||||
}
|
||||
|
||||
impl Config {
|
||||
pub fn load() -> Result<Self> {
|
||||
load_or_make_default("config.json", include_str!("configs/config.default.json"))
|
||||
}
|
||||
}
|
||||
|
||||
#[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)?)
|
||||
}
|
|
@ -1,55 +1,56 @@
|
|||
{
|
||||
"ocr_regions": [
|
||||
{
|
||||
"name": "lap",
|
||||
"x": 2300,
|
||||
"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": "best",
|
||||
"x": 2325,
|
||||
"y": 169,
|
||||
"width": 183,
|
||||
"height": 43
|
||||
},
|
||||
{
|
||||
"name": "lap_time",
|
||||
"x": 2325,
|
||||
"y": 222,
|
||||
"width": 183,
|
||||
"height": 43
|
||||
}
|
||||
],
|
||||
"track_region": {
|
||||
"name": "track",
|
||||
"x": 2020,
|
||||
"y": 1030,
|
||||
"width": 540,
|
||||
"height": 410,
|
||||
"threshold": 0.85
|
||||
},
|
||||
"ocr_server_endpoint": "https://tesserver.spruett.dev/"
|
||||
{
|
||||
"ocr_regions": [
|
||||
{
|
||||
"name": "lap",
|
||||
"x": 2300,
|
||||
"y": 46,
|
||||
"width": 142,
|
||||
"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": "best",
|
||||
"x": 2325,
|
||||
"y": 169,
|
||||
"width": 183,
|
||||
"height": 43
|
||||
},
|
||||
{
|
||||
"name": "lap_time",
|
||||
"x": 2325,
|
||||
"y": 222,
|
||||
"width": 183,
|
||||
"height": 43,
|
||||
"use_ocr_cache": false
|
||||
}
|
||||
],
|
||||
"track_region": {
|
||||
"name": "track",
|
||||
"x": 2020,
|
||||
"y": 1030,
|
||||
"width": 540,
|
||||
"height": 410,
|
||||
"threshold": 0.85
|
||||
},
|
||||
"ocr_server_endpoint": "https://tesserver.spruett.dev/"
|
||||
}
|
|
@ -1,4 +1,4 @@
|
|||
{
|
||||
"learned_images": {},
|
||||
"learned_tracks": {}
|
||||
{
|
||||
"learned_images": {},
|
||||
"learned_tracks": {}
|
||||
}
|
|
@ -57,6 +57,7 @@ fn merge_frames(prev: &ParsedFrame, next: &ParsedFrame) -> ParsedFrame {
|
|||
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),
|
||||
striked: false,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -158,7 +159,7 @@ fn run_loop_once(capturer: &mut Capturer, state: &SharedAppState) -> Result<()>
|
|||
}
|
||||
{
|
||||
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);
|
||||
state.raw_data = ocr_results;
|
||||
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) {
|
||||
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,
|
||||
height: usize,
|
||||
pub threshold: Option<f64>,
|
||||
pub use_ocr_cache: Option<bool>,
|
||||
}
|
||||
|
||||
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
|
||||
|
||||
mod capture;
|
||||
mod config;
|
||||
mod control_loop;
|
||||
mod image_processing;
|
||||
mod ocr;
|
||||
mod state;
|
||||
mod config;
|
||||
mod stats_writer;
|
||||
|
||||
use std::{
|
||||
sync::{Arc, Mutex},
|
||||
time::Duration, thread,
|
||||
thread,
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
|
||||
use config::{Config, LearnedConfig};
|
||||
use eframe::{
|
||||
egui::{self, Ui},
|
||||
epaint::Color32, emath::Vec2,
|
||||
egui::{self, Ui, Visuals},
|
||||
emath::Vec2,
|
||||
epaint::Color32,
|
||||
};
|
||||
use state::{AppState, RaceState, SharedAppState};
|
||||
use state::{AppState, RaceState, SharedAppState, ParsedFrame};
|
||||
use stats_writer::export_race_stats;
|
||||
|
||||
fn main() -> anyhow::Result<()> {
|
||||
|
@ -92,18 +94,23 @@ struct MyApp {
|
|||
state: SharedAppState,
|
||||
|
||||
config_load_err: Option<String>,
|
||||
|
||||
|
||||
hash_to_learn: String,
|
||||
value_to_learn: String,
|
||||
}
|
||||
|
||||
impl MyApp {
|
||||
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| {
|
||||
ui.label("Lap");
|
||||
ui.label("Time");
|
||||
|
@ -113,9 +120,9 @@ fn show_race_state(ui: &mut Ui, race_name: &str, race: &RaceState) {
|
|||
ui.label("Gas");
|
||||
ui.label("Tyres");
|
||||
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 {
|
||||
let prev_lap = race.laps.get(i - 1);
|
||||
ui.label(format!("#{}", lap.lap.unwrap_or(i + 1)));
|
||||
|
||||
ui.label(format_time(lap_time));
|
||||
|
@ -141,14 +148,24 @@ fn show_race_state(ui: &mut Ui, race_name: &str, race: &RaceState) {
|
|||
&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();
|
||||
}
|
||||
prev_lap = Some(lap);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
impl eframe::App for MyApp {
|
||||
fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) {
|
||||
ctx.set_visuals(Visuals::dark());
|
||||
let mut state = self.state.lock().unwrap();
|
||||
egui::SidePanel::left("frame").show(ctx, |ui| {
|
||||
if let Some(frame) = &state.last_frame {
|
||||
|
@ -175,95 +192,107 @@ impl eframe::App for MyApp {
|
|||
));
|
||||
}
|
||||
|
||||
ui.separator();
|
||||
ui.heading("Raw OCR results");
|
||||
let mut raw_data_sorted: Vec<_> = state.raw_data.iter().collect();
|
||||
raw_data_sorted.sort();
|
||||
for (key, val) in raw_data_sorted {
|
||||
ui.label(format!("{}: {:?}", key, val));
|
||||
if state.debug_frames {
|
||||
ui.separator();
|
||||
ui.heading("Raw OCR results");
|
||||
let mut raw_data_sorted: Vec<_> = state.raw_data.iter().collect();
|
||||
raw_data_sorted.sort();
|
||||
for (key, val) in raw_data_sorted {
|
||||
ui.label(format!("{}: {:?}", key, val));
|
||||
}
|
||||
}
|
||||
|
||||
ui.separator();
|
||||
ui.checkbox(&mut state.debug_frames, "Debug OCR regions");
|
||||
});
|
||||
egui::CentralPanel::default().show(ctx, |ui| {
|
||||
if let Some(race) = &state.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));
|
||||
egui::ScrollArea::vertical().show(ui, |ui| {
|
||||
if let Some(race) = &mut state.current_race {
|
||||
ui.heading("Current Race");
|
||||
show_race_state(ui, "current", race);
|
||||
}
|
||||
if !race.exported {
|
||||
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() {
|
||||
match export_race_stats(race) {
|
||||
Ok(_) => {
|
||||
race.exported = true;
|
||||
}
|
||||
Err(e) => {
|
||||
race.export_error = Some(format!("failed to export race: {:?}", e));
|
||||
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 {
|
||||
ui.label("Car:");
|
||||
ui.text_edit_singleline(&mut race.car);
|
||||
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 {
|
||||
egui::SidePanel::right("screenshots").show(ctx, |ui| {
|
||||
let mut screenshots_sorted: Vec<_> = state.saved_frames.iter().collect();
|
||||
screenshots_sorted.sort_by_key(|(name, _)| name.clone());
|
||||
for (name, image) in screenshots_sorted {
|
||||
ui.label(name);
|
||||
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;
|
||||
egui::ScrollArea::vertical().show(ui, |ui| {
|
||||
let mut screenshots_sorted: Vec<_> = state.saved_frames.iter().collect();
|
||||
screenshots_sorted.sort_by_key(|(name, _)| name.clone());
|
||||
for (name, image) in screenshots_sorted {
|
||||
ui.label(name);
|
||||
if ui.button(&image.img_hash).on_hover_text("Copy").clicked() {
|
||||
ui.output().copied_text = image.img_hash.clone();
|
||||
}
|
||||
Err(e) => {
|
||||
self.config_load_err = Some(format!("failed to load config: {:?}", e));
|
||||
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) => {
|
||||
self.config_load_err =
|
||||
Some(format!("failed to load config: {:?}", e));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if let Some(e) = &self.config_load_err {
|
||||
ui.colored_label(Color32::RED, e);
|
||||
}
|
||||
if let Some(e) = &self.config_load_err {
|
||||
ui.colored_label(Color32::RED, e);
|
||||
}
|
||||
|
||||
ui.separator();
|
||||
ui.label("Hash");
|
||||
ui.text_edit_singleline(&mut self.hash_to_learn);
|
||||
ui.label("Value");
|
||||
ui.text_edit_singleline(&mut self.value_to_learn);
|
||||
if ui.button("Learn").clicked() {
|
||||
let mut learned_config = (*state.learned).clone();
|
||||
learned_config.learned_images.insert(self.hash_to_learn.clone(), self.value_to_learn.clone());
|
||||
learned_config.save().unwrap();
|
||||
state.learned = Arc::new(learned_config);
|
||||
ui.separator();
|
||||
ui.label("Hash");
|
||||
ui.text_edit_singleline(&mut self.hash_to_learn);
|
||||
ui.label("Value");
|
||||
ui.text_edit_singleline(&mut self.value_to_learn);
|
||||
if ui.button("Learn").clicked() {
|
||||
let mut learned_config = (*state.learned).clone();
|
||||
learned_config
|
||||
.learned_images
|
||||
.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.value_to_learn = "".to_owned();
|
||||
}
|
||||
self.hash_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();
|
||||
locked.get(&hash).cloned()
|
||||
};
|
||||
if let Some(cached) = cached {
|
||||
cached
|
||||
let use_cache = region.use_ocr_cache.unwrap_or(true) && config.use_ocr_cache.unwrap_or(true);
|
||||
if cached.is_some() && use_cache {
|
||||
cached.unwrap()
|
||||
} else {
|
||||
match run_ocr(&filtered_image, &config.ocr_server_endpoint).await {
|
||||
Ok(v) => {
|
||||
|
|
|
@ -17,6 +17,8 @@ pub struct ParsedFrame {
|
|||
|
||||
pub best_time: Option<Duration>,
|
||||
pub lap_time: Option<Duration>,
|
||||
|
||||
pub striked: bool,
|
||||
}
|
||||
|
||||
fn parse_duration(time: &str) -> Option<Duration> {
|
||||
|
@ -54,6 +56,7 @@ impl ParsedFrame {
|
|||
tyres: parse_to_0_100(raw.get("tyres")),
|
||||
best_time: parse_to_duration(raw.get("best")),
|
||||
lap_time: parse_to_duration(raw.get("lap_time")),
|
||||
striked: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -71,6 +74,7 @@ pub struct RaceState {
|
|||
|
||||
pub car: String,
|
||||
pub track: String,
|
||||
pub comments: String,
|
||||
}
|
||||
|
||||
impl RaceState {
|
||||
|
|
|
@ -1,52 +1,57 @@
|
|||
use std::{
|
||||
io::BufWriter,
|
||||
time::{Duration},
|
||||
};
|
||||
|
||||
use crate::state::RaceState;
|
||||
|
||||
use anyhow::Result;
|
||||
|
||||
|
||||
pub fn export_race_stats(race_stats: &mut RaceState) -> Result<()> {
|
||||
let race_name = race_stats.name();
|
||||
|
||||
let file = std::fs::OpenOptions::new()
|
||||
.create(true)
|
||||
.append(true)
|
||||
.open("race_stats.csv")?;
|
||||
let writer = BufWriter::new(file);
|
||||
let mut csv_writer = csv::Writer::from_writer(writer);
|
||||
|
||||
for lap in &race_stats.laps {
|
||||
csv_writer.write_record(vec![
|
||||
race_name.clone(),
|
||||
race_stats.track.clone(),
|
||||
race_stats.car.clone(),
|
||||
lap.lap
|
||||
.map(|x| x.to_string())
|
||||
.unwrap_or_else(|| "?".to_owned()),
|
||||
format!(
|
||||
"{:.3}",
|
||||
lap.lap_time.unwrap_or(Duration::from_secs(0)).as_secs_f64()
|
||||
),
|
||||
format!(
|
||||
"{:.3}",
|
||||
lap.best_time
|
||||
.unwrap_or(Duration::from_secs(0))
|
||||
.as_secs_f64()
|
||||
),
|
||||
lap.health
|
||||
.map(|x| x.to_string())
|
||||
.unwrap_or_else(|| "".to_owned()),
|
||||
lap.gas
|
||||
.map(|x| x.to_string())
|
||||
.unwrap_or_else(|| "".to_owned()),
|
||||
lap.tyres
|
||||
.map(|x| x.to_string())
|
||||
.unwrap_or_else(|| "".to_owned()),
|
||||
])?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
use std::{
|
||||
io::BufWriter,
|
||||
time::{Duration},
|
||||
};
|
||||
|
||||
use crate::state::RaceState;
|
||||
|
||||
use anyhow::Result;
|
||||
|
||||
|
||||
pub fn export_race_stats(race_stats: &mut RaceState) -> Result<()> {
|
||||
let race_name = race_stats.name();
|
||||
|
||||
let file = std::fs::OpenOptions::new()
|
||||
.create(true)
|
||||
.append(true)
|
||||
.open("race_stats.csv")?;
|
||||
let writer = BufWriter::new(file);
|
||||
let mut csv_writer = csv::Writer::from_writer(writer);
|
||||
|
||||
for lap in &race_stats.laps {
|
||||
if lap.striked {
|
||||
continue;
|
||||
}
|
||||
|
||||
csv_writer.write_record(vec![
|
||||
race_name.clone(),
|
||||
race_stats.track.clone(),
|
||||
race_stats.car.clone(),
|
||||
lap.lap
|
||||
.map(|x| x.to_string())
|
||||
.unwrap_or_else(|| "?".to_owned()),
|
||||
format!(
|
||||
"{:.3}",
|
||||
lap.lap_time.unwrap_or(Duration::from_secs(0)).as_secs_f64()
|
||||
),
|
||||
format!(
|
||||
"{:.3}",
|
||||
lap.best_time
|
||||
.unwrap_or(Duration::from_secs(0))
|
||||
.as_secs_f64()
|
||||
),
|
||||
lap.health
|
||||
.map(|x| x.to_string())
|
||||
.unwrap_or_else(|| "".to_owned()),
|
||||
lap.gas
|
||||
.map(|x| x.to_string())
|
||||
.unwrap_or_else(|| "".to_owned()),
|
||||
lap.tyres
|
||||
.map(|x| x.to_string())
|
||||
.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