initial stats writing
This commit is contained in:
parent
3bffdd1b8d
commit
9af050dde3
|
@ -106,6 +106,18 @@ version = "0.1.6"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "0d8c1fef690941d3e7788d328517591fecc684c084084702d6ff1641e993699a"
|
checksum = "0d8c1fef690941d3e7788d328517591fecc684c084084702d6ff1641e993699a"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "bstr"
|
||||||
|
version = "0.2.17"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "ba3569f383e8f1598449f1a423e72e99569137b47740b1da11ef19af3d5c3223"
|
||||||
|
dependencies = [
|
||||||
|
"lazy_static",
|
||||||
|
"memchr",
|
||||||
|
"regex-automata",
|
||||||
|
"serde",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "bumpalo"
|
name = "bumpalo"
|
||||||
version = "3.9.1"
|
version = "3.9.1"
|
||||||
|
@ -387,6 +399,28 @@ dependencies = [
|
||||||
"lazy_static",
|
"lazy_static",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "csv"
|
||||||
|
version = "1.1.6"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "22813a6dc45b335f9bade10bf7271dc477e81113e89eb251a0bc2a8a81c536e1"
|
||||||
|
dependencies = [
|
||||||
|
"bstr",
|
||||||
|
"csv-core",
|
||||||
|
"itoa 0.4.8",
|
||||||
|
"ryu",
|
||||||
|
"serde",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "csv-core"
|
||||||
|
version = "0.1.10"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "2b2466559f260f48ad25fe6317b3c8dac77b5bdb5763ac7d9d6103530663bc90"
|
||||||
|
dependencies = [
|
||||||
|
"memchr",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "cty"
|
name = "cty"
|
||||||
version = "0.2.2"
|
version = "0.2.2"
|
||||||
|
@ -938,7 +972,7 @@ checksum = "ff8670570af52249509a86f5e3e18a08c60b177071826898fde8997cf5f6bfbb"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bytes",
|
"bytes",
|
||||||
"fnv",
|
"fnv",
|
||||||
"itoa",
|
"itoa 1.0.2",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
@ -979,7 +1013,7 @@ dependencies = [
|
||||||
"http-body",
|
"http-body",
|
||||||
"httparse",
|
"httparse",
|
||||||
"httpdate",
|
"httpdate",
|
||||||
"itoa",
|
"itoa 1.0.2",
|
||||||
"pin-project-lite",
|
"pin-project-lite",
|
||||||
"socket2",
|
"socket2",
|
||||||
"tokio",
|
"tokio",
|
||||||
|
@ -1102,6 +1136,12 @@ version = "2.5.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "879d54834c8c76457ef4293a689b2a8c59b076067ad77b15efafbb05f92a592b"
|
checksum = "879d54834c8c76457ef4293a689b2a8c59b076067ad77b15efafbb05f92a592b"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "itoa"
|
||||||
|
version = "0.4.8"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "b71991ff56294aa922b450139ee08b3bfc70982c6b2c7562771375cf73542dd4"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "itoa"
|
name = "itoa"
|
||||||
version = "1.0.2"
|
version = "1.0.2"
|
||||||
|
@ -1791,6 +1831,12 @@ dependencies = [
|
||||||
"bitflags",
|
"bitflags",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "regex-automata"
|
||||||
|
version = "0.1.10"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "remove_dir_all"
|
name = "remove_dir_all"
|
||||||
version = "0.5.3"
|
version = "0.5.3"
|
||||||
|
@ -1999,7 +2045,7 @@ version = "1.0.81"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "9b7ce2b32a1aed03c558dc61a5cd328f15aff2dbc17daad8fb8af04d2100e15c"
|
checksum = "9b7ce2b32a1aed03c558dc61a5cd328f15aff2dbc17daad8fb8af04d2100e15c"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"itoa",
|
"itoa 1.0.2",
|
||||||
"ryu",
|
"ryu",
|
||||||
"serde",
|
"serde",
|
||||||
]
|
]
|
||||||
|
@ -2011,7 +2057,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd"
|
checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"form_urlencoded",
|
"form_urlencoded",
|
||||||
"itoa",
|
"itoa 1.0.2",
|
||||||
"ryu",
|
"ryu",
|
||||||
"serde",
|
"serde",
|
||||||
]
|
]
|
||||||
|
@ -2129,6 +2175,7 @@ name = "supper"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
|
"csv",
|
||||||
"eframe",
|
"eframe",
|
||||||
"egui_extras",
|
"egui_extras",
|
||||||
"ehttp",
|
"ehttp",
|
||||||
|
|
|
@ -23,3 +23,5 @@ tokio = { version = "1", features = ["full"] }
|
||||||
reqwest = { version = "0.11", features = ["json"] }
|
reqwest = { version = "0.11", features = ["json"] }
|
||||||
|
|
||||||
img_hash = "3"
|
img_hash = "3"
|
||||||
|
|
||||||
|
csv = "1"
|
15
config.json
15
config.json
|
@ -2,7 +2,7 @@
|
||||||
"ocr_regions": [
|
"ocr_regions": [
|
||||||
{
|
{
|
||||||
"name": "lap",
|
"name": "lap",
|
||||||
"x": 2290,
|
"x": 2300,
|
||||||
"y": 46,
|
"y": 46,
|
||||||
"width": 145,
|
"width": 145,
|
||||||
"height": 90
|
"height": 90
|
||||||
|
@ -29,20 +29,27 @@
|
||||||
"height": 24
|
"height": 24
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "lap_time",
|
"name": "best",
|
||||||
"x": 2325,
|
"x": 2325,
|
||||||
"y": 169,
|
"y": 169,
|
||||||
"width": 183,
|
"width": 183,
|
||||||
"height": 43
|
"height": 43
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "best_time",
|
"name": "lap_time",
|
||||||
"x": 2325,
|
"x": 2325,
|
||||||
"y": 222,
|
"y": 222,
|
||||||
"width": 183,
|
"width": 183,
|
||||||
"height": 43
|
"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_server_endpoint": "https://tesserver.spruett.dev/"
|
||||||
}
|
}
|
|
@ -0,0 +1,47 @@
|
||||||
|
{
|
||||||
|
"ocr_regions": [
|
||||||
|
{
|
||||||
|
"name": "lap",
|
||||||
|
"x": 1718,
|
||||||
|
"y": 34,
|
||||||
|
"width": 109,
|
||||||
|
"height": 68
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "health",
|
||||||
|
"x": 68,
|
||||||
|
"y": 1023,
|
||||||
|
"width": 39,
|
||||||
|
"height": 18
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "gas",
|
||||||
|
"x": 156,
|
||||||
|
"y": 1023,
|
||||||
|
"width": 39,
|
||||||
|
"height": 18
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "tyres",
|
||||||
|
"x": 244,
|
||||||
|
"y": 1023,
|
||||||
|
"width": 39,
|
||||||
|
"height": 18
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "best_time",
|
||||||
|
"x": 1744,
|
||||||
|
"y": 127,
|
||||||
|
"width": 137,
|
||||||
|
"height": 32
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "lap_time",
|
||||||
|
"x": 1744,
|
||||||
|
"y": 166,
|
||||||
|
"width": 137,
|
||||||
|
"height": 32
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"ocr_server_endpoint": "https://tesserver.spruett.dev/"
|
||||||
|
}
|
|
@ -0,0 +1,41 @@
|
||||||
|
#!/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)
|
||||||
|
print(json.dumps(config, indent=4))
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
main()
|
|
@ -2,7 +2,7 @@ use std::{time::Duration, thread};
|
||||||
|
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
use image::{RgbImage, Rgb};
|
use image::{RgbImage, Rgb};
|
||||||
use scrap::{Capturer, Display};
|
use scrap::Capturer;
|
||||||
|
|
||||||
fn get_raw_frame(capturer: &mut Capturer) -> Result<Vec<u8>> {
|
fn get_raw_frame(capturer: &mut Capturer) -> Result<Vec<u8>> {
|
||||||
loop {
|
loop {
|
||||||
|
|
|
@ -10,15 +10,14 @@ 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 use_ocr_cache: Option<bool>,
|
||||||
}
|
}
|
||||||
|
|
||||||
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"))
|
||||||
}
|
}
|
||||||
pub fn save(&self) -> Result<()> {
|
|
||||||
save_json_config("config.json", self)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Default, Serialize, Deserialize, Clone)]
|
#[derive(Default, Serialize, Deserialize, Clone)]
|
||||||
|
|
|
@ -29,14 +29,14 @@
|
||||||
"height": 24
|
"height": 24
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "lap_time",
|
"name": "best",
|
||||||
"x": 2325,
|
"x": 2325,
|
||||||
"y": 169,
|
"y": 169,
|
||||||
"width": 183,
|
"width": 183,
|
||||||
"height": 43
|
"height": 43
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "best_time",
|
"name": "lap_time",
|
||||||
"x": 2325,
|
"x": 2325,
|
||||||
"y": 222,
|
"y": 222,
|
||||||
"width": 183,
|
"width": 183,
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
use std::{
|
use std::{
|
||||||
collections::HashMap,
|
collections::HashMap,
|
||||||
time::{Duration, Instant}, thread,
|
thread,
|
||||||
|
time::{Duration, Instant},
|
||||||
};
|
};
|
||||||
|
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
|
@ -8,12 +9,11 @@ use egui_extras::RetainedImage;
|
||||||
use image::RgbImage;
|
use image::RgbImage;
|
||||||
use scrap::{Capturer, Display};
|
use scrap::{Capturer, Display};
|
||||||
|
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
capture,
|
capture,
|
||||||
image_processing::{self, hash_image},
|
image_processing::{self, hash_image, Region, extract_and_filter},
|
||||||
ocr,
|
ocr,
|
||||||
state::{AppState, DebugOcrFrame, ParsedFrame, RaceState, SharedAppState},
|
state::{AppState, DebugOcrFrame, ParsedFrame, RaceState, SharedAppState}, config::Config,
|
||||||
};
|
};
|
||||||
|
|
||||||
fn is_finished_lap(state: &AppState, frame: &ParsedFrame) -> bool {
|
fn is_finished_lap(state: &AppState, frame: &ParsedFrame) -> bool {
|
||||||
|
@ -104,24 +104,15 @@ fn handle_new_frame(state: &mut AppState, frame: ParsedFrame, image: &RgbImage)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn run_loop_once(state: &SharedAppState) -> Result<()> {
|
fn add_saved_frame(
|
||||||
let mut capturer = Capturer::new(Display::primary()?)?;
|
saved_frames: &mut HashMap<String, DebugOcrFrame>,
|
||||||
let config = state.lock().unwrap().config.clone();
|
frame: &RgbImage,
|
||||||
let learned_config = state.lock().unwrap().learned.clone();
|
region: &Region,
|
||||||
let frame = capture::get_frame(&mut capturer)?;
|
config: &Config,
|
||||||
let ocr_results = ocr::ocr_all_regions(&frame, config.clone(), learned_config.clone());
|
) {
|
||||||
|
let extracted = extract_and_filter(frame, region);
|
||||||
let mut saved_frames = HashMap::new();
|
let retained =
|
||||||
|
RetainedImage::from_image_bytes(®ion.name, &image_processing::to_png_bytes(&extracted))
|
||||||
if state.lock().unwrap().debug_frames {
|
|
||||||
let hasher = img_hash::HasherConfig::new().to_hasher();
|
|
||||||
for region in &config.ocr_regions {
|
|
||||||
let mut extracted = image_processing::extract_region(&frame, region);
|
|
||||||
image_processing::filter_to_white(&mut extracted);
|
|
||||||
let retained = RetainedImage::from_image_bytes(
|
|
||||||
®ion.name,
|
|
||||||
&image_processing::to_png_bytes(&extracted),
|
|
||||||
)
|
|
||||||
.unwrap();
|
.unwrap();
|
||||||
let hash = hash_image(&extracted);
|
let hash = hash_image(&extracted);
|
||||||
saved_frames.insert(
|
saved_frames.insert(
|
||||||
|
@ -133,6 +124,28 @@ fn run_loop_once(state: &SharedAppState) -> Result<()> {
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn run_loop_once(capturer: &mut Capturer, state: &SharedAppState) -> Result<()> {
|
||||||
|
let (config, learned_config, ocr_cache) = {
|
||||||
|
let locked = state.lock().unwrap();
|
||||||
|
(
|
||||||
|
locked.config.clone(),
|
||||||
|
locked.learned.clone(),
|
||||||
|
locked.ocr_cache.clone(),
|
||||||
|
)
|
||||||
|
};
|
||||||
|
let frame = capture::get_frame(capturer)?;
|
||||||
|
let ocr_results = ocr::ocr_all_regions(&frame, config.clone(), learned_config, ocr_cache);
|
||||||
|
|
||||||
|
let mut saved_frames = HashMap::new();
|
||||||
|
|
||||||
|
if state.lock().unwrap().debug_frames {
|
||||||
|
for region in &config.ocr_regions {
|
||||||
|
add_saved_frame(&mut saved_frames, &frame, region, config.as_ref());
|
||||||
|
}
|
||||||
|
if let Some(track_region) = &config.track_region {
|
||||||
|
add_saved_frame(&mut saved_frames, &frame, track_region, config.as_ref());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
{
|
{
|
||||||
let mut state = state.lock().unwrap();
|
let mut state = state.lock().unwrap();
|
||||||
|
@ -145,8 +158,9 @@ fn run_loop_once(state: &SharedAppState) -> Result<()> {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn run_control_loop(state: SharedAppState) {
|
pub fn run_control_loop(state: SharedAppState) {
|
||||||
|
let mut capturer = Capturer::new(Display::primary().unwrap()).unwrap();
|
||||||
loop {
|
loop {
|
||||||
if let Err(e) = run_loop_once(&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(500));
|
thread::sleep(Duration::from_millis(500));
|
||||||
|
|
|
@ -1,6 +1,8 @@
|
||||||
use image::{codecs::png::PngEncoder, ColorType, ImageEncoder, Rgb, RgbImage};
|
use image::{codecs::png::PngEncoder, ColorType, ImageEncoder, Rgb, RgbImage};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
use crate::config::Config;
|
||||||
|
|
||||||
#[derive(Clone, Deserialize, Serialize)]
|
#[derive(Clone, Deserialize, Serialize)]
|
||||||
pub struct Region {
|
pub struct Region {
|
||||||
pub name: String,
|
pub name: String,
|
||||||
|
@ -8,6 +10,13 @@ pub struct Region {
|
||||||
y: usize,
|
y: usize,
|
||||||
width: usize,
|
width: usize,
|
||||||
height: usize,
|
height: usize,
|
||||||
|
pub threshold: Option<f64>,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn extract_and_filter(image: &RgbImage, region: &Region) -> RgbImage {
|
||||||
|
let mut extracted = extract_region(image, region);
|
||||||
|
filter_to_white(&mut extracted, ®ion.threshold);
|
||||||
|
extracted
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn extract_region(image: &RgbImage, region: &Region) -> RgbImage {
|
pub fn extract_region(image: &RgbImage, region: &Region) -> RgbImage {
|
||||||
|
@ -24,9 +33,9 @@ pub fn extract_region(image: &RgbImage, region: &Region) -> RgbImage {
|
||||||
buffer
|
buffer
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn filter_to_white(image: &mut RgbImage) {
|
pub fn filter_to_white(image: &mut RgbImage, threshold: &Option<f64>) {
|
||||||
let threshold = 0.98;
|
let threshold = threshold.unwrap_or(0.95);
|
||||||
let variance_threshold = 0.02;
|
let variance_threshold = 1.0 - threshold;
|
||||||
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() {
|
||||||
|
|
26
src/main.rs
26
src/main.rs
|
@ -6,6 +6,7 @@ mod image_processing;
|
||||||
mod ocr;
|
mod ocr;
|
||||||
mod state;
|
mod state;
|
||||||
mod config;
|
mod config;
|
||||||
|
mod stats_writer;
|
||||||
|
|
||||||
use std::{
|
use std::{
|
||||||
sync::{Arc, Mutex},
|
sync::{Arc, Mutex},
|
||||||
|
@ -18,6 +19,7 @@ use eframe::{
|
||||||
epaint::Color32, emath::Vec2,
|
epaint::Color32, emath::Vec2,
|
||||||
};
|
};
|
||||||
use state::{AppState, RaceState, SharedAppState};
|
use state::{AppState, RaceState, SharedAppState};
|
||||||
|
use stats_writer::export_race_stats;
|
||||||
|
|
||||||
fn main() -> anyhow::Result<()> {
|
fn main() -> anyhow::Result<()> {
|
||||||
let mut app_state = AppState::default();
|
let mut app_state = AppState::default();
|
||||||
|
@ -101,8 +103,8 @@ impl MyApp {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn show_race_state(ui: &mut Ui, race: &RaceState) {
|
fn show_race_state(ui: &mut Ui, race_name: &str, race: &RaceState) {
|
||||||
egui::Grid::new("current-race").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");
|
||||||
ui.label("Δ Previous");
|
ui.label("Δ Previous");
|
||||||
|
@ -187,22 +189,36 @@ impl eframe::App for MyApp {
|
||||||
egui::CentralPanel::default().show(ctx, |ui| {
|
egui::CentralPanel::default().show(ctx, |ui| {
|
||||||
if let Some(race) = &state.current_race {
|
if let Some(race) = &state.current_race {
|
||||||
ui.heading("Current Race");
|
ui.heading("Current Race");
|
||||||
show_race_state(ui, race);
|
show_race_state(ui, "current", race);
|
||||||
}
|
}
|
||||||
let len = state.past_races.len();
|
let len = state.past_races.len();
|
||||||
for (i, race) in state.past_races.iter_mut().enumerate() {
|
for (i, race) in state.past_races.iter_mut().enumerate() {
|
||||||
ui.separator();
|
ui.separator();
|
||||||
ui.heading(format!("Race #{}", len - i));
|
ui.heading(format!("Race #{}", len - i));
|
||||||
show_race_state(ui, race);
|
show_race_state(ui, &format!("{}", i), race);
|
||||||
if let Some(img) = &race.screencap {
|
if let Some(img) = &race.screencap {
|
||||||
img.show_max_size(ui, Vec2::new(600.0, 500.0));
|
img.show_max_size(ui, Vec2::new(600.0, 500.0));
|
||||||
}
|
}
|
||||||
|
if !race.exported {
|
||||||
ui.label("Car:");
|
ui.label("Car:");
|
||||||
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);
|
||||||
if ui.button("Export").clicked() {
|
if ui.button("Export").clicked() {
|
||||||
println!("EXPORT: TODO");
|
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 ✅");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
33
src/ocr.rs
33
src/ocr.rs
|
@ -1,6 +1,6 @@
|
||||||
use std::{
|
use std::{
|
||||||
collections::HashMap,
|
collections::HashMap,
|
||||||
sync::{Arc, Mutex},
|
sync::{Arc, Mutex, RwLock},
|
||||||
};
|
};
|
||||||
|
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
|
@ -9,7 +9,7 @@ use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
config::{Config, LearnedConfig},
|
config::{Config, LearnedConfig},
|
||||||
image_processing::{extract_region, filter_to_white, hash_image, Region},
|
image_processing::{extract_region, filter_to_white, hash_image, extract_and_filter},
|
||||||
};
|
};
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, Debug)]
|
#[derive(Serialize, Deserialize, Debug)]
|
||||||
|
@ -54,26 +54,41 @@ pub async fn ocr_all_regions(
|
||||||
image: &RgbImage,
|
image: &RgbImage,
|
||||||
config: Arc<Config>,
|
config: Arc<Config>,
|
||||||
learned: Arc<LearnedConfig>,
|
learned: Arc<LearnedConfig>,
|
||||||
|
ocr_cache: Arc<RwLock<HashMap<String, Option<String>>>>,
|
||||||
) -> HashMap<String, Option<String>> {
|
) -> HashMap<String, Option<String>> {
|
||||||
let results = Arc::new(Mutex::new(HashMap::new()));
|
let results = Arc::new(Mutex::new(HashMap::new()));
|
||||||
|
|
||||||
let mut handles = Vec::new();
|
let mut handles = Vec::new();
|
||||||
for region in &config.ocr_regions {
|
for region in &config.ocr_regions {
|
||||||
let filtered_image = extract_region(image, region);
|
let filtered_image = extract_and_filter(image, region);
|
||||||
let region = region.clone();
|
let region = region.clone();
|
||||||
let results = results.clone();
|
let results = results.clone();
|
||||||
let config = config.clone();
|
let config = config.clone();
|
||||||
let learned = learned.clone();
|
let learned = learned.clone();
|
||||||
|
let ocr_cache = ocr_cache.clone();
|
||||||
handles.push(tokio::spawn(async move {
|
handles.push(tokio::spawn(async move {
|
||||||
let mut image = filtered_image;
|
let filtered_image = filtered_image;
|
||||||
filter_to_white(&mut image);
|
let hash = hash_image(&filtered_image);
|
||||||
let hash = hash_image(&image);
|
|
||||||
let value = if let Some(learned_value) = learned.learned_images.get(&hash) {
|
let value = if let Some(learned_value) = learned.learned_images.get(&hash) {
|
||||||
Some(learned_value.clone())
|
Some(learned_value.clone())
|
||||||
} else {
|
} else {
|
||||||
run_ocr(&image, &config.ocr_server_endpoint)
|
let cached = {
|
||||||
.await
|
let locked = ocr_cache.read().unwrap();
|
||||||
.unwrap_or(None)
|
locked.get(&hash).cloned()
|
||||||
|
};
|
||||||
|
if let Some(cached) = cached {
|
||||||
|
cached
|
||||||
|
} else {
|
||||||
|
match run_ocr(&filtered_image, &config.ocr_server_endpoint).await {
|
||||||
|
Ok(v) => {
|
||||||
|
if config.use_ocr_cache.unwrap_or(true) {
|
||||||
|
ocr_cache.write().unwrap().insert(hash.clone(), v.clone());
|
||||||
|
}
|
||||||
|
v
|
||||||
|
}
|
||||||
|
Err(_) => None
|
||||||
|
}
|
||||||
|
}
|
||||||
};
|
};
|
||||||
results.lock().unwrap().insert(region.name, value);
|
results.lock().unwrap().insert(region.name, value);
|
||||||
}));
|
}));
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
use std::{sync::{Arc, Mutex}, time::{Duration, Instant}, collections::{HashMap, VecDeque}};
|
use std::{sync::{Arc, Mutex, RwLock}, time::{Duration, Instant}, collections::{HashMap, VecDeque}};
|
||||||
|
|
||||||
use egui_extras::RetainedImage;
|
use egui_extras::RetainedImage;
|
||||||
use image::RgbImage;
|
use image::RgbImage;
|
||||||
|
@ -65,6 +65,7 @@ pub struct RaceState {
|
||||||
pub screencap: Option<RetainedImage>,
|
pub screencap: Option<RetainedImage>,
|
||||||
|
|
||||||
pub exported: bool,
|
pub exported: bool,
|
||||||
|
pub export_error: Option<String>,
|
||||||
|
|
||||||
pub car: String,
|
pub car: String,
|
||||||
pub track: String,
|
pub track: String,
|
||||||
|
@ -92,6 +93,8 @@ pub struct AppState {
|
||||||
|
|
||||||
pub config: Arc<Config>,
|
pub config: Arc<Config>,
|
||||||
pub learned: Arc<LearnedConfig>,
|
pub learned: Arc<LearnedConfig>,
|
||||||
|
|
||||||
|
pub ocr_cache: Arc<RwLock<HashMap<String, Option<String>>>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub type SharedAppState = Arc<Mutex<AppState>>;
|
pub type SharedAppState = Arc<Mutex<AppState>>;
|
|
@ -0,0 +1,45 @@
|
||||||
|
use std::{
|
||||||
|
io::BufWriter,
|
||||||
|
time::{Duration, Instant},
|
||||||
|
};
|
||||||
|
|
||||||
|
use crate::state::RaceState;
|
||||||
|
|
||||||
|
use anyhow::Result;
|
||||||
|
|
||||||
|
pub fn export_race_stats(race_stats: &mut RaceState) -> Result<()> {
|
||||||
|
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_stats.track.clone(),
|
||||||
|
race_stats.car.clone(),
|
||||||
|
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(())
|
||||||
|
}
|
Loading…
Reference in New Issue