tyrwh commited on
Commit
471a225
·
1 Parent(s): e3446e1

Bug fixes on plotting and saving

Browse files
Files changed (3) hide show
  1. app.py +36 -14
  2. static/script.js +55 -47
  3. templates/index.html +4 -1
app.py CHANGED
@@ -18,6 +18,7 @@ import zipfile
18
  import cv2
19
  import numpy as np
20
  import csv
 
21
 
22
  from yolo_utils import load_model, detect_image
23
 
@@ -70,6 +71,28 @@ def process_image(args):
70
  f.write(image_bytes)
71
  return {'filename': filename, 'detections': detections}
72
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
73
  @app.route('/process', methods=['POST'])
74
  def process_images():
75
  try:
@@ -77,19 +100,13 @@ def process_images():
77
  if not files or files[0].filename == '':
78
  return jsonify({'error': 'No files uploaded'}), 400
79
 
80
- # Read all files into memory as (filename, bytes)
81
  file_data = [(secure_filename(f.filename), f.read()) for f in files]
82
-
83
- # Use multiprocessing to process images in parallel
84
- with Pool(processes=min(cpu_count(), len(file_data))) as pool:
85
- results = pool.map(process_image, file_data)
86
-
87
- # Aggregate results in the main process
88
- for result in results:
89
- all_detections[result['filename']] = result['detections']
90
-
91
- # Return all detections for all images
92
- return jsonify({'results': results})
93
  except Exception as e:
94
  print(f"Error in /process: {e}")
95
  print(traceback.format_exc())
@@ -107,7 +124,6 @@ def annotate_image():
107
  # Draw boxes
108
  for det in filtered:
109
  x1, y1, x2, y2 = map(int, det['bbox'])
110
- print(f"[annotate_image] Drawing box: ({x1}, {y1}), ({x2}, {y2})")
111
  cv2.rectangle(img, (x1, y1), (x2, y2), (0,0,255), 3)
112
  temp_path = os.path.join(tempfile.gettempdir(), 'annotated.png')
113
  cv2.imwrite(temp_path, img)
@@ -116,7 +132,13 @@ def annotate_image():
116
  @app.route('/progress/<job_id>')
117
  def get_progress(job_id):
118
  status = job_status.get(job_id)
119
- return jsonify(status) if status else (jsonify({"status": "error", "error": "Job ID not found"}), 404)
 
 
 
 
 
 
120
 
121
  @app.route('/results/<job_id>/<path:filename>')
122
  def download_file(job_id, filename):
 
18
  import cv2
19
  import numpy as np
20
  import csv
21
+ from threading import Thread
22
 
23
  from yolo_utils import load_model, detect_image
24
 
 
71
  f.write(image_bytes)
72
  return {'filename': filename, 'detections': detections}
73
 
74
+ def async_process_images(job_id, file_data):
75
+ try:
76
+ job_status[job_id] = {'status': 'running', 'progress': 0, 'results': []}
77
+ total = len(file_data)
78
+ results = []
79
+ with Pool(processes=min(cpu_count(), total)) as pool:
80
+ for idx, result in enumerate(pool.imap(process_image, file_data)):
81
+ results.append(result)
82
+ # Update progress (0-100)
83
+ job_status[job_id]['progress'] = int((idx + 1) / total * 100)
84
+ # Aggregate results
85
+ for result in results:
86
+ all_detections[result['filename']] = result['detections']
87
+ # Add num_eggs to each result for frontend compatibility
88
+ for result in results:
89
+ result['num_eggs'] = sum(1 for d in result['detections'] if d.get('class') == 'egg')
90
+ job_status[job_id]['status'] = 'success'
91
+ job_status[job_id]['results'] = results
92
+ job_status[job_id]['progress'] = 100
93
+ except Exception as e:
94
+ job_status[job_id] = {'status': 'error', 'error': str(e), 'progress': 100}
95
+
96
  @app.route('/process', methods=['POST'])
97
  def process_images():
98
  try:
 
100
  if not files or files[0].filename == '':
101
  return jsonify({'error': 'No files uploaded'}), 400
102
 
 
103
  file_data = [(secure_filename(f.filename), f.read()) for f in files]
104
+ job_id = str(uuid.uuid4())
105
+ job_status[job_id] = {'status': 'starting', 'progress': 0}
106
+ thread = Thread(target=async_process_images, args=(job_id, file_data))
107
+ thread.daemon = True
108
+ thread.start()
109
+ return jsonify({'jobId': job_id})
 
 
 
 
 
110
  except Exception as e:
111
  print(f"Error in /process: {e}")
