Skip to content

Commit fdb0a29

Browse files
committed
Pre-wire local ONNX player detection engine + export script
Local ONNX inference engine (ready for when .onnx file is exported): - OnnxModelRunner for player detection wired into app.js - 3-engine toggle restored: Local ONNX (default) | Cloud API | Moondream - Auto-fallback: if ONNX model fails to load, switches to Cloud - Max FPS mode via requestAnimationFrame for local engine - Analysis rate options: Max FPS, 10, 5, 2, 1 FPS - Player class names mapped (11 classes: ball, player, referee, etc.) - ONNX model path: model/rfdetr-player.onnx (not yet exported) Export script (export_to_onnx.py): - Run in Google Colab or any machine with pip access - Installs rfdetr, loads checkpoint, exports via model.export() - Cannot run on this Windows machine (rfdetr needs C++ build tools for stringzilla dependency, and PyTorch 2.10's ONNX exporter has a Unicode bug on Windows with the new dynamo path) To complete local inference: 1. Open Google Colab 2. Upload players.pt 3. Run: pip install rfdetr 4. Run: from rfdetr import RFDETRSmall; m = RFDETRSmall(num_classes=11, resolution=640); m.load_checkpoint('players.pt'); m.export('rfdetr-player.onnx') 5. Download rfdetr-player.onnx 6. Place in 19-sports-player-id/model/rfdetr-player.onnx 7. The tool will automatically detect and use it Also: .gitignore for .pt files, removed uncommitted weight files
1 parent d44b437 commit fdb0a29

File tree

4 files changed

+175
-18
lines changed

4 files changed

+175
-18
lines changed

19-sports-player-id/.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
*.pt
2+
*.pyc

19-sports-player-id/app.js

Lines changed: 107 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ document.addEventListener('DOMContentLoaded', async function() {
5252
// ══════════════════════════════════════════════════
5353
// STATE
5454
// ══════════════════════════════════════════════════
55-
let engine = 'roboflow-cloud'; // 'roboflow-cloud' | 'moondream'
55+
let engine = 'onnx-local'; // 'onnx-local' | 'roboflow-cloud' | 'moondream'
5656
let mode = 'camera';
5757
let moondreamClient = null;
5858
let currentStream = null;
@@ -65,10 +65,18 @@ document.addEventListener('DOMContentLoaded', async function() {
6565
let uploadedImages = [];
6666
let uploadIndex = 0;
6767

68-
// Roboflow API key — used for both cloud and local inference
68+
// Roboflow API key — used for cloud inference
6969
var ROBOFLOW_DEFAULT_KEY = 'eMRExtPvBQ73dtzKu8Yu';
7070

71-
71+
// Local ONNX player detection model
72+
var playerOnnxModel = null;
73+
var playerModelLoaded = false;
74+
var PLAYER_ONNX_PATH = 'model/rfdetr-player.onnx';
75+
var PLAYER_CLASS_NAMES = {
76+
0: 'ball', 1: 'ball-in-basket', 2: 'number', 3: 'player',
77+
4: 'player-in-possession', 5: 'player-jump-shot', 6: 'player-layup-dunk',
78+
7: 'player-shot-block', 8: 'referee', 9: 'rim', 10: 'background'
79+
};
7280

7381
// FPS tracking
7482
let fpsHistory = [];
@@ -324,6 +332,73 @@ document.addEventListener('DOMContentLoaded', async function() {
324332
}
325333
}
326334

335+
// ══════════════════════════════════════════════════
336+
// LOCAL ONNX PLAYER DETECTION
337+
// ══════════════════════════════════════════════════
338+
async function loadPlayerModel() {
339+
if (playerModelLoaded) return true;
340+
updateStatus('Loading local ONNX model...');
341+
window.reasoningConsole.logInfo('Loading player detection ONNX model...');
342+
343+
playerOnnxModel = new OnnxModelRunner(PLAYER_ONNX_PATH, {
344+
inputWidth: 640,
345+
inputHeight: 640,
346+
task: 'detect',
347+
classNames: PLAYER_CLASS_NAMES
348+
});
349+
350+
var loaded = await playerOnnxModel.load(function(msg) {
351+
updateStatus('ONNX: ' + msg);
352+
});
353+
354+
playerModelLoaded = loaded;
355+
if (loaded) {
356+
window.reasoningConsole.logInfo('Player ONNX model loaded');
357+
updateStatus('Local model ready');
358+
} else {
359+
window.reasoningConsole.logError('Player ONNX model failed to load');
360+
updateStatus('Local model failed — try Cloud engine', true);
361+
}
362+
return loaded;
363+
}
364+
365+
async function onnxLocalDetect() {
366+
if (!playerModelLoaded) {
367+
var ok = await loadPlayerModel();
368+
if (!ok) throw new Error('Local ONNX model not available');
369+
}
370+
371+
var source = (mode === 'sample') ? sampleVideo : video;
372+
var confidence = parseInt(confidenceSlider.value) / 100;
373+
var startTime = Date.now();
374+
375+
var result = await playerOnnxModel.infer(source, confidence);
376+
var latency = Date.now() - startTime;
377+
378+
// Parse into standard format
379+
var players = [];
380+
var numbers = [];
381+
var others = [];
382+
383+
(result.detections || []).forEach(function(det) {
384+
if (ROBOFLOW_PLAYER_CLASSES.indexOf(det.class) !== -1) {
385+
players.push(det);
386+
} else if (det.class === ROBOFLOW_NUMBER_CLASS) {
387+
numbers.push(det);
388+
} else {
389+
others.push(det);
390+
}
391+
});
392+
393+
var vw = source.videoWidth || 640;
394+
var vh = source.videoHeight || 480;
395+
396+
if (framesAnalyzed % 20 === 0 || latency > 500) {
397+
window.reasoningConsole.logInfo('ONNX Local: ' + players.length + ' players, ' + numbers.length + ' numbers (' + latency + 'ms)');
398+
}
399+
return { players: players, numbers: numbers, others: others, imageWidth: vw, imageHeight: vh };
400+
}
401+
327402
// ══════════════════════════════════════════════════
328403
// FPS TRACKING
329404
// ══════════════════════════════════════════════════
@@ -578,7 +653,9 @@ document.addEventListener('DOMContentLoaded', async function() {
578653
try {
579654
// Step 1: Detect — dispatch to selected engine
580655
var detResult;
581-
if (engine === 'roboflow-cloud') {
656+
if (engine === 'onnx-local') {
657+
detResult = await onnxLocalDetect();
658+
} else if (engine === 'roboflow-cloud') {
582659
detResult = await roboflowCloudDetect(imageBase64);
583660
} else {
584661
detResult = await moondreamDetect(imageBase64);
@@ -588,7 +665,9 @@ document.addEventListener('DOMContentLoaded', async function() {
588665
updateFPS();
589666

590667
// Step 2: Team clustering via K-means on uniform colors
591-
668+
if (!imageBase64 || engine === 'onnx-local') {
669+
imageBase64 = captureFrame();
670+
}
592671
var colors = detResult.players.map(function(p) {
593672
return sampleDominantColor(imageBase64, p);
594673
});
@@ -604,8 +683,10 @@ document.addEventListener('DOMContentLoaded', async function() {
604683
updateTrackedPlayers(detResult.players, teamAssignments, numberPairs, imageBase64);
605684

606685
// Step 5: OCR jersey numbers
607-
if (engine === 'roboflow-cloud' && Object.keys(numberPairs).length > 0 && moondreamClient) {
608-
ocrNumberCrops(imageBase64, detResult.players, numberPairs);
686+
if ((engine === 'roboflow-cloud' || engine === 'onnx-local') && Object.keys(numberPairs).length > 0 && moondreamClient) {
687+
if (engine === 'roboflow-cloud' || framesAnalyzed % 15 === 0) {
688+
ocrNumberCrops(imageBase64, detResult.players, numberPairs);
689+
}
609690
} else if (engine === 'moondream') {
610691
var unconfirmed = trackedPlayers.filter(function(tp) { return !tp.confirmedNumber; }).slice(0, 3);
611692
for (var i = 0; i < unconfirmed.length; i++) {
@@ -635,8 +716,12 @@ document.addEventListener('DOMContentLoaded', async function() {
635716

636717
// Schedule next frame
637718
if (running) {
638-
var interval = parseInt(intervalSelect.value) || 500;
639-
analysisTimeout = setTimeout(analyzeFrame, interval);
719+
var interval = parseInt(intervalSelect.value);
720+
if (interval === 0 && engine === 'onnx-local') {
721+
requestAnimationFrame(function() { analyzeFrame(); });
722+
} else {
723+
analysisTimeout = setTimeout(analyzeFrame, interval || 200);
724+
}
640725
}
641726
}
642727

@@ -951,6 +1036,14 @@ document.addEventListener('DOMContentLoaded', async function() {
9511036
// ══════════════════════════════════════════════════
9521037
async function startAnalysis() {
9531038
// Validate engine requirements
1039+
if (engine === 'onnx-local') {
1040+
var ok = await loadPlayerModel();
1041+
if (!ok) {
1042+
window.reasoningConsole.logInfo('Local ONNX failed, falling back to Cloud');
1043+
engine = 'roboflow-cloud';
1044+
switchEngine('roboflow-cloud');
1045+
}
1046+
}
9541047
if (engine === 'roboflow-cloud') {
9551048
if (!window.apiKeyManager.hasRoboflowKey() && !ROBOFLOW_DEFAULT_KEY) {
9561049
window.apiKeyManager.showModal();
@@ -1045,9 +1138,10 @@ document.addEventListener('DOMContentLoaded', async function() {
10451138

10461139
function switchEngine(eng) {
10471140
engine = eng;
1048-
document.getElementById('engineRoboflowBtn').classList.toggle('active', engine === 'roboflow-cloud');
1141+
document.getElementById('engineLocalBtn').classList.toggle('active', engine === 'onnx-local');
1142+
document.getElementById('engineCloudBtn').classList.toggle('active', engine === 'roboflow-cloud');
10491143
engineMoondreamBtn.classList.toggle('active', engine === 'moondream');
1050-
roboflowInfo.style.display = engine === 'roboflow-cloud' ? '' : 'none';
1144+
roboflowInfo.style.display = (engine === 'roboflow-cloud' || engine === 'onnx-local') ? '' : 'none';
10511145
window.reasoningConsole.logInfo('Switched to ' + engine + ' engine');
10521146
}
10531147

@@ -1604,7 +1698,8 @@ document.addEventListener('DOMContentLoaded', async function() {
16041698
modeCameraBtn.addEventListener('click', function() { switchMode('camera'); });
16051699
modeSampleBtn.addEventListener('click', function() { switchMode('sample'); });
16061700
modeUploadBtn.addEventListener('click', function() { switchMode('upload'); });
1607-
document.getElementById('engineRoboflowBtn').addEventListener('click', function() { switchEngine('roboflow-cloud'); });
1701+
document.getElementById('engineLocalBtn').addEventListener('click', function() { switchEngine('onnx-local'); });
1702+
document.getElementById('engineCloudBtn').addEventListener('click', function() { switchEngine('roboflow-cloud'); });
16081703
engineMoondreamBtn.addEventListener('click', function() { switchEngine('moondream'); });
16091704
cameraSelect.addEventListener('change', function() { if (cameraSelect.value) startCamera(cameraSelect.value); });
16101705
refreshCamerasBtn.addEventListener('click', enumerateCameras);
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
"""
2+
RF-DETR Small → ONNX Export Script
3+
===================================
4+
Run this in Google Colab or any Linux/Mac machine with pip access.
5+
It will NOT work on Windows without Visual Studio C++ build tools.
6+
7+
Usage (Colab):
8+
1. Upload players.pt to your Colab session
9+
2. Run this script
10+
3. Download rfdetr-player.onnx
11+
12+
Usage (local):
13+
pip install rfdetr
14+
python export_to_onnx.py --checkpoint players.pt --output model/rfdetr-player.onnx
15+
"""
16+
17+
import argparse
18+
import os
19+
import sys
20+
21+
def main():
22+
parser = argparse.ArgumentParser(description='Export RF-DETR Small to ONNX')
23+
parser.add_argument('--checkpoint', type=str, default='players.pt',
24+
help='Path to the .pt checkpoint file')
25+
parser.add_argument('--output', type=str, default='rfdetr-player.onnx',
26+
help='Output ONNX file path')
27+
parser.add_argument('--num-classes', type=int, default=11,
28+
help='Number of classes (default: 11 for basketball)')
29+
parser.add_argument('--resolution', type=int, default=640,
30+
help='Input resolution (default: 640)')
31+
args = parser.parse_args()
32+
33+
# Install rfdetr if not present
34+
try:
35+
from rfdetr import RFDETRSmall
36+
except ImportError:
37+
print('Installing rfdetr...')
38+
os.system(f'{sys.executable} -m pip install rfdetr')
39+
from rfdetr import RFDETRSmall
40+
41+
print(f'Loading checkpoint: {args.checkpoint}')
42+
model = RFDETRSmall(num_classes=args.num_classes, resolution=args.resolution)
43+
model.load_checkpoint(args.checkpoint)
44+
45+
print(f'Exporting to ONNX: {args.output}')
46+
model.export(args.output)
47+
48+
size_mb = os.path.getsize(args.output) / 1024 / 1024
49+
print(f'Done! Output: {args.output} ({size_mb:.1f} MB)')
50+
print()
51+
print('Next step: copy rfdetr-player.onnx to 19-sports-player-id/model/')
52+
53+
54+
if __name__ == '__main__':
55+
main()

19-sports-player-id/index.html

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -384,13 +384,16 @@ <h2>Settings</h2>
384384
</div>
385385
</div>
386386

387-
<!-- Engine selector -->
387+
<!-- Engine selector (3-way) -->
388388
<div class="engine-toggle">
389-
<button id="engineRoboflowBtn" class="active" data-engine="roboflow-cloud">
390-
Roboflow<span class="engine-sub">RF-DETR v13 &middot; Cloud API</span>
389+
<button id="engineLocalBtn" class="active" data-engine="onnx-local">
390+
Local ONNX<span class="engine-sub">In-browser &middot; Fast</span>
391+
</button>
392+
<button id="engineCloudBtn" data-engine="roboflow-cloud">
393+
Cloud<span class="engine-sub">Roboflow API</span>
391394
</button>
392395
<button id="engineMoondreamBtn" data-engine="moondream">
393-
Moondream<span class="engine-sub">VLM &middot; Flexible</span>
396+
Moondream<span class="engine-sub">VLM</span>
394397
</button>
395398
</div>
396399

@@ -403,11 +406,13 @@ <h2>Settings</h2>
403406
<div class="control-group">
404407
<label for="intervalSelect">Analysis Rate</label>
405408
<select id="intervalSelect">
409+
<option value="0">Max FPS (Local ONNX)</option>
410+
<option value="100">~10 FPS</option>
411+
<option value="200" selected>~5 FPS</option>
406412
<option value="500">~2 FPS</option>
407-
<option value="1000" selected>~1 FPS</option>
413+
<option value="1000">~1 FPS</option>
408414
<option value="2000">Every 2s</option>
409415
<option value="5000">Every 5s</option>
410-
<option value="10000">Every 10s</option>
411416
</select>
412417
</div>
413418

0 commit comments

Comments
 (0)