kapil commited on
Commit
90dd6a4
·
1 Parent(s): c6c01fc

feat: initialize project structure with core inference logic and interactive dashboard UI

Browse files
Files changed (7) hide show
  1. README.md +16 -9
  2. src/args.rs +40 -0
  3. src/inference.rs +1 -1
  4. src/lib.rs +9 -0
  5. src/main.rs +27 -78
  6. src/tests.rs +57 -0
  7. 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 (Configured for 50 Epochs)
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
- After training is complete (generating the `model_weights.bin` file), launch the testing interface:
69
  ```bash
70
- # Starts the Axum web server
71
  cargo run -- gui
72
  ```
73
  **Features:**
74
- - **Image Upload**: Test local image samples via the dashboard.
75
- - **Point Visualization**: Inspect detected calibration points and dart locations.
76
- - **Automatic Scoring**: Instant sector calculation and latency reporting.
 
 
 
 
 
 
 
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::{DynamicImage, GenericImageView};
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 crate::model::DartVisionModel;
2
- use crate::server::start_gui;
3
- use crate::train::{train, TrainingConfig};
 
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
- fn test_model(device: WgpuDevice, img_path: &str) {
45
- println!("🔍 Testing model on: {}", img_path);
46
- let recorder = BinFileRecorder::<FullPrecisionSettings>::default();
47
- let model = DartVisionModel::<Wgpu>::new(&device);
48
-
49
- let record = match recorder.load("model_weights".into(), &device) {
50
- Ok(r) => r,
51
- Err(_) => {
52
- println!("⚠️ Weights not found, using initial model.");
53
- model.clone().into_record()
54
  }
55
- };
56
- let model = model.load_record(record);
57
-
58
- let img = image::open(img_path).unwrap_or_else(|_| {
59
- println!(" Image not found at {}. Using random tensor.", img_path);
60
- image::DynamicImage::new_rgb8(416, 416)
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
- let obj = burn::tensor::activation::sigmoid(out.clone().narrow(1, 4, 1));
80
- let (max_val, _) = obj.reshape([1, 676]).max_dim_with_indices(1);
 
 
 
81
 
82
- let score = max_val
83
- .to_data()
84
- .convert::<f32>()
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.05);
17
- --glass-border: rgba(255, 255, 255, 0.1);
 
18
  }
19
 
20
  * {
@@ -32,78 +33,102 @@
32
  flex-direction: column;
33
  align-items: center;
34
  overflow-x: hidden;
 
 
 
35
  }
36
 
37
- .bg-glow {
38
  position: fixed;
39
- top: 0;
40
- left: 0;
41
- right: 0;
42
- bottom: 0;
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(to right, var(--primary), var(--secondary));
58
  -webkit-background-clip: text;
59
  background-clip: text;
60
  -webkit-text-fill-color: transparent;
61
  margin-bottom: 0.5rem;
62
- letter-spacing: -1px;
 
63
  }
64
 
65
  p.subtitle {
66
  color: #8892b0;
67
- font-size: 1.1rem;
68
- letter-spacing: 2px;
69
  text-transform: uppercase;
 
 
70
  }
71
 
72
  main {
73
- width: 90%;
74
- max-width: 1200px;
75
  display: grid;
76
- grid-template-columns: 1fr 380px;
77
- gap: 2rem;
78
- padding: 2rem 0;
 
 
 
 
 
 
79
  }
80
 
81
  .visual-area {
82
  display: flex;
83
  flex-direction: column;
84
- gap: 1rem;
85
  }
86
 
87
  .upload-container {
88
  background: var(--card-bg);
89
- backdrop-filter: blur(20px);
90
  border: 1px solid var(--glass-border);
91
- border-radius: 24px;
92
- padding: 1rem;
93
  display: flex;
94
  flex-direction: column;
95
  align-items: center;
96
  justify-content: center;
97
  cursor: pointer;
98
- transition: all 0.4s;
99
- min-height: 600px;
100
  position: relative;
101
  overflow: hidden;
 
102
  }
103
 
104
- .upload-container:hover, .upload-container.drag-over {
105
  border-color: var(--primary);
106
- box-shadow: 0 0 40px rgba(0, 255, 136, 0.1);
 
 
 
 
 
 
 
 
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: 550px;
121
- border-radius: 12px;
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
- .upload-icon { font-size: 4rem; margin-bottom: 1rem; }
136
- .upload-text { font-size: 1.5rem; color: #ccd6f6; }
137
- .upload-subtext { color: #8892b0; margin-top: 1rem; }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
138
 
139
  .stats-panel {
140
  display: flex;
@@ -143,25 +190,38 @@
143
  }
144
 
145
  .stat-card {
146
- background: rgba(255, 255, 255, 0.03);
 
147
  border: 1px solid var(--glass-border);
148
- border-radius: 20px;
149
- padding: 1.5rem;
 
 
 
 
 
 
 
 
 
 
150
  }
151
 
152
- .stat-label { color: #8892b0; font-size: 0.9rem; margin-bottom: 0.5rem; text-transform: uppercase; }
153
- .stat-value { font-size: 2rem; font-weight: 600; }
154
- .conf-bar { height: 8px; background: rgba(255,255,255,0.1); border-radius: 4px; margin-top: 1rem; overflow: hidden; }
155
- .conf-fill { height: 100%; width: 0%; background: linear-gradient(to right, var(--primary), var(--secondary)); transition: 1s; }
156
 
157
  .loading-spinner {
158
- width: 40px; height: 40px;
159
- border: 3px solid rgba(0, 255, 136, 0.1);
160
- border-top: 3px solid var(--primary);
161
  border-radius: 50%;
162
- animation: spin 1s linear infinite;
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: 2px;
173
- filter: drop-shadow(0 0 5px var(--primary));
174
- animation: pulse 1.5s infinite;
175
  }
176
 
177
- @keyframes pulse {
178
- 0% { r: 5; opacity: 1; }
179
- 50% { r: 8; opacity: 0.7; }
180
- 100% { r: 5; opacity: 1; }
181
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
182
  </style>
183
  </head>
184
  <body>
185
- <div class="bg-glow"></div>
186
  <header>
187
- <h1>DARTVISION <span style="font-weight: 300; opacity: 0.5;">AI</span></h1>
188
- <p class="subtitle">Intelligent Labelling & Scoring Dashboard</p>
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">🎯</div>
196
- <div class="upload-text">Drag & Drop Image</div>
197
- <div class="upload-subtext">calibration and scoring results will appear here</div>
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.2rem; color: var(--primary);">System Ready</div>
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 Image Uploaded</div>
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
- document.getElementById('status-text').innerText = 'Error';
 
 
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-top: 1rem; font-size: 0.9rem; line-height: 1.5;">${data.message}</div>`;
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.8rem; opacity: 0.6; margin-top: 5px;">Results & Mapping:</div>`;
 
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
- // Get score label if it's a dart
288
- let scoreLabel = "";
289
  if (classIdx >= 4 && data.scores && data.scores[classIdx - 4]) {
290
- scoreLabel = ` <span style="background: #ff4d4d; color: white; padding: 2px 6px; border-radius: 4px; font-weight: 800; margin-left: 10px;">${data.scores[classIdx - 4]}</span>`;
 
 
291
  }
292
 
293
- resultHtml += `<div style="font-size: 0.85rem; padding: 6px 0; border-bottom: 1px solid rgba(255,255,255,0.05); display: flex; align-items: center; justify-content: space-between;">
294
- <span><span style="color: ${i < 8 ? '#00ff88' : '#ff4d4d'}">${name}</span>: [${x}, ${y}]</span>
295
- ${scoreLabel}
296
- </div>`;
 
 
 
 
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 = ["Cal 1", "Cal 2", "Cal 3", "Cal 4", "Dart"];
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 classIdx = i / 2;
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", 6);
332
  circle.setAttribute("class", "keypoint-marker");
333
- if (classIdx >= 4) circle.style.fill = "#ff4d4d"; // Red for dart
 
 
 
334
 
 
 
 
 
 
 
 
 
 
335
  const label = document.createElementNS("http://www.w3.org/2000/svg", "text");
336
- label.setAttribute("x", x + 10);
337
- label.setAttribute("y", y - 10);
338
- label.setAttribute("fill", classIdx < 4 ? "#00ff88" : "#ff4d4d");
339
- label.setAttribute("font-size", "14px");
340
- label.setAttribute("font-weight", "600");
 
341
  label.textContent = name;
342
 
343
- svgOverlay.appendChild(circle);
344
- svgOverlay.appendChild(label);
 
 
345
  }
346
- }, 50);
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>