112
  print(traceback.format_exc())
 
124
  # Draw boxes
125
  for det in filtered:
126
  x1, y1, x2, y2 = map(int, det['bbox'])
 
127
  cv2.rectangle(img, (x1, y1), (x2, y2), (0,0,255), 3)
128
  temp_path = os.path.join(tempfile.gettempdir(), 'annotated.png')
129
  cv2.imwrite(temp_path, img)
 
132
  @app.route('/progress/<job_id>')
133
  def get_progress(job_id):
134
  status = job_status.get(job_id)
135
+ if not status:
136
+ return jsonify({"status": "error", "error": "Job ID not found"}), 404
137
+ # Add a mapping from filename to detections for frontend plotting
138
+ if 'results' in status:
139
+ detections_by_filename = {r['filename']: r['detections'] for r in status['results']}
140
+ status['detections_by_filename'] = detections_by_filename
141
+ return jsonify(status)
142
 
143
  @app.route('/results/<job_id>/<path:filename>')
144
  def download_file(job_id, filename):
static/script.js CHANGED
@@ -303,16 +303,12 @@ document.addEventListener('DOMContentLoaded', () => {
303
  }
304
 
305
  const mode = inputMode.value;
306
- // Removed single file warning, as backend now handles it
307
- // if (mode === 'single' && files.length > 1) { ... }
308
-
309
- // Start processing
310
  setLoading(true);
311
  logStatus('Starting upload and processing...');
312
  updateProgress(0, 'Uploading files...');
313
- resultsTableBody.innerHTML = ''; // Clear previous results
314
- clearPreview(); // Clear image preview
315
- currentResults = []; // Reset results data
316
  if (progressInterval) {
317
  clearInterval(progressInterval);
318
  progressInterval = null;
@@ -337,34 +333,24 @@ document.addEventListener('DOMContentLoaded', () => {
337
  errorText += `: ${errorData.error || 'Unknown server error'}`;
338
  if (errorData.log) logStatus(`Server Log: ${errorData.log}`);
339
  } catch (e) {
340
- // Response wasn't JSON
341
  errorText += ` - ${response.statusText}`;
342
- }
343
  throw new Error(errorText);
344
  }
345
  const data = await response.json();
346
  if (data.error) {
347
- logStatus(`Error starting process: ${data.error}`);
348
- if(data.log) logStatus(`Details: ${data.log}`);
349
- throw new Error(data.error); // Throw to trigger catch block
350
- }
351
- // Store all detections for frontend filtering
352
- allDetections = data.results;
353
- allImageData = {};
354
- for (const file of files) {
355
- const reader = new FileReader();
356
- reader.onload = (e) => {
357
- allImageData[file.name] = e.target.result;
358
- };
359
- reader.readAsDataURL(file);
360
  }
361
- setTimeout(() => {
362
- updateResultsTable();
 
 
 
 
363
  setLoading(false);
364
- updateProgress(100, 'Processing complete');
365
- logStatus('Processing finished successfully.');
366
- onProcessingComplete();
367
- }, 500);
368
  } catch (error) {
369
  logStatus(`Error: ${error.message}`);
370
  updateProgress(0, 'Error occurred');
@@ -374,13 +360,12 @@ document.addEventListener('DOMContentLoaded', () => {
374
  progressInterval = null;
375
  }
376
  }
377
- // Removed finally block here, setLoading(false) is handled by pollProgress or catch block
378
  });
379
 
380
  // --- Filtering and Table Update ---
