davanstrien HF Staff Claude commited on
Commit
f33570b
·
1 Parent(s): bf50f05

Build complete SAM3 Object Detection Browser

Browse files

- Built interactive static HTML app for browsing SAM3 detection results
- Progressive loading: displays first batch in 1-2 seconds, continues loading in background
- Tufte-inspired minimal design: clean typography, subtle borders, high data-ink ratio
- Real-time filtering: confidence threshold slider and detection toggle
- Statistics dashboard showing filtered images, detections, and averages
- Canvas-based bounding box visualization with color-coded confidence scores
- Example dataset links for quick navigation (photographs/illustrations)
- Handles multiple detections per image with proper canvas clearing
- Fixed filtering logic to use filtered dataset for consistent stats
- Added debugging logs for bbox rendering troubleshooting
- Link to SAM3 detection script for users to create their own datasets

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <[email protected]>

Files changed (3) hide show
  1. README.md +88 -5
  2. index.html +778 -18
  3. style.css +0 -28
README.md CHANGED
@@ -1,10 +1,93 @@
1
  ---
2
- title: Sam3 Detection Browser
3
- emoji: 🐨
4
- colorFrom: green
5
- colorTo: indigo
6
  sdk: static
7
  pinned: false
8
  ---
9
 
10
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  ---
2
+ title: SAM3 Detection Browser
3
+ emoji: 🔍
4
+ colorFrom: blue
5
+ colorTo: green
6
  sdk: static
7
  pinned: false
8
  ---
9
 
