working on config
This commit is contained in:
parent
aa1c01a6dd
commit
3121905620
|
@ -512,7 +512,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||
checksum = "877bfcce06463cdbcfd7f4efd57608b1384d6d9ae03b33e503fbba1d1a899a52"
|
||||
dependencies = [
|
||||
"egui",
|
||||
"image",
|
||||
"image 0.24.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
@ -1018,6 +1018,20 @@ dependencies = [
|
|||
"unicode-normalization",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "image"
|
||||
version = "0.23.14"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "24ffcb7e7244a9bf19d35bf2883b9c080c4ced3c07a9895572178cdb8f13f6a1"
|
||||
dependencies = [
|
||||
"bytemuck",
|
||||
"byteorder",
|
||||
"color_quant",
|
||||
"num-iter",
|
||||
"num-rational 0.3.2",
|
||||
"num-traits",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "image"
|
||||
version = "0.24.2"
|
||||
|
@ -1031,13 +1045,26 @@ dependencies = [
|
|||
"gif",
|
||||
"jpeg-decoder",
|
||||
"num-iter",
|
||||
"num-rational",
|
||||
"num-rational 0.4.0",
|
||||
"num-traits",
|
||||
"png",
|
||||
"scoped_threadpool",
|
||||
"tiff",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "img_hash"
|
||||
version = "3.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5ea4eac6fc4f64ed363d5c210732b747bfa5ddd8a25ac347d887f298c3a70b49"
|
||||
dependencies = [
|
||||
"base64",
|
||||
"image 0.23.14",
|
||||
"rustdct",
|
||||
"serde",
|
||||
"transpose 0.2.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "indexmap"
|
||||
version = "1.8.1"
|
||||
|
@ -1390,6 +1417,16 @@ dependencies = [
|
|||
"minimal-lexical",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "num-complex"
|
||||
version = "0.2.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b6b19411a9719e753aff12e5187b74d60d3dc449ec3f4dc21e3989c3f554bc95"
|
||||
dependencies = [
|
||||
"autocfg",
|
||||
"num-traits",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "num-integer"
|
||||
version = "0.1.45"
|
||||
|
@ -1411,6 +1448,17 @@ dependencies = [
|
|||
"num-traits",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "num-rational"
|
||||
version = "0.3.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "12ac428b1cb17fce6f731001d307d351ec70a6d202fc2e60f7d4c5e42d8f4f07"
|
||||
dependencies = [
|
||||
"autocfg",
|
||||
"num-integer",
|
||||
"num-traits",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "num-rational"
|
||||
version = "0.4.0"
|
||||
|
@ -1803,6 +1851,28 @@ dependencies = [
|
|||
"winapi 0.3.9",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustdct"
|
||||
version = "0.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ef4d167674b4cf68c2114bdbcd34c95aa9071652b73b0f43b19298f1d2780b7d"
|
||||
dependencies = [
|
||||
"rustfft",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustfft"
|
||||
version = "3.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "77008ed59a8923c8b4ac2e5eaa6d28fbe893d3b9515098a4a5fc7767d6430fe5"
|
||||
dependencies = [
|
||||
"num-complex",
|
||||
"num-integer",
|
||||
"num-traits",
|
||||
"strength_reduce",
|
||||
"transpose 0.1.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustls"
|
||||
version = "0.20.6"
|
||||
|
@ -2042,6 +2112,12 @@ version = "1.0.5"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d44a3643b4ff9caf57abcee9c2c621d6c03d9135e0d8b589bd9afb5992cb176a"
|
||||
|
||||
[[package]]
|
||||
name = "strength_reduce"
|
||||
version = "0.2.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a3ff2f71c82567c565ba4b3009a9350a96a7269eaa4001ebedae926230bc2254"
|
||||
|
||||
[[package]]
|
||||
name = "strsim"
|
||||
version = "0.10.0"
|
||||
|
@ -2057,11 +2133,13 @@ dependencies = [
|
|||
"egui_extras",
|
||||
"ehttp",
|
||||
"futures",
|
||||
"image",
|
||||
"image 0.24.2",
|
||||
"img_hash",
|
||||
"poll-promise",
|
||||
"reqwest",
|
||||
"scrap",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"tokio",
|
||||
]
|
||||
|
||||
|
@ -2247,6 +2325,22 @@ dependencies = [
|
|||
"lazy_static",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "transpose"
|
||||
version = "0.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "643e21580bb0627c7bb09e5cedbb42c8705b19d012de593ed6b0309270b3cd1e"
|
||||
|
||||
[[package]]
|
||||
name = "transpose"
|
||||
version = "0.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "95f9c900aa98b6ea43aee227fd680550cdec726526aab8ac801549eadb25e39f"
|
||||
dependencies = [
|
||||
"num-integer",
|
||||
"strength_reduce",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "try-lock"
|
||||
version = "0.2.3"
|
||||
|
|
|
@ -17,6 +17,9 @@ scrap = "0.5"
|
|||
anyhow = "1.0"
|
||||
futures = "0.3"
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
serde_json = "1"
|
||||
tokio = { version = "1", features = ["full"] }
|
||||
|
||||
reqwest = { version = "0.11", features = ["json"] }
|
||||
|
||||
img_hash = "3"
|
|
@ -0,0 +1,47 @@
|
|||
{
|
||||
"ocr_regions": [
|
||||
{
|
||||
"name": "lap",
|
||||
"x": 2290,
|
||||
"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": "lap_time",
|
||||
"x": 2325,
|
||||
"y": 169,
|
||||
"width": 183,
|
||||
"height": 43
|
||||
},
|
||||
{
|
||||
"name": "best_time",
|
||||
"x": 2325,
|
||||
"y": 222,
|
||||
"width": 183,
|
||||
"height": 43
|
||||
}
|
||||
],
|
||||
"ocr_server_endpoint": "http://localhost:3000/"
|
||||
}
|
|
@ -0,0 +1,4 @@
|
|||
{
|
||||
"learned_images": {},
|
||||
"learned_tracks": {}
|
||||
}
|
|
@ -0,0 +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,
|
||||
}
|
||||
|
||||
impl Config {
|
||||
pub fn load() -> Result<Self> {
|
||||
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)]
|
||||
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)?)
|
||||
}
|
|
@ -0,0 +1,47 @@
|
|||
{
|
||||
"ocr_regions": [
|
||||
{
|
||||
"name": "lap",
|
||||
"x": 2290,
|
||||
"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": "lap_time",
|
||||
"x": 2325,
|
||||
"y": 169,
|
||||
"width": 183,
|
||||
"height": 43
|
||||
},
|
||||
{
|
||||
"name": "best_time",
|
||||
"x": 2325,
|
||||
"y": 222,
|
||||
"width": 183,
|
||||
"height": 43
|
||||
}
|
||||
],
|
||||
"ocr_server_endpoint": "http://localhost:3000/"
|
||||
}
|
|
@ -0,0 +1,4 @@
|
|||
{
|
||||
"learned_images": {},
|
||||
"learned_tracks": {}
|
||||
}
|
|
@ -1,39 +1,145 @@
|
|||
use std::{collections::HashMap, time::Duration};
|
||||
use std::{
|
||||
collections::HashMap,
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
|
||||
use anyhow::Result;
|
||||
use egui_extras::RetainedImage;
|
||||
use image::RgbImage;
|
||||
use scrap::{Capturer, Display};
|
||||
|
||||
use crate::{
|
||||
capture,
|
||||
image_processing::{self, Region},
|
||||
ocr,
|
||||
state::{ParsedFrame, SharedAppState},
|
||||
state::{AppState, DebugOcrFrame, ParsedFrame, RaceState, SharedAppState},
|
||||
};
|
||||
|
||||
async fn run_loop_once(state: &SharedAppState, regions: &[Region]) -> Result<()> {
|
||||
fn is_finished_lap(state: &AppState, frame: &ParsedFrame) -> bool {
|
||||
if let Some(race) = &state.current_race {
|
||||
if let Some(last_finish) = &race.last_lap_record_time {
|
||||
let diff = Instant::now().duration_since(*last_finish);
|
||||
if diff < Duration::from_secs(5) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
if let Some(prev_frame) = state.buffered_frames.back() {
|
||||
prev_frame.lap_time.is_some()
|
||||
&& prev_frame.lap_time == frame.lap_time
|
||||
&& prev_frame.lap_time.unwrap() > Duration::from_secs(5)
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
fn merge_with_max<T: Ord + Clone>(a: &Option<T>, b: &Option<T>) -> Option<T> {
|
||||
match (a, b) {
|
||||
(Some(a), None) => Some(a.clone()),
|
||||
(None, Some(b)) => Some(b.clone()),
|
||||
(None, None) => None,
|
||||
(Some(a), Some(b)) => {
|
||||
if a > b {
|
||||
Some(a.clone())
|
||||
} else {
|
||||
Some(b.clone())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn merge_frames(prev: &ParsedFrame, next: &ParsedFrame) -> ParsedFrame {
|
||||
ParsedFrame {
|
||||
lap: merge_with_max(&prev.lap, &next.lap),
|
||||
health: merge_with_max(&prev.health, &next.health),
|
||||
gas: merge_with_max(&prev.gas, &next.gas),
|
||||
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),
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_new_frame(state: &mut AppState, frame: ParsedFrame, image: &RgbImage) {
|
||||
if frame.lap_time.is_some() {
|
||||
state.last_frame = Some(frame.clone());
|
||||
state.frames_without_lap = 0;
|
||||
|
||||
if state.current_race.is_none() {
|
||||
let mut race = RaceState::default();
|
||||
race.screencap = Some(
|
||||
RetainedImage::from_image_bytes(
|
||||
"screencap",
|
||||
&image_processing::to_png_bytes(image),
|
||||
)
|
||||
.expect("failed to save screenshot"),
|
||||
);
|
||||
state.current_race = Some(race);
|
||||
}
|
||||
} else {
|
||||
state.frames_without_lap += 1;
|
||||
}
|
||||
|
||||
if state.frames_without_lap >= 10 {
|
||||
if let Some(race) = state.current_race.take() {
|
||||
state.past_races.push_front(race);
|
||||
}
|
||||
}
|
||||
|
||||
if is_finished_lap(state, &frame) {
|
||||
let mut merged = merge_frames(state.buffered_frames.back().unwrap(), &frame);
|
||||
if let Some(lap) = &merged.lap {
|
||||
merged.lap = Some(lap - 1);
|
||||
}
|
||||
|
||||
if let Some(race) = state.current_race.as_mut() {
|
||||
race.laps.push(merged);
|
||||
race.last_lap_record_time = Some(Instant::now());
|
||||
}
|
||||
}
|
||||
|
||||
state.buffered_frames.push_back(frame);
|
||||
if state.buffered_frames.len() >= 20 {
|
||||
state.buffered_frames.pop_front();
|
||||
}
|
||||
}
|
||||
|
||||
async fn run_loop_once(state: &SharedAppState) -> Result<()> {
|
||||
let config = state.lock().unwrap().config.clone();
|
||||
let frame = capture::get_frame()?;
|
||||
let ocr_results = ocr::ocr_all_regions(&frame, ®ions).await;
|
||||
let ocr_results = ocr::ocr_all_regions(&frame, &config.ocr_regions).await;
|
||||
|
||||
let mut saved_frames = HashMap::new();
|
||||
if state.lock().unwrap().debug_frames {
|
||||
for region in regions {
|
||||
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, 0.95, 0.05);
|
||||
image_processing::filter_to_white(&mut extracted);
|
||||
let retained = RetainedImage::from_image_bytes(
|
||||
®ion.name,
|
||||
&image_processing::to_png_bytes(&extracted),
|
||||
)
|
||||
.unwrap();
|
||||
saved_frames.insert(region.name.clone(), retained);
|
||||
let have_to_use_other_image_library_version = img_hash::image::RgbImage::from_raw(
|
||||
extracted.width(),
|
||||
extracted.height(),
|
||||
extracted.as_raw().to_vec(),
|
||||
)
|
||||
.unwrap();
|
||||
let hash = hasher.hash_image(&have_to_use_other_image_library_version);
|
||||
saved_frames.insert(
|
||||
region.name.clone(),
|
||||
DebugOcrFrame {
|
||||
image: retained,
|
||||
rgb_image: extracted,
|
||||
img_hash: hash,
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
{
|
||||
let mut state = state.lock().unwrap();
|
||||
let parsed = ParsedFrame::parse(&ocr_results);
|
||||
if parsed.lap_time.is_some() {
|
||||
state.last_frame = Some(parsed);
|
||||
}
|
||||
handle_new_frame(&mut state, parsed, &frame);
|
||||
state.raw_data = ocr_results;
|
||||
state.saved_frames = saved_frames;
|
||||
}
|
||||
|
@ -41,17 +147,8 @@ async fn run_loop_once(state: &SharedAppState, regions: &[Region]) -> Result<()>
|
|||
}
|
||||
|
||||
pub async fn run_control_loop(state: SharedAppState) -> Result<()> {
|
||||
let regions = vec![
|
||||
Region::parse("health 91 1364 52 24").unwrap(),
|
||||
Region::parse("gas 208 1364 52 24").unwrap(),
|
||||
Region::parse("tyres 325 1364 52 24").unwrap(),
|
||||
Region::parse("lap 2295 46 140 87").unwrap(),
|
||||
Region::parse("best 2325 169 183 43").unwrap(),
|
||||
Region::parse("lap_time 2325 222 183 43").unwrap(),
|
||||
];
|
||||
|
||||
loop {
|
||||
if let Err(e) = run_loop_once(&state, ®ions).await {
|
||||
if let Err(e) = run_loop_once(&state).await {
|
||||
eprintln!("Error in control loop: {:?}", e)
|
||||
}
|
||||
tokio::time::sleep(Duration::from_millis(500)).await;
|
||||
|
|
|
@ -1,57 +1,46 @@
|
|||
use anyhow::Result;
|
||||
use image::{RgbImage, Rgb, codecs::png::PngEncoder, ColorType, ImageEncoder};
|
||||
use image::{codecs::png::PngEncoder, ColorType, ImageEncoder, Rgb, RgbImage};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Clone)]
|
||||
#[derive(Clone, Deserialize, Serialize)]
|
||||
pub struct Region {
|
||||
pub name: String,
|
||||
x: usize,
|
||||
y: usize,
|
||||
width: usize,
|
||||
height: usize
|
||||
height: usize,
|
||||
}
|
||||
|
||||
impl Region {
|
||||
pub fn parse(encoded: &str) -> Result<Region> {
|
||||
let split: Vec<_> = encoded.split(' ').collect();
|
||||
Ok(Region {
|
||||
name: split.get(0).ok_or(anyhow::anyhow!("expected name in region"))?.to_string(),
|
||||
x: split.get(1).ok_or(anyhow::anyhow!("failed to parse x from region"))?.parse()?,
|
||||
y: split.get(2).ok_or(anyhow::anyhow!("failed to parse y from region"))?.parse()?,
|
||||
width: split.get(3).ok_or(anyhow::anyhow!("failed to parse width from region"))?.parse()?,
|
||||
height: split.get(4).ok_or(anyhow::anyhow!("failed to parse height from region"))?.parse()?,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
pub fn extract_region(
|
||||
image: &RgbImage,
|
||||
region: &Region,
|
||||
) -> RgbImage {
|
||||
pub fn extract_region(image: &RgbImage, region: &Region) -> RgbImage {
|
||||
let mut buffer = RgbImage::new(region.width as u32, region.height as u32);
|
||||
for y in 0..region.height {
|
||||
for x in 0..region.width {
|
||||
buffer.put_pixel(
|
||||
x as u32,
|
||||
y as u32,
|
||||
*image.get_pixel((region.x + x) as u32, (region.y + y) as u32)
|
||||
*image.get_pixel((region.x + x) as u32, (region.y + y) as u32),
|
||||
);
|
||||
}
|
||||
}
|
||||
buffer
|
||||
}
|
||||
|
||||
pub fn filter_to_white(
|
||||
image: &mut RgbImage,
|
||||
threshold: f64,
|
||||
variance_threshold: f64
|
||||
) {
|
||||
pub fn filter_to_white(image: &mut RgbImage) {
|
||||
let threshold = 0.98;
|
||||
let variance_threshold = 0.02;
|
||||
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);
|
||||
for y in 0..image.height() {
|
||||
for x in 0..image.width() {
|
||||
let pixel = image.get_pixel(x, y);
|
||||
let [r, g, b] = pixel.0;
|
||||
if past_threshold_color(r) && past_threshold_color(g) && past_threshold_color(b) && color_diff(r, g) < variance_threshold && color_diff(r, b) < variance_threshold && color_diff(b, g) < variance_threshold {
|
||||
if past_threshold_color(r)
|
||||
&& past_threshold_color(g)
|
||||
&& past_threshold_color(b)
|
||||
&& color_diff(r, g) < variance_threshold
|
||||
&& color_diff(r, b) < variance_threshold
|
||||
&& color_diff(b, g) < variance_threshold
|
||||
{
|
||||
// This is a white pixel, make it black
|
||||
image.put_pixel(x, y, Rgb([0, 0, 0]));
|
||||
} else {
|
||||
|
@ -65,11 +54,13 @@ pub fn filter_to_white(
|
|||
pub fn to_png_bytes(image: &RgbImage) -> Vec<u8> {
|
||||
let mut buffer: Vec<u8> = Vec::new();
|
||||
let encoder = PngEncoder::new(&mut buffer);
|
||||
encoder.write_image(
|
||||
image.as_raw(),
|
||||
image.width(),
|
||||
image.height(),
|
||||
ColorType::Rgb8
|
||||
).expect("failed encoding image to PNG");
|
||||
encoder
|
||||
.write_image(
|
||||
image.as_raw(),
|
||||
image.width(),
|
||||
image.height(),
|
||||
ColorType::Rgb8,
|
||||
)
|
||||
.expect("failed encoding image to PNG");
|
||||
buffer
|
||||
}
|
132
src/main.rs
132
src/main.rs
|
@ -5,19 +5,26 @@ mod control_loop;
|
|||
mod image_processing;
|
||||
mod ocr;
|
||||
mod state;
|
||||
mod config;
|
||||
|
||||
use std::{
|
||||
sync::{Arc, Mutex},
|
||||
time::Duration,
|
||||
};
|
||||
|
||||
use eframe::egui;
|
||||
use egui_extras::RetainedImage;
|
||||
use state::{AppState, SharedAppState};
|
||||
use config::{Config, LearnedConfig};
|
||||
use eframe::{
|
||||
egui::{self, Ui},
|
||||
epaint::Color32, emath::Vec2,
|
||||
};
|
||||
use state::{AppState, RaceState, SharedAppState};
|
||||
|
||||
#[tokio::main(flavor = "multi_thread", worker_threads = 8)]
|
||||
async fn main() -> anyhow::Result<()> {
|
||||
let state = Arc::new(Mutex::new(AppState::default()));
|
||||
let mut app_state = AppState::default();
|
||||
app_state.config = Arc::new(Config::load().unwrap());
|
||||
app_state.learned = Arc::new(LearnedConfig::load().unwrap());
|
||||
let state = Arc::new(Mutex::new(app_state));
|
||||
{
|
||||
let state = state.clone();
|
||||
let _ = tokio::spawn(async move {
|
||||
|
@ -35,6 +42,53 @@ async fn main() -> anyhow::Result<()> {
|
|||
);
|
||||
}
|
||||
|
||||
fn show_optional_usize_with_diff(
|
||||
ui: &mut Ui,
|
||||
color: Color32,
|
||||
old: &Option<usize>,
|
||||
v: &Option<usize>,
|
||||
) {
|
||||
let diff_string = match (old, v) {
|
||||
(Some(old), Some(v)) => {
|
||||
let diff = *v as isize - *old as isize;
|
||||
if diff.is_positive() {
|
||||
format!(" (+{})", diff)
|
||||
} else {
|
||||
format!(" ({})", diff)
|
||||
}
|
||||
}
|
||||
_ => "".to_owned(),
|
||||
};
|
||||
let value = match v {
|
||||
Some(v) => v.to_string(),
|
||||
None => "???".to_owned(),
|
||||
};
|
||||
let text = format!("{}{}", value, diff_string);
|
||||
ui.colored_label(color, text);
|
||||
}
|
||||
|
||||
fn format_time(time: Duration) -> String {
|
||||
format!("{:.3}", time.as_secs_f64())
|
||||
}
|
||||
|
||||
fn label_time_delta(ui: &mut Ui, time: Duration, old: Option<Duration>) {
|
||||
if let Some(old) = old {
|
||||
if time > old {
|
||||
ui.colored_label(
|
||||
Color32::LIGHT_RED,
|
||||
"+".to_owned() + &format_time(time - old),
|
||||
);
|
||||
} else {
|
||||
ui.colored_label(
|
||||
Color32::LIGHT_GREEN,
|
||||
"-".to_owned() + &format_time(old - time),
|
||||
);
|
||||
}
|
||||
} else {
|
||||
ui.label("--");
|
||||
}
|
||||
}
|
||||
|
||||
struct MyApp {
|
||||
state: SharedAppState,
|
||||
}
|
||||
|
@ -45,6 +99,50 @@ impl MyApp {
|
|||
}
|
||||
}
|
||||
|
||||
fn show_race_state(ui: &mut Ui, race: &RaceState) {
|
||||
egui::Grid::new("current-race").show(ui, |ui| {
|
||||
ui.label("Lap");
|
||||
ui.label("Time");
|
||||
ui.label("Δ Previous");
|
||||
ui.label("Δ Best");
|
||||
ui.label("Health");
|
||||
ui.label("Gas");
|
||||
ui.label("Tyres");
|
||||
ui.end_row();
|
||||
for (i, lap) in race.laps.iter().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));
|
||||
label_time_delta(ui, lap_time, prev_lap.and_then(|p| p.lap_time));
|
||||
label_time_delta(ui, lap_time, lap.best_time);
|
||||
|
||||
show_optional_usize_with_diff(
|
||||
ui,
|
||||
Color32::RED,
|
||||
&prev_lap.and_then(|p| p.health),
|
||||
&lap.health,
|
||||
);
|
||||
show_optional_usize_with_diff(
|
||||
ui,
|
||||
Color32::GRAY,
|
||||
&prev_lap.and_then(|p| p.gas),
|
||||
&lap.gas,
|
||||
);
|
||||
show_optional_usize_with_diff(
|
||||
ui,
|
||||
Color32::GREEN,
|
||||
&prev_lap.and_then(|p| p.tyres),
|
||||
&lap.tyres,
|
||||
);
|
||||
|
||||
ui.end_row();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
impl eframe::App for MyApp {
|
||||
fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) {
|
||||
let mut state = self.state.lock().unwrap();
|
||||
|
@ -84,7 +182,28 @@ impl eframe::App for MyApp {
|
|||
ui.separator();
|
||||
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 {
|
||||
ui.heading("Current Race");
|
||||
show_race_state(ui, 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, race);
|
||||
if let Some(img) = &race.screencap {
|
||||
img.show_max_size(ui, Vec2::new(600.0, 500.0));
|
||||
}
|
||||
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() {
|
||||
println!("EXPORT: TODO");
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if state.debug_frames {
|
||||
egui::SidePanel::right("screenshots").show(ctx, |ui| {
|
||||
|
@ -92,7 +211,8 @@ impl eframe::App for MyApp {
|
|||
screenshots_sorted.sort_by_key(|(name, _)| name.clone());
|
||||
for (name, image) in screenshots_sorted {
|
||||
ui.label(name);
|
||||
image.show_max_size(ui, ui.available_size());
|
||||
ui.label(image.img_hash.to_base64());
|
||||
image.image.show_max_size(ui, ui.available_size());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
|
@ -43,7 +43,7 @@ pub async fn ocr_all_regions(image: &RgbImage, regions: &[Region]) -> HashMap<St
|
|||
let results = results.clone();
|
||||
handles.push(tokio::spawn(async move {
|
||||
let mut image = filtered_image;
|
||||
filter_to_white(&mut image, 0.95, 0.05);
|
||||
filter_to_white(&mut image);
|
||||
let ocr_results = run_ocr(&image).await;
|
||||
let value = match ocr_results {
|
||||
Ok(ocr_regions) => {
|
||||
|
|
37
src/state.rs
37
src/state.rs
|
@ -1,8 +1,12 @@
|
|||
use std::{sync::{Arc, Mutex}, time::Duration, collections::HashMap};
|
||||
use std::{sync::{Arc, Mutex}, time::{Duration, Instant}, collections::{HashMap, VecDeque}};
|
||||
|
||||
use egui_extras::RetainedImage;
|
||||
use image::RgbImage;
|
||||
|
||||
use crate::config::{Config, LearnedConfig};
|
||||
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ParsedFrame {
|
||||
pub lap: Option<usize>,
|
||||
|
||||
|
@ -18,7 +22,6 @@ fn parse_duration(time: &str) -> Option<Duration> {
|
|||
let sep = time.find(':')?;
|
||||
let (minutes, secs) = time.split_at(sep);
|
||||
|
||||
println!("{} {}", minutes, secs);
|
||||
let minutes = minutes.parse::<f64>().ok()?;
|
||||
let secs = secs[1..secs.len()].parse::<f64>().ok()?;
|
||||
|
||||
|
@ -54,13 +57,41 @@ impl ParsedFrame {
|
|||
}
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct RaceState {
|
||||
pub laps: Vec<ParsedFrame>,
|
||||
pub last_lap_record_time: Option<Instant>,
|
||||
|
||||
pub screencap: Option<RetainedImage>,
|
||||
|
||||
pub exported: bool,
|
||||
|
||||
pub car: String,
|
||||
pub track: String,
|
||||
}
|
||||
|
||||
pub struct DebugOcrFrame {
|
||||
pub image: RetainedImage,
|
||||
pub rgb_image: RgbImage,
|
||||
pub img_hash: img_hash::ImageHash,
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct AppState {
|
||||
pub raw_data: HashMap<String, Option<String>>,
|
||||
pub last_frame: Option<ParsedFrame>,
|
||||
|
||||
pub buffered_frames: VecDeque<ParsedFrame>,
|
||||
|
||||
pub frames_without_lap: usize,
|
||||
pub current_race: Option<RaceState>,
|
||||
pub past_races: VecDeque<RaceState>,
|
||||
|
||||
pub debug_frames: bool,
|
||||
pub saved_frames: HashMap<String, RetainedImage>,
|
||||
pub saved_frames: HashMap<String, DebugOcrFrame>,
|
||||
|
||||
pub config: Arc<Config>,
|
||||
pub learned: Arc<LearnedConfig>,
|
||||
}
|
||||
|
||||
pub type SharedAppState = Arc<Mutex<AppState>>;
|
Loading…
Reference in New Issue