UI touchup
This commit is contained in:
parent
d2f4539641
commit
41a17f16aa
132
config.json
132
config.json
|
@ -1,62 +1,72 @@
|
||||||
{
|
{
|
||||||
"ocr_regions": [
|
"ocr_regions": [
|
||||||
{
|
{
|
||||||
"name": "lap",
|
"name": "lap",
|
||||||
"x": 2300,
|
"x": 2300,
|
||||||
"y": 46,
|
"y": 46,
|
||||||
"width": 140,
|
"width": 140,
|
||||||
"height": 90
|
"height": 90,
|
||||||
},
|
"threshold": null,
|
||||||
{
|
"use_ocr_cache": null
|
||||||
"name": "health",
|
},
|
||||||
"x": 90,
|
{
|
||||||
"y": 1364,
|
"name": "health",
|
||||||
"width": 52,
|
"x": 90,
|
||||||
"height": 24
|
"y": 1364,
|
||||||
},
|
"width": 52,
|
||||||
{
|
"height": 24,
|
||||||
"name": "gas",
|
"threshold": null,
|
||||||
"x": 208,
|
"use_ocr_cache": null
|
||||||
"y": 1364,
|
},
|
||||||
"width": 52,
|
{
|
||||||
"height": 24
|
"name": "gas",
|
||||||
},
|
"x": 208,
|
||||||
{
|
"y": 1364,
|
||||||
"name": "tyres",
|
"width": 52,
|
||||||
"x": 325,
|
"height": 24,
|
||||||
"y": 1364,
|
"threshold": null,
|
||||||
"width": 52,
|
"use_ocr_cache": null
|
||||||
"height": 24
|
},
|
||||||
},
|
{
|
||||||
{
|
"name": "tyres",
|
||||||
"name": "best",
|
"x": 325,
|
||||||
"x": 2325,
|
"y": 1364,
|
||||||
"y": 169,
|
"width": 52,
|
||||||
"width": 183,
|
"height": 24,
|
||||||
"height": 43
|
"threshold": null,
|
||||||
},
|
"use_ocr_cache": null
|
||||||
{
|
},
|
||||||
"name": "lap_time",
|
{
|
||||||
"x": 2325,
|
"name": "best",
|
||||||
"y": 222,
|
"x": 2325,
|
||||||
"width": 183,
|
"y": 169,
|
||||||
"height": 43
|
"width": 183,
|
||||||
}
|
"height": 43,
|
||||||
],
|
"threshold": null,
|
||||||
"track_region": {
|
"use_ocr_cache": null
|
||||||
"name": "track",
|
},
|
||||||
"x": 2020,
|
{
|
||||||
"y": 1030,
|
"name": "lap_time",
|
||||||
"width": 540,
|
"x": 2325,
|
||||||
"height": 410,
|
"y": 222,
|
||||||
"threshold": 0.85
|
"width": 183,
|
||||||
},
|
"height": 43,
|
||||||
"penalty_orange_region": {
|
"threshold": null,
|
||||||
"name": "penalty",
|
"use_ocr_cache": null
|
||||||
"x": 989,
|
}
|
||||||
"y": 117,
|
],
|
||||||
"width": 30,
|
"track_region": {
|
||||||
"height": 30
|
"name": "track",
|
||||||
},
|
"x": 2020,
|
||||||
"track_recognition_threshold": 10
|
"y": 1030,
|
||||||
|
"width": 540,
|
||||||
|
"height": 410,
|
||||||
|
"threshold": 0.85,
|
||||||
|
"use_ocr_cache": null
|
||||||
|
},
|
||||||
|
"ocr_interval_ms": 500,
|
||||||
|
"track_recognition_threshold": 10,
|
||||||
|
"dump_frame_fraction": null,
|
||||||
|
"light_mode": false,
|
||||||
|
"font_scale": 1.3
|
||||||
}
|
}
|
|
@ -145,7 +145,7 @@ fn add_saved_frame(
|
||||||
image: retained,
|
image: retained,
|
||||||
rgb_image: extracted,
|
rgb_image: extracted,
|
||||||
img_hash: hash,
|
img_hash: hash,
|
||||||
recognized_text: ocr_results.get(®ion.name).cloned(),
|
recognized_text: ocr_results.get(®ion.name).cloned().unwrap_or_default(),
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -224,7 +224,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)
|
||||||
}
|
}
|
||||||
let interval = state.lock().unwrap().config.ocr_interval_ms.unwrap_or(500);
|
let interval = state.lock().unwrap().config.ocr_interval_ms;
|
||||||
thread::sleep(Duration::from_millis(interval));
|
thread::sleep(Duration::from_millis(interval));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,23 +1,55 @@
|
||||||
use std::path::PathBuf;
|
use std::{path::PathBuf, sync::Arc};
|
||||||
|
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
use serde::{de::DeserializeOwned, Deserialize, Serialize};
|
use serde::{de::DeserializeOwned, Deserialize, Serialize};
|
||||||
|
|
||||||
use crate::image_processing::Region;
|
use crate::image_processing::Region;
|
||||||
|
|
||||||
|
fn default_track_recognition_threshold() -> u32 {
|
||||||
|
10
|
||||||
|
}
|
||||||
|
fn default_ocr_interval_ms() -> u64 {
|
||||||
|
500
|
||||||
|
}
|
||||||
|
fn default_font_scale() -> f32 {
|
||||||
|
1.0
|
||||||
|
}
|
||||||
|
|
||||||
#[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_interval_ms: Option<u64>,
|
|
||||||
pub track_recognition_threshold: Option<u32>,
|
#[serde(default = "default_ocr_interval_ms")]
|
||||||
|
pub ocr_interval_ms: u64,
|
||||||
|
|
||||||
|
#[serde(default = "default_track_recognition_threshold")]
|
||||||
|
pub track_recognition_threshold: u32,
|
||||||
|
|
||||||
pub dump_frame_fraction: Option<f64>,
|
pub dump_frame_fraction: Option<f64>,
|
||||||
|
|
||||||
|
#[serde(default = "Default::default")]
|
||||||
|
pub light_mode: bool,
|
||||||
|
|
||||||
|
#[serde(default = "default_font_scale")]
|
||||||
|
pub font_scale: f32,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Config {
|
impl Config {
|
||||||
pub fn load() -> Result<Self> {
|
pub fn load() -> Result<Self> {
|
||||||
load_config_or_make_default("config.json", include_str!("configs/config.default.json"))
|
load_config_or_make_default("config.json", include_str!("configs/config.default.json"))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn update_and_save<F: Fn(&mut Self)>(
|
||||||
|
self: &mut Arc<Self>,
|
||||||
|
update_fn: F,
|
||||||
|
) -> Result<()> {
|
||||||
|
let mut config = (**self).clone();
|
||||||
|
update_fn(&mut config);
|
||||||
|
save_json_config("config.json", &config)?;
|
||||||
|
*self = Arc::new(config);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn load_config_or_make_default<T: DeserializeOwned>(path: &str, default: &str) -> Result<T> {
|
pub fn load_config_or_make_default<T: DeserializeOwned>(path: &str, default: &str) -> Result<T> {
|
||||||
|
|
|
@ -28,8 +28,7 @@ impl LearnedTracks {
|
||||||
for (learned_hash_b64, learned_track) in &self.learned_tracks {
|
for (learned_hash_b64, learned_track) in &self.learned_tracks {
|
||||||
let learned_hash: ImageHash<Vec<u8>> = ImageHash::from_base64(learned_hash_b64).ok()?;
|
let learned_hash: ImageHash<Vec<u8>> = ImageHash::from_base64(learned_hash_b64).ok()?;
|
||||||
let current_hash: ImageHash<Vec<u8>> = ImageHash::from_base64(hash).ok()?;
|
let current_hash: ImageHash<Vec<u8>> = ImageHash::from_base64(hash).ok()?;
|
||||||
if current_hash.dist(&learned_hash) <= config.track_recognition_threshold.unwrap_or(10)
|
if current_hash.dist(&learned_hash) <= config.track_recognition_threshold {
|
||||||
{
|
|
||||||
return Some(learned_track.to_owned());
|
return Some(learned_track.to_owned());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
106
src/main.rs
106
src/main.rs
|
@ -23,7 +23,7 @@ use std::{
|
||||||
use analysis::save_frames_from;
|
use analysis::save_frames_from;
|
||||||
use config::Config;
|
use config::Config;
|
||||||
use eframe::{
|
use eframe::{
|
||||||
egui::{self, Ui, Visuals},
|
egui::{self, FontTweak, Ui, Visuals},
|
||||||
emath::Vec2,
|
emath::Vec2,
|
||||||
epaint::Color32,
|
epaint::Color32,
|
||||||
};
|
};
|
||||||
|
@ -238,20 +238,22 @@ fn show_race_state(
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
fn show_debug_frames(ui: &mut Ui, debug_frames: &mut HashMap<String, DebugOcrFrame>) {
|
fn show_debug_frames(
|
||||||
|
ui: &mut Ui,
|
||||||
|
ocr_db: &OcrDatabase,
|
||||||
|
debug_frames: &mut HashMap<String, DebugOcrFrame>,
|
||||||
|
) {
|
||||||
for (name, debug_image) in debug_frames.iter_mut() {
|
for (name, debug_image) in debug_frames.iter_mut() {
|
||||||
ui.label(name);
|
ui.label(name);
|
||||||
if let Some(text) = &debug_image.recognized_text {
|
ui.text_edit_singleline(&mut debug_image.recognized_text);
|
||||||
ui.label(text);
|
|
||||||
}
|
|
||||||
if ui
|
|
||||||
.button(&debug_image.img_hash)
|
|
||||||
.on_hover_text("Copy")
|
|
||||||
.clicked()
|
|
||||||
{
|
|
||||||
ui.output().copied_text = debug_image.img_hash.clone();
|
|
||||||
}
|
|
||||||
debug_image.image.show_max_size(ui, Vec2::new(300.0, 300.0));
|
debug_image.image.show_max_size(ui, Vec2::new(300.0, 300.0));
|
||||||
|
|
||||||
|
if ui.button("Learn OCR").clicked() {
|
||||||
|
let hashes = ocr::compute_box_hashes(&debug_image.rgb_image);
|
||||||
|
ocr_db
|
||||||
|
.learn_phrase(&hashes, &debug_image.recognized_text)
|
||||||
|
.unwrap();
|
||||||
|
}
|
||||||
ui.separator();
|
ui.separator();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -298,6 +300,20 @@ fn show_combo_box(ui: &mut Ui, name: &str, label: &str, options: &[String], valu
|
||||||
impl eframe::App for AppUi {
|
impl eframe::App for AppUi {
|
||||||
fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) {
|
fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) {
|
||||||
let mut state = self.state.lock().unwrap();
|
let mut state = self.state.lock().unwrap();
|
||||||
|
if state.config.light_mode {
|
||||||
|
ctx.set_visuals(Visuals::light());
|
||||||
|
} else {
|
||||||
|
ctx.set_visuals(Visuals::dark());
|
||||||
|
}
|
||||||
|
let mut fonts = egui::FontDefinitions::default();
|
||||||
|
for font in fonts.font_data.values_mut() {
|
||||||
|
*font = font.clone().tweak(FontTweak {
|
||||||
|
scale: state.config.font_scale,
|
||||||
|
..Default::default()
|
||||||
|
});
|
||||||
|
}
|
||||||
|
ctx.set_fonts(fonts);
|
||||||
|
|
||||||
let ocr_db = state.ocr_db.clone();
|
let ocr_db = state.ocr_db.clone();
|
||||||
|
|
||||||
let mut debug_lap_window = self.ui_state.debug_lap.is_some();
|
let mut debug_lap_window = self.ui_state.debug_lap.is_some();
|
||||||
|
@ -310,7 +326,7 @@ impl eframe::App for AppUi {
|
||||||
.show_max_size(ui, Vec2::new(800.0, 600.0));
|
.show_max_size(ui, Vec2::new(800.0, 600.0));
|
||||||
ui.separator();
|
ui.separator();
|
||||||
if let Some(debug_lap) = &mut self.ui_state.debug_lap {
|
if let Some(debug_lap) = &mut self.ui_state.debug_lap {
|
||||||
show_debug_frames(ui, &mut debug_lap.debug_regions);
|
show_debug_frames(ui, &ocr_db, &mut debug_lap.debug_regions);
|
||||||
}
|
}
|
||||||
show_config_controls(ui, &mut self.ui_state, state.deref_mut());
|
show_config_controls(ui, &mut self.ui_state, state.deref_mut());
|
||||||
}
|
}
|
||||||
|
@ -320,7 +336,6 @@ impl eframe::App for AppUi {
|
||||||
self.ui_state.debug_lap = None;
|
self.ui_state.debug_lap = None;
|
||||||
}
|
}
|
||||||
|
|
||||||
ctx.set_visuals(Visuals::dark());
|
|
||||||
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 {
|
||||||
ui.heading("Race data");
|
ui.heading("Race data");
|
||||||
|
@ -349,7 +364,7 @@ impl eframe::App for AppUi {
|
||||||
ui.separator();
|
ui.separator();
|
||||||
ui.heading("Strategy");
|
ui.heading("Strategy");
|
||||||
if let Some(tyre_wear) = race.tyre_wear() {
|
if let Some(tyre_wear) = race.tyre_wear() {
|
||||||
ui.heading(&format!("p50 Tyre Wear: {}", tyre_wear));
|
ui.label(&format!("Median Tyre Wear: {}", tyre_wear));
|
||||||
if let Some(tyres) = frame.tyres {
|
if let Some(tyres) = frame.tyres {
|
||||||
ui.label(&format!(
|
ui.label(&format!(
|
||||||
"Out of tires in {:.1} lap(s)",
|
"Out of tires in {:.1} lap(s)",
|
||||||
|
@ -358,7 +373,7 @@ impl eframe::App for AppUi {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if let Some(gas_wear) = race.gas_per_lap() {
|
if let Some(gas_wear) = race.gas_per_lap() {
|
||||||
ui.heading(&format!("p50 Gas Wear: {}", gas_wear));
|
ui.label(&format!("Median Gas Wear: {}", gas_wear));
|
||||||
if let Some(gas) = frame.gas {
|
if let Some(gas) = frame.gas {
|
||||||
ui.label(&format!(
|
ui.label(&format!(
|
||||||
"Out of gas in {:.1} lap(s)",
|
"Out of gas in {:.1} lap(s)",
|
||||||
|
@ -368,20 +383,55 @@ impl eframe::App for AppUi {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
ui.separator();
|
|
||||||
ui.checkbox(&mut state.debug_frames, "Debug OCR regions");
|
|
||||||
if state.config.dump_frame_fraction.is_some() {
|
|
||||||
ui.checkbox(
|
|
||||||
&mut state.should_sample_ocr_data,
|
|
||||||
"Dump OCR training frames",
|
|
||||||
);
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
egui::CentralPanel::default().show(ctx, |ui| {
|
egui::CentralPanel::default().show(ctx, |ui| {
|
||||||
|
egui::menu::bar(ui, |ui| {
|
||||||
|
egui::menu::menu_button(ui, "Control", |ui| {
|
||||||
|
if ui.button("Debug OCR regions").clicked() {
|
||||||
|
state.debug_frames = !state.debug_frames;
|
||||||
|
}
|
||||||
|
if state.config.dump_frame_fraction.is_some() {
|
||||||
|
let button_text = if state.should_sample_ocr_data {
|
||||||
|
"Stop OCR training dump"
|
||||||
|
} else {
|
||||||
|
"Dump OCR training data"
|
||||||
|
};
|
||||||
|
if ui.button(button_text).clicked() {
|
||||||
|
state.should_sample_ocr_data = !state.should_sample_ocr_data;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
show_config_controls(ui, &mut self.ui_state, &mut state);
|
||||||
|
});
|
||||||
|
|
||||||
|
egui::menu::menu_button(ui, "Preferences", |ui| {
|
||||||
|
let light_mode_text = if state.config.light_mode {
|
||||||
|
"☀ light mode"
|
||||||
|
} else {
|
||||||
|
"☀ dark mode"
|
||||||
|
};
|
||||||
|
if ui.button(light_mode_text).clicked() {
|
||||||
|
state
|
||||||
|
.config
|
||||||
|
.update_and_save(|config| config.light_mode = !config.light_mode)
|
||||||
|
.unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut font_scale = state.config.font_scale;
|
||||||
|
ui.add(egui::Slider::new(&mut font_scale, 0.1..=5.0).text("Font scale"));
|
||||||
|
if font_scale != state.config.font_scale {
|
||||||
|
state
|
||||||
|
.config
|
||||||
|
.update_and_save(|config| {
|
||||||
|
config.font_scale = font_scale;
|
||||||
|
})
|
||||||
|
.unwrap();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
ui.separator();
|
||||||
|
|
||||||
egui::ScrollArea::vertical().show(ui, |ui| {
|
egui::ScrollArea::vertical().show(ui, |ui| {
|
||||||
let config = state.config.clone();
|
let config = state.config.clone();
|
||||||
let _learned_tracks = state.learned_tracks.clone();
|
|
||||||
if let Some(race) = &mut state.current_race {
|
if let Some(race) = &mut state.current_race {
|
||||||
ui.heading(&format!("Current Race: {}", race.name()));
|
ui.heading(&format!("Current Race: {}", race.name()));
|
||||||
show_race_state(
|
show_race_state(
|
||||||
|
@ -465,8 +515,8 @@ impl eframe::App for AppUi {
|
||||||
if state.debug_frames {
|
if state.debug_frames {
|
||||||
egui::SidePanel::right("screenshots").show(ctx, |ui| {
|
egui::SidePanel::right("screenshots").show(ctx, |ui| {
|
||||||
egui::ScrollArea::vertical().show(ui, |ui| {
|
egui::ScrollArea::vertical().show(ui, |ui| {
|
||||||
show_debug_frames(ui, &mut state.saved_frames);
|
let ocr_db = state.ocr_db.clone();
|
||||||
show_config_controls(ui, &mut self.ui_state, state.deref_mut());
|
show_debug_frames(ui, ocr_db.as_ref(), &mut state.saved_frames);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -128,7 +128,7 @@ pub struct DebugOcrFrame {
|
||||||
pub image: RetainedImage,
|
pub image: RetainedImage,
|
||||||
pub rgb_image: RgbImage,
|
pub rgb_image: RgbImage,
|
||||||
pub img_hash: String,
|
pub img_hash: String,
|
||||||
pub recognized_text: Option<String>,
|
pub recognized_text: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Default)]
|
#[derive(Default)]
|
||||||
|
|
Loading…
Reference in New Issue