381
  function updateResultsTable() {
382
  const threshold = parseFloat(confidenceSlider.value);
383
- // Group detections by image
384
  const grouped = {};
385
  allDetections.forEach(imgResult => {
386
  const filtered = imgResult.detections.filter(det => det.score >= threshold);
@@ -504,7 +489,7 @@ document.addEventListener('DOMContentLoaded', () => {
504
  const data = await response.json();
505
 
506
  // Update UI based on status
507
- updateProgress(data.progress || 0, data.status);
508
 
509
  // Update image counter based on progress percentage
510
  if (data.status === 'Running' || data.status === 'Starting') {
@@ -517,7 +502,8 @@ document.addEventListener('DOMContentLoaded', () => {
517
  progressInterval = null;
518
  updateProgress(100, 'Processing complete');
519
  logStatus("Processing finished successfully.");
520
- displayResults(data.results || []);
 
521
  setLoading(false);
522
  } else if (data.status === 'error') {
523
  clearInterval(progressInterval);
@@ -628,7 +614,18 @@ document.addEventListener('DOMContentLoaded', () => {
628
  }
629
 
630
  // Results Display
631
- function displayResults(results) {
 
 
 
 
 
 
 
 
 
 
 
632
  currentResults = results;
633
  resultsTableBody.innerHTML = '';
634
  currentImageIndex = -1;
@@ -1191,29 +1188,35 @@ document.addEventListener('DOMContentLoaded', () => {
1191
  showlegend: false
1192
  };
1193
 
1194
- // Vertical line (drawn beneath dots/lines)
 
 
 
 
 
 
 
 
 
 
1195
  const layout = {
1196
  margin: {t: 20, r: 20, l: 40, b: 40},
1197
- xaxis: {title: 'Threshold', dtick: 0.1, range: [0, 1]},
1198
- yaxis: {title: 'Total Eggs Detected', rangemode: 'tozero'},
1199
  showlegend: false,
1200
- height: 320,
1201
- shapes: [
1202
- {
1203
- type: 'line',
1204
- x0: currentConf, x1: currentConf,
1205
- y0: 0, y1: Math.max(...yVals, 1),
1206
- line: {color: 'red', width: 2, dash: 'dot'},
1207
- layer: 'below' // Draw beneath traces
1208
- }
1209
- ]
1210
  };
1211
 
1212
- Plotly.newPlot('confidence-plot', [greyTrace, blueTrace], layout, {
1213
  displayModeBar: false,
1214
  responsive: true,
1215
  staticPlot: true // disables zoom/pan/drag
1216
  });
 
 
 
 
 
1217
  }
1218
 
1219
  // Call this after processing completes
@@ -1408,4 +1411,9 @@ document.addEventListener('DOMContentLoaded', () => {
1408
 
1409
  // Hide plot on page load and after clearing files
1410
  document.getElementById('confidence-plot').style.display = 'none';
 
 
 
 
 
1411
  });
 
303
  }
304
 
305
  const mode = inputMode.value;
 
 
 
 
306
  setLoading(true);
307
  logStatus('Starting upload and processing...');
308
  updateProgress(0, 'Uploading files...');
309
+ resultsTableBody.innerHTML = '';
310
+ clearPreview();
311
+ currentResults = [];
312
  if (progressInterval) {
313
  clearInterval(progressInterval);
314
  progressInterval = null;
 
333
  errorText += `: ${errorData.error || 'Unknown server error'}`;
334
  if (errorData.log) logStatus(`Server Log: ${errorData.log}`);
335
  } catch (e) {
 
336
  errorText += ` - ${response.statusText}`;
337
+ }
338
  throw new Error(errorText);
339
  }
340
  const data = await response.json();
341
  if (data.error) {
342
+ logStatus(`Error starting process: ${data.error}`);
343
+ if(data.log) logStatus(`Details: ${data.log}`);
344
+ throw new Error(data.error);
 
 
 
 
 
 
 
 
 
 
345
  }
346
+ // --- ASYNC JOB: Start polling for progress ---
347
+ if (data.jobId) {
348
+ currentJobId = data.jobId;
349
+ pollProgress(currentJobId);
350
+ } else {
351
+ logStatus('Error: No jobId returned from server.');
352
  setLoading(false);
353
+ }
 
 
 
354
  } catch (error) {
355
  logStatus(`Error: ${error.message}`);
356
  updateProgress(0, 'Error occurred');
 
360
  progressInterval = null;
361
  }
362
  }
 
363
  });
364
 
365
  // --- Filtering and Table Update ---
366
  function updateResultsTable() {
367
  const threshold = parseFloat(confidenceSlider.value);
368
+ // Use allDetections (now an array of {filename, detections})
369
  const grouped = {};
370
  allDetections.forEach(imgResult => {
371
  const filtered = imgResult.detections.filter(det => det.score >= threshold);
 
489
  const data = await response.json();
490
 
491
  // Update UI based on status
492
+ updateProgress(data.progress || 0, data.status.charAt(0).toUpperCase() + data.status.slice(1));
493
 
494
  // Update image counter based on progress percentage
495
  if (data.status === 'Running' || data.status === 'Starting') {
 
502
  progressInterval = null;
503
  updateProgress(100, 'Processing complete');
504
  logStatus("Processing finished successfully.");
505
+ displayResults(data.results || [], data.detections_by_filename || null);
506
+ renderConfidencePlot();
507
  setLoading(false);
508
  } else if (data.status === 'error') {
509
  clearInterval(progressInterval);
 
614
  }
615
 
616
  // Results Display
617
+ function displayResults(results, detectionsByFilename) {
618
+ // If detectionsByFilename is provided, update allDetections for plot logic
619
+ if (detectionsByFilename) {
620
+ allDetections = Object.keys(detectionsByFilename).map(filename => ({
621
+ filename,
622
+ detections: detectionsByFilename[filename]
623
+ }));
624
+ } else {
625
+ // Fallback: use results array
626
+ allDetections = results.map(r => ({ filename: r.filename, detections: r.detections }));
627
+ }
628
+ // Now update the table
629
  currentResults = results;
630
  resultsTableBody.innerHTML = '';
631
  currentImageIndex = -1;
 
1188
  showlegend: false
1189
  };
1190
 
1191
+ // Vertical line as a trace (above grid, below data)
1192
+ const vlineTrace = {
1193
+ x: [currentConf, currentConf],
1194
+ y: [0, Math.max(...yVals, 1)],
1195
+ mode: 'lines',
1196
+ line: {color: 'red', width: 2, dash: 'dot'},
1197
+ hoverinfo: 'skip',
1198
+ showlegend: false
1199
+ };
1200
+
1201
+ // Remove the vertical line from layout.shapes
1202
  const layout = {
1203
  margin: {t: 20, r: 20, l: 40, b: 40},
1204
+ xaxis: {title: 'Threshold', dtick: 0.1, range: [0, 1], title_standoff: 18},
1205
+ yaxis: {title: 'Total Eggs Detected', rangemode: 'tozero', title_standoff: 18},
1206
  showlegend: false,
1207
+ height: 320
 
 
 
 
 
 
 
 
 
1208
  };
1209
 
1210
+ Plotly.newPlot('confidence-plot', [vlineTrace, greyTrace, blueTrace], layout, {
1211
  displayModeBar: false,
1212
  responsive: true,
1213
  staticPlot: true // disables zoom/pan/drag
1214
  });
1215
+
1216
+ // Update total eggs detected text
1217
+ const totalEggs = allDetections.reduce((sum, imgResult) => sum + imgResult.detections.filter(det => det.score >= currentConf).length, 0);
1218
+ const totalEggsElem = document.getElementById('total-eggs-count');
1219
+ if (totalEggsElem) totalEggsElem.textContent = totalEggs;
1220
  }
1221
 
1222
  // Call this after processing completes
 
1411
 
1412
  // Hide plot on page load and after clearing files
1413
  document.getElementById('confidence-plot').style.display = 'none';
1414
+
1415
+ // Reset confidence slider to 0.6 and force UI update on page load
1416
+ confidenceSlider.value = 0.6;
1417
+ confidenceSlider.dispatchEvent(new Event('input', { bubbles: true }));
1418
+ confidenceSlider.dispatchEvent(new Event('change', { bubbles: true }));
1419
  });
templates/index.html CHANGED
@@ -77,7 +77,7 @@
77
  <div class="form-group">
78
  <div class="range-with-value" style="width:100%; display:flex; flex-direction:row; align-items:center; gap:1em;">
79
  <input type="range" id="confidence-threshold" name="confidence-threshold"
80
- min="0.05" max="0.95" step="0.05" value="0.6" list="confidence-ticks" style="width:100%;">
81
  <datalist id="confidence-ticks">
82
  <option value="0.05" label="0.05"></option>
83
  <option value="0.10"></option>
@@ -102,6 +102,9 @@
102
  <span id="confidence-value" style="font-size:1.1em; min-width:2.5em; text-align:right;">0.6</span>
103
  </div>
104
  </div>
 
 
 
105
  <div id="confidence-plot" style="margin-top:1.5rem;"></div>
106
  </div>
107
 
 
77
  <div class="form-group">
78
  <div class="range-with-value" style="width:100%; display:flex; flex-direction:row; align-items:center; gap:1em;">
79
  <input type="range" id="confidence-threshold" name="confidence-threshold"
80
+ min="0.05" max="0.95" step="0.05" value="0.6" list="confidence-ticks" style="width:100%;" autocomplete="off">
81
  <datalist id="confidence-ticks">
82
  <option value="0.05" label="0.05"></option>
83
  <option value="0.10"></option>
 
102
  <span id="confidence-value" style="font-size:1.1em; min-width:2.5em; text-align:right;">0.6</span>
103
  </div>
104
  </div>
105
+ <div id="total-eggs-detected" style="margin-top:0.5em; font-size:1.1em; color:var(--text-muted);">
106
+ Total Eggs Detected: <span id="total-eggs-count">0</span>
107
+ </div>
108
  <div id="confidence-plot" style="margin-top:1.5rem;"></div>
109
  </div>
110