10
+ # 🔍 SAM3 Object Detection Browser
11
+
12
+ A simple, interactive web application for browsing and exploring object detection results from Meta's [SAM3 (Segment Anything Model 3)](https://huggingface.co/facebook/sam3).
13
+
14
+ ## Features
15
+
16
+ - **Visual Detection Display**: View images with bounding boxes overlaid on detected objects
17
+ - **Confidence Filtering**: Adjust confidence threshold with a slider to filter detections
18
+ - **Statistics Dashboard**: See overall detection statistics including:
19
+ - Total filtered images
20
+ - Images with detections
21
+ - Total detections
22
+ - Average detections per image
23
+ - **Flexible Navigation**: Browse through datasets with pagination
24
+ - **Real-time Updates**: Filters update visualization instantly
25
+ - **Color-coded Confidence**: Bounding boxes and scores are color-coded:
26
+ - 🟢 Green: High confidence (≥70%)
27
+ - 🟠 Orange: Medium confidence (40-70%)
28
+ - 🔴 Red: Low confidence (<40%)
29
+
30
+ ## How to Use
31
+
32
+ 1. **Enter a Dataset ID**: Input any HuggingFace dataset that contains object detection results from SAM3
33
+ - Default: `davanstrien/newspapers-image-predictions`
34
+ - Format: `username/dataset-name`
35
+
36
+ 2. **Select Split**: Choose which dataset split to view (train/validation/test)
37
+
38
+ 3. **Adjust Filters**:
39
+ - Move the confidence slider to filter detections by minimum confidence score
40
+ - Check "Show only images with detections" to hide images without any detected objects
41
+
42
+ 4. **Browse**: Navigate through pages using Previous/Next buttons
43
+
44
+ ## Dataset Requirements
45
+
46
+ This browser works with any HuggingFace dataset that has:
47
+ - An `image` column containing images
48
+ - An `objects` column with the structure:
49
+ ```python
50
+ {
51
+ "bbox": [[x, y, width, height], ...], # Bounding box coordinates
52
+ "category": [0, 0, ...], # Category indices
53
+ "score": [0.8, 0.6, ...] # Confidence scores
54
+ }
55
+ ```
56
+
57
+ This matches the output format from the [detect-objects.py](https://huggingface.co/datasets/uv-scripts/sam3/blob/main/detect-objects.py) script.
58
+
59
+ ## Example Datasets
60
+
61
+ - [davanstrien/newspapers-image-predictions](https://huggingface.co/datasets/davanstrien/newspapers-image-predictions) - Photograph detections in historical newspapers
62
+
63
+ ## Creating Your Own Detection Dataset
64
+
65
+ Use the SAM3 detection script to create your own datasets:
66
+
67
+ ```bash
68
+ # Detect objects in your images
69
+ uv run https://huggingface.co/datasets/uv-scripts/sam3/raw/main/detect-objects.py \
70
+ your-dataset \
71
+ your-output-dataset \
72
+ --class-name "photograph" \
73
+ --confidence-threshold 0.5
74
+ ```
75
+
76
+ Then view the results by entering `your-output-dataset` in this browser!
77
+
78
+ ## Technical Details
79
+
80
+ - **Technology**: Pure HTML/CSS/JavaScript (no backend required)
81
+ - **Data Source**: HuggingFace Datasets API (parquet endpoint)
82
+ - **Rendering**: HTML5 Canvas for bounding box overlays
83
+ - **Performance**: Loads 12 images per page for optimal performance
84
+
85
+ ## Related Projects
86
+
87
+ - [SAM3 Detection Script](https://huggingface.co/datasets/uv-scripts/sam3) - Create detection datasets
88
+ - [SAM3 Model](https://huggingface.co/facebook/sam3) - The base model
89
+ - [UV Scripts Organization](https://huggingface.co/uv-scripts) - More ready-to-run ML scripts
90
+
91
+ ## License
92
+
93
+ MIT License - Feel free to use and modify for your own projects!
index.html CHANGED
@@ -1,19 +1,779 @@
1
- <!doctype html>
2
- <html>
3
- <head>
4
- <meta charset="utf-8" />
5
- <meta name="viewport" content="width=device-width" />
6
- <title>My static Space</title>
7
- <link rel="stylesheet" href="style.css" />
8
- </head>
9
- <body>
10
- <div class="card">
11
- <h1>Welcome to your static Space!</h1>
12
- <p>You can modify this app directly by editing <i>index.html</i> in the Files and versions tab.</p>
13
- <p>
14
- Also don't forget to check the
15
- <a href="https://huggingface.co/docs/hub/spaces" target="_blank">Spaces documentation</a>.
16
- </p>
17
- </div>
18
- </body>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
19
  </html>
 
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>SAM3 Object Detection Browser</title>
7
+ <style>
8
+ * {
9
+ margin: 0;
10
+ padding: 0;
11
+ box-sizing: border-box;
12
+ }
13
+
14
+ body {
15
+ font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
16
+ line-height: 1.5;
17
+ color: #222;
18
+ background: #fff;
19
+ font-size: 15px;
20
+ }
21
+
22
+ .container {
23
+ max-width: 1400px;
24
+ margin: 0 auto;
25
+ padding: 40px 20px;
26
+ }
27
+
28
+ header {
29
+ border-bottom: 1px solid #ddd;
30
+ padding-bottom: 20px;
31
+ margin-bottom: 40px;
32
+ }
33
+
34
+ h1 {
35
+ color: #222;
36
+ font-weight: 400;
37
+ font-size: 28px;
38
+ margin-bottom: 8px;
39
+ }
40
+
41
+ .subtitle {
42
+ color: #666;
43
+ font-size: 15px;
44
+ font-weight: 300;
45
+ margin-bottom: 12px;
46
+ }
47
+
48
+ .header-links {
49
+ font-size: 13px;
50
+ color: #666;
51
+ }
52
+
53
+ .header-links a {
54
+ color: #222;
55
+ text-decoration: none;
56
+ border-bottom: 1px solid #222;
57
+ }
58
+
59
+ .header-links a:hover {
60
+ border-bottom-color: #666;
61
+ }
62
+
63
+ .example-datasets {
64
+ margin-top: 8px;
65
+ font-size: 12px;
66
+ color: #666;
67
+ }
68
+
69
+ .example-datasets a {
70
+ color: #666;
71
+ text-decoration: none;
72
+ border-bottom: 1px dotted #999;
73
+ cursor: pointer;
74
+ margin-right: 12px;
75
+ }
76
+
77
+ .example-datasets a:hover {
78
+ color: #222;
79
+ border-bottom-color: #222;
80
+ }
81
+
82
+ .controls {
83
+ border: 1px solid #ddd;
84
+ padding: 20px;
85
+ margin-bottom: 40px;
86
+ background: #fafafa;
87
+ }
88
+
89
+ .control-group {
90
+ margin-bottom: 18px;
91
+ }
92
+
93
+ .control-group:last-child {
94
+ margin-bottom: 0;
95
+ }
96
+
97
+ label {
98
+ display: block;
99
+ font-weight: 400;
100
+ margin-bottom: 6px;
101
+ color: #444;
102
+ font-size: 14px;
103
+ }
104
+
105
+ input[type="text"],
106
+ select {
107
+ width: 100%;
108
+ padding: 8px;
109
+ border: 1px solid #ccc;
110
+ border-radius: 2px;
111
+ font-size: 14px;
112
+ background: white;
113
+ }
114
+
115
+ input[type="text"]:focus,
116
+ select:focus {
117
+ outline: none;
118
+ border-color: #888;
119
+ }
120
+
121
+ input[type="range"] {
122
+ width: 100%;
123
+ margin-right: 10px;
124
+ }
125
+
126
+ .slider-container {
127
+ display: flex;
128
+ align-items: center;
129
+ gap: 15px;
130
+ }
131
+
132
+ .slider-value {
133
+ min-width: 50px;
134
+ font-weight: 400;
135
+ color: #222;
136
+ }
137
+
138
+ .checkbox-container {
139
+ display: flex;
140
+ align-items: center;
141
+ gap: 10px;
142
+ }
143
+
144
+ input[type="checkbox"] {
145
+ width: 18px;
146
+ height: 18px;
147
+ cursor: pointer;
148
+ }
149
+
150
+ .stats {
151
+ border-top: 1px solid #ddd;
152
+ border-bottom: 1px solid #ddd;
153
+ padding: 25px 0;
154
+ margin-bottom: 40px;
155
+ }
156
+
157
+ .stats-grid {
158
+ display: grid;
159
+ grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
160
+ gap: 30px;
161
+ }
162
+
163
+ .stat-item {
164
+ text-align: center;
165
+ padding: 0;
166
+ }
167
+
168
+ .stat-value {
169
+ font-size: 36px;
170
+ font-weight: 300;
171
+ color: #222;
172
+ margin-bottom: 4px;
173
+ letter-spacing: -0.5px;
174
+ }
175
+
176
+ .stat-label {
177
+ font-size: 13px;
178
+ color: #666;
179
+ font-weight: 400;
180
+ text-transform: uppercase;
181
+ letter-spacing: 0.5px;
182
+ }
183
+
184
+ .loading {
185
+ text-align: center;
186
+ padding: 40px;
187
+ font-size: 14px;
188
+ color: #666;
189
+ }
190
+
191
+ .error {
192
+ background: #fff;
193
+ border: 1px solid #d00;
194
+ padding: 15px;
195
+ margin: 20px 0;
196
+ color: #d00;
197
+ font-size: 14px;
198
+ }
199
+
200
+ .image-grid {
201
+ display: grid;
202
+ grid-template-columns: repeat(auto-fill, minmax(400px, 1fr));
203
+ gap: 40px;
204
+ margin-bottom: 40px;
205
+ }
206
+
207
+ .image-card {
208
+ background: white;
209
+ border: 1px solid #ddd;
210
+ overflow: hidden;
211
+ }
212
+
213
+ .image-card:hover {
214
+ border-color: #999;
215
+ }
216
+
217
+ .image-container {
218
+ position: relative;
219
+ width: 100%;
220
+ background: #000;
221
+ }
222
+
223
+ .image-container img {
224
+ width: 100%;
225
+ height: auto;
226
+ display: block;
227
+ }
228
+
229
+ .image-container canvas {
230
+ position: absolute;
231
+ top: 0;
232
+ left: 0;
233
+ width: 100%;
234
+ height: 100%;
235
+ pointer-events: none;
236
+ z-index: 10;
237
+ }
238
+
239
+ .image-info {
240
+ padding: 12px;
241
+ border-top: 1px solid #eee;
242
+ }
243
+
244
+ .image-index {
245
+ font-size: 11px;
246
+ color: #999;
247
+ margin-bottom: 6px;
248
+ text-transform: uppercase;
249
+ letter-spacing: 0.5px;
250
+ }
251
+
252
+ .detections-count {
253
+ font-weight: 400;
254
+ color: #222;
255
+ margin-bottom: 8px;
256
+ font-size: 14px;
257
+ }
258
+
259
+ .detections-list {
260
+ font-size: 13px;
261
+ color: #666;
262
+ }
263
+
264
+ .detection-item {
265
+ padding: 3px 0;
266
+ }
267
+
268
+ .confidence-high {
269
+ color: #27ae60;
270
+ font-weight: 600;
271
+ }
272
+
273
+ .confidence-medium {
274
+ color: #f39c12;
275
+ font-weight: 600;
276
+ }
277
+
278
+ .confidence-low {
279
+ color: #e74c3c;
280
+ font-weight: 600;
281
+ }
282
+
283
+ .pagination {
284
+ display: flex;
285
+ justify-content: center;
286
+ align-items: center;
287
+ gap: 20px;
288
+ margin: 40px 0;
289
+ padding-top: 20px;
290
+ border-top: 1px solid #ddd;
291
+ }
292
+
293
+ .pagination button {
294
+ padding: 8px 16px;
295
+ border: 1px solid #666;
296
+ background: white;
297
+ color: #222;
298
+ cursor: pointer;
299
+ font-size: 13px;
300
+ transition: all 0.2s;
301
+ }
302
+
303
+ .pagination button:hover:not(:disabled) {
304
+ background: #222;
305
+ color: white;
306
+ }
307
+
308
+ .pagination button:disabled {
309
+ border-color: #ddd;
310
+ color: #ccc;
311
+ cursor: not-allowed;
312
+ }
313
+
314
+ .pagination span {
315
+ color: #666;
316
+ font-size: 13px;
317
+ }
318
+
319
+ .load-button {
320
+ padding: 10px 20px;
321
+ border: 1px solid #666;
322
+ background: white;
323
+ color: #222;
324
+ cursor: pointer;
325
+ font-size: 14px;
326
+ font-weight: 400;
327
+ transition: all 0.2s;
328
+ width: 100%;
329
+ margin-top: 12px;
330
+ }
331
+
332
+ .load-button:hover {
333
+ background: #222;
334
+ color: white;
335
+ }
336
+
337
+ .load-button:disabled {
338
+ border-color: #ddd;
339
+ color: #ccc;
340
+ cursor: not-allowed;
341
+ }
342
+ </style>
343
+ </head>
344
+ <body>
345
+ <div class="container">
346
+ <header>
347
+ <h1>SAM3 Object Detection Browser</h1>
348
+ <p class="subtitle">Browse and explore object detection results from Meta's SAM3 model</p>
349
+ <p class="header-links">Create your own detection dataset with the <a href="https://huggingface.co/datasets/uv-scripts/sam3" target="_blank">SAM3 detection script</a></p>
350
+ </header>
351
+
352
+ <div class="controls">
353
+ <div class="control-group">
354
+ <label for="dataset-id">Dataset ID:</label>
355
+ <input type="text" id="dataset-id" value="davanstrien/newspapers-image-predictions" placeholder="username/dataset-name">
356
+ <div class="example-datasets">
357
+ Examples:
358
+ <a href="#" data-dataset="davanstrien/newspapers-image-predictions">photographs</a>
359
+ <a href="#" data-dataset="davanstrien/newspapers-illustration-predictions">illustrations</a>
360
+ </div>
361
+ </div>
362
+
363
+ <div class="control-group">
364
+ <label for="split">Split:</label>
365
+ <select id="split">
366
+ <option value="train">train</option>
367
+ <option value="validation">validation</option>
368
+ <option value="test">test</option>
369
+ </select>
370
+ </div>
371
+
372
+ <div class="control-group">
373
+ <label for="confidence-threshold">Confidence Threshold: <span id="confidence-value" class="slider-value">0.50</span></label>
374
+ <div class="slider-container">
375
+ <input type="range" id="confidence-threshold" min="0" max="1" step="0.05" value="0.5">
376
+ </div>
377
+ </div>
378
+
379
+ <div class="control-group">
380
+ <div class="checkbox-container">
381
+ <input type="checkbox" id="only-detections">
382
+ <label for="only-detections" style="margin-bottom: 0;">Show only images with detections</label>
383
+ </div>
384
+ </div>
385
+
386
+ <button class="load-button" id="load-dataset">Load Dataset</button>
387
+ </div>
388
+
389
+ <div id="stats-container"></div>
390
+ <div id="loading" class="loading" style="display: none;">Loading dataset...</div>
391
+ <div id="error" class="error" style="display: none;"></div>
392
+ <div id="image-grid" class="image-grid"></div>
393
+ <div id="pagination" class="pagination" style="display: none;"></div>
394
+ </div>
395
+
396
+ <script>
397
+ // Global state
398
+ let dataset = [];
399
+ let filteredDataset = [];
400
+ let currentPage = 0;
401
+ const itemsPerPage = 12;
402
+ let confidenceThreshold = 0.5;
403
+ let showOnlyDetections = false;
404
+
405
+ // DOM elements
406
+ const datasetIdInput = document.getElementById('dataset-id');
407
+ const splitSelect = document.getElementById('split');
408
+ const confidenceSlider = document.getElementById('confidence-threshold');
409
+ const confidenceValue = document.getElementById('confidence-value');
410
+ const onlyDetectionsCheckbox = document.getElementById('only-detections');
411
+ const loadButton = document.getElementById('load-dataset');
412
+ const statsContainer = document.getElementById('stats-container');
413
+ const loadingDiv = document.getElementById('loading');
414
+ const errorDiv = document.getElementById('error');
415
+ const imageGrid = document.getElementById('image-grid');
416
+ const paginationDiv = document.getElementById('pagination');
417
+
418
+ // Initialize state from DOM (in case browser persists checkbox state)
419
+ showOnlyDetections = onlyDetectionsCheckbox.checked;
420
+
421
+ // Event listeners
422
+ confidenceSlider.addEventListener('input', (e) => {
423
+ confidenceThreshold = parseFloat(e.target.value);
424
+ confidenceValue.textContent = confidenceThreshold.toFixed(2);
425
+ if (dataset.length > 0) {
426
+ filterAndRender();
427
+ }
428
+ });
429
+
430
+ onlyDetectionsCheckbox.addEventListener('change', (e) => {
431
+ showOnlyDetections = e.target.checked;
432
+ if (dataset.length > 0) {
433
+ filterAndRender();
434
+ }
435
+ });
436
+
437
+ loadButton.addEventListener('click', loadDataset);
438
+
439
+ // Handle example dataset links
440
+ document.querySelectorAll('.example-datasets a').forEach(link => {
441
+ link.addEventListener('click', (e) => {
442
+ e.preventDefault();
443
+ const datasetId = e.target.getAttribute('data-dataset');
444
+ datasetIdInput.value = datasetId;
445
+ loadDataset();
446
+ });
447
+ });
448
+
449
+ // Load dataset from HuggingFace with progressive rendering
450
+ async function loadDataset() {
451
+ const datasetId = datasetIdInput.value.trim();
452
+ const split = splitSelect.value;
453
+
454
+ if (!datasetId) {
455
+ showError('Please enter a dataset ID');
456
+ return;
457
+ }
458
+
459
+ loadingDiv.style.display = 'block';
460
+ errorDiv.style.display = 'none';
461
+ statsContainer.innerHTML = '';
462
+ imageGrid.innerHTML = '';
463
+ paginationDiv.style.display = 'none';
464
+ loadButton.disabled = true;
465
+
466
+ dataset = [];
467
+ let isLoadingComplete = false;
468
+
469
+ try {
470
+ // Fetch rows progressively and render after each batch
471
+ let offset = 0;
472
+ const batchSize = 50; // Smaller batches for faster initial display
473
+ let hasMore = true;
474
+ let isFirstBatch = true;
475
+
476
+ while (hasMore) {
477
+ const url = `https://datasets-server.huggingface.co/rows?dataset=${encodeURIComponent(datasetId)}&config=default&split=${split}&offset=${offset}&length=${batchSize}`;
478
+ const response = await fetch(url);
479
+
480
+ if (!response.ok) {
481
+ if (offset === 0) {
482
+ throw new Error(`Failed to load dataset. Check dataset ID and split name.`);
483
+ }
484
+ break;
485
+ }
486
+
487
+ const data = await response.json();
488
+
489
+ if (!data.rows || data.rows.length === 0) {
490
+ hasMore = false;
491
+ break;
492
+ }
493
+
494
+ // Convert rows to dataset format
495
+ const newRows = data.rows.map(item => ({
496
+ index: offset + item.row_idx,
497
+ image: item.row.image, // Keep as object or string
498
+ objects: item.row.objects || { bbox: [], category: [], score: [] },
499
+ ...item.row
500
+ }));
501
+
502
+ dataset = dataset.concat(newRows);
503
+ offset += data.rows.length;
504
+
505
+ // Render immediately after first batch
506
+ if (isFirstBatch) {
507
+ filterAndRender();
508
+ loadingDiv.textContent = `Loading more... (${dataset.length} rows loaded)`;
509
+ isFirstBatch = false;
510
+ } else {
511
+ // Update view and stats as we load more
512
+ filterDataset();
513
+ renderStats();
514
+ // Only re-render current page if we're on page 1 (to avoid disrupting user)
515
+ if (currentPage === 0) {
516
+ renderPage();
517
+ renderPagination();
518
+ }
519
+ loadingDiv.textContent = `Loading more... (${dataset.length} rows loaded)`;
520
+ }
521
+
522
+ // Stop if we got fewer rows than requested (end of dataset)
523
+ if (data.rows.length < batchSize) {
524
+ hasMore = false;
525
+ }
526
+
527
+ // Limit to prevent overwhelming the browser
528
+ if (dataset.length >= 10000) {
529
+ console.log('Loaded 10,000 rows, stopping to prevent performance issues');
530
+ hasMore = false;
531
+ }
532
+ }
533
+
534
+ if (dataset.length === 0) {
535
+ throw new Error('No data found in dataset');
536
+ }
537
+
538
+ // Final update
539
+ isLoadingComplete = true;
540
+ loadingDiv.style.display = 'none';
541
+ filterAndRender();
542
+
543
+ } catch (error) {
544
+ loadingDiv.style.display = 'none';
545
+ showError(`Failed to load dataset: ${error.message}`);
546
+ } finally {
547
+ loadButton.disabled = false;
548
+ if (isLoadingComplete) {
549
+ loadingDiv.textContent = 'Loading dataset...';
550
+ }
551
+ }
552
+ }
553
+
554
+ // Filter dataset based on current filters
555
+ function filterDataset() {
556
+ filteredDataset = dataset.filter(item => {
557
+ const objects = item.objects || { bbox: [], category: [], score: [] };
558
+
559
+ // Filter by confidence threshold
560
+ const validDetections = objects.score.filter(score => score >= confidenceThreshold);
561
+
562
+ // If "only detections" is checked, filter out images without detections
563
+ if (showOnlyDetections && validDetections.length === 0) {
564
+ return false;
565
+ }
566
+
567
+ return true;
568
+ });
569
+ }
570
+
571
+ // Filter and render
572
+ function filterAndRender() {
573
+ filterDataset();
574
+ currentPage = 0;
575
+ renderStats();
576
+ renderPage();
577
+ renderPagination();
578
+ }
579
+
580
+ // Render statistics
581
+ function renderStats() {
582
+ // Calculate stats from FILTERED dataset for consistency
583
+ const totalImages = filteredDataset.length;
584
+ const totalDetections = filteredDataset.reduce((sum, item) => {
585
+ const objects = item.objects || { bbox: [], category: [], score: [] };
586
+ return sum + objects.score.filter(score => score >= confidenceThreshold).length;
587
+ }, 0);
588
+
589
+ const imagesWithDetections = filteredDataset.filter(item => {
590
+ const objects = item.objects || { bbox: [], category: [], score: [] };
591
+ return objects.score.some(score => score >= confidenceThreshold);
592
+ }).length;
593
+
594
+ const avgDetections = totalImages > 0 ? (totalDetections / totalImages).toFixed(2) : 0;
595
+
596
+ statsContainer.innerHTML = `
597
+ <div class="stats">
598
+ <div class="stats-grid">
599
+ <div class="stat-item">
600
+ <div class="stat-value">${filteredDataset.length}</div>
601
+ <div class="stat-label">Filtered Images</div>
602
+ </div>
603
+ <div class="stat-item">
604
+ <div class="stat-value">${imagesWithDetections}</div>
605
+ <div class="stat-label">Images with Detections</div>
606
+ </div>
607
+ <div class="stat-item">
608
+ <div class="stat-value">${totalDetections}</div>
609
+ <div class="stat-label">Total Detections</div>
610
+ </div>
611
+ <div class="stat-item">
612
+ <div class="stat-value">${avgDetections}</div>
613
+ <div class="stat-label">Avg per Image</div>
614
+ </div>
615
+ </div>
616
+ </div>
617
+ `;
618
+ }
619
+
620
+ // Render current page
621
+ function renderPage() {
622
+ const start = currentPage * itemsPerPage;
623
+ const end = start + itemsPerPage;
624
+ const pageItems = filteredDataset.slice(start, end);
625
+
626
+ imageGrid.innerHTML = pageItems.map(item => {
627
+ const objects = item.objects || { bbox: [], category: [], score: [] };
628
+ const validDetections = objects.score
629
+ .map((score, idx) => ({ score, idx }))
630
+ .filter(({ score }) => score >= confidenceThreshold);
631
+
632
+ // Extract image URL properly (handle both object with src and direct string)
633
+ const imageUrl = typeof item.image === 'object' ? item.image?.src : item.image;
634
+
635
+ return `
636
+ <div class="image-card">
637
+ <div class="image-container" id="container-${item.index}">
638
+ <img src="${imageUrl}" alt="Image ${item.index}" crossorigin="anonymous" onload="drawBoundingBoxes(${item.index})">
639
+ <canvas id="canvas-${item.index}"></canvas>
640
+ </div>
641
+ <div class="image-info">
642
+ <div class="image-index">Image #${item.index}</div>
643
+ <div class="detections-count">${validDetections.length} detection(s)</div>
644
+ <div class="detections-list">
645
+ ${validDetections.map(({ score, idx }) => {
646
+ const confidenceClass = score >= 0.7 ? 'confidence-high' : score >= 0.4 ? 'confidence-medium' : 'confidence-low';
647
+ const category = objects.category && objects.category[idx] !== undefined ? objects.category[idx] : 0;
648
+ return `
649
+ <div class="detection-item">
650
+ <span class="${confidenceClass}">${(score * 100).toFixed(1)}%</span>
651
+ confidence
652
+ </div>
653
+ `;
654
+ }).join('')}
655
+ </div>
656
+ </div>
657
+ </div>
658
+ `;
659
+ }).join('');
660
+ }
661
+
662
+ // Draw bounding boxes on canvas
663
+ window.drawBoundingBoxes = function(itemIndex) {
664
+ const item = dataset.find(d => d.index === itemIndex);
665
+ if (!item) return;
666
+
667
+ const container = document.getElementById(`container-${itemIndex}`);
668
+ const canvas = document.getElementById(`canvas-${itemIndex}`);
669
+ const img = container.querySelector('img');
670
+
671
+ if (!canvas || !img) return;
672
+
673
+ // Set canvas size to match image
674
+ canvas.width = img.naturalWidth;
675
+ canvas.height = img.naturalHeight;
676
+
677
+ const ctx = canvas.getContext('2d');
678
+
679
+ // Clear canvas before drawing
680
+ ctx.clearRect(0, 0, canvas.width, canvas.height);
681
+
682
+ const objects = item.objects || { bbox: [], category: [], score: [] };
683
+
684
+ // Debug: log drawing info
685
+ const validBoxes = objects.bbox.filter((_, idx) => objects.score[idx] >= confidenceThreshold);
686
+ if (validBoxes.length > 0) {
687
+ console.log(`Drawing ${validBoxes.length} boxes for image #${itemIndex}`);
688
+ }
689
+
690
+ // Draw each bounding box
691
+ objects.bbox.forEach((bbox, idx) => {
692
+ const score = objects.score[idx];
693
+ if (score < confidenceThreshold) return;
694
+
695
+ const [x, y, width, height] = bbox;
696
+
697
+ // Debug: log bbox coordinates
698
+ console.log(` Box ${idx}: [${x}, ${y}, ${width}, ${height}] score: ${score}`);
699
+
700
+ // Choose color based on confidence
701
+ let color;
702
+ if (score >= 0.7) {
703
+ color = '#27ae60'; // Green
704
+ } else if (score >= 0.4) {
705
+ color = '#f39c12'; // Orange
706
+ } else {
707
+ color = '#e74c3c'; // Red
708
+ }
709
+
710
+ // Draw rectangle
711
+ ctx.strokeStyle = color;
712
+ ctx.lineWidth = 3;
713
+ ctx.strokeRect(x, y, width, height);
714
+
715
+ console.log(` Drew box at [${x}, ${y}] size [${width}, ${height}] in ${color}`);
716
+
717
+ // Draw label background
718
+ const label = `${(score * 100).toFixed(1)}%`;
719
+ ctx.font = 'bold 16px Arial';
720
+ const textWidth = ctx.measureText(label).width;
721
+ const textHeight = 20;
722
+
723
+ ctx.fillStyle = color;
724
+ ctx.fillRect(x, y - textHeight - 4, textWidth + 10, textHeight + 4);
725
+
726
+ // Draw label text
727
+ ctx.fillStyle = 'white';
728
+ ctx.fillText(label, x + 5, y - 8);
729
+ });
730
+ };
731
+
732
+ // Render pagination
733
+ function renderPagination() {
734
+ const totalPages = Math.ceil(filteredDataset.length / itemsPerPage);
735
+
736
+ if (totalPages <= 1) {
737
+ paginationDiv.style.display = 'none';
738
+ return;
739
+ }
740
+
741
+ paginationDiv.style.display = 'flex';
742
+ paginationDiv.innerHTML = `
743
+ <button id="prev-btn" ${currentPage === 0 ? 'disabled' : ''}>Previous</button>
744
+ <span>Page ${currentPage + 1} of ${totalPages}</span>
745
+ <button id="next-btn" ${currentPage >= totalPages - 1 ? 'disabled' : ''}>Next</button>
746
+ `;
747
+
748
+ document.getElementById('prev-btn').addEventListener('click', () => {
749
+ if (currentPage > 0) {
750
+ currentPage--;
751
+ renderPage();
752
+ renderPagination();
753
+ window.scrollTo({ top: 0, behavior: 'smooth' });
754
+ }
755
+ });
756
+
757
+ document.getElementById('next-btn').addEventListener('click', () => {
758
+ if (currentPage < totalPages - 1) {
759
+ currentPage++;
760
+ renderPage();
761
+ renderPagination();
762
+ window.scrollTo({ top: 0, behavior: 'smooth' });
763
+ }
764
+ });
765
+ }
766
+
767
+ // Show error message
768
+ function showError(message) {
769
+ errorDiv.textContent = message;
770
+ errorDiv.style.display = 'block';
771
+ }
772
+
773
+ // Auto-load default dataset on page load
774
+ window.addEventListener('load', () => {
775
+ loadDataset();
776
+ });
777
+ </script>
778
+ </body>
779
  </html>
style.css DELETED
@@ -1,28 +0,0 @@
1
- body {
2
- padding: 2rem;
3
- font-family: -apple-system, BlinkMacSystemFont, "Arial", sans-serif;
4
- }
5
-
6
- h1 {
7
- font-size: 16px;
8
- margin-top: 0;
9
- }
10
-
11
- p {
12
- color: rgb(107, 114, 128);
13
- font-size: 15px;
14
- margin-bottom: 10px;
15
- margin-top: 5px;
16
- }
17
-
18
- .card {
19
- max-width: 620px;
20
- margin: 0 auto;
21
- padding: 16px;
22
- border: 1px solid lightgray;
23
- border-radius: 16px;
24
- }
25
-
26
- .card p:last-child {
27
- margin-bottom: 0;
28
- }