Spaces:
Build error
Build error
kapil commited on
Commit ·
90dd6a4
1
Parent(s): c6c01fc
feat: initialize project structure with core inference logic and interactive dashboard UI
Browse files- README.md +16 -9
- src/args.rs +40 -0
- src/inference.rs +1 -1
- src/lib.rs +9 -0
- src/main.rs +27 -78
- src/tests.rs +57 -0
- static/index.html +208 -83
README.md
CHANGED
|
@@ -59,21 +59,27 @@ cargo build --release
|
|
| 59 |
### Step 1: Training the AI Model
|
| 60 |
To optimize the neural network for your local environment, run the training mode:
|
| 61 |
```bash
|
| 62 |
-
# Starts the training cycle
|
| 63 |
-
cargo run
|
| 64 |
```
|
| 65 |
-
*Tip: Allow the loss to converge below 0.05 for optimal results.*
|
| 66 |
|
| 67 |
-
### Step 2: Running the Dashboard
|
| 68 |
-
|
| 69 |
```bash
|
| 70 |
-
# Starts the Axum web server
|
| 71 |
cargo run -- gui
|
| 72 |
```
|
| 73 |
**Features:**
|
| 74 |
-
- **Image Upload**: Test
|
| 75 |
-
- **Point
|
| 76 |
-
- **
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 77 |
|
| 78 |
---
|
| 79 |
|
|
@@ -125,6 +131,7 @@ If you encounter a bug or wish to provide performance optimizations, please subm
|
|
| 125 |
|
| 126 |
## Resources
|
| 127 |
|
|
|
|
| 128 |
- **Original Inspiration**: [Paper: Keypoints as Objects for Automatic Scorekeeping](https://arxiv.org/abs/2105.09880)
|
| 129 |
- **Model Training Resources**: [Download from Google Drive](https://drive.google.com/file/d/1ZEvuzg9zYbPd1FdZgV6v1aT4sqbqmLqp/view?usp=sharing)
|
| 130 |
- **Official Documentation Reference**: [IEEE Dataport Dataset](https://ieee-dataport.org/open-access/deepdarts-dataset)
|
|
|
|
| 59 |
### Step 1: Training the AI Model
|
| 60 |
To optimize the neural network for your local environment, run the training mode:
|
| 61 |
```bash
|
| 62 |
+
# Starts the training cycle
|
| 63 |
+
cargo run -- train
|
| 64 |
```
|
|
|
|
| 65 |
|
| 66 |
+
### Step 2: Running the Professional Dashboard
|
| 67 |
+
Launch the visual testing interface to see real-time detections and scores:
|
| 68 |
```bash
|
| 69 |
+
# Starts the modular Axum web server
|
| 70 |
cargo run -- gui
|
| 71 |
```
|
| 72 |
**Features:**
|
| 73 |
+
- **Dynamic Image Upload**: Test board imagery via the premium glassmorphism dashboard.
|
| 74 |
+
- **Neural Point Mapping**: Inspect detected calibration corners and dart locations with hover effects.
|
| 75 |
+
- **Real-time Scoring**: Instant sector calculation based on official BDO geometry.
|
| 76 |
+
|
| 77 |
+
### Step 3: CLI Model Testing
|
| 78 |
+
Test individual images directly from the terminal:
|
| 79 |
+
```bash
|
| 80 |
+
# Test a specific image
|
| 81 |
+
cargo run -- test path/to/image.jpg
|
| 82 |
+
```
|
| 83 |
|
| 84 |
---
|
| 85 |
|
|
|
|
| 131 |
|
| 132 |
## Resources
|
| 133 |
|
| 134 |
+
- **Core AI Framework**: [Burn - A Flexible & Comprehensive Deep Learning Framework](https://burn.dev/)
|
| 135 |
- **Original Inspiration**: [Paper: Keypoints as Objects for Automatic Scorekeeping](https://arxiv.org/abs/2105.09880)
|
| 136 |
- **Model Training Resources**: [Download from Google Drive](https://drive.google.com/file/d/1ZEvuzg9zYbPd1FdZgV6v1aT4sqbqmLqp/view?usp=sharing)
|
| 137 |
- **Official Documentation Reference**: [IEEE Dataport Dataset](https://ieee-dataport.org/open-access/deepdarts-dataset)
|
src/args.rs
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
pub enum Command {
|
| 2 |
+
Gui,
|
| 3 |
+
Test { img_path: String },
|
| 4 |
+
Train,
|
| 5 |
+
}
|
| 6 |
+
|
| 7 |
+
pub struct AppArgs {
|
| 8 |
+
pub command: Command,
|
| 9 |
+
}
|
| 10 |
+
|
| 11 |
+
impl AppArgs {
|
| 12 |
+
pub fn parse() -> Self {
|
| 13 |
+
let args: Vec<String> = std::env::args().collect();
|
| 14 |
+
|
| 15 |
+
if args.len() > 1 {
|
| 16 |
+
match args[1].as_str() {
|
| 17 |
+
"gui" => Self {
|
| 18 |
+
command: Command::Gui,
|
| 19 |
+
},
|
| 20 |
+
"test" => {
|
| 21 |
+
let img_path = if args.len() > 2 {
|
| 22 |
+
args[2].clone()
|
| 23 |
+
} else {
|
| 24 |
+
"test.jpg".to_string()
|
| 25 |
+
};
|
| 26 |
+
Self {
|
| 27 |
+
command: Command::Test { img_path },
|
| 28 |
+
}
|
| 29 |
+
}
|
| 30 |
+
_ => Self {
|
| 31 |
+
command: Command::Train,
|
| 32 |
+
},
|
| 33 |
+
}
|
| 34 |
+
} else {
|
| 35 |
+
Self {
|
| 36 |
+
command: Command::Train,
|
| 37 |
+
}
|
| 38 |
+
}
|
| 39 |
+
}
|
| 40 |
+
}
|
src/inference.rs
CHANGED
|
@@ -3,7 +3,7 @@ use burn::module::Module;
|
|
| 3 |
use burn::record::{BinFileRecorder, FullPrecisionSettings, Recorder};
|
| 4 |
use burn::tensor::backend::Backend;
|
| 5 |
use burn::tensor::{Tensor, TensorData};
|
| 6 |
-
use image
|
| 7 |
|
| 8 |
pub fn run_inference<B: Backend>(device: &B::Device, image_path: &str) {
|
| 9 |
println!("🔍 Loading model for inference...");
|
|
|
|
| 3 |
use burn::record::{BinFileRecorder, FullPrecisionSettings, Recorder};
|
| 4 |
use burn::tensor::backend::Backend;
|
| 5 |
use burn::tensor::{Tensor, TensorData};
|
| 6 |
+
use image;
|
| 7 |
|
| 8 |
pub fn run_inference<B: Backend>(device: &B::Device, image_path: &str) {
|
| 9 |
println!("🔍 Loading model for inference...");
|
src/lib.rs
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
pub mod args;
|
| 2 |
+
pub mod data;
|
| 3 |
+
pub mod loss;
|
| 4 |
+
pub mod model;
|
| 5 |
+
pub mod scoring;
|
| 6 |
+
pub mod server;
|
| 7 |
+
pub mod train;
|
| 8 |
+
pub mod tests;
|
| 9 |
+
pub mod inference;
|
src/main.rs
CHANGED
|
@@ -1,88 +1,37 @@
|
|
| 1 |
-
use
|
| 2 |
-
use
|
| 3 |
-
use
|
|
|
|
| 4 |
use burn::backend::wgpu::WgpuDevice;
|
| 5 |
use burn::backend::Wgpu;
|
| 6 |
-
use burn::prelude::*;
|
| 7 |
-
use burn::record::{BinFileRecorder, FullPrecisionSettings, Recorder};
|
| 8 |
-
|
| 9 |
-
pub mod data;
|
| 10 |
-
pub mod loss;
|
| 11 |
-
pub mod model;
|
| 12 |
-
pub mod scoring;
|
| 13 |
-
pub mod server;
|
| 14 |
-
pub mod train;
|
| 15 |
|
| 16 |
fn main() {
|
|
|
|
| 17 |
let device = WgpuDevice::default();
|
| 18 |
-
let args: Vec<String> = std::env::args().collect();
|
| 19 |
-
|
| 20 |
-
if args.len() > 1 && args[1] == "gui" {
|
| 21 |
-
println!("🌐 [Burn-DartVision] Starting Professional Dashboard...");
|
| 22 |
-
tokio::runtime::Builder::new_multi_thread()
|
| 23 |
-
.enable_all()
|
| 24 |
-
.build()
|
| 25 |
-
.unwrap()
|
| 26 |
-
.block_on(start_gui(device));
|
| 27 |
-
} else if args.len() > 1 && args[1] == "test" {
|
| 28 |
-
let img_path = if args.len() > 2 { &args[2] } else { "test.jpg" };
|
| 29 |
-
test_model(device, img_path);
|
| 30 |
-
} else {
|
| 31 |
-
println!("🚀 [Burn-DartVision] Starting Full Project Training...");
|
| 32 |
-
let dataset_path = "dataset/labels.json";
|
| 33 |
-
|
| 34 |
-
let config = TrainingConfig {
|
| 35 |
-
num_epochs: 50,
|
| 36 |
-
batch_size: 1,
|
| 37 |
-
lr: 1e-3,
|
| 38 |
-
};
|
| 39 |
-
|
| 40 |
-
train::<burn::backend::Autodiff<Wgpu>>(device, dataset_path, config);
|
| 41 |
-
}
|
| 42 |
-
}
|
| 43 |
|
| 44 |
-
|
| 45 |
-
|
| 46 |
-
|
| 47 |
-
|
| 48 |
-
|
| 49 |
-
|
| 50 |
-
|
| 51 |
-
|
| 52 |
-
println!("⚠️ Weights not found, using initial model.");
|
| 53 |
-
model.clone().into_record()
|
| 54 |
}
|
| 55 |
-
|
| 56 |
-
|
| 57 |
-
|
| 58 |
-
|
| 59 |
-
|
| 60 |
-
|
| 61 |
-
});
|
| 62 |
-
let resized = img.resize_exact(416, 416, image::imageops::FilterType::Triangle);
|
| 63 |
-
let pixels: Vec<f32> = resized
|
| 64 |
-
.to_rgb8()
|
| 65 |
-
.pixels()
|
| 66 |
-
.flat_map(|p| {
|
| 67 |
-
vec![
|
| 68 |
-
p[0] as f32 / 255.0,
|
| 69 |
-
p[1] as f32 / 255.0,
|
| 70 |
-
p[2] as f32 / 255.0,
|
| 71 |
-
]
|
| 72 |
-
})
|
| 73 |
-
.collect();
|
| 74 |
-
|
| 75 |
-
let tensor_data = TensorData::new(pixels, [1, 416, 416, 3]);
|
| 76 |
-
let input = Tensor::<Wgpu, 4>::from_data(tensor_data, &device).permute([0, 3, 1, 2]);
|
| 77 |
-
let (out, _): (Tensor<Wgpu, 4>, _) = model.forward(input);
|
| 78 |
|
| 79 |
-
|
| 80 |
-
|
|
|
|
|
|
|
|
|
|
| 81 |
|
| 82 |
-
|
| 83 |
-
|
| 84 |
-
|
| 85 |
-
.as_slice::<f32>()
|
| 86 |
-
.unwrap()[0];
|
| 87 |
-
println!("📊 Max Objectness Score: {:.6}", score);
|
| 88 |
}
|
|
|
|
| 1 |
+
use rust_auto_score_engine::args::{AppArgs, Command};
|
| 2 |
+
use rust_auto_score_engine::server::start_gui;
|
| 3 |
+
use rust_auto_score_engine::train::{train, TrainingConfig};
|
| 4 |
+
use rust_auto_score_engine::tests::test_model;
|
| 5 |
use burn::backend::wgpu::WgpuDevice;
|
| 6 |
use burn::backend::Wgpu;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 7 |
|
| 8 |
fn main() {
|
| 9 |
+
let app_args = AppArgs::parse();
|
| 10 |
let device = WgpuDevice::default();
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 11 |
|
| 12 |
+
match app_args.command {
|
| 13 |
+
Command::Gui => {
|
| 14 |
+
println!("🌐 [Burn-DartVision] Starting Professional Dashboard...");
|
| 15 |
+
tokio::runtime::Builder::new_multi_thread()
|
| 16 |
+
.enable_all()
|
| 17 |
+
.build()
|
| 18 |
+
.unwrap()
|
| 19 |
+
.block_on(start_gui(device));
|
|
|
|
|
|
|
| 20 |
}
|
| 21 |
+
Command::Test { img_path } => {
|
| 22 |
+
test_model(device, &img_path);
|
| 23 |
+
}
|
| 24 |
+
Command::Train => {
|
| 25 |
+
println!("🚀 [Burn-DartVision] Starting Full Project Training...");
|
| 26 |
+
let dataset_path = "dataset/labels.json";
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 27 |
|
| 28 |
+
let config = TrainingConfig {
|
| 29 |
+
num_epochs: 50,
|
| 30 |
+
batch_size: 1,
|
| 31 |
+
lr: 1e-3,
|
| 32 |
+
};
|
| 33 |
|
| 34 |
+
train::<burn::backend::Autodiff<Wgpu>>(device, dataset_path, config);
|
| 35 |
+
}
|
| 36 |
+
}
|
|
|
|
|
|
|
|
|
|
| 37 |
}
|
src/tests.rs
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
use crate::model::DartVisionModel;
|
| 2 |
+
use burn::backend::Wgpu;
|
| 3 |
+
use burn::backend::wgpu::WgpuDevice;
|
| 4 |
+
use burn::prelude::*;
|
| 5 |
+
use burn::record::{BinFileRecorder, FullPrecisionSettings, Recorder};
|
| 6 |
+
|
| 7 |
+
pub fn test_model(device: WgpuDevice, img_path: &str) {
|
| 8 |
+
println!("🔍 Testing model on: {}", img_path);
|
| 9 |
+
let recorder = BinFileRecorder::<FullPrecisionSettings>::default();
|
| 10 |
+
let model = DartVisionModel::<Wgpu>::new(&device);
|
| 11 |
+
|
| 12 |
+
let record = match recorder.load("model_weights".into(), &device) {
|
| 13 |
+
Ok(r) => r,
|
| 14 |
+
Err(_) => {
|
| 15 |
+
println!("⚠️ Weights not found, using initial model.");
|
| 16 |
+
model.clone().into_record()
|
| 17 |
+
}
|
| 18 |
+
};
|
| 19 |
+
let model = model.load_record(record);
|
| 20 |
+
|
| 21 |
+
let img = image::open(img_path).unwrap_or_else(|_| {
|
| 22 |
+
println!("❌ Image not found at {}. Using random tensor.", img_path);
|
| 23 |
+
image::DynamicImage::new_rgb8(416, 416)
|
| 24 |
+
});
|
| 25 |
+
let resized = img.resize_exact(416, 416, image::imageops::FilterType::Triangle);
|
| 26 |
+
let pixels: Vec<f32> = resized
|
| 27 |
+
.to_rgb8()
|
| 28 |
+
.pixels()
|
| 29 |
+
.flat_map(|p| {
|
| 30 |
+
vec![
|
| 31 |
+
p[0] as f32 / 255.0,
|
| 32 |
+
p[1] as f32 / 255.0,
|
| 33 |
+
p[2] as f32 / 255.0,
|
| 34 |
+
]
|
| 35 |
+
})
|
| 36 |
+
.collect();
|
| 37 |
+
|
| 38 |
+
let tensor_data = TensorData::new(pixels, [1, 416, 416, 3]);
|
| 39 |
+
let input = Tensor::<Wgpu, 4>::from_data(tensor_data, &device).permute([0, 3, 1, 2]);
|
| 40 |
+
let (out, _): (Tensor<Wgpu, 4>, _) = model.forward(input);
|
| 41 |
+
|
| 42 |
+
let obj = burn::tensor::activation::sigmoid(out.clone().narrow(1, 4, 1));
|
| 43 |
+
let (max_val, _) = obj.reshape([1, 676]).max_dim_with_indices(1);
|
| 44 |
+
|
| 45 |
+
let score = max_val
|
| 46 |
+
.to_data()
|
| 47 |
+
.convert::<f32>()
|
| 48 |
+
.as_slice::<f32>()
|
| 49 |
+
.unwrap()[0];
|
| 50 |
+
println!("📊 Max Objectness Score: {:.6}", score);
|
| 51 |
+
|
| 52 |
+
if score > 0.1 {
|
| 53 |
+
println!("✅ Model detection looks promising!");
|
| 54 |
+
} else {
|
| 55 |
+
println!("⚠️ Low confidence detection. Training may still be in progress.");
|
| 56 |
+
}
|
| 57 |
+
}
|
static/index.html
CHANGED
|
@@ -13,8 +13,9 @@
|
|
| 13 |
--secondary: #00d4ff;
|
| 14 |
--accent: #ff4d4d;
|
| 15 |
--bg: #0a0e14;
|
| 16 |
-
--card-bg: rgba(255, 255, 255, 0.
|
| 17 |
-
--glass-border: rgba(255, 255, 255, 0.
|
|
|
|
| 18 |
}
|
| 19 |
|
| 20 |
* {
|
|
@@ -32,78 +33,102 @@
|
|
| 32 |
flex-direction: column;
|
| 33 |
align-items: center;
|
| 34 |
overflow-x: hidden;
|
|
|
|
|
|
|
|
|
|
| 35 |
}
|
| 36 |
|
| 37 |
-
.bg-
|
| 38 |
position: fixed;
|
| 39 |
-
top: 0;
|
| 40 |
-
|
| 41 |
-
|
| 42 |
-
|
| 43 |
-
background: radial-gradient(circle at 50% 50%, #00ff8811 0%, transparent 50%),
|
| 44 |
-
radial-gradient(circle at 80% 20%, #00d4ff11 0%, transparent 40%);
|
| 45 |
z-index: -1;
|
| 46 |
pointer-events: none;
|
| 47 |
}
|
| 48 |
|
| 49 |
header {
|
| 50 |
-
padding: 2rem;
|
| 51 |
text-align: center;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 52 |
}
|
| 53 |
|
| 54 |
h1 {
|
| 55 |
font-size: 3.5rem;
|
| 56 |
font-weight: 800;
|
| 57 |
-
background: linear-gradient(
|
| 58 |
-webkit-background-clip: text;
|
| 59 |
background-clip: text;
|
| 60 |
-webkit-text-fill-color: transparent;
|
| 61 |
margin-bottom: 0.5rem;
|
| 62 |
-
letter-spacing: -
|
|
|
|
| 63 |
}
|
| 64 |
|
| 65 |
p.subtitle {
|
| 66 |
color: #8892b0;
|
| 67 |
-
font-size:
|
| 68 |
-
letter-spacing:
|
| 69 |
text-transform: uppercase;
|
|
|
|
|
|
|
| 70 |
}
|
| 71 |
|
| 72 |
main {
|
| 73 |
-
width:
|
| 74 |
-
max-width:
|
| 75 |
display: grid;
|
| 76 |
-
grid-template-columns: 1fr
|
| 77 |
-
gap:
|
| 78 |
-
padding:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 79 |
}
|
| 80 |
|
| 81 |
.visual-area {
|
| 82 |
display: flex;
|
| 83 |
flex-direction: column;
|
| 84 |
-
gap:
|
| 85 |
}
|
| 86 |
|
| 87 |
.upload-container {
|
| 88 |
background: var(--card-bg);
|
| 89 |
-
backdrop-filter: blur(
|
| 90 |
border: 1px solid var(--glass-border);
|
| 91 |
-
border-radius:
|
| 92 |
-
padding:
|
| 93 |
display: flex;
|
| 94 |
flex-direction: column;
|
| 95 |
align-items: center;
|
| 96 |
justify-content: center;
|
| 97 |
cursor: pointer;
|
| 98 |
-
transition: all 0.
|
| 99 |
-
min-height:
|
| 100 |
position: relative;
|
| 101 |
overflow: hidden;
|
|
|
|
| 102 |
}
|
| 103 |
|
| 104 |
-
.upload-container:hover
|
| 105 |
border-color: var(--primary);
|
| 106 |
-
box-shadow: 0 0
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 107 |
}
|
| 108 |
|
| 109 |
.preview-wrapper {
|
|
@@ -113,14 +138,17 @@
|
|
| 113 |
display: none;
|
| 114 |
justify-content: center;
|
| 115 |
align-items: center;
|
|
|
|
|
|
|
| 116 |
}
|
| 117 |
|
| 118 |
#preview-img {
|
| 119 |
max-width: 100%;
|
| 120 |
-
max-height:
|
| 121 |
-
border-radius:
|
| 122 |
display: block;
|
| 123 |
object-fit: contain;
|
|
|
|
| 124 |
}
|
| 125 |
|
| 126 |
#overlay-svg {
|
|
@@ -130,11 +158,30 @@
|
|
| 130 |
width: 100%;
|
| 131 |
height: 100%;
|
| 132 |
pointer-events: none;
|
|
|
|
| 133 |
}
|
| 134 |
|
| 135 |
-
.
|
| 136 |
-
|
| 137 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 138 |
|
| 139 |
.stats-panel {
|
| 140 |
display: flex;
|
|
@@ -143,25 +190,38 @@
|
|
| 143 |
}
|
| 144 |
|
| 145 |
.stat-card {
|
| 146 |
-
background:
|
|
|
|
| 147 |
border: 1px solid var(--glass-border);
|
| 148 |
-
border-radius:
|
| 149 |
-
padding: 1.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 150 |
}
|
| 151 |
|
| 152 |
-
.stat-label { color: #8892b0; font-size: 0.
|
| 153 |
-
.stat-value { font-size: 2rem; font-weight:
|
| 154 |
-
.conf-bar { height: 8px; background: rgba(255,255,255,0.
|
| 155 |
-
.conf-fill { height: 100%; width: 0%; background: linear-gradient(
|
| 156 |
|
| 157 |
.loading-spinner {
|
| 158 |
-
width:
|
| 159 |
-
border:
|
| 160 |
-
border-top:
|
| 161 |
border-radius: 50%;
|
| 162 |
-
animation: spin
|
| 163 |
display: none;
|
| 164 |
position: absolute;
|
|
|
|
|
|
|
| 165 |
}
|
| 166 |
|
| 167 |
@keyframes spin { 100% { transform: rotate(360deg); } }
|
|
@@ -169,32 +229,58 @@
|
|
| 169 |
.keypoint-marker {
|
| 170 |
fill: var(--primary);
|
| 171 |
stroke: #fff;
|
| 172 |
-
stroke-width:
|
| 173 |
-
filter: drop-shadow(0 0
|
| 174 |
-
animation: pulse
|
| 175 |
}
|
| 176 |
|
| 177 |
-
@keyframes pulse {
|
| 178 |
-
0% { r:
|
| 179 |
-
50% { r:
|
| 180 |
-
100% { r: 5; opacity: 1; }
|
| 181 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 182 |
</style>
|
| 183 |
</head>
|
| 184 |
<body>
|
| 185 |
-
<div class="bg-
|
| 186 |
<header>
|
| 187 |
-
<h1>DARTVISION <span style="font-weight:
|
| 188 |
-
<p class="subtitle">
|
| 189 |
</header>
|
| 190 |
|
| 191 |
<main>
|
| 192 |
<section class="visual-area">
|
| 193 |
<div class="upload-container" id="drop-zone">
|
|
|
|
| 194 |
<div id="initial-state">
|
| 195 |
-
<div class="upload-icon">
|
| 196 |
-
<div class="upload-text">
|
| 197 |
-
<div class="upload-subtext">
|
| 198 |
</div>
|
| 199 |
<div class="preview-wrapper" id="preview-wrapper">
|
| 200 |
<img id="preview-img">
|
|
@@ -208,7 +294,7 @@
|
|
| 208 |
<aside class="stats-panel">
|
| 209 |
<div class="stat-card">
|
| 210 |
<div class="stat-label">Model Status</div>
|
| 211 |
-
<div class="stat-value" id="status-text" style="font-size: 1.
|
| 212 |
</div>
|
| 213 |
<div class="stat-card">
|
| 214 |
<div class="stat-label">AI Confidence</div>
|
|
@@ -217,7 +303,7 @@
|
|
| 217 |
</div>
|
| 218 |
<div class="stat-card">
|
| 219 |
<div class="stat-label">Detection Result</div>
|
| 220 |
-
<div class="stat-value" id="result-text" style="font-size: 1.1rem; opacity: 0.8;">No
|
| 221 |
</div>
|
| 222 |
</aside>
|
| 223 |
</main>
|
|
@@ -230,6 +316,7 @@
|
|
| 230 |
const initialState = document.getElementById('initial-state');
|
| 231 |
const svgOverlay = document.getElementById('overlay-svg');
|
| 232 |
const spinner = document.getElementById('spinner');
|
|
|
|
| 233 |
|
| 234 |
dropZone.onclick = () => fileInput.click();
|
| 235 |
fileInput.onchange = (e) => handleFile(e.target.files[0]);
|
|
@@ -251,21 +338,36 @@
|
|
| 251 |
reader.readAsDataURL(file);
|
| 252 |
|
| 253 |
spinner.style.display = 'block';
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 254 |
const formData = new FormData();
|
| 255 |
formData.append('image', file);
|
| 256 |
|
| 257 |
try {
|
| 258 |
const response = await fetch('/api/predict', { method: 'POST', body: formData });
|
| 259 |
const data = await response.json();
|
|
|
|
| 260 |
spinner.style.display = 'none';
|
|
|
|
| 261 |
|
| 262 |
if (data.status === 'success') {
|
|
|
|
|
|
|
| 263 |
updateUI(data);
|
| 264 |
drawKeypoints(data.keypoints);
|
|
|
|
|
|
|
|
|
|
| 265 |
}
|
| 266 |
} catch (err) {
|
| 267 |
spinner.style.display = 'none';
|
| 268 |
-
|
|
|
|
|
|
|
| 269 |
}
|
| 270 |
}
|
| 271 |
|
|
@@ -274,26 +376,33 @@
|
|
| 274 |
document.getElementById('conf-val').innerText = `${conf}%`;
|
| 275 |
document.getElementById('conf-fill').style.width = `${conf}%`;
|
| 276 |
|
| 277 |
-
let resultHtml = `<div style="margin
|
| 278 |
if (data.keypoints && data.keypoints.length >= 8) {
|
| 279 |
const names = ["Cal 1", "Cal 2", "Cal 3", "Cal 4", "Dart"];
|
| 280 |
-
resultHtml += `<div style="font-size: 0.
|
|
|
|
| 281 |
for (let i = 0; i < data.keypoints.length; i += 2) {
|
| 282 |
const classIdx = i / 2;
|
|
|
|
| 283 |
const name = names[classIdx] || `Dart ${Math.floor(classIdx - 3)}`;
|
| 284 |
const x = data.keypoints[i].toFixed(3);
|
| 285 |
const y = data.keypoints[i+1].toFixed(3);
|
| 286 |
|
| 287 |
-
|
| 288 |
-
let scoreLabel = "";
|
| 289 |
if (classIdx >= 4 && data.scores && data.scores[classIdx - 4]) {
|
| 290 |
-
|
|
|
|
|
|
|
| 291 |
}
|
| 292 |
|
| 293 |
-
resultHtml += `
|
| 294 |
-
<
|
| 295 |
-
|
| 296 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 297 |
}
|
| 298 |
}
|
| 299 |
document.getElementById('result-text').innerHTML = resultHtml;
|
|
@@ -307,43 +416,59 @@
|
|
| 307 |
clearKeypoints();
|
| 308 |
if (!pts || pts.length === 0) return;
|
| 309 |
|
| 310 |
-
// Wait for image to render to get actual display dimensions
|
| 311 |
setTimeout(() => {
|
| 312 |
const rect = previewImg.getBoundingClientRect();
|
| 313 |
const wrapperRect = previewWrapper.getBoundingClientRect();
|
| 314 |
|
| 315 |
-
// Offset calculation relative to wrapper
|
| 316 |
const offsetX = rect.left - wrapperRect.left;
|
| 317 |
const offsetY = rect.top - wrapperRect.top;
|
| 318 |
const width = rect.width;
|
| 319 |
const height = rect.height;
|
| 320 |
|
| 321 |
-
const classNames = ["
|
| 322 |
for (let i = 0; i < pts.length; i += 2) {
|
|
|
|
|
|
|
| 323 |
const x = pts[i] * width + offsetX;
|
| 324 |
const y = pts[i+1] * height + offsetY;
|
| 325 |
-
const
|
| 326 |
-
const name = classNames[classIdx] || `Dart ${Math.floor(classIdx - 3)}`;
|
| 327 |
|
|
|
|
|
|
|
| 328 |
const circle = document.createElementNS("http://www.w3.org/2000/svg", "circle");
|
| 329 |
circle.setAttribute("cx", x);
|
| 330 |
circle.setAttribute("cy", y);
|
| 331 |
-
circle.setAttribute("r",
|
| 332 |
circle.setAttribute("class", "keypoint-marker");
|
| 333 |
-
if (
|
|
|
|
|
|
|
|
|
|
| 334 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 335 |
const label = document.createElementNS("http://www.w3.org/2000/svg", "text");
|
| 336 |
-
label.setAttribute("x", x +
|
| 337 |
-
label.setAttribute("y", y -
|
| 338 |
-
label.setAttribute("fill",
|
| 339 |
-
label.setAttribute("font-size", "
|
| 340 |
-
label.setAttribute("font-weight", "
|
|
|
|
| 341 |
label.textContent = name;
|
| 342 |
|
| 343 |
-
|
| 344 |
-
|
|
|
|
|
|
|
| 345 |
}
|
| 346 |
-
},
|
| 347 |
}
|
| 348 |
</script>
|
| 349 |
</body>
|
|
|
|
| 13 |
--secondary: #00d4ff;
|
| 14 |
--accent: #ff4d4d;
|
| 15 |
--bg: #0a0e14;
|
| 16 |
+
--card-bg: rgba(255, 255, 255, 0.03);
|
| 17 |
+
--glass-border: rgba(255, 255, 255, 0.08);
|
| 18 |
+
--panel-shadow: 0 20px 50px rgba(0, 0, 0, 0.5);
|
| 19 |
}
|
| 20 |
|
| 21 |
* {
|
|
|
|
| 33 |
flex-direction: column;
|
| 34 |
align-items: center;
|
| 35 |
overflow-x: hidden;
|
| 36 |
+
background-image:
|
| 37 |
+
radial-gradient(circle at 20% 20%, rgba(0, 255, 136, 0.05) 0%, transparent 40%),
|
| 38 |
+
radial-gradient(circle at 80% 80%, rgba(0, 212, 255, 0.05) 0%, transparent 40%);
|
| 39 |
}
|
| 40 |
|
| 41 |
+
.bg-grid {
|
| 42 |
position: fixed;
|
| 43 |
+
top: 0; left: 0; width: 100%; height: 100%;
|
| 44 |
+
background-image: linear-gradient(rgba(255,255,255,0.02) 1px, transparent 1px),
|
| 45 |
+
linear-gradient(90deg, rgba(255,255,255,0.02) 1px, transparent 1px);
|
| 46 |
+
background-size: 50px 50px;
|
|
|
|
|
|
|
| 47 |
z-index: -1;
|
| 48 |
pointer-events: none;
|
| 49 |
}
|
| 50 |
|
| 51 |
header {
|
| 52 |
+
padding: 3rem 2rem;
|
| 53 |
text-align: center;
|
| 54 |
+
width: 100%;
|
| 55 |
+
background: linear-gradient(to bottom, rgba(10, 14, 20, 0.8), transparent);
|
| 56 |
+
backdrop-filter: blur(10px);
|
| 57 |
+
border-bottom: 1px solid var(--glass-border);
|
| 58 |
+
margin-bottom: 2rem;
|
| 59 |
}
|
| 60 |
|
| 61 |
h1 {
|
| 62 |
font-size: 3.5rem;
|
| 63 |
font-weight: 800;
|
| 64 |
+
background: linear-gradient(135deg, var(--primary), var(--secondary));
|
| 65 |
-webkit-background-clip: text;
|
| 66 |
background-clip: text;
|
| 67 |
-webkit-text-fill-color: transparent;
|
| 68 |
margin-bottom: 0.5rem;
|
| 69 |
+
letter-spacing: -2px;
|
| 70 |
+
filter: drop-shadow(0 0 20px rgba(0, 255, 136, 0.3));
|
| 71 |
}
|
| 72 |
|
| 73 |
p.subtitle {
|
| 74 |
color: #8892b0;
|
| 75 |
+
font-size: 0.9rem;
|
| 76 |
+
letter-spacing: 4px;
|
| 77 |
text-transform: uppercase;
|
| 78 |
+
font-weight: 400;
|
| 79 |
+
opacity: 0.8;
|
| 80 |
}
|
| 81 |
|
| 82 |
main {
|
| 83 |
+
width: 95%;
|
| 84 |
+
max-width: 1400px;
|
| 85 |
display: grid;
|
| 86 |
+
grid-template-columns: 1fr 400px;
|
| 87 |
+
gap: 2.5rem;
|
| 88 |
+
padding: 0 0 4rem 0;
|
| 89 |
+
animation: fadeIn 0.8s ease-out;
|
| 90 |
+
}
|
| 91 |
+
|
| 92 |
+
@keyframes fadeIn {
|
| 93 |
+
from { opacity: 0; transform: translateY(20px); }
|
| 94 |
+
to { opacity: 1; transform: translateY(0); }
|
| 95 |
}
|
| 96 |
|
| 97 |
.visual-area {
|
| 98 |
display: flex;
|
| 99 |
flex-direction: column;
|
| 100 |
+
gap: 1.5rem;
|
| 101 |
}
|
| 102 |
|
| 103 |
.upload-container {
|
| 104 |
background: var(--card-bg);
|
| 105 |
+
backdrop-filter: blur(30px);
|
| 106 |
border: 1px solid var(--glass-border);
|
| 107 |
+
border-radius: 32px;
|
| 108 |
+
padding: 1.5rem;
|
| 109 |
display: flex;
|
| 110 |
flex-direction: column;
|
| 111 |
align-items: center;
|
| 112 |
justify-content: center;
|
| 113 |
cursor: pointer;
|
| 114 |
+
transition: all 0.5s cubic-bezier(0.4, 0, 0.2, 1);
|
| 115 |
+
min-height: 700px;
|
| 116 |
position: relative;
|
| 117 |
overflow: hidden;
|
| 118 |
+
box-shadow: var(--panel-shadow);
|
| 119 |
}
|
| 120 |
|
| 121 |
+
.upload-container:hover {
|
| 122 |
border-color: var(--primary);
|
| 123 |
+
box-shadow: 0 0 60px rgba(0, 255, 136, 0.15);
|
| 124 |
+
background: rgba(255, 255, 255, 0.05);
|
| 125 |
+
transform: scale(1.005);
|
| 126 |
+
}
|
| 127 |
+
|
| 128 |
+
.upload-container.drag-over {
|
| 129 |
+
border-color: var(--secondary);
|
| 130 |
+
background: rgba(0, 212, 255, 0.08);
|
| 131 |
+
transform: scale(1.01);
|
| 132 |
}
|
| 133 |
|
| 134 |
.preview-wrapper {
|
|
|
|
| 138 |
display: none;
|
| 139 |
justify-content: center;
|
| 140 |
align-items: center;
|
| 141 |
+
border-radius: 20px;
|
| 142 |
+
overflow: hidden;
|
| 143 |
}
|
| 144 |
|
| 145 |
#preview-img {
|
| 146 |
max-width: 100%;
|
| 147 |
+
max-height: 650px;
|
| 148 |
+
border-radius: 24px;
|
| 149 |
display: block;
|
| 150 |
object-fit: contain;
|
| 151 |
+
transition: 0.3s;
|
| 152 |
}
|
| 153 |
|
| 154 |
#overlay-svg {
|
|
|
|
| 158 |
width: 100%;
|
| 159 |
height: 100%;
|
| 160 |
pointer-events: none;
|
| 161 |
+
z-index: 10;
|
| 162 |
}
|
| 163 |
|
| 164 |
+
.scanline {
|
| 165 |
+
position: absolute;
|
| 166 |
+
top: -100%;
|
| 167 |
+
left: 0;
|
| 168 |
+
width: 100%;
|
| 169 |
+
height: 80px;
|
| 170 |
+
background: linear-gradient(to bottom, transparent, rgba(0, 255, 136, 0.2), transparent);
|
| 171 |
+
opacity: 0.8;
|
| 172 |
+
display: none;
|
| 173 |
+
z-index: 20;
|
| 174 |
+
pointer-events: none;
|
| 175 |
+
}
|
| 176 |
+
|
| 177 |
+
@keyframes scan {
|
| 178 |
+
0% { top: -10%; }
|
| 179 |
+
100% { top: 110%; }
|
| 180 |
+
}
|
| 181 |
+
|
| 182 |
+
.upload-icon { font-size: 5rem; margin-bottom: 1.5rem; filter: drop-shadow(0 0 15px rgba(255,255,255,0.2)); }
|
| 183 |
+
.upload-text { font-size: 1.8rem; font-weight: 600; color: #fff; letter-spacing: -0.5px; }
|
| 184 |
+
.upload-subtext { color: #8892b0; margin-top: 1rem; font-size: 1rem; }
|
| 185 |
|
| 186 |
.stats-panel {
|
| 187 |
display: flex;
|
|
|
|
| 190 |
}
|
| 191 |
|
| 192 |
.stat-card {
|
| 193 |
+
background: var(--card-bg);
|
| 194 |
+
backdrop-filter: blur(20px);
|
| 195 |
border: 1px solid var(--glass-border);
|
| 196 |
+
border-radius: 28px;
|
| 197 |
+
padding: 1.8rem;
|
| 198 |
+
position: relative;
|
| 199 |
+
overflow: hidden;
|
| 200 |
+
box-shadow: 0 10px 30px rgba(0,0,0,0.2);
|
| 201 |
+
transition: 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
| 202 |
+
}
|
| 203 |
+
|
| 204 |
+
.stat-card:hover {
|
| 205 |
+
border-color: rgba(255,255,255,0.2);
|
| 206 |
+
background: rgba(255, 255, 255, 0.06);
|
| 207 |
+
transform: translateY(-5px);
|
| 208 |
}
|
| 209 |
|
| 210 |
+
.stat-label { color: #8892b0; font-size: 0.75rem; font-weight: 700; margin-bottom: 0.8rem; text-transform: uppercase; letter-spacing: 2px; }
|
| 211 |
+
.stat-value { font-size: 2.2rem; font-weight: 800; margin-bottom: 0.5rem; }
|
| 212 |
+
.conf-bar { height: 8px; background: rgba(255,255,255,0.05); border-radius: 4px; margin-top: 1.2rem; overflow: hidden; }
|
| 213 |
+
.conf-fill { height: 100%; width: 0%; background: linear-gradient(90deg, var(--primary), var(--secondary)); transition: 1.5s cubic-bezier(0.19, 1, 0.22, 1); }
|
| 214 |
|
| 215 |
.loading-spinner {
|
| 216 |
+
width: 70px; height: 70px;
|
| 217 |
+
border: 5px solid rgba(0, 255, 136, 0.05);
|
| 218 |
+
border-top: 5px solid var(--primary);
|
| 219 |
border-radius: 50%;
|
| 220 |
+
animation: spin 0.8s cubic-bezier(0.68, -0.55, 0.265, 1.55) infinite;
|
| 221 |
display: none;
|
| 222 |
position: absolute;
|
| 223 |
+
z-index: 30;
|
| 224 |
+
filter: drop-shadow(0 0 15px var(--primary));
|
| 225 |
}
|
| 226 |
|
| 227 |
@keyframes spin { 100% { transform: rotate(360deg); } }
|
|
|
|
| 229 |
.keypoint-marker {
|
| 230 |
fill: var(--primary);
|
| 231 |
stroke: #fff;
|
| 232 |
+
stroke-width: 2.5px;
|
| 233 |
+
filter: drop-shadow(0 0 10px var(--primary));
|
| 234 |
+
animation: pulse-marker 2s infinite cubic-bezier(0.4, 0, 0.6, 1);
|
| 235 |
}
|
| 236 |
|
| 237 |
+
@keyframes pulse-marker {
|
| 238 |
+
0%, 100% { r: 6; opacity: 1; }
|
| 239 |
+
50% { r: 10; opacity: 0.6; }
|
|
|
|
| 240 |
}
|
| 241 |
+
|
| 242 |
+
.result-item {
|
| 243 |
+
padding: 14px 0;
|
| 244 |
+
border-bottom: 1px solid rgba(255,255,255,0.04);
|
| 245 |
+
display: flex;
|
| 246 |
+
align-items: center;
|
| 247 |
+
justify-content: space-between;
|
| 248 |
+
animation: slideIn 0.4s ease-out forwards;
|
| 249 |
+
}
|
| 250 |
+
|
| 251 |
+
@keyframes slideIn {
|
| 252 |
+
from { opacity: 0; transform: translateX(20px); }
|
| 253 |
+
to { opacity: 1; transform: translateX(0); }
|
| 254 |
+
}
|
| 255 |
+
|
| 256 |
+
.badge {
|
| 257 |
+
padding: 5px 12px;
|
| 258 |
+
border-radius: 20px;
|
| 259 |
+
font-size: 0.75rem;
|
| 260 |
+
font-weight: 800;
|
| 261 |
+
text-transform: uppercase;
|
| 262 |
+
letter-spacing: 0.5px;
|
| 263 |
+
}
|
| 264 |
+
|
| 265 |
+
.badge-dart { background: linear-gradient(135deg, #ff4d4d, #c00); color: white; box-shadow: 0 4px 15px rgba(255, 77, 77, 0.4); }
|
| 266 |
+
.badge-cal { background: linear-gradient(135deg, #00ff88, #00d4ff); color: #0a0e14; box-shadow: 0 4px 15px rgba(0, 255, 136, 0.2); }
|
| 267 |
</style>
|
| 268 |
</head>
|
| 269 |
<body>
|
| 270 |
+
<div class="bg-grid"></div>
|
| 271 |
<header>
|
| 272 |
+
<h1>DARTVISION <span style="font-weight: 200; opacity: 0.4;">CORE</span></h1>
|
| 273 |
+
<p class="subtitle">Neural Scoring & Board Analytics</p>
|
| 274 |
</header>
|
| 275 |
|
| 276 |
<main>
|
| 277 |
<section class="visual-area">
|
| 278 |
<div class="upload-container" id="drop-zone">
|
| 279 |
+
<div class="scanline" id="scanline"></div>
|
| 280 |
<div id="initial-state">
|
| 281 |
+
<div class="upload-icon">💠</div>
|
| 282 |
+
<div class="upload-text">Initialize Vision System</div>
|
| 283 |
+
<div class="upload-subtext">Drop board imagery for real-time neural mapping</div>
|
| 284 |
</div>
|
| 285 |
<div class="preview-wrapper" id="preview-wrapper">
|
| 286 |
<img id="preview-img">
|
|
|
|
| 294 |
<aside class="stats-panel">
|
| 295 |
<div class="stat-card">
|
| 296 |
<div class="stat-label">Model Status</div>
|
| 297 |
+
<div class="stat-value" id="status-text" style="font-size: 1.4rem; color: var(--primary);">System Ready</div>
|
| 298 |
</div>
|
| 299 |
<div class="stat-card">
|
| 300 |
<div class="stat-label">AI Confidence</div>
|
|
|
|
| 303 |
</div>
|
| 304 |
<div class="stat-card">
|
| 305 |
<div class="stat-label">Detection Result</div>
|
| 306 |
+
<div class="stat-value" id="result-text" style="font-size: 1.1rem; opacity: 0.8;">No Frame Captured</div>
|
| 307 |
</div>
|
| 308 |
</aside>
|
| 309 |
</main>
|
|
|
|
| 316 |
const initialState = document.getElementById('initial-state');
|
| 317 |
const svgOverlay = document.getElementById('overlay-svg');
|
| 318 |
const spinner = document.getElementById('spinner');
|
| 319 |
+
const scanline = document.getElementById('scanline');
|
| 320 |
|
| 321 |
dropZone.onclick = () => fileInput.click();
|
| 322 |
fileInput.onchange = (e) => handleFile(e.target.files[0]);
|
|
|
|
| 338 |
reader.readAsDataURL(file);
|
| 339 |
|
| 340 |
spinner.style.display = 'block';
|
| 341 |
+
scanline.style.display = 'block';
|
| 342 |
+
scanline.style.animation = 'scan 2s linear infinite';
|
| 343 |
+
|
| 344 |
+
document.getElementById('status-text').innerText = 'Neural Processing...';
|
| 345 |
+
document.getElementById('status-text').style.color = 'var(--secondary)';
|
| 346 |
+
|
| 347 |
const formData = new FormData();
|
| 348 |
formData.append('image', file);
|
| 349 |
|
| 350 |
try {
|
| 351 |
const response = await fetch('/api/predict', { method: 'POST', body: formData });
|
| 352 |
const data = await response.json();
|
| 353 |
+
|
| 354 |
spinner.style.display = 'none';
|
| 355 |
+
scanline.style.display = 'none';
|
| 356 |
|
| 357 |
if (data.status === 'success') {
|
| 358 |
+
document.getElementById('status-text').innerText = 'Analysis Complete';
|
| 359 |
+
document.getElementById('status-text').style.color = 'var(--primary)';
|
| 360 |
updateUI(data);
|
| 361 |
drawKeypoints(data.keypoints);
|
| 362 |
+
} else {
|
| 363 |
+
document.getElementById('status-text').innerText = 'Analysis Failed';
|
| 364 |
+
document.getElementById('status-text').style.color = 'var(--accent)';
|
| 365 |
}
|
| 366 |
} catch (err) {
|
| 367 |
spinner.style.display = 'none';
|
| 368 |
+
scanline.style.display = 'none';
|
| 369 |
+
document.getElementById('status-text').innerText = 'System Error';
|
| 370 |
+
document.getElementById('status-text').style.color = 'var(--accent)';
|
| 371 |
}
|
| 372 |
}
|
| 373 |
|
|
|
|
| 376 |
document.getElementById('conf-val').innerText = `${conf}%`;
|
| 377 |
document.getElementById('conf-fill').style.width = `${conf}%`;
|
| 378 |
|
| 379 |
+
let resultHtml = `<div style="margin: 1.5rem 0 1rem 0; font-size: 0.95rem; line-height: 1.6; color: rgba(255,255,255,0.9);">${data.message}</div>`;
|
| 380 |
if (data.keypoints && data.keypoints.length >= 8) {
|
| 381 |
const names = ["Cal 1", "Cal 2", "Cal 3", "Cal 4", "Dart"];
|
| 382 |
+
resultHtml += `<div style="font-size: 0.75rem; color: #8892b0; margin: 1.5rem 0 0.8rem 0; letter-spacing: 2px; text-transform: uppercase;">MAPPED DATA</div>`;
|
| 383 |
+
|
| 384 |
for (let i = 0; i < data.keypoints.length; i += 2) {
|
| 385 |
const classIdx = i / 2;
|
| 386 |
+
const isCal = classIdx < 4;
|
| 387 |
const name = names[classIdx] || `Dart ${Math.floor(classIdx - 3)}`;
|
| 388 |
const x = data.keypoints[i].toFixed(3);
|
| 389 |
const y = data.keypoints[i+1].toFixed(3);
|
| 390 |
|
| 391 |
+
let scoreHtml = "";
|
|
|
|
| 392 |
if (classIdx >= 4 && data.scores && data.scores[classIdx - 4]) {
|
| 393 |
+
scoreHtml = `<span class="badge badge-dart">${data.scores[classIdx - 4]}</span>`;
|
| 394 |
+
} else if (isCal) {
|
| 395 |
+
scoreHtml = `<span class="badge badge-cal" style="font-size: 0.6rem;">LOCKED</span>`;
|
| 396 |
}
|
| 397 |
|
| 398 |
+
resultHtml += `
|
| 399 |
+
<div class="result-item" style="animation-delay: ${classIdx * 0.1}s">
|
| 400 |
+
<div style="display: flex; flex-direction: column;">
|
| 401 |
+
<span style="font-weight: 600; font-size: 0.95rem; color: ${isCal ? 'var(--primary)' : 'var(--accent)'}">${name}</span>
|
| 402 |
+
<span style="font-size: 0.7rem; color: #8892b0; font-family: monospace; letter-spacing: 0.5px;">X:${x} Y:${y}</span>
|
| 403 |
+
</div>
|
| 404 |
+
${scoreHtml}
|
| 405 |
+
</div>`;
|
| 406 |
}
|
| 407 |
}
|
| 408 |
document.getElementById('result-text').innerHTML = resultHtml;
|
|
|
|
| 416 |
clearKeypoints();
|
| 417 |
if (!pts || pts.length === 0) return;
|
| 418 |
|
|
|
|
| 419 |
setTimeout(() => {
|
| 420 |
const rect = previewImg.getBoundingClientRect();
|
| 421 |
const wrapperRect = previewWrapper.getBoundingClientRect();
|
| 422 |
|
|
|
|
| 423 |
const offsetX = rect.left - wrapperRect.left;
|
| 424 |
const offsetY = rect.top - wrapperRect.top;
|
| 425 |
const width = rect.width;
|
| 426 |
const height = rect.height;
|
| 427 |
|
| 428 |
+
const classNames = ["CALIBRATION CORNER 1", "CALIBRATION CORNER 2", "CALIBRATION CORNER 3", "CALIBRATION CORNER 4", "DART POINT"];
|
| 429 |
for (let i = 0; i < pts.length; i += 2) {
|
| 430 |
+
const classIdx = i / 2;
|
| 431 |
+
const isCal = classIdx < 4;
|
| 432 |
const x = pts[i] * width + offsetX;
|
| 433 |
const y = pts[i+1] * height + offsetY;
|
| 434 |
+
const name = classNames[classIdx] || `DART POINT`;
|
|
|
|
| 435 |
|
| 436 |
+
const group = document.createElementNS("http://www.w3.org/2000/svg", "g");
|
| 437 |
+
|
| 438 |
const circle = document.createElementNS("http://www.w3.org/2000/svg", "circle");
|
| 439 |
circle.setAttribute("cx", x);
|
| 440 |
circle.setAttribute("cy", y);
|
| 441 |
+
circle.setAttribute("r", 7);
|
| 442 |
circle.setAttribute("class", "keypoint-marker");
|
| 443 |
+
if (!isCal) {
|
| 444 |
+
circle.style.fill = "#ff4d4d";
|
| 445 |
+
circle.style.filter = "drop-shadow(0 0 15px rgba(255, 77, 77, 0.6))";
|
| 446 |
+
}
|
| 447 |
|
| 448 |
+
const labelBg = document.createElementNS("http://www.w3.org/2000/svg", "rect");
|
| 449 |
+
labelBg.setAttribute("x", x + 15);
|
| 450 |
+
labelBg.setAttribute("y", y - 25);
|
| 451 |
+
labelBg.setAttribute("width", name.length * 7 + 12);
|
| 452 |
+
labelBg.setAttribute("height", "22");
|
| 453 |
+
labelBg.setAttribute("rx", "11");
|
| 454 |
+
labelBg.setAttribute("fill", "rgba(0,0,0,0.7)");
|
| 455 |
+
labelBg.setAttribute("style", "backdrop-filter: blur(8px); stroke: rgba(255,255,255,0.1); stroke-width: 1;");
|
| 456 |
+
|
| 457 |
const label = document.createElementNS("http://www.w3.org/2000/svg", "text");
|
| 458 |
+
label.setAttribute("x", x + 21);
|
| 459 |
+
label.setAttribute("y", y - 9);
|
| 460 |
+
label.setAttribute("fill", isCal ? "#00ff88" : "#ff4d4d");
|
| 461 |
+
label.setAttribute("font-size", "10px");
|
| 462 |
+
label.setAttribute("font-weight", "900");
|
| 463 |
+
label.setAttribute("style", "text-transform: uppercase; letter-spacing: 1.5px;");
|
| 464 |
label.textContent = name;
|
| 465 |
|
| 466 |
+
group.appendChild(circle);
|
| 467 |
+
group.appendChild(labelBg);
|
| 468 |
+
group.appendChild(label);
|
| 469 |
+
svgOverlay.appendChild(group);
|
| 470 |
}
|
| 471 |
+
}, 100);
|
| 472 |
}
|
| 473 |
</script>
|
| 474 |
</body>
|