Wizzas commited on
Commit
b0de141
·
verified ·
1 Parent(s): 1c95014

Upload 9 files

Browse files
Files changed (8) hide show
  1. app.js +945 -913
  2. browser-models.json +96 -81
  3. browser-runtime.js +185 -185
  4. charts.css +241 -0
  5. charts.js +193 -0
  6. index.html +496 -459
  7. per_class.json +660 -646
  8. results.json +225 -210
app.js CHANGED
@@ -1,913 +1,945 @@
1
- 'use strict';
2
-
3
- // PaddyLiteX: resolve class results beside the running app.js.
4
- const paddyClassResultsUrl = new URL('per_class.json', document.currentScript?.src || document.baseURI);
5
-
6
-
7
- // Application state is shared by the detection and benchmark views.
8
- const getElement = (id) => document.getElementById(id);
9
- const formatNumber = (value) => new Intl.NumberFormat('en-US').format(value);
10
- const performanceMetrics = {
11
- map5095: { label: 'mAP@0.5:0.95', unit: '%' },
12
- map50: { label: 'mAP@0.5', unit: '%' },
13
- precision: { label: 'Precision', unit: '%' },
14
- recall: { label: 'Recall', unit: '%' },
15
- };
16
- const complexityMetrics = {
17
- parameters: {
18
- label: 'Parameters',
19
- axisLabel: 'Parameters (millions)',
20
- scale: 1000000,
21
- unit: 'M',
22
- },
23
- gflops: { label: 'GFLOPs', axisLabel: 'GFLOPs', scale: 1, unit: 'G' },
24
- };
25
-
26
- let benchmarkData;
27
- let currentStudy = 'main';
28
- let selectedFile = null;
29
- let previewUrl = null;
30
- let comparisonRunning = false;
31
- let latestResults = null;
32
- let classData = null;
33
- let classDataError = '';
34
- const classMetricLabels = {
35
- map5095: 'AP@0.5:0.95',
36
- map50: 'AP@0.5',
37
- precision: 'Precision',
38
- recall: 'Recall',
39
- };
40
- // Dataset class IDs supplied for this thesis. Keep names independent of scores.
41
- const paddyClassNames = [
42
- 'Rice__BacterialLeafBlight',
43
- 'Rice__BrownSpot',
44
- 'Rice__Healthy',
45
- 'Rice__Hispa',
46
- 'Rice__LeafBlast',
47
- 'Rice__LeafScald',
48
- 'Rice__LeafSmut',
49
- 'Rice__NarrowBrownLeafSpot',
50
- 'Rice__NeckBlast',
51
- ];
52
-
53
- function classRows() {
54
- const supplied = new Map((classData?.classes || []).map((row) => [row.id, row]));
55
- return paddyClassNames.map((name, id) => ({
56
- id,
57
- name,
58
- models: supplied.get(id)?.models || {},
59
- }));
60
- }
61
-
62
- function escapeHtml(value) {
63
- const entities = { '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' };
64
- return String(value).replace(/[&<>"']/g, (character) => entities[character]);
65
- }
66
-
67
- function displayModelName(model) {
68
- // Older local configurations still use the previous baseline label.
69
- return model.label.replace(/\bDenseNet121\b/g, 'Yolov8n_DenseNet121');
70
- }
71
-
72
- function showView(viewName) {
73
- document.querySelectorAll('.view').forEach((view) => {
74
- const active = view.id === viewName;
75
- view.hidden = !active;
76
- view.classList.toggle('active', active);
77
- });
78
-
79
- document.querySelectorAll('.tab').forEach((button) => {
80
- const active = button.dataset.view === viewName;
81
- button.classList.toggle('active', active);
82
- button.setAttribute('aria-selected', String(active));
83
- });
84
- }
85
-
86
- async function readResponse(response) {
87
- let payload;
88
- try {
89
- payload = await response.json();
90
- } catch {
91
- throw new Error('The server returned an unexpected response. Please try again.');
92
- }
93
-
94
- if (!response.ok) {
95
- throw new Error(
96
- typeof payload.detail === 'string' ? payload.detail : 'The request could not be completed.',
97
- );
98
- }
99
- return payload;
100
- }
101
-
102
- // Detection studio
103
- function resultHeading(model) {
104
- return `
105
- <div class="card-head">
106
- <p class="role">${escapeHtml(model.role)}</p>
107
- <h3>${escapeHtml(displayModelName(model))}</h3>
108
- </div>
109
- `;
110
- }
111
-
112
- function showEmptyResults(message = 'Upload an image to compare predictions.') {
113
- if (!benchmarkData) return;
114
-
115
- getElement('results').innerHTML = benchmarkData.main
116
- .map(
117
- (model) => `
118
- <article class="result-card" style="--model-color: ${model.color}">
119
- ${resultHeading(model)}
120
- <div class="result-stage"><p>${escapeHtml(message)}</p></div>
121
- <div class="result-body">${escapeHtml(model.id)}</div>
122
- </article>
123
- `,
124
- )
125
- .join('');
126
- }
127
-
128
- function showLoadingResults() {
129
- getElement('results').innerHTML = benchmarkData.main
130
- .map(
131
- (model) => `
132
- <article class="result-card" style="--model-color: ${model.color}">
133
- ${resultHeading(model)}
134
- <div class="result-stage">
135
- <div>
136
- <div class="spinner" style="margin: auto"></div>
137
- <p>Comparing models…</p>
138
- </div>
139
- </div>
140
- <div class="result-body">Identical image and settings</div>
141
- </article>
142
- `,
143
- )
144
- .join('');
145
- }
146
-
147
- function renderResults(results) {
148
- const classMappings = results
149
- .filter((result) => result.status === 'success')
150
- .map((result) => JSON.stringify(Object.entries(result.class_names).sort()));
151
-
152
- if (new Set(classMappings).size > 1) {
153
- getElement('message').textContent =
154
- 'The checkpoints have different class mappings. Verify their dataset configuration before treating this as a matched comparison.';
155
- }
156
-
157
- getElement('results').innerHTML = benchmarkData.main
158
- .map((model) => {
159
- const result = results.find((item) => item.id === model.id);
160
- if (!result || result.status !== 'success') {
161
- const description =
162
- result?.status === 'unavailable' ? 'Model unavailable' : 'Inference error';
163
- return `
164
- <article class="result-card" style="--model-color: ${model.color}">
165
- ${resultHeading(model)}
166
- <div class="result-stage">
167
- <p>${escapeHtml(result?.message || 'No result returned.')}</p>
168
- </div>
169
- <div class="result-body">${description}</div>
170
- </article>
171
- `;
172
- }
173
-
174
- const detections = result.detections;
175
- const detectionRows = detections.length
176
- ? detections
177
- .map(
178
- (detection) => `
179
- <div class="detection-row">
180
- <span>${escapeHtml(detection.class_name)}</span>
181
- <strong>${(detection.confidence * 100).toFixed(1)}%</strong>
182
- </div>
183
- `,
184
- )
185
- .join('')
186
- : '<p>No boxes exceeded the selected threshold. This does not confirm that the leaf is healthy.</p>';
187
-
188
- return `
189
- <article class="result-card" style="--model-color: ${model.color}">
190
- ${resultHeading(model)}
191
- <div class="result-stage">
192
- <img src="${escapeHtml(result.image)}" alt="${escapeHtml(displayModelName(model))} annotated detection result">
193
- </div>
194
- <div class="result-body">
195
- <strong>${detections.length} detection${detections.length === 1 ? '' : 's'}</strong>
196
- ${detectionRows}
197
- <a class="download-link" href="${escapeHtml(result.image)}" download="${escapeHtml(model.id)}_prediction.png">
198
- Download annotated image
199
- </a>
200
- </div>
201
- </article>
202
- `;
203
- })
204
- .join('');
205
- }
206
-
207
- function updateControls() {
208
- getElement('compare').disabled = !selectedFile || comparisonRunning || !benchmarkData;
209
- getElement('compare').textContent = comparisonRunning
210
- ? 'Comparing models…'
211
- : 'Compare all three models';
212
- ['file', 'clear', 'confidence', 'image-size'].forEach((id) => {
213
- getElement(id).disabled = comparisonRunning;
214
- });
215
- }
216
-
217
- function clearFile() {
218
- if (comparisonRunning) return;
219
- if (previewUrl) URL.revokeObjectURL(previewUrl);
220
-
221
- selectedFile = null;
222
- previewUrl = null;
223
- latestResults = null;
224
- getElement('file').value = '';
225
- getElement('input-preview').hidden = true;
226
- getElement('input-preview').removeAttribute('src');
227
- getElement('upload-hint').hidden = false;
228
- getElement('clear').hidden = true;
229
- getElement('filename').textContent = 'No image selected';
230
- getElement('message').textContent = '';
231
- showEmptyResults();
232
- updateControls();
233
- }
234
-
235
- // JPG upload compatibility: some files have an alias or no MIME type.
236
- function isSupportedImageFile(file) {
237
- if (!file) return false;
238
-
239
- const imageType = (file.type || '').toLowerCase();
240
- const supportedTypes = [
241
- 'image/jpeg',
242
- 'image/jpg',
243
- 'image/pjpeg',
244
- 'image/png',
245
- 'image/webp',
246
- ];
247
-
248
- if (supportedTypes.includes(imageType)) return true;
249
-
250
- // The existing image decoder still verifies that this is an actual image.
251
- return (
252
- ['', 'application/octet-stream'].includes(imageType) &&
253
- /\.(jpg|jpeg|png|webp)$/i.test(file.name || '')
254
- );
255
- }
256
-
257
- async function selectFile(file) {
258
- if (comparisonRunning || !file) return;
259
- getElement('message').textContent = '';
260
-
261
- if (!isSupportedImageFile(file)) {
262
- getElement('message').textContent = 'Choose a JPG, JPEG, PNG, or WebP image.';
263
- return;
264
- }
265
- if (file.size > 10 * 1024 * 1024) {
266
- getElement('message').textContent = 'Image exceeds 10 MB. Choose a smaller file.';
267
- return;
268
- }
269
-
270
- let bitmap;
271
- try {
272
- bitmap = await createImageBitmap(file);
273
- if (bitmap.width * bitmap.height > 12000000) {
274
- throw new Error('Image exceeds 12 megapixels. Resize it first.');
275
- }
276
- } catch (error) {
277
- getElement('message').textContent = error.message.startsWith('Image exceeds')
278
- ? error.message
279
- : 'The file cannot be decoded as an image.';
280
- return;
281
- } finally {
282
- bitmap?.close();
283
- }
284
-
285
- if (previewUrl) URL.revokeObjectURL(previewUrl);
286
- selectedFile = file;
287
- latestResults = null;
288
- previewUrl = URL.createObjectURL(file);
289
- getElement('input-preview').src = previewUrl;
290
- getElement('input-preview').hidden = false;
291
- getElement('upload-hint').hidden = true;
292
- getElement('clear').hidden = false;
293
- getElement('filename').textContent = `${file.name} · ${(file.size / 1024).toFixed(0)} KB`;
294
- showEmptyResults();
295
- updateControls();
296
- }
297
-
298
- async function compareModels() {
299
- if (!selectedFile || comparisonRunning || !benchmarkData) return;
300
- comparisonRunning = true;
301
- updateControls();
302
- showLoadingResults();
303
- getElement('message').textContent = '';
304
-
305
- const form = new FormData();
306
- form.append('file', selectedFile);
307
- form.append('confidence', getElement('confidence').value);
308
- form.append('image_size', getElement('image-size').value);
309
-
310
- try {
311
- const response = await PaddyLiteXBrowser.fetch('/api/compare', { method: 'POST', body: form });
312
- latestResults = await readResponse(response);
313
- renderResults(latestResults.results);
314
- } catch (error) {
315
- showEmptyResults('Comparison could not complete. See the message above.');
316
- getElement('message').textContent = error.message;
317
- } finally {
318
- comparisonRunning = false;
319
- updateControls();
320
- }
321
- }
322
-
323
- // Benchmark charts: both bar charts and the scatter plot use these selectors.
324
- function formatMetric(value, metric) {
325
- if (performanceMetrics[metric]) return `${value.toFixed(2)}%`;
326
- if (metric === 'parameters') return `${(value / 1000000).toFixed(2)} M`;
327
- return `${value.toFixed(1)} G`;
328
- }
329
-
330
- function axisScale(maximum) {
331
- // Choose round tick values and keep all points inside the plotting area.
332
- const approximateStep = ((maximum || 1) * 1.08) / 6;
333
- const magnitude = 10 ** Math.floor(Math.log10(approximateStep));
334
- const normalizedStep = approximateStep / magnitude;
335
- const multiplier = [1, 2, 5, 10].find((value) => value >= normalizedStep);
336
- const step = multiplier * magnitude;
337
- const limit = Math.ceil(((maximum || 1) * 1.08) / step) * step;
338
- const ticks = [];
339
- for (let index = 0; index <= Math.round(limit / step); index += 1) {
340
- ticks.push(Number((index * step).toPrecision(10)));
341
- }
342
- return { limit, ticks };
343
- }
344
-
345
- function renderBarChart(containerId, models, metric) {
346
- const percentage = Boolean(performanceMetrics[metric]);
347
- const maximum = Math.max(...models.map((model) => model[metric]));
348
- const limit = percentage ? 100 : axisScale(maximum).limit;
349
- const axisEnd = percentage ? '100%' : formatMetric(limit, metric);
350
-
351
- getElement(containerId).innerHTML =
352
- models
353
- .map(
354
- (model) => `
355
- <div>
356
- <div class="bar-label">
357
- <span>${escapeHtml(displayModelName(model))}</span>
358
- <strong>${formatMetric(model[metric], metric)}</strong>
359
- </div>
360
- <div class="bar-track" role="img" aria-label="${escapeHtml(displayModelName(model))}: ${formatMetric(model[metric], metric)}">
361
- <div class="bar-fill" style="width: ${(model[metric] / limit) * 100}%; background: ${model.color}"></div>
362
- </div>
363
- </div>
364
- `,
365
- )
366
- .join('') +
367
- `
368
- <div class="axis"><span>0</span><span>${axisEnd}</span></div>
369
- `;
370
- }
371
-
372
- function renderScatterPlot(models, performanceMetric, complexityMetric) {
373
- const performance = performanceMetrics[performanceMetric];
374
- const complexity = complexityMetrics[complexityMetric];
375
- const width = 1000;
376
- const height = 340;
377
- const margin = { left: 75, right: 40, top: 30, bottom: 65 };
378
- const plotWidth = width - margin.left - margin.right;
379
- const plotHeight = height - margin.top - margin.bottom;
380
- const horizontalScale = axisScale(
381
- Math.max(...models.map((model) => model[complexityMetric] / complexity.scale)),
382
- );
383
- const x = (value) => margin.left + (value / horizontalScale.limit) * plotWidth;
384
- const y = (value) => height - margin.bottom - (value / 100) * plotHeight;
385
-
386
- const horizontalGrid = [0, 20, 40, 60, 80, 100]
387
- .map(
388
- (value) => `
389
- <line x1="${margin.left}" x2="${width - margin.right}" y1="${y(value)}" y2="${y(value)}" stroke="#e7edf4" />
390
- <text x="${margin.left - 14}" y="${y(value) + 4}" text-anchor="end" fill="#68758a" font-size="13">${value}</text>
391
- `,
392
- )
393
- .join('');
394
-
395
- const horizontalTicks = horizontalScale.ticks
396
- .map(
397
- (value) => `
398
- <text x="${x(value)}" y="${height - margin.bottom + 23}" text-anchor="middle" fill="#68758a" font-size="13">${value}</text>
399
- `,
400
- )
401
- .join('');
402
-
403
- const points = models
404
- .map((model, index) => {
405
- const pointX = x(model[complexityMetric] / complexity.scale);
406
- const pointY = y(model[performanceMetric]);
407
- const tooltip = `${displayModelName(model)}: ${complexity.label} ${formatMetric(model[complexityMetric], complexityMetric)}; ${performance.label} ${formatMetric(model[performanceMetric], performanceMetric)}`;
408
- const label =
409
- currentStudy === 'main'
410
- ? `
411
- <text x="${pointX}" y="${pointY + (index === 0 ? 25 : -16)}" text-anchor="middle" font-size="13" fill="${model.color}">
412
- ${model.role === 'Proposed' ? 'Proposed' : escapeHtml(displayModelName(model))}
413
- </text>
414
- `
415
- : '';
416
-
417
- return `
418
- <circle data-model-id="${escapeHtml(model.id)}" cx="${pointX}" cy="${pointY}" r="${model.role === 'Proposed' ? 9 : 7}" fill="${model.color}" stroke="white" stroke-width="2">
419
- <title>${escapeHtml(tooltip)}</title>
420
- </circle>
421
- ${label}
422
- `;
423
- })
424
- .join('');
425
-
426
- getElement('scatter').innerHTML = `
427
- <svg viewBox="0 0 ${width} ${height}" role="img" aria-label="${complexity.axisLabel} versus ${performance.label} (%)">
428
- ${horizontalGrid}
429
- ${horizontalTicks}
430
- <text data-axis="x" x="${margin.left + plotWidth / 2}" y="${height - 8}" text-anchor="middle" fill="#68758a" font-size="14">${complexity.axisLabel}</text>
431
- <text data-axis="y" x="18" y="${margin.top + plotHeight / 2}" transform="rotate(-90 18 ${margin.top + plotHeight / 2})" text-anchor="middle" fill="#68758a" font-size="14">${performance.label} (%)</text>
432
- ${points}
433
- </svg>
434
- `;
435
-
436
- getElement('scatter-metrics').textContent = `${complexity.label} vs. ${performance.label}`;
437
- getElement('legend').innerHTML = models
438
- .map(
439
- (model) => `
440
- <span><i style="background: ${model.color}"></i>${escapeHtml(displayModelName(model))}</span>
441
- `,
442
- )
443
- .join('');
444
- }
445
-
446
- function renderBenchmarks() {
447
- if (!benchmarkData) return;
448
- const models = benchmarkData[currentStudy];
449
- const performanceMetric = getElement('metric').value;
450
- const complexityMetric = getElement('cost-metric').value;
451
-
452
- renderBarChart('quality-chart', models, performanceMetric);
453
- renderBarChart('cost-chart', models, complexityMetric);
454
- renderScatterPlot(models, performanceMetric, complexityMetric);
455
- renderClassChart();
456
-
457
- getElement('metrics-table').innerHTML = models
458
- .map(
459
- (model) => `
460
- <tr class="${model.role === 'Proposed' ? 'proposed' : ''}">
461
- <td>${escapeHtml(displayModelName(model))}</td>
462
- <td>${model.precision.toFixed(2)}%</td>
463
- <td>${model.recall.toFixed(2)}%</td>
464
- <td>${model.map50.toFixed(2)}%</td>
465
- <td>${model.map5095.toFixed(2)}%</td>
466
- <td>${formatNumber(model.parameters)}</td>
467
- <td>${model.gflops.toFixed(1)}</td>
468
- </tr>
469
- `,
470
- )
471
- .join('');
472
- }
473
-
474
- // Class metrics come from a separate file so checkpoint paths never need editing.
475
- function validateClassData(data) {
476
- if (data?.schema_version !== 1 || data.unit !== 'percent' || !Array.isArray(data.classes)) {
477
- throw new Error('The class results file has an unsupported format.');
478
- }
479
- const ids = new Set();
480
- for (const row of data.classes) {
481
- if (
482
- !Number.isInteger(row.id) ||
483
- row.id < 0 ||
484
- row.id >= paddyClassNames.length ||
485
- ids.has(row.id) ||
486
- typeof row.name !== 'string' ||
487
- !row.name.trim() ||
488
- !row.models ||
489
- typeof row.models !== 'object' ||
490
- Array.isArray(row.models)
491
- ) {
492
- throw new Error('Check the class IDs, names and model results in the class results file.');
493
- }
494
- ids.add(row.id);
495
- for (const scores of Object.values(row.models)) {
496
- if (!scores || typeof scores !== 'object' || Array.isArray(scores)) {
497
- throw new Error('Each model must have a class metric object.');
498
- }
499
- for (const metric of Object.keys(classMetricLabels)) {
500
- const value = scores[metric];
501
- if (
502
- value != null &&
503
- (typeof value !== 'number' || !Number.isFinite(value) || value < 0 || value > 100)
504
- ) {
505
- throw new Error('Class metrics must be percentages between 0 and 100, or null.');
506
- }
507
- }
508
- }
509
- }
510
- return data;
511
- }
512
-
513
- function classScore(row, modelId, metric) {
514
- const value = row.models[modelId]?.[metric];
515
- return typeof value === 'number' && Number.isFinite(value) ? value : null;
516
- }
517
-
518
- // Ablation studies may have class results for only some of their models.
519
- function classChartModels() {
520
- if (!benchmarkData) return [];
521
- if (currentStudy === 'main') return benchmarkData.main;
522
- const rows = classRows();
523
- return benchmarkData.ablation.filter((model) =>
524
- rows.some((row) =>
525
- Object.keys(classMetricLabels).some((metric) => classScore(row, model.id, metric) !== null),
526
- ),
527
- );
528
- }
529
-
530
- function classModelCode(model, index) {
531
- if (model.role === 'Proposed') return 'P';
532
- return currentStudy === 'main' ? `B${index + 1}` : `A${index + 1}`;
533
- }
534
-
535
- function renderClassChart() {
536
- if (!benchmarkData) return;
537
- const panel = getElement('class-panel');
538
- panel.hidden = false;
539
- const models = classChartModels();
540
- const mainStudy = currentStudy === 'main';
541
- getElement('class-heading').textContent = mainStudy
542
- ? 'Every class, three perspectives.'
543
- : 'Class results across available ablations.';
544
- getElement('class-heading').nextElementSibling.textContent = mainStudy
545
- ? 'Compare your two baselines and final proposed model for each class.'
546
- : 'Compare the ablation models with supplied class results and the final proposed model.';
547
- const metric = getElement('class-metric').value;
548
- const label = classMetricLabels[metric];
549
- const rows = classRows();
550
- const proposed = models.find((model) => model.role === 'Proposed');
551
- const order = getElement('class-order').value;
552
- if (order !== 'dataset' && proposed) {
553
- rows.sort((a, b) => {
554
- const first = classScore(a, proposed.id, metric);
555
- const second = classScore(b, proposed.id, metric);
556
- if (first == null) return second == null ? a.id - b.id : 1;
557
- if (second == null) return -1;
558
- return (order === 'lowest' ? first - second : second - first) || a.id - b.id;
559
- });
560
- }
561
-
562
- getElement('class-legend').innerHTML = models
563
- .map(
564
- (model) => `
565
- <span><i style="background: ${model.color}"></i>${escapeHtml(model.role)} · ${escapeHtml(displayModelName(model))}</span>
566
- `,
567
- )
568
- .join('');
569
- const hasValues = rows.some((row) =>
570
- models.some((model) =>
571
- Object.keys(classMetricLabels).some((key) => classScore(row, model.id, key) !== null),
572
- ),
573
- );
574
- getElement('download-class-csv').disabled = !hasValues;
575
- const split = classData?.context?.split || 'test';
576
- getElement('class-context').textContent = rows.length
577
- ? `${split} set · ${rows.length} classes · ${label} (%)`
578
- : '';
579
- getElement('class-chart').innerHTML = `
580
- <div class="class-axis" aria-hidden="true">
581
- <span>${escapeHtml(label)} (%)</span>
582
- <div>${[0, 20, 40, 60, 80, 100].map((value) => `<span>${value}%</span>`).join('')}</div>
583
- </div>
584
- ${rows
585
- .map(
586
- (row) => `
587
- <div class="class-group" data-class-id="${row.id}">
588
- <div class="class-name"><strong>${escapeHtml(row.name)}</strong><small>Class ${row.id}</small></div>
589
- <div class="class-series">
590
- ${models
591
- .map((model, index) => {
592
- const value = classScore(row, model.id, metric);
593
- const text = value === null ? 'N/A' : `${value.toFixed(2)}%`;
594
- const caption = `${row.name} · ${displayModelName(model)} · ${label}: ${text}`;
595
- return `
596
- <div class="class-bar" data-model-id="${escapeHtml(model.id)}" role="img" aria-label="${escapeHtml(caption)}" title="${escapeHtml(caption)}">
597
- <span class="class-short" style="color: ${model.color}">${classModelCode(model, index)}</span>
598
- <div class="class-track"><div class="class-fill" style="width: ${value ?? 0}%; background: ${model.color}" ${value === null ? 'hidden' : ''}></div></div>
599
- <strong>${text}</strong>
600
- </div>`;
601
- })
602
- .join('')}
603
- </div>
604
- </div>`,
605
- )
606
- .join('')}`;
607
- const missingModels = currentStudy === 'ablation'
608
- ? benchmarkData.ablation.length - models.length
609
- : 0;
610
- const coverageNote = missingModels > 0
611
- ? ` Class results are available for ${models.length} of ${benchmarkData.ablation.length} ablation models. Models without supplied class results are omitted.`
612
- : '';
613
- getElement('class-note').textContent =
614
- classDataError ||
615
- (hasValues
616
- ? `${split} set · percent · higher is better · 0–100% scale. N/A means no evaluated value was supplied. AP is reported per class; mAP averages across classes.${coverageNote}`
617
- : 'All nine disease classes are shown. N/A means their evaluation scores have not been supplied yet.');
618
- }
619
-
620
- async function loadClassMetrics() {
621
- try {
622
- const url = new URL(paddyClassResultsUrl);
623
- url.searchParams.set('v', 'c2f1-results-20261012-batch2');
624
- const response = await fetch(url, { cache: 'no-store' });
625
- if (!response.ok) {
626
- throw new Error(`per_class.json could not load (HTTP ${response.status}). Upload it beside app.js.`);
627
- }
628
- const raw = await response.text();
629
- let data;
630
- try {
631
- data = JSON.parse(raw);
632
- } catch {
633
- throw new Error(raw.trimStart().startsWith('<')
634
- ? 'per_class.json returned an HTML page. Check its upload location beside app.js.'
635
- : 'per_class.json is not valid JSON. Replace it with the supplied results file.');
636
- }
637
- classData = validateClassData(data);
638
- classDataError = '';
639
- } catch (error) {
640
- classDataError = `Class results could not load. ${error.message}`;
641
- }
642
- renderClassChart();
643
- }
644
-
645
- function downloadClassMetrics() {
646
- if (!classData?.classes.length || !benchmarkData) return;
647
- const rows = [
648
- [
649
- 'Class ID',
650
- 'Class',
651
- 'Model',
652
- 'Role',
653
- 'Precision (%)',
654
- 'Recall (%)',
655
- 'AP@0.5 (%)',
656
- 'AP@0.5:0.95 (%)',
657
- ],
658
- ];
659
- for (const row of classRows()) {
660
- for (const model of classChartModels()) {
661
- rows.push([
662
- row.id,
663
- row.name,
664
- displayModelName(model),
665
- model.role,
666
- ...['precision', 'recall', 'map50', 'map5095'].map(
667
- (metric) => classScore(row, model.id, metric) ?? '',
668
- ),
669
- ]);
670
- }
671
- }
672
- const content = rows
673
- .map((row) => row.map((value) => `"${String(value).replaceAll('"', '""')}"`).join(','))
674
- .join('\r\n');
675
- const url = URL.createObjectURL(new Blob([content], { type: 'text/csv;charset=utf-8' }));
676
- const link = document.createElement('a');
677
- link.href = url;
678
- link.download = `paddylitex_${currentStudy}_per_class.csv`;
679
- link.click();
680
- setTimeout(() => URL.revokeObjectURL(url), 1000);
681
- }
682
-
683
- function renderSummaryCards() {
684
- const context = benchmarkData.context;
685
- const proposed = benchmarkData.main.find((model) => model.role === 'Proposed');
686
- const baseline = benchmarkData.main.find((model) => model.id === 'yolov8n_densenet121');
687
- getElement('test-images').textContent = formatNumber(context.images);
688
- getElement('test-objects').textContent = formatNumber(context.objects);
689
- getElement('test-classes').textContent = formatNumber(context.classes);
690
-
691
- if (proposed) {
692
- getElement('proposed-map5095').innerHTML = `${proposed.map5095.toFixed(2)}<span>%</span>`;
693
- getElement('proposed-map50').innerHTML = `${proposed.map50.toFixed(2)}<span>%</span>`;
694
- }
695
- if (proposed && baseline) {
696
- const improvements = [
697
- { metric: 'map5095', valueId: 'map-improvement', changeId: 'map5095-change' },
698
- { metric: 'map50', valueId: 'map50-improvement', changeId: 'map50-change' },
699
- ];
700
- for (const { metric, valueId, changeId } of improvements) {
701
- const scoreGain = proposed[metric] - baseline[metric];
702
- getElement(valueId).textContent =
703
- `${scoreGain >= 0 ? '+' : ''}${scoreGain.toFixed(2)}%`;
704
- // C2f x 1 result updates: show signed score differences.
705
- getElement(valueId).style.color = scoreGain < 0 ? '#b5475a' : '';
706
- const scoreLabel = getElement(valueId).nextElementSibling;
707
- if (scoreLabel) {
708
- scoreLabel.textContent = `${performanceMetrics[metric].label} score difference`;
709
- }
710
- getElement(valueId).title =
711
- `Score difference: ${scoreGain.toFixed(2)} percentage points (${proposed[metric].toFixed(2)}% − ${baseline[metric].toFixed(2)}%).`;
712
- getElement(changeId).textContent =
713
- `${baseline[metric].toFixed(2)}% → ${proposed[metric].toFixed(2)}%`;
714
- }
715
- getElement('parameter-reduction').textContent =
716
- `${(((baseline.parameters - proposed.parameters) / baseline.parameters) * 100).toFixed(2)}% fewer`;
717
- getElement('flops-reduction').textContent =
718
- `${(((baseline.gflops - proposed.gflops) / baseline.gflops) * 100).toFixed(2)}% fewer`;
719
- }
720
- }
721
-
722
- function downloadBenchmarks() {
723
- if (!benchmarkData) return;
724
- const rows = [
725
- [
726
- 'Model',
727
- 'Precision (%)',
728
- 'Recall (%)',
729
- 'mAP@0.5 (%)',
730
- 'mAP@0.5:0.95 (%)',
731
- 'Parameters',
732
- 'GFLOPs',
733
- ],
734
- ...benchmarkData[currentStudy].map((model) => [
735
- displayModelName(model),
736
- model.precision,
737
- model.recall,
738
- model.map50,
739
- model.map5095,
740
- model.parameters,
741
- model.gflops,
742
- ]),
743
- ];
744
- const content = rows
745
- .map((row) => row.map((value) => `"${String(value).replaceAll('"', '""')}"`).join(','))
746
- .join('\r\n');
747
- const url = URL.createObjectURL(new Blob([content], { type: 'text/csv;charset=utf-8' }));
748
- const link = document.createElement('a');
749
- link.href = url;
750
- link.download = `paddylitex_${currentStudy}_benchmarks.csv`;
751
- link.click();
752
- setTimeout(() => URL.revokeObjectURL(url), 1000);
753
- }
754
-
755
- // Event listeners
756
- function markSettingsChanged() {
757
- if (latestResults)
758
- getElement('message').textContent = 'Settings changed. Compare again to update predictions.';
759
- }
760
-
761
- document.querySelectorAll('[data-view]').forEach((button) => {
762
- button.addEventListener('click', () => showView(button.dataset.view));
763
- });
764
-
765
- document.querySelectorAll('[data-scope]').forEach((button) => {
766
- button.addEventListener('click', () => {
767
- currentStudy = button.dataset.scope;
768
- document
769
- .querySelectorAll('[data-scope]')
770
- .forEach((item) => item.classList.toggle('selected', item === button));
771
- renderBenchmarks();
772
- });
773
- });
774
-
775
- getElement('metric').addEventListener('change', renderBenchmarks);
776
- getElement('cost-metric').addEventListener('change', renderBenchmarks);
777
- getElement('download-csv').addEventListener('click', downloadBenchmarks);
778
- getElement('class-metric').addEventListener('change', renderClassChart);
779
- getElement('class-order').addEventListener('change', renderClassChart);
780
- getElement('download-class-csv').addEventListener('click', downloadClassMetrics);
781
- getElement('file').addEventListener('change', (event) => selectFile(event.target.files[0]));
782
- getElement('clear').addEventListener('click', clearFile);
783
- getElement('compare').addEventListener('click', compareModels);
784
- getElement('image-size').addEventListener('change', markSettingsChanged);
785
- getElement('confidence').addEventListener('input', () => {
786
- getElement('confidence-value').textContent = Number(getElement('confidence').value).toFixed(2);
787
- markSettingsChanged();
788
- });
789
-
790
- getElement('dropzone').addEventListener('keydown', (event) => {
791
- if ((event.key === 'Enter' || event.key === ' ') && !comparisonRunning) {
792
- event.preventDefault();
793
- getElement('file').click();
794
- }
795
- });
796
-
797
- ['dragenter', 'dragover'].forEach((eventName) => {
798
- getElement('dropzone').addEventListener(eventName, (event) => {
799
- event.preventDefault();
800
- if (!comparisonRunning) getElement('dropzone').classList.add('dragging');
801
- });
802
- });
803
-
804
- ['dragleave', 'drop'].forEach((eventName) => {
805
- getElement('dropzone').addEventListener(eventName, (event) => {
806
- event.preventDefault();
807
- getElement('dropzone').classList.remove('dragging');
808
- });
809
- });
810
-
811
- getElement('dropzone').addEventListener('drop', (event) => {
812
- if (!comparisonRunning) selectFile(event.dataTransfer.files[0]);
813
- });
814
-
815
- async function initialize() {
816
- try {
817
- benchmarkData = await readResponse(await PaddyLiteXBrowser.fetch('/api/benchmarks'));
818
- const status = await readResponse(await PaddyLiteXBrowser.fetch('/api/status'));
819
- const available = status.models.filter((model) => model.checkpoint_present).length;
820
- const statusBadge = getElement('service-status');
821
- // Hide the installed-checkpoint badge without claiming inference was verified.
822
- statusBadge.hidden = status.inference_enabled && available === benchmarkData.main.length;
823
- statusBadge.textContent = !status.inference_enabled
824
- ? 'Inference paused'
825
- : `${available}/${benchmarkData.main.length} checkpoints installed`;
826
-
827
- getElement('model-list').innerHTML = benchmarkData.main
828
- .map(
829
- (model) => `
830
- <div class="model-item">
831
- <span class="model-color" style="background: ${model.color}"></span>
832
- <div>
833
- <strong>${escapeHtml(displayModelName(model))}</strong>
834
- <small>${escapeHtml(model.role)} · ${(model.parameters / 1000000).toFixed(2)} M parameters</small>
835
- </div>
836
- <span class="model-state">${status.models.find((item) => item.id === model.id)?.checkpoint_present ? 'Checkpoint installed' : 'Awaiting checkpoint'}</span>
837
- </div>
838
- `,
839
- )
840
- .join('');
841
-
842
- renderSummaryCards();
843
- showEmptyResults();
844
- renderBenchmarks();
845
- updateControls();
846
- } catch (error) {
847
- getElement('service-status').hidden = false;
848
- getElement('service-status').textContent = 'Service unavailable';
849
- getElement('message').textContent = error.message;
850
- }
851
- }
852
-
853
- // Presentation controls. The illustration is separate from model predictions.
854
- function initializePresentation() {
855
- const hero = getElement('studio-hero');
856
- const replayButton = getElement('replay-intro');
857
- const motionButton = getElement('toggle-motion');
858
- const preference =
859
- typeof matchMedia === 'function'
860
- ? matchMedia('(prefers-reduced-motion: reduce)')
861
- : { matches: false };
862
- let motionPaused = false;
863
-
864
- function updateMotionControls() {
865
- const reduced = preference.matches;
866
- hero.classList.toggle('motion-paused', motionPaused || reduced);
867
- replayButton.disabled = reduced;
868
- motionButton.disabled = reduced;
869
- motionButton.textContent = reduced
870
- ? 'Motion reduced'
871
- : motionPaused
872
- ? 'Play motion'
873
- : 'Pause motion';
874
- motionButton.setAttribute('aria-pressed', String(motionPaused || reduced));
875
- }
876
-
877
- motionButton.addEventListener('click', () => {
878
- motionPaused = !motionPaused;
879
- updateMotionControls();
880
- });
881
-
882
- replayButton.addEventListener('click', () => {
883
- if (preference.matches) return;
884
- motionPaused = false;
885
- updateMotionControls();
886
- hero.classList.remove('intro-playing');
887
- // Restart the entrance after the browser has painted the reset state.
888
- requestAnimationFrame(() => requestAnimationFrame(() => hero.classList.add('intro-playing')));
889
- });
890
-
891
- getElement('start-comparison').addEventListener('click', () => {
892
- getElement('studio-input').scrollIntoView({
893
- behavior: preference.matches ? 'auto' : 'smooth',
894
- block: 'start',
895
- });
896
- getElement('dropzone').focus({ preventScroll: true });
897
- });
898
-
899
- if (typeof IntersectionObserver === 'function') {
900
- const observer = new IntersectionObserver(([entry]) => {
901
- hero.classList.toggle('motion-offscreen', !entry.isIntersecting);
902
- });
903
- observer.observe(hero);
904
- }
905
-
906
- preference.addEventListener?.('change', updateMotionControls);
907
- updateMotionControls();
908
- }
909
-
910
- initializePresentation();
911
-
912
- initialize();
913
- loadClassMetrics();
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ 'use strict';
2
+
3
+ // PaddyLiteX: resolve class results beside the running app.js.
4
+ const paddyClassResultsUrl = new URL('per_class.json', document.currentScript?.src || document.baseURI);
5
+
6
+
7
+ // Application state is shared by the detection and benchmark views.
8
+ const getElement = (id) => document.getElementById(id);
9
+ const formatNumber = (value) => new Intl.NumberFormat('en-US').format(value);
10
+ const performanceMetrics = {
11
+ map5095: { label: 'mAP@0.5:0.95', unit: '%' },
12
+ map50: { label: 'mAP@0.5', unit: '%' },
13
+ precision: { label: 'Precision', unit: '%' },
14
+ recall: { label: 'Recall', unit: '%' },
15
+ };
16
+ const complexityMetrics = {
17
+ parameters: {
18
+ label: 'Parameters',
19
+ axisLabel: 'Parameters (millions)',
20
+ scale: 1000000,
21
+ unit: 'M',
22
+ },
23
+ gflops: { label: 'GFLOPs', axisLabel: 'GFLOPs', scale: 1, unit: 'G' },
24
+ };
25
+
26
+ let benchmarkData;
27
+ let currentStudy = 'main';
28
+ let selectedFile = null;
29
+ let previewUrl = null;
30
+ let comparisonRunning = false;
31
+ let latestResults = null;
32
+ let classData = null;
33
+ let classDataError = '';
34
+ const classMetricLabels = {
35
+ map5095: 'AP@0.5:0.95',
36
+ map50: 'AP@0.5',
37
+ precision: 'Precision',
38
+ recall: 'Recall',
39
+ };
40
+ // Dataset class IDs supplied for this thesis. Keep names independent of scores.
41
+ const paddyClassNames = [
42
+ 'Rice__BacterialLeafBlight',
43
+ 'Rice__BrownSpot',
44
+ 'Rice__Healthy',
45
+ 'Rice__Hispa',
46
+ 'Rice__LeafBlast',
47
+ 'Rice__LeafScald',
48
+ 'Rice__LeafSmut',
49
+ 'Rice__NarrowBrownLeafSpot',
50
+ 'Rice__NeckBlast',
51
+ ];
52
+
53
+ function classRows() {
54
+ const supplied = new Map((classData?.classes || []).map((row) => [row.id, row]));
55
+ return paddyClassNames.map((name, id) => ({
56
+ id,
57
+ name,
58
+ models: supplied.get(id)?.models || {},
59
+ }));
60
+ }
61
+
62
+ function escapeHtml(value) {
63
+ const entities = { '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' };
64
+ return String(value).replace(/[&<>"']/g, (character) => entities[character]);
65
+ }
66
+
67
+ function displayModelName(model) {
68
+ // Older local configurations still use the previous baseline label.
69
+ return model.label.replace(/\bDenseNet121\b/g, 'Yolov8n_DenseNet121');
70
+ }
71
+
72
+ function showView(viewName) {
73
+ document.querySelectorAll('.view').forEach((view) => {
74
+ const active = view.id === viewName;
75
+ view.hidden = !active;
76
+ view.classList.toggle('active', active);
77
+ });
78
+
79
+ document.querySelectorAll('.tab').forEach((button) => {
80
+ const active = button.dataset.view === viewName;
81
+ button.classList.toggle('active', active);
82
+ button.setAttribute('aria-selected', String(active));
83
+ });
84
+ }
85
+
86
+ async function readResponse(response) {
87
+ let payload;
88
+ try {
89
+ payload = await response.json();
90
+ } catch {
91
+ throw new Error('The server returned an unexpected response. Please try again.');
92
+ }
93
+
94
+ if (!response.ok) {
95
+ throw new Error(
96
+ typeof payload.detail === 'string' ? payload.detail : 'The request could not be completed.',
97
+ );
98
+ }
99
+ return payload;
100
+ }
101
+
102
+ // Detection studio
103
+ function resultHeading(model) {
104
+ return `
105
+ <div class="card-head">
106
+ <p class="role">${escapeHtml(model.role)}</p>
107
+ <h3>${escapeHtml(displayModelName(model))}</h3>
108
+ </div>
109
+ `;
110
+ }
111
+
112
+ function showEmptyResults(message = 'Upload an image to compare predictions.') {
113
+ if (!benchmarkData) return;
114
+
115
+ getElement('results').innerHTML = benchmarkData.main
116
+ .map(
117
+ (model) => `
118
+ <article class="result-card" style="--model-color: ${model.color}">
119
+ ${resultHeading(model)}
120
+ <div class="result-stage"><p>${escapeHtml(message)}</p></div>
121
+ <div class="result-body">${escapeHtml(model.id)}</div>
122
+ </article>
123
+ `,
124
+ )
125
+ .join('');
126
+ }
127
+
128
+ function showLoadingResults() {
129
+ getElement('results').innerHTML = benchmarkData.main
130
+ .map(
131
+ (model) => `
132
+ <article class="result-card" style="--model-color: ${model.color}">
133
+ ${resultHeading(model)}
134
+ <div class="result-stage">
135
+ <div>
136
+ <div class="spinner" style="margin: auto"></div>
137
+ <p>Comparing models…</p>
138
+ </div>
139
+ </div>
140
+ <div class="result-body">Identical image and settings</div>
141
+ </article>
142
+ `,
143
+ )
144
+ .join('');
145
+ }
146
+
147
+ function renderResults(results) {
148
+ const classMappings = results
149
+ .filter((result) => result.status === 'success')
150
+ .map((result) => JSON.stringify(Object.entries(result.class_names).sort()));
151
+
152
+ if (new Set(classMappings).size > 1) {
153
+ getElement('message').textContent =
154
+ 'The checkpoints have different class mappings. Verify their dataset configuration before treating this as a matched comparison.';
155
+ }
156
+
157
+ getElement('results').innerHTML = benchmarkData.main
158
+ .map((model) => {
159
+ const result = results.find((item) => item.id === model.id);
160
+ if (!result || result.status !== 'success') {
161
+ const description =
162
+ result?.status === 'unavailable' ? 'Model unavailable' : 'Inference error';
163
+ return `
164
+ <article class="result-card" style="--model-color: ${model.color}">
165
+ ${resultHeading(model)}
166
+ <div class="result-stage">
167
+ <p>${escapeHtml(result?.message || 'No result returned.')}</p>
168
+ </div>
169
+ <div class="result-body">${description}</div>
170
+ </article>
171
+ `;
172
+ }
173
+
174
+ const detections = result.detections;
175
+ const detectionRows = detections.length
176
+ ? detections
177
+ .map(
178
+ (detection) => `
179
+ <div class="detection-row">
180
+ <span>${escapeHtml(detection.class_name)}</span>
181
+ <strong>${(detection.confidence * 100).toFixed(1)}%</strong>
182
+ </div>
183
+ `,
184
+ )
185
+ .join('')
186
+ : '<p>No boxes exceeded the selected threshold. This does not confirm that the leaf is healthy.</p>';
187
+
188
+ return `
189
+ <article class="result-card" style="--model-color: ${model.color}">
190
+ ${resultHeading(model)}
191
+ <div class="result-stage">
192
+ <img src="${escapeHtml(result.image)}" alt="${escapeHtml(displayModelName(model))} annotated detection result">
193
+ </div>
194
+ <div class="result-body">
195
+ <strong>${detections.length} detection${detections.length === 1 ? '' : 's'}</strong>
196
+ ${detectionRows}
197
+ <a class="download-link" href="${escapeHtml(result.image)}" download="${escapeHtml(model.id)}_prediction.png">
198
+ Download annotated image
199
+ </a>
200
+ </div>
201
+ </article>
202
+ `;
203
+ })
204
+ .join('');
205
+ }
206
+
207
+ function updateControls() {
208
+ getElement('compare').disabled = !selectedFile || comparisonRunning || !benchmarkData;
209
+ getElement('compare').textContent = comparisonRunning
210
+ ? 'Comparing models…'
211
+ : 'Compare all three models';
212
+ ['file', 'clear', 'confidence', 'image-size'].forEach((id) => {
213
+ getElement(id).disabled = comparisonRunning;
214
+ });
215
+ }
216
+
217
+ function clearFile() {
218
+ if (comparisonRunning) return;
219
+ if (previewUrl) URL.revokeObjectURL(previewUrl);
220
+
221
+ selectedFile = null;
222
+ previewUrl = null;
223
+ latestResults = null;
224
+ getElement('file').value = '';
225
+ getElement('input-preview').hidden = true;
226
+ getElement('input-preview').removeAttribute('src');
227
+ getElement('upload-hint').hidden = false;
228
+ getElement('clear').hidden = true;
229
+ getElement('filename').textContent = 'No image selected';
230
+ getElement('message').textContent = '';
231
+ showEmptyResults();
232
+ updateControls();
233
+ }
234
+
235
+ // JPG upload compatibility: some files have an alias or no MIME type.
236
+ function isSupportedImageFile(file) {
237
+ if (!file) return false;
238
+
239
+ const imageType = (file.type || '').toLowerCase();
240
+ const supportedTypes = [
241
+ 'image/jpeg',
242
+ 'image/jpg',
243
+ 'image/pjpeg',
244
+ 'image/png',
245
+ 'image/webp',
246
+ ];
247
+
248
+ if (supportedTypes.includes(imageType)) return true;
249
+
250
+ // The existing image decoder still verifies that this is an actual image.
251
+ return (
252
+ ['', 'application/octet-stream'].includes(imageType) &&
253
+ /\.(jpg|jpeg|png|webp)$/i.test(file.name || '')
254
+ );
255
+ }
256
+
257
+ async function selectFile(file) {
258
+ if (comparisonRunning || !file) return;
259
+ getElement('message').textContent = '';
260
+
261
+ if (!isSupportedImageFile(file)) {
262
+ getElement('message').textContent = 'Choose a JPG, JPEG, PNG, or WebP image.';
263
+ return;
264
+ }
265
+ if (file.size > 10 * 1024 * 1024) {
266
+ getElement('message').textContent = 'Image exceeds 10 MB. Choose a smaller file.';
267
+ return;
268
+ }
269
+
270
+ let bitmap;
271
+ try {
272
+ bitmap = await createImageBitmap(file);
273
+ if (bitmap.width * bitmap.height > 12000000) {
274
+ throw new Error('Image exceeds 12 megapixels. Resize it first.');
275
+ }
276
+ } catch (error) {
277
+ getElement('message').textContent = error.message.startsWith('Image exceeds')
278
+ ? error.message
279
+ : 'The file cannot be decoded as an image.';
280
+ return;
281
+ } finally {
282
+ bitmap?.close();
283
+ }
284
+
285
+ if (previewUrl) URL.revokeObjectURL(previewUrl);
286
+ selectedFile = file;
287
+ latestResults = null;
288
+ previewUrl = URL.createObjectURL(file);
289
+ getElement('input-preview').src = previewUrl;
290
+ getElement('input-preview').hidden = false;
291
+ getElement('upload-hint').hidden = true;
292
+ getElement('clear').hidden = false;
293
+ getElement('filename').textContent = `${file.name} · ${(file.size / 1024).toFixed(0)} KB`;
294
+ showEmptyResults();
295
+ updateControls();
296
+ }
297
+
298
+ async function compareModels() {
299
+ if (!selectedFile || comparisonRunning || !benchmarkData) return;
300
+ comparisonRunning = true;
301
+ updateControls();
302
+ showLoadingResults();
303
+ getElement('message').textContent = '';
304
+
305
+ const form = new FormData();
306
+ form.append('file', selectedFile);
307
+ form.append('confidence', getElement('confidence').value);
308
+ form.append('image_size', getElement('image-size').value);
309
+
310
+ try {
311
+ const response = await PaddyLiteXBrowser.fetch('/api/compare', { method: 'POST', body: form });
312
+ latestResults = await readResponse(response);
313
+ renderResults(latestResults.results);
314
+ } catch (error) {
315
+ showEmptyResults('Comparison could not complete. See the message above.');
316
+ getElement('message').textContent = error.message;
317
+ } finally {
318
+ comparisonRunning = false;
319
+ updateControls();
320
+ }
321
+ }
322
+
323
+ // Benchmark charts: both bar charts and the scatter plot use these selectors.
324
+ function formatMetric(value, metric) {
325
+ if (performanceMetrics[metric]) return `${value.toFixed(2)}%`;
326
+ if (metric === 'parameters') return `${(value / 1000000).toFixed(2)} M`;
327
+ return `${value.toFixed(1)} G`;
328
+ }
329
+
330
+ function axisScale(maximum) {
331
+ // Choose round tick values and keep all points inside the plotting area.
332
+ const approximateStep = ((maximum || 1) * 1.08) / 6;
333
+ const magnitude = 10 ** Math.floor(Math.log10(approximateStep));
334
+ const normalizedStep = approximateStep / magnitude;
335
+ const multiplier = [1, 2, 5, 10].find((value) => value >= normalizedStep);
336
+ const step = multiplier * magnitude;
337
+ const limit = Math.ceil(((maximum || 1) * 1.08) / step) * step;
338
+ const ticks = [];
339
+ for (let index = 0; index <= Math.round(limit / step); index += 1) {
340
+ ticks.push(Number((index * step).toPrecision(10)));
341
+ }
342
+ return { limit, ticks };
343
+ }
344
+
345
+ function renderBarChart(containerId, models, metric) {
346
+ const percentage = Boolean(performanceMetrics[metric]);
347
+ const maximum = Math.max(...models.map((model) => model[metric]));
348
+ const limit = percentage ? 100 : axisScale(maximum).limit;
349
+ const axisEnd = percentage ? '100%' : formatMetric(limit, metric);
350
+
351
+ getElement(containerId).innerHTML =
352
+ models
353
+ .map(
354
+ (model) => `
355
+ <div>
356
+ <div class="bar-label">
357
+ <span>${escapeHtml(displayModelName(model))}</span>
358
+ <strong>${formatMetric(model[metric], metric)}</strong>
359
+ </div>
360
+ <div class="bar-track" role="img" aria-label="${escapeHtml(displayModelName(model))}: ${formatMetric(model[metric], metric)}">
361
+ <div class="bar-fill" style="width: ${(model[metric] / limit) * 100}%; background: ${model.color}"></div>
362
+ </div>
363
+ </div>
364
+ `,
365
+ )
366
+ .join('') +
367
+ `
368
+ <div class="axis"><span>0</span><span>${axisEnd}</span></div>
369
+ `;
370
+ }
371
+
372
+ function renderComparisonChart(containerId, models, metric) {
373
+ const quality = containerId === 'quality-chart';
374
+ const type = getElement(quality ? 'quality-chart-type' : 'cost-chart-type').value;
375
+ const percentage = Boolean(performanceMetrics[metric]);
376
+ const limit = percentage ? 100 : axisScale(Math.max(...models.map((model) => model[metric]))).limit;
377
+ if (type === 'bar') {
378
+ renderBarChart(containerId, models, metric);
379
+ } else {
380
+ PaddyLiteXCharts.renderComparison(containerId, models, metric, {
381
+ type, study: currentStudy, limit,
382
+ format: (value) => formatMetric(value, metric),
383
+ });
384
+ }
385
+ if (quality) {
386
+ getElement('quality-chart-note').textContent = ['radar', 'heatmap'].includes(type)
387
+ ? 'All four detection metrics (%) · shared 0–100% scale. The metric selector still controls the scatter plot.'
388
+ : 'Selected detection metric (%) · higher is better · scale starts at zero.';
389
+ }
390
+ }
391
+
392
+ function renderScatterPlot(models, performanceMetric, complexityMetric) {
393
+ const performance = performanceMetrics[performanceMetric];
394
+ const complexity = complexityMetrics[complexityMetric];
395
+ const width = 1000;
396
+ const height = 340;
397
+ const margin = { left: 75, right: 40, top: 30, bottom: 65 };
398
+ const plotWidth = width - margin.left - margin.right;
399
+ const plotHeight = height - margin.top - margin.bottom;
400
+ const horizontalScale = axisScale(
401
+ Math.max(...models.map((model) => model[complexityMetric] / complexity.scale)),
402
+ );
403
+ const x = (value) => margin.left + (value / horizontalScale.limit) * plotWidth;
404
+ const y = (value) => height - margin.bottom - (value / 100) * plotHeight;
405
+
406
+ const horizontalGrid = [0, 20, 40, 60, 80, 100]
407
+ .map(
408
+ (value) => `
409
+ <line x1="${margin.left}" x2="${width - margin.right}" y1="${y(value)}" y2="${y(value)}" stroke="#e7edf4" />
410
+ <text x="${margin.left - 14}" y="${y(value) + 4}" text-anchor="end" fill="#68758a" font-size="13">${value}</text>
411
+ `,
412
+ )
413
+ .join('');
414
+
415
+ const horizontalTicks = horizontalScale.ticks
416
+ .map(
417
+ (value) => `
418
+ <text x="${x(value)}" y="${height - margin.bottom + 23}" text-anchor="middle" fill="#68758a" font-size="13">${value}</text>
419
+ `,
420
+ )
421
+ .join('');
422
+
423
+ const points = models
424
+ .map((model, index) => {
425
+ const pointX = x(model[complexityMetric] / complexity.scale);
426
+ const pointY = y(model[performanceMetric]);
427
+ const tooltip = `${displayModelName(model)}: ${complexity.label} ${formatMetric(model[complexityMetric], complexityMetric)}; ${performance.label} ${formatMetric(model[performanceMetric], performanceMetric)}`;
428
+ const label =
429
+ currentStudy === 'main'
430
+ ? `
431
+ <text x="${pointX}" y="${pointY + (index === 0 ? 25 : -16)}" text-anchor="middle" font-size="13" fill="${model.color}">
432
+ ${model.role === 'Proposed' ? 'Proposed' : escapeHtml(displayModelName(model))}
433
+ </text>
434
+ `
435
+ : '';
436
+
437
+ return `
438
+ <circle data-model-id="${escapeHtml(model.id)}" cx="${pointX}" cy="${pointY}" r="${model.role === 'Proposed' ? 9 : 7}" fill="${model.color}" stroke="white" stroke-width="2">
439
+ <title>${escapeHtml(tooltip)}</title>
440
+ </circle>
441
+ ${label}
442
+ `;
443
+ })
444
+ .join('');
445
+
446
+ getElement('scatter').innerHTML = `
447
+ <svg viewBox="0 0 ${width} ${height}" role="img" aria-label="${complexity.axisLabel} versus ${performance.label} (%)">
448
+ ${horizontalGrid}
449
+ ${horizontalTicks}
450
+ <text data-axis="x" x="${margin.left + plotWidth / 2}" y="${height - 8}" text-anchor="middle" fill="#68758a" font-size="14">${complexity.axisLabel}</text>
451
+ <text data-axis="y" x="18" y="${margin.top + plotHeight / 2}" transform="rotate(-90 18 ${margin.top + plotHeight / 2})" text-anchor="middle" fill="#68758a" font-size="14">${performance.label} (%)</text>
452
+ ${points}
453
+ </svg>
454
+ `;
455
+
456
+ getElement('scatter-metrics').textContent = `${complexity.label} vs. ${performance.label}`;
457
+ getElement('legend').innerHTML = models
458
+ .map(
459
+ (model) => `
460
+ <span><i style="background: ${model.color}"></i>${escapeHtml(displayModelName(model))}</span>
461
+ `,
462
+ )
463
+ .join('');
464
+ }
465
+
466
+ function renderBenchmarks() {
467
+ if (!benchmarkData) return;
468
+ const models = benchmarkData[currentStudy];
469
+ const performanceMetric = getElement('metric').value;
470
+ const complexityMetric = getElement('cost-metric').value;
471
+
472
+ renderComparisonChart('quality-chart', models, performanceMetric);
473
+ renderComparisonChart('cost-chart', models, complexityMetric);
474
+ renderScatterPlot(models, performanceMetric, complexityMetric);
475
+ renderClassChart();
476
+
477
+ getElement('metrics-table').innerHTML = models
478
+ .map(
479
+ (model) => `
480
+ <tr class="${model.role === 'Proposed' ? 'proposed' : ''}">
481
+ <td>${escapeHtml(displayModelName(model))}</td>
482
+ <td>${model.precision.toFixed(2)}%</td>
483
+ <td>${model.recall.toFixed(2)}%</td>
484
+ <td>${model.map50.toFixed(2)}%</td>
485
+ <td>${model.map5095.toFixed(2)}%</td>
486
+ <td>${formatNumber(model.parameters)}</td>
487
+ <td>${model.gflops.toFixed(1)}</td>
488
+ </tr>
489
+ `,
490
+ )
491
+ .join('');
492
+ }
493
+
494
+ // Class metrics come from a separate file so checkpoint paths never need editing.
495
+ function validateClassData(data) {
496
+ if (data?.schema_version !== 1 || data.unit !== 'percent' || !Array.isArray(data.classes)) {
497
+ throw new Error('The class results file has an unsupported format.');
498
+ }
499
+ const ids = new Set();
500
+ for (const row of data.classes) {
501
+ if (
502
+ !Number.isInteger(row.id) ||
503
+ row.id < 0 ||
504
+ row.id >= paddyClassNames.length ||
505
+ ids.has(row.id) ||
506
+ typeof row.name !== 'string' ||
507
+ !row.name.trim() ||
508
+ !row.models ||
509
+ typeof row.models !== 'object' ||
510
+ Array.isArray(row.models)
511
+ ) {
512
+ throw new Error('Check the class IDs, names and model results in the class results file.');
513
+ }
514
+ ids.add(row.id);
515
+ for (const scores of Object.values(row.models)) {
516
+ if (!scores || typeof scores !== 'object' || Array.isArray(scores)) {
517
+ throw new Error('Each model must have a class metric object.');
518
+ }
519
+ for (const metric of Object.keys(classMetricLabels)) {
520
+ const value = scores[metric];
521
+ if (
522
+ value != null &&
523
+ (typeof value !== 'number' || !Number.isFinite(value) || value < 0 || value > 100)
524
+ ) {
525
+ throw new Error('Class metrics must be percentages between 0 and 100, or null.');
526
+ }
527
+ }
528
+ }
529
+ }
530
+ return data;
531
+ }
532
+
533
+ function classScore(row, modelId, metric) {
534
+ const value = row.models[modelId]?.[metric];
535
+ return typeof value === 'number' && Number.isFinite(value) ? value : null;
536
+ }
537
+
538
+ // Ablation studies may have class results for only some of their models.
539
+ function classChartModels() {
540
+ if (!benchmarkData) return [];
541
+ if (currentStudy === 'main') return benchmarkData.main;
542
+ const rows = classRows();
543
+ return benchmarkData.ablation.filter((model) =>
544
+ rows.some((row) =>
545
+ Object.keys(classMetricLabels).some((metric) => classScore(row, model.id, metric) !== null),
546
+ ),
547
+ );
548
+ }
549
+
550
+ function classModelCode(model, index) {
551
+ if (model.role === 'Proposed') return 'P';
552
+ return currentStudy === 'main' ? `B${index + 1}` : `A${index + 1}`;
553
+ }
554
+
555
+ function renderClassChart() {
556
+ if (!benchmarkData) return;
557
+ const panel = getElement('class-panel');
558
+ panel.hidden = false;
559
+ const models = classChartModels();
560
+ const mainStudy = currentStudy === 'main';
561
+ getElement('class-heading').textContent = mainStudy
562
+ ? 'Every class, three perspectives.'
563
+ : 'Class results across available ablations.';
564
+ getElement('class-heading').nextElementSibling.textContent = mainStudy
565
+ ? 'Compare your two baselines and final proposed model for each class.'
566
+ : 'Compare the ablation models with supplied class results and the final proposed model.';
567
+ const metric = getElement('class-metric').value;
568
+ const label = classMetricLabels[metric];
569
+ const rows = classRows();
570
+ const proposed = models.find((model) => model.role === 'Proposed');
571
+ const order = getElement('class-order').value;
572
+ if (order !== 'dataset' && proposed) {
573
+ rows.sort((a, b) => {
574
+ const first = classScore(a, proposed.id, metric);
575
+ const second = classScore(b, proposed.id, metric);
576
+ if (first == null) return second == null ? a.id - b.id : 1;
577
+ if (second == null) return -1;
578
+ return (order === 'lowest' ? first - second : second - first) || a.id - b.id;
579
+ });
580
+ }
581
+
582
+ getElement('class-legend').innerHTML = models
583
+ .map(
584
+ (model) => `
585
+ <span><i style="background: ${model.color}"></i>${escapeHtml(model.role)} · ${escapeHtml(displayModelName(model))}</span>
586
+ `,
587
+ )
588
+ .join('');
589
+ const hasValues = rows.some((row) =>
590
+ models.some((model) =>
591
+ Object.keys(classMetricLabels).some((key) => classScore(row, model.id, key) !== null),
592
+ ),
593
+ );
594
+ getElement('download-class-csv').disabled = !hasValues;
595
+ const split = classData?.context?.split || 'test';
596
+ getElement('class-context').textContent = rows.length
597
+ ? `${split} set · ${rows.length} classes · ${label} (%)`
598
+ : '';
599
+ getElement('class-chart').innerHTML = `
600
+ <div class="class-axis" aria-hidden="true">
601
+ <span>${escapeHtml(label)} (%)</span>
602
+ <div>${[0, 20, 40, 60, 80, 100].map((value) => `<span>${value}%</span>`).join('')}</div>
603
+ </div>
604
+ ${rows
605
+ .map(
606
+ (row) => `
607
+ <div class="class-group" data-class-id="${row.id}">
608
+ <div class="class-name"><strong>${escapeHtml(row.name)}</strong><small>Class ${row.id}</small></div>
609
+ <div class="class-series">
610
+ ${models
611
+ .map((model, index) => {
612
+ const value = classScore(row, model.id, metric);
613
+ const text = value === null ? 'N/A' : `${value.toFixed(2)}%`;
614
+ const caption = `${row.name} · ${displayModelName(model)} · ${label}: ${text}`;
615
+ return `
616
+ <div class="class-bar" data-model-id="${escapeHtml(model.id)}" role="img" aria-label="${escapeHtml(caption)}" title="${escapeHtml(caption)}">
617
+ <span class="class-short" style="color: ${model.color}">${classModelCode(model, index)}</span>
618
+ <div class="class-track"><div class="class-fill" style="width: ${value ?? 0}%; background: ${model.color}" ${value === null ? 'hidden' : ''}></div></div>
619
+ <strong>${text}</strong>
620
+ </div>`;
621
+ })
622
+ .join('')}
623
+ </div>
624
+ </div>`,
625
+ )
626
+ .join('')}`;
627
+ const missingModels = currentStudy === 'ablation'
628
+ ? benchmarkData.ablation.length - models.length
629
+ : 0;
630
+ const coverageNote = missingModels > 0
631
+ ? ` Class results are available for ${models.length} of ${benchmarkData.ablation.length} ablation models. Models without supplied class results are omitted.`
632
+ : '';
633
+ getElement('class-note').textContent =
634
+ classDataError ||
635
+ (hasValues
636
+ ? `${split} set · percent · higher is better · 0–100% scale. N/A means no evaluated value was supplied. AP is reported per class; mAP averages across classes.${coverageNote}`
637
+ : 'All nine disease classes are shown. N/A means their evaluation scores have not been supplied yet.');
638
+ const chartType = getElement('class-chart-type').value;
639
+ if (chartType !== 'bar' && hasValues) {
640
+ PaddyLiteXCharts.renderClasses(models, rows, metric, {
641
+ type: chartType, study: currentStudy, label,
642
+ });
643
+ if (chartType === 'radar') {
644
+ getElement('class-note').textContent += ' Radar axes stay in dataset class order; model names toggle the series.';
645
+ }
646
+ }
647
+ }
648
+
649
+ async function loadClassMetrics() {
650
+ try {
651
+ const url = new URL(paddyClassResultsUrl);
652
+ url.searchParams.set('v', 'baseline1-charts-20261013');
653
+ const response = await fetch(url, { cache: 'no-store' });
654
+ if (!response.ok) {
655
+ throw new Error(`per_class.json could not load (HTTP ${response.status}). Upload it beside app.js.`);
656
+ }
657
+ const raw = await response.text();
658
+ let data;
659
+ try {
660
+ data = JSON.parse(raw);
661
+ } catch {
662
+ throw new Error(raw.trimStart().startsWith('<')
663
+ ? 'per_class.json returned an HTML page. Check its upload location beside app.js.'
664
+ : 'per_class.json is not valid JSON. Replace it with the supplied results file.');
665
+ }
666
+ classData = validateClassData(data);
667
+ classDataError = '';
668
+ } catch (error) {
669
+ classDataError = `Class results could not load. ${error.message}`;
670
+ }
671
+ renderClassChart();
672
+ }
673
+
674
+ function downloadClassMetrics() {
675
+ if (!classData?.classes.length || !benchmarkData) return;
676
+ const rows = [
677
+ [
678
+ 'Class ID',
679
+ 'Class',
680
+ 'Model',
681
+ 'Role',
682
+ 'Precision (%)',
683
+ 'Recall (%)',
684
+ 'AP@0.5 (%)',
685
+ 'AP@0.5:0.95 (%)',
686
+ ],
687
+ ];
688
+ for (const row of classRows()) {
689
+ for (const model of classChartModels()) {
690
+ rows.push([
691
+ row.id,
692
+ row.name,
693
+ displayModelName(model),
694
+ model.role,
695
+ ...['precision', 'recall', 'map50', 'map5095'].map(
696
+ (metric) => classScore(row, model.id, metric) ?? '',
697
+ ),
698
+ ]);
699
+ }
700
+ }
701
+ const content = rows
702
+ .map((row) => row.map((value) => `"${String(value).replaceAll('"', '""')}"`).join(','))
703
+ .join('\r\n');
704
+ const url = URL.createObjectURL(new Blob([content], { type: 'text/csv;charset=utf-8' }));
705
+ const link = document.createElement('a');
706
+ link.href = url;
707
+ link.download = `paddylitex_${currentStudy}_per_class.csv`;
708
+ link.click();
709
+ setTimeout(() => URL.revokeObjectURL(url), 1000);
710
+ }
711
+
712
+ function renderSummaryCards() {
713
+ const context = benchmarkData.context;
714
+ const proposed = benchmarkData.main.find((model) => model.role === 'Proposed');
715
+ const baseline = benchmarkData.main.find((model) => model.id === 'yolov8n_densenet121');
716
+ getElement('test-images').textContent = formatNumber(context.images);
717
+ getElement('test-objects').textContent = formatNumber(context.objects);
718
+ getElement('test-classes').textContent = formatNumber(context.classes);
719
+
720
+ if (proposed) {
721
+ getElement('proposed-map5095').innerHTML = `${proposed.map5095.toFixed(2)}<span>%</span>`;
722
+ getElement('proposed-map50').innerHTML = `${proposed.map50.toFixed(2)}<span>%</span>`;
723
+ }
724
+ if (proposed && baseline) {
725
+ const improvements = [
726
+ { metric: 'map5095', valueId: 'map-improvement', changeId: 'map5095-change' },
727
+ { metric: 'map50', valueId: 'map50-improvement', changeId: 'map50-change' },
728
+ ];
729
+ for (const { metric, valueId, changeId } of improvements) {
730
+ const scoreGain = proposed[metric] - baseline[metric];
731
+ getElement(valueId).textContent =
732
+ `${scoreGain >= 0 ? '+' : ''}${scoreGain.toFixed(2)}%`;
733
+ // C2f x 1 result updates: show signed score differences.
734
+ getElement(valueId).style.color = scoreGain < 0 ? '#b5475a' : '';
735
+ const scoreLabel = getElement(valueId).nextElementSibling;
736
+ if (scoreLabel) {
737
+ scoreLabel.textContent = `${performanceMetrics[metric].label} score difference`;
738
+ }
739
+ getElement(valueId).title =
740
+ `Score difference: ${scoreGain.toFixed(2)} percentage points (${proposed[metric].toFixed(2)}% − ${baseline[metric].toFixed(2)}%).`;
741
+ getElement(changeId).textContent =
742
+ `${baseline[metric].toFixed(2)}% → ${proposed[metric].toFixed(2)}%`;
743
+ }
744
+ getElement('parameter-reduction').textContent =
745
+ `${(((baseline.parameters - proposed.parameters) / baseline.parameters) * 100).toFixed(2)}% fewer`;
746
+ getElement('flops-reduction').textContent =
747
+ `${(((baseline.gflops - proposed.gflops) / baseline.gflops) * 100).toFixed(2)}% fewer`;
748
+ }
749
+ }
750
+
751
+ function downloadBenchmarks() {
752
+ if (!benchmarkData) return;
753
+ const rows = [
754
+ [
755
+ 'Model',
756
+ 'Precision (%)',
757
+ 'Recall (%)',
758
+ 'mAP@0.5 (%)',
759
+ 'mAP@0.5:0.95 (%)',
760
+ 'Parameters',
761
+ 'GFLOPs',
762
+ ],
763
+ ...benchmarkData[currentStudy].map((model) => [
764
+ displayModelName(model),
765
+ model.precision,
766
+ model.recall,
767
+ model.map50,
768
+ model.map5095,
769
+ model.parameters,
770
+ model.gflops,
771
+ ]),
772
+ ];
773
+ const content = rows
774
+ .map((row) => row.map((value) => `"${String(value).replaceAll('"', '""')}"`).join(','))
775
+ .join('\r\n');
776
+ const url = URL.createObjectURL(new Blob([content], { type: 'text/csv;charset=utf-8' }));
777
+ const link = document.createElement('a');
778
+ link.href = url;
779
+ link.download = `paddylitex_${currentStudy}_benchmarks.csv`;
780
+ link.click();
781
+ setTimeout(() => URL.revokeObjectURL(url), 1000);
782
+ }
783
+
784
+ // Event listeners
785
+ function markSettingsChanged() {
786
+ if (latestResults)
787
+ getElement('message').textContent = 'Settings changed. Compare again to update predictions.';
788
+ }
789
+
790
+ document.querySelectorAll('[data-view]').forEach((button) => {
791
+ button.addEventListener('click', () => showView(button.dataset.view));
792
+ });
793
+
794
+ document.querySelectorAll('[data-scope]').forEach((button) => {
795
+ button.addEventListener('click', () => {
796
+ currentStudy = button.dataset.scope;
797
+ document
798
+ .querySelectorAll('[data-scope]')
799
+ .forEach((item) => item.classList.toggle('selected', item === button));
800
+ renderBenchmarks();
801
+ });
802
+ });
803
+
804
+ getElement('metric').addEventListener('change', renderBenchmarks);
805
+ getElement('quality-chart-type').addEventListener('change', renderBenchmarks);
806
+ getElement('cost-chart-type').addEventListener('change', renderBenchmarks);
807
+ getElement('class-chart-type').addEventListener('change', renderClassChart);
808
+ getElement('cost-metric').addEventListener('change', renderBenchmarks);
809
+ getElement('download-csv').addEventListener('click', downloadBenchmarks);
810
+ getElement('class-metric').addEventListener('change', renderClassChart);
811
+ getElement('class-order').addEventListener('change', renderClassChart);
812
+ getElement('download-class-csv').addEventListener('click', downloadClassMetrics);
813
+ getElement('file').addEventListener('change', (event) => selectFile(event.target.files[0]));
814
+ getElement('clear').addEventListener('click', clearFile);
815
+ getElement('compare').addEventListener('click', compareModels);
816
+ getElement('image-size').addEventListener('change', markSettingsChanged);
817
+ getElement('confidence').addEventListener('input', () => {
818
+ getElement('confidence-value').textContent = Number(getElement('confidence').value).toFixed(2);
819
+ markSettingsChanged();
820
+ });
821
+
822
+ getElement('dropzone').addEventListener('keydown', (event) => {
823
+ if ((event.key === 'Enter' || event.key === ' ') && !comparisonRunning) {
824
+ event.preventDefault();
825
+ getElement('file').click();
826
+ }
827
+ });
828
+
829
+ ['dragenter', 'dragover'].forEach((eventName) => {
830
+ getElement('dropzone').addEventListener(eventName, (event) => {
831
+ event.preventDefault();
832
+ if (!comparisonRunning) getElement('dropzone').classList.add('dragging');
833
+ });
834
+ });
835
+
836
+ ['dragleave', 'drop'].forEach((eventName) => {
837
+ getElement('dropzone').addEventListener(eventName, (event) => {
838
+ event.preventDefault();
839
+ getElement('dropzone').classList.remove('dragging');
840
+ });
841
+ });
842
+
843
+ getElement('dropzone').addEventListener('drop', (event) => {
844
+ if (!comparisonRunning) selectFile(event.dataTransfer.files[0]);
845
+ });
846
+
847
+ async function initialize() {
848
+ try {
849
+ benchmarkData = await readResponse(await PaddyLiteXBrowser.fetch('/api/benchmarks'));
850
+ const status = await readResponse(await PaddyLiteXBrowser.fetch('/api/status'));
851
+ const available = status.models.filter((model) => model.checkpoint_present).length;
852
+ const statusBadge = getElement('service-status');
853
+ // Hide the installed-checkpoint badge without claiming inference was verified.
854
+ statusBadge.hidden = status.inference_enabled && available === benchmarkData.main.length;
855
+ statusBadge.textContent = !status.inference_enabled
856
+ ? 'Inference paused'
857
+ : `${available}/${benchmarkData.main.length} checkpoints installed`;
858
+
859
+ getElement('model-list').innerHTML = benchmarkData.main
860
+ .map(
861
+ (model) => `
862
+ <div class="model-item">
863
+ <span class="model-color" style="background: ${model.color}"></span>
864
+ <div>
865
+ <strong>${escapeHtml(displayModelName(model))}</strong>
866
+ <small>${escapeHtml(model.role)} · ${(model.parameters / 1000000).toFixed(2)} M parameters</small>
867
+ </div>
868
+ <span class="model-state">${status.models.find((item) => item.id === model.id)?.checkpoint_present ? 'Checkpoint installed' : 'Awaiting checkpoint'}</span>
869
+ </div>
870
+ `,
871
+ )
872
+ .join('');
873
+
874
+ renderSummaryCards();
875
+ showEmptyResults();
876
+ renderBenchmarks();
877
+ updateControls();
878
+ } catch (error) {
879
+ getElement('service-status').hidden = false;
880
+ getElement('service-status').textContent = 'Service unavailable';
881
+ getElement('message').textContent = error.message;
882
+ }
883
+ }
884
+
885
+ // Presentation controls. The illustration is separate from model predictions.
886
+ function initializePresentation() {
887
+ const hero = getElement('studio-hero');
888
+ const replayButton = getElement('replay-intro');
889
+ const motionButton = getElement('toggle-motion');
890
+ const preference =
891
+ typeof matchMedia === 'function'
892
+ ? matchMedia('(prefers-reduced-motion: reduce)')
893
+ : { matches: false };
894
+ let motionPaused = false;
895
+
896
+ function updateMotionControls() {
897
+ const reduced = preference.matches;
898
+ hero.classList.toggle('motion-paused', motionPaused || reduced);
899
+ replayButton.disabled = reduced;
900
+ motionButton.disabled = reduced;
901
+ motionButton.textContent = reduced
902
+ ? 'Motion reduced'
903
+ : motionPaused
904
+ ? 'Play motion'
905
+ : 'Pause motion';
906
+ motionButton.setAttribute('aria-pressed', String(motionPaused || reduced));
907
+ }
908
+
909
+ motionButton.addEventListener('click', () => {
910
+ motionPaused = !motionPaused;
911
+ updateMotionControls();
912
+ });
913
+
914
+ replayButton.addEventListener('click', () => {
915
+ if (preference.matches) return;
916
+ motionPaused = false;
917
+ updateMotionControls();
918
+ hero.classList.remove('intro-playing');
919
+ // Restart the entrance after the browser has painted the reset state.
920
+ requestAnimationFrame(() => requestAnimationFrame(() => hero.classList.add('intro-playing')));
921
+ });
922
+
923
+ getElement('start-comparison').addEventListener('click', () => {
924
+ getElement('studio-input').scrollIntoView({
925
+ behavior: preference.matches ? 'auto' : 'smooth',
926
+ block: 'start',
927
+ });
928
+ getElement('dropzone').focus({ preventScroll: true });
929
+ });
930
+
931
+ if (typeof IntersectionObserver === 'function') {
932
+ const observer = new IntersectionObserver(([entry]) => {
933
+ hero.classList.toggle('motion-offscreen', !entry.isIntersecting);
934
+ });
935
+ observer.observe(hero);
936
+ }
937
+
938
+ preference.addEventListener?.('change', updateMotionControls);
939
+ updateMotionControls();
940
+ }
941
+
942
+ initializePresentation();
943
+
944
+ initialize();
945
+ loadClassMetrics();
browser-models.json CHANGED
@@ -1,81 +1,96 @@
1
- {
2
- "ready": true,
3
- "runtime": "onnxruntime-web@1.22.0/wasm",
4
- "models": [
5
- {
6
- "id": "yolov8n",
7
- "label": "YOLOv8n",
8
- "names": [
9
- "Rice__BacterialLeafBlight",
10
- "Rice__BrownSpot",
11
- "Rice__Healthy",
12
- "Rice__Hispa",
13
- "Rice__LeafBlast",
14
- "Rice__LeafScald",
15
- "Rice__LeafSmut",
16
- "Rice__NarrowBrownLeafSpot",
17
- "Rice__NeckBlast"
18
- ],
19
- "files": {
20
- "320": "models/yolov8n_320.onnx",
21
- "512": "models/yolov8n_512.onnx",
22
- "640": "models/yolov8n_640.onnx"
23
- }
24
- },
25
- {
26
- "id": "yolov8n_densenet121",
27
- "label": "Yolov8n_DenseNet121",
28
- "names": [
29
- "Rice__BacterialLeafBlight",
30
- "Rice__BrownSpot",
31
- "Rice__Healthy",
32
- "Rice__Hispa",
33
- "Rice__LeafBlast",
34
- "Rice__LeafScald",
35
- "Rice__LeafSmut",
36
- "Rice__NarrowBrownLeafSpot",
37
- "Rice__NeckBlast"
38
- ],
39
- "files": {
40
- "320": "models/yolov8n_densenet121_320.onnx",
41
- "512": "models/yolov8n_densenet121_512.onnx",
42
- "640": "models/yolov8n_densenet121_640.onnx"
43
- },
44
- "checkpoint_sha256": "defcf4ff895e72763baa71005139515c84b52ead8f1b4819cd793fd94154ea42",
45
- "evaluation_snapshot": "c2f1-results-20261012-batch2",
46
- "onnx_sha256": {
47
- "320": "9408e8a513d2fd1f7bbeeda36101a0182c74ee54f5ea7eb63087ab0e36456356",
48
- "512": "17a2d6a492747dd75033e60803ac7c703f7d9029e50320ef0839a1151ae256a0",
49
- "640": "eb48addb5a3109e2029a001998075bedc8cd05b6783d37be648f9b8506e613a0"
50
- }
51
- },
52
- {
53
- "id": "yolov8n_denselitex_sppf_c2psa",
54
- "label": "DenseLiteX + SPPF + C2PSA",
55
- "names": [
56
- "Rice__BacterialLeafBlight",
57
- "Rice__BrownSpot",
58
- "Rice__Healthy",
59
- "Rice__Hispa",
60
- "Rice__LeafBlast",
61
- "Rice__LeafScald",
62
- "Rice__LeafSmut",
63
- "Rice__NarrowBrownLeafSpot",
64
- "Rice__NeckBlast"
65
- ],
66
- "files": {
67
- "320": "models/yolov8n_denselitex_sppf_c2psa_320.onnx",
68
- "512": "models/yolov8n_denselitex_sppf_c2psa_512.onnx",
69
- "640": "models/yolov8n_denselitex_sppf_c2psa_640.onnx"
70
- },
71
- "checkpoint_sha256": "049cd63cc4ffa06c15d6e728f581bffb2b4c24f97cf0917642102aae79cc2aae",
72
- "evaluation_snapshot": "c2f1-results-20261012-batch2",
73
- "onnx_sha256": {
74
- "320": "9278ce196a3f37cc2cbc106dc4be986758972c9cd8fefdde335bcf76d74c62e8",
75
- "512": "4ef8bba2c9a07f4abda4b720dc2c91b59ffd3c455f1ee14ef7377dda3760a55a",
76
- "640": "1f8e6be3aa7fb319ac251c364887eb4963bb7aa005ced158ad67ec6064b7539f"
77
- }
78
- }
79
- ],
80
- "model_file_layout": "clean-size-names-v1"
81
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "ready": true,
3
+ "runtime": "onnxruntime-web@1.22.0/wasm",
4
+ "models": [
5
+ {
6
+ "id": "yolov8n",
7
+ "label": "YOLOv8n",
8
+ "names": [
9
+ "Rice__BacterialLeafBlight",
10
+ "Rice__BrownSpot",
11
+ "Rice__Healthy",
12
+ "Rice__Hispa",
13
+ "Rice__LeafBlast",
14
+ "Rice__LeafScald",
15
+ "Rice__LeafSmut",
16
+ "Rice__NarrowBrownLeafSpot",
17
+ "Rice__NeckBlast"
18
+ ],
19
+ "files": {
20
+ "320": "models/yolov8n_320.onnx",
21
+ "512": "models/yolov8n_512.onnx",
22
+ "640": "models/yolov8n_640.onnx"
23
+ },
24
+ "checkpoint_sha256": "1df03d8b994367b4795cd0e26b819cb0971000f40d2c6609a4d1077c573e0b83",
25
+ "onnx_sha256": {
26
+ "320": "e1deb016b84198dcb274c382443cb0796f9d2dee09e503965b768c5ad1880b98",
27
+ "512": "f1d486a6002a8cf93d2109f342116b3b4d8d26b7ac0198647cda62a0dc54191b",
28
+ "640": "6ccd67015f90e6255e6ad1547260c0cda60ca733605997194c3799ea2bea8cc8"
29
+ },
30
+ "evaluation_snapshot": "baseline1-charts-20261013",
31
+ "export_settings": {
32
+ "format": "onnx",
33
+ "opset": 17,
34
+ "batch": 1,
35
+ "dynamic": false,
36
+ "half": false,
37
+ "nms": false
38
+ }
39
+ },
40
+ {
41
+ "id": "yolov8n_densenet121",
42
+ "label": "Yolov8n_DenseNet121",
43
+ "names": [
44
+ "Rice__BacterialLeafBlight",
45
+ "Rice__BrownSpot",
46
+ "Rice__Healthy",
47
+ "Rice__Hispa",
48
+ "Rice__LeafBlast",
49
+ "Rice__LeafScald",
50
+ "Rice__LeafSmut",
51
+ "Rice__NarrowBrownLeafSpot",
52
+ "Rice__NeckBlast"
53
+ ],
54
+ "files": {
55
+ "320": "models/yolov8n_densenet121_320.onnx",
56
+ "512": "models/yolov8n_densenet121_512.onnx",
57
+ "640": "models/yolov8n_densenet121_640.onnx"
58
+ },
59
+ "checkpoint_sha256": "defcf4ff895e72763baa71005139515c84b52ead8f1b4819cd793fd94154ea42",
60
+ "evaluation_snapshot": "c2f1-results-20261012-batch2",
61
+ "onnx_sha256": {
62
+ "320": "9408e8a513d2fd1f7bbeeda36101a0182c74ee54f5ea7eb63087ab0e36456356",
63
+ "512": "17a2d6a492747dd75033e60803ac7c703f7d9029e50320ef0839a1151ae256a0",
64
+ "640": "eb48addb5a3109e2029a001998075bedc8cd05b6783d37be648f9b8506e613a0"
65
+ }
66
+ },
67
+ {
68
+ "id": "yolov8n_denselitex_sppf_c2psa",
69
+ "label": "DenseLiteX + SPPF + C2PSA",
70
+ "names": [
71
+ "Rice__BacterialLeafBlight",
72
+ "Rice__BrownSpot",
73
+ "Rice__Healthy",
74
+ "Rice__Hispa",
75
+ "Rice__LeafBlast",
76
+ "Rice__LeafScald",
77
+ "Rice__LeafSmut",
78
+ "Rice__NarrowBrownLeafSpot",
79
+ "Rice__NeckBlast"
80
+ ],
81
+ "files": {
82
+ "320": "models/yolov8n_denselitex_sppf_c2psa_320.onnx",
83
+ "512": "models/yolov8n_denselitex_sppf_c2psa_512.onnx",
84
+ "640": "models/yolov8n_denselitex_sppf_c2psa_640.onnx"
85
+ },
86
+ "checkpoint_sha256": "049cd63cc4ffa06c15d6e728f581bffb2b4c24f97cf0917642102aae79cc2aae",
87
+ "evaluation_snapshot": "c2f1-results-20261012-batch2",
88
+ "onnx_sha256": {
89
+ "320": "9278ce196a3f37cc2cbc106dc4be986758972c9cd8fefdde335bcf76d74c62e8",
90
+ "512": "4ef8bba2c9a07f4abda4b720dc2c91b59ffd3c455f1ee14ef7377dda3760a55a",
91
+ "640": "1f8e6be3aa7fb319ac251c364887eb4963bb7aa005ced158ad67ec6064b7539f"
92
+ }
93
+ }
94
+ ],
95
+ "model_file_layout": "clean-size-names-v1"
96
+ }
browser-runtime.js CHANGED
@@ -1,185 +1,185 @@
1
- /* Local adapter for the existing UI. There are no Python or Render requests. */
2
- (function () {
3
- 'use strict';
4
- const root = new URL('./', document.currentScript.src);
5
- const nativeFetch = window.fetch.bind(window);
6
- let manifestPromise;
7
- let worker;
8
- let workerFailure;
9
- let sequence = 0;
10
- let busy = false;
11
- const pending = new Map();
12
- const manifest = () => manifestPromise ??= nativeFetch(new URL('browser-models.json', root), { cache: 'no-store' })
13
- .then(response => { if (!response.ok) throw new Error('Browser model manifest is missing.'); return response.json(); });
14
- const reply = (data, status = 200) => new Response(JSON.stringify(data), {
15
- status, headers: { 'Content-Type': 'application/json' },
16
- });
17
-
18
- function getWorker() {
19
- if (workerFailure) throw new Error(workerFailure);
20
- if (worker) return worker;
21
- worker = new Worker(new URL('detection-worker.js?v=clean-model-names-20261012', root));
22
- worker.onmessage = ({ data }) => {
23
- const task = pending.get(data.requestId);
24
- if (!task) return;
25
- if (data.stage) {
26
- const message = document.getElementById('message');
27
- if (message) message.textContent = data.stage;
28
- return;
29
- }
30
- pending.delete(data.requestId);
31
- if (data.error) task.reject(new Error(data.error));
32
- else task.resolve(data.detections);
33
- };
34
- worker.onerror = () => {
35
- workerFailure = 'Browser runtime could not start. Check that the vendor runtime files were uploaded.';
36
- for (const task of pending.values()) task.reject(new Error(workerFailure));
37
- pending.clear();
38
- worker.terminate();
39
- worker = null;
40
- };
41
- return worker;
42
- }
43
-
44
- function runModel(model, size, input, transform, confidence) {
45
- const detector = getWorker();
46
- const requestId = ++sequence;
47
- return new Promise((resolve, reject) => {
48
- pending.set(requestId, { resolve, reject });
49
- // Transfer a separate tensor for each model; the original stays reusable.
50
- const copy = input.slice();
51
- detector.postMessage({ requestId, model, size, input: copy.buffer, transform, confidence }, [copy.buffer]);
52
- });
53
- }
54
-
55
- function prepareImage(image, size) {
56
- const ratio = Math.min(size / image.width, size / image.height);
57
- const resizedWidth = Math.round(image.width * ratio);
58
- const resizedHeight = Math.round(image.height * ratio);
59
- const left = Math.round((size - resizedWidth) / 2 - 0.1);
60
- const top = Math.round((size - resizedHeight) / 2 - 0.1);
61
- const canvas = document.createElement('canvas');
62
- canvas.width = canvas.height = size;
63
- const ctx = canvas.getContext('2d', { willReadFrequently: true });
64
- ctx.fillStyle = 'rgb(114, 114, 114)';
65
- ctx.fillRect(0, 0, size, size);
66
- ctx.drawImage(image, left, top, resizedWidth, resizedHeight);
67
- const pixels = ctx.getImageData(0, 0, size, size).data;
68
- const plane = size * size;
69
- const input = new Float32Array(3 * plane);
70
- for (let pixel = 0; pixel < plane; pixel += 1) {
71
- for (let channel = 0; channel < 3; channel += 1) {
72
- input[channel * plane + pixel] = pixels[pixel * 4 + channel] / 255;
73
- }
74
- }
75
- return { input, transform: { ratio, left, top, width: image.width, height: image.height } };
76
- }
77
-
78
- function annotate(image, detections) {
79
- const canvas = document.createElement('canvas');
80
- canvas.width = image.width;
81
- canvas.height = image.height;
82
- const ctx = canvas.getContext('2d');
83
- ctx.drawImage(image, 0, 0);
84
- const fontSize = Math.max(12, Math.round(Math.max(image.width, image.height) / 45));
85
- ctx.font = '600 ' + fontSize + 'px system-ui, sans-serif';
86
- ctx.lineWidth = Math.max(2, Math.round(fontSize / 6));
87
- const colors = ['#ff4848', '#ee9635', '#2aab65', '#2a98dd', '#8861d6', '#dc5697', '#b58530', '#278d84', '#cc6144'];
88
- for (const detection of detections) {
89
- const [x1, y1, x2, y2] = detection.box;
90
- const color = colors[detection.class_id % colors.length];
91
- ctx.strokeStyle = color;
92
- ctx.strokeRect(x1, y1, x2 - x1, y2 - y1);
93
- const label = detection.class_name + ' ' + (detection.confidence * 100).toFixed(1) + '%';
94
- const labelWidth = Math.min(canvas.width, ctx.measureText(label).width + 10);
95
- const labelX = Math.max(0, Math.min(x1, canvas.width - labelWidth));
96
- const labelY = Math.max(0, y1 - fontSize - 8);
97
- ctx.fillStyle = color;
98
- ctx.fillRect(labelX, labelY, labelWidth, fontSize + 8);
99
- ctx.fillStyle = '#ffffff';
100
- ctx.fillText(label, labelX + 5, labelY + fontSize + 1, labelWidth - 10);
101
- }
102
- return canvas.toDataURL('image/png');
103
- }
104
-
105
- // JPG upload compatibility: some files have an alias or no MIME type.
106
- function isSupportedImageFile(file) {
107
- if (!file) return false;
108
-
109
- const imageType = (file.type || '').toLowerCase();
110
- const supportedTypes = [
111
- 'image/jpeg',
112
- 'image/jpg',
113
- 'image/pjpeg',
114
- 'image/png',
115
- 'image/webp',
116
- ];
117
-
118
- if (supportedTypes.includes(imageType)) return true;
119
-
120
- // The existing image decoder still verifies that this is an actual image.
121
- return (
122
- ['', 'application/octet-stream'].includes(imageType) &&
123
- /\.(jpg|jpeg|png|webp)$/i.test(file.name || '')
124
- );
125
- }
126
-
127
- async function compare(form) {
128
- if (busy) return reply({ detail: 'A comparison is already running on this device.' }, 429);
129
- busy = true;
130
- let image;
131
- try {
132
- const models = await manifest();
133
- if (!models.ready || models.models.length !== 3) {
134
- return reply({ detail: models.message || 'Export and upload the three ONNX models first.' }, 503);
135
- }
136
- const file = form.get('file');
137
- const confidence = Number(form.get('confidence'));
138
- const size = Number(form.get('image_size'));
139
- if (!isSupportedImageFile(file)) {
140
- throw new Error('Choose a JPG, JPEG, PNG, or WebP image.');
141
- }
142
- if (file.size > 10 * 1024 * 1024) throw new Error('Image exceeds 10 MB.');
143
- if (![320, 512, 640].includes(size) || !Number.isFinite(confidence) || confidence < 0.05 || confidence > 0.95) throw new Error('Invalid detection settings.');
144
- image = await createImageBitmap(file);
145
- if (image.width * image.height > 12000000) throw new Error('Image exceeds 12 megapixels.');
146
- const { input, transform } = prepareImage(image, size);
147
- const results = [];
148
- for (const model of models.models) {
149
- try {
150
- if (!model.files[String(size)]) throw new Error('This input size has not been exported.');
151
- const detections = await runModel(model, size, input, transform, confidence);
152
- results.push({ id: model.id, status: 'success', detections,
153
- image: annotate(image, detections),
154
- class_names: Object.fromEntries(model.names.map((name, id) => [String(id), name])) });
155
- } catch (error) {
156
- results.push({ id: model.id, status: 'error', message: error.message });
157
- }
158
- }
159
- document.getElementById('message').textContent = results.some(result => result.status === 'error')
160
- ? 'Some browser models could not run. See the model cards for details.'
161
- : 'Comparison completed on your device.';
162
- return reply({ results, image_size: [image.width, image.height], settings: { confidence, imgsz: size, iou: 0.7 } });
163
- } catch (error) {
164
- return reply({ detail: error.message }, 400);
165
- } finally {
166
- image?.close();
167
- busy = false;
168
- }
169
- }
170
-
171
- window.PaddyLiteXBrowser = {
172
- async fetch(url, options = {}) {
173
- if (url === '/api/benchmarks') return nativeFetch(new URL('results.json', root), { cache: 'no-store' });
174
- if (url === '/api/status') {
175
- const models = await manifest();
176
- const benchmarks = await (await nativeFetch(new URL('results.json', root), { cache: 'no-store' })).json();
177
- return reply({ inference_enabled: Boolean(models.ready), models: benchmarks.main.map(model => ({
178
- id: model.id, checkpoint_present: Boolean(models.ready && models.models.some(item => item.id === model.id)),
179
- })) });
180
- }
181
- if (url === '/api/compare') return compare(options.body);
182
- throw new Error('Unsupported browser API route: ' + url);
183
- },
184
- };
185
- })();
 
1
+ /* Local adapter for the existing UI. There are no Python or Render requests. */
2
+ (function () {
3
+ 'use strict';
4
+ const root = new URL('./', document.currentScript.src);
5
+ const nativeFetch = window.fetch.bind(window);
6
+ let manifestPromise;
7
+ let worker;
8
+ let workerFailure;
9
+ let sequence = 0;
10
+ let busy = false;
11
+ const pending = new Map();
12
+ const manifest = () => manifestPromise ??= nativeFetch(new URL('browser-models.json', root), { cache: 'no-store' })
13
+ .then(response => { if (!response.ok) throw new Error('Browser model manifest is missing.'); return response.json(); });
14
+ const reply = (data, status = 200) => new Response(JSON.stringify(data), {
15
+ status, headers: { 'Content-Type': 'application/json' },
16
+ });
17
+
18
+ function getWorker() {
19
+ if (workerFailure) throw new Error(workerFailure);
20
+ if (worker) return worker;
21
+ worker = new Worker(new URL('detection-worker.js?v=baseline1-charts-20261013', root));
22
+ worker.onmessage = ({ data }) => {
23
+ const task = pending.get(data.requestId);
24
+ if (!task) return;
25
+ if (data.stage) {
26
+ const message = document.getElementById('message');
27
+ if (message) message.textContent = data.stage;
28
+ return;
29
+ }
30
+ pending.delete(data.requestId);
31
+ if (data.error) task.reject(new Error(data.error));
32
+ else task.resolve(data.detections);
33
+ };
34
+ worker.onerror = () => {
35
+ workerFailure = 'Browser runtime could not start. Check that the vendor runtime files were uploaded.';
36
+ for (const task of pending.values()) task.reject(new Error(workerFailure));
37
+ pending.clear();
38
+ worker.terminate();
39
+ worker = null;
40
+ };
41
+ return worker;
42
+ }
43
+
44
+ function runModel(model, size, input, transform, confidence) {
45
+ const detector = getWorker();
46
+ const requestId = ++sequence;
47
+ return new Promise((resolve, reject) => {
48
+ pending.set(requestId, { resolve, reject });
49
+ // Transfer a separate tensor for each model; the original stays reusable.
50
+ const copy = input.slice();
51
+ detector.postMessage({ requestId, model, size, input: copy.buffer, transform, confidence }, [copy.buffer]);
52
+ });
53
+ }
54
+
55
+ function prepareImage(image, size) {
56
+ const ratio = Math.min(size / image.width, size / image.height);
57
+ const resizedWidth = Math.round(image.width * ratio);
58
+ const resizedHeight = Math.round(image.height * ratio);
59
+ const left = Math.round((size - resizedWidth) / 2 - 0.1);
60
+ const top = Math.round((size - resizedHeight) / 2 - 0.1);
61
+ const canvas = document.createElement('canvas');
62
+ canvas.width = canvas.height = size;
63
+ const ctx = canvas.getContext('2d', { willReadFrequently: true });
64
+ ctx.fillStyle = 'rgb(114, 114, 114)';
65
+ ctx.fillRect(0, 0, size, size);
66
+ ctx.drawImage(image, left, top, resizedWidth, resizedHeight);
67
+ const pixels = ctx.getImageData(0, 0, size, size).data;
68
+ const plane = size * size;
69
+ const input = new Float32Array(3 * plane);
70
+ for (let pixel = 0; pixel < plane; pixel += 1) {
71
+ for (let channel = 0; channel < 3; channel += 1) {
72
+ input[channel * plane + pixel] = pixels[pixel * 4 + channel] / 255;
73
+ }
74
+ }
75
+ return { input, transform: { ratio, left, top, width: image.width, height: image.height } };
76
+ }
77
+
78
+ function annotate(image, detections) {
79
+ const canvas = document.createElement('canvas');
80
+ canvas.width = image.width;
81
+ canvas.height = image.height;
82
+ const ctx = canvas.getContext('2d');
83
+ ctx.drawImage(image, 0, 0);
84
+ const fontSize = Math.max(12, Math.round(Math.max(image.width, image.height) / 45));
85
+ ctx.font = '600 ' + fontSize + 'px system-ui, sans-serif';
86
+ ctx.lineWidth = Math.max(2, Math.round(fontSize / 6));
87
+ const colors = ['#ff4848', '#ee9635', '#2aab65', '#2a98dd', '#8861d6', '#dc5697', '#b58530', '#278d84', '#cc6144'];
88
+ for (const detection of detections) {
89
+ const [x1, y1, x2, y2] = detection.box;
90
+ const color = colors[detection.class_id % colors.length];
91
+ ctx.strokeStyle = color;
92
+ ctx.strokeRect(x1, y1, x2 - x1, y2 - y1);
93
+ const label = detection.class_name + ' ' + (detection.confidence * 100).toFixed(1) + '%';
94
+ const labelWidth = Math.min(canvas.width, ctx.measureText(label).width + 10);
95
+ const labelX = Math.max(0, Math.min(x1, canvas.width - labelWidth));
96
+ const labelY = Math.max(0, y1 - fontSize - 8);
97
+ ctx.fillStyle = color;
98
+ ctx.fillRect(labelX, labelY, labelWidth, fontSize + 8);
99
+ ctx.fillStyle = '#ffffff';
100
+ ctx.fillText(label, labelX + 5, labelY + fontSize + 1, labelWidth - 10);
101
+ }
102
+ return canvas.toDataURL('image/png');
103
+ }
104
+
105
+ // JPG upload compatibility: some files have an alias or no MIME type.
106
+ function isSupportedImageFile(file) {
107
+ if (!file) return false;
108
+
109
+ const imageType = (file.type || '').toLowerCase();
110
+ const supportedTypes = [
111
+ 'image/jpeg',
112
+ 'image/jpg',
113
+ 'image/pjpeg',
114
+ 'image/png',
115
+ 'image/webp',
116
+ ];
117
+
118
+ if (supportedTypes.includes(imageType)) return true;
119
+
120
+ // The existing image decoder still verifies that this is an actual image.
121
+ return (
122
+ ['', 'application/octet-stream'].includes(imageType) &&
123
+ /\.(jpg|jpeg|png|webp)$/i.test(file.name || '')
124
+ );
125
+ }
126
+
127
+ async function compare(form) {
128
+ if (busy) return reply({ detail: 'A comparison is already running on this device.' }, 429);
129
+ busy = true;
130
+ let image;
131
+ try {
132
+ const models = await manifest();
133
+ if (!models.ready || models.models.length !== 3) {
134
+ return reply({ detail: models.message || 'Export and upload the three ONNX models first.' }, 503);
135
+ }
136
+ const file = form.get('file');
137
+ const confidence = Number(form.get('confidence'));
138
+ const size = Number(form.get('image_size'));
139
+ if (!isSupportedImageFile(file)) {
140
+ throw new Error('Choose a JPG, JPEG, PNG, or WebP image.');
141
+ }
142
+ if (file.size > 10 * 1024 * 1024) throw new Error('Image exceeds 10 MB.');
143
+ if (![320, 512, 640].includes(size) || !Number.isFinite(confidence) || confidence < 0.05 || confidence > 0.95) throw new Error('Invalid detection settings.');
144
+ image = await createImageBitmap(file);
145
+ if (image.width * image.height > 12000000) throw new Error('Image exceeds 12 megapixels.');
146
+ const { input, transform } = prepareImage(image, size);
147
+ const results = [];
148
+ for (const model of models.models) {
149
+ try {
150
+ if (!model.files[String(size)]) throw new Error('This input size has not been exported.');
151
+ const detections = await runModel(model, size, input, transform, confidence);
152
+ results.push({ id: model.id, status: 'success', detections,
153
+ image: annotate(image, detections),
154
+ class_names: Object.fromEntries(model.names.map((name, id) => [String(id), name])) });
155
+ } catch (error) {
156
+ results.push({ id: model.id, status: 'error', message: error.message });
157
+ }
158
+ }
159
+ document.getElementById('message').textContent = results.some(result => result.status === 'error')
160
+ ? 'Some browser models could not run. See the model cards for details.'
161
+ : 'Comparison completed on your device.';
162
+ return reply({ results, image_size: [image.width, image.height], settings: { confidence, imgsz: size, iou: 0.7 } });
163
+ } catch (error) {
164
+ return reply({ detail: error.message }, 400);
165
+ } finally {
166
+ image?.close();
167
+ busy = false;
168
+ }
169
+ }
170
+
171
+ window.PaddyLiteXBrowser = {
172
+ async fetch(url, options = {}) {
173
+ if (url === '/api/benchmarks') return nativeFetch(new URL('results.json', root), { cache: 'no-store' });
174
+ if (url === '/api/status') {
175
+ const models = await manifest();
176
+ const benchmarks = await (await nativeFetch(new URL('results.json', root), { cache: 'no-store' })).json();
177
+ return reply({ inference_enabled: Boolean(models.ready), models: benchmarks.main.map(model => ({
178
+ id: model.id, checkpoint_present: Boolean(models.ready && models.models.some(item => item.id === model.id)),
179
+ })) });
180
+ }
181
+ if (url === '/api/compare') return compare(options.body);
182
+ throw new Error('Unsupported browser API route: ' + url);
183
+ },
184
+ };
185
+ })();
charts.css ADDED
@@ -0,0 +1,241 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /* Chart controls and alternative views; existing website styles remain intact. */
2
+ .chart-grid {
3
+ grid-template-columns: repeat(2, minmax(0, 1fr));
4
+ }
5
+
6
+ .chart-grid > .panel,
7
+ #quality-chart,
8
+ #cost-chart,
9
+ #class-chart {
10
+ min-width: 0;
11
+ }
12
+
13
+ .chart-grid .panel-heading {
14
+ flex-wrap: wrap;
15
+ align-items: flex-start;
16
+ }
17
+
18
+ .chart-dot-row .bar-label span {
19
+ min-width: 0;
20
+ overflow-wrap: anywhere;
21
+ }
22
+
23
+ .chart-dot-row .bar-label strong {
24
+ flex-shrink: 0;
25
+ }
26
+
27
+ .chart-options {
28
+ display: flex;
29
+ flex-wrap: wrap;
30
+ justify-content: flex-end;
31
+ align-items: flex-end;
32
+ gap: 12px;
33
+ }
34
+
35
+ .chart-options label {
36
+ display: grid;
37
+ gap: 5px;
38
+ font-size: 11px;
39
+ font-weight: 600;
40
+ color: #627487;
41
+ }
42
+
43
+ .chart-options select {
44
+ max-width: 180px;
45
+ }
46
+
47
+ .chart-radar {
48
+ width: 100%;
49
+ min-width: 0;
50
+ }
51
+
52
+ .chart-radar svg {
53
+ display: block;
54
+ width: 100%;
55
+ max-height: 590px;
56
+ }
57
+
58
+ .chart-series-controls {
59
+ display: flex;
60
+ flex-wrap: wrap;
61
+ gap: 8px;
62
+ padding-top: 8px;
63
+ }
64
+
65
+ .chart-series-toggle {
66
+ display: inline-flex;
67
+ align-items: center;
68
+ gap: 7px;
69
+ padding: 8px 10px;
70
+ border: 1px solid #d7e3e9;
71
+ border-radius: 12px;
72
+ background: #f4f9fb;
73
+ color: #344d61;
74
+ font: inherit;
75
+ font-size: 11px;
76
+ cursor: pointer;
77
+ }
78
+
79
+ .chart-series-toggle i {
80
+ width: 9px;
81
+ height: 9px;
82
+ border-radius: 50%;
83
+ flex: 0 0 auto;
84
+ }
85
+
86
+ .chart-series-toggle[aria-pressed="false"] {
87
+ opacity: 0.45;
88
+ text-decoration: line-through;
89
+ }
90
+
91
+ .chart-series-toggle:focus-visible {
92
+ outline: 3px solid #8fc7f0;
93
+ outline-offset: 2px;
94
+ }
95
+
96
+ .chart-tip {
97
+ margin: 10px 0 0;
98
+ color: #627487;
99
+ font-size: 11px;
100
+ line-height: 1.6;
101
+ }
102
+
103
+ .chart-table-scroll {
104
+ max-width: 100%;
105
+ overflow-x: auto;
106
+ border: 1px solid #dce6ec;
107
+ border-radius: 14px;
108
+ }
109
+
110
+ table.score-heatmap {
111
+ width: 100%;
112
+ min-width: 560px;
113
+ border-collapse: separate;
114
+ border-spacing: 3px;
115
+ table-layout: auto;
116
+ }
117
+
118
+ .score-heatmap caption {
119
+ padding: 10px;
120
+ text-align: left;
121
+ font-size: 12px;
122
+ color: #627487;
123
+ }
124
+
125
+ .score-heatmap th,
126
+ .score-heatmap td {
127
+ padding: 12px 10px;
128
+ border: 0;
129
+ border-radius: 7px;
130
+ font-size: 12px;
131
+ line-height: 1.5;
132
+ }
133
+
134
+ .score-heatmap thead th {
135
+ min-width: 105px;
136
+ max-width: 180px;
137
+ white-space: normal;
138
+ overflow-wrap: normal;
139
+ background: #eef5f8;
140
+ color: #344d61;
141
+ text-align: center;
142
+ }
143
+
144
+ .score-heatmap tbody th {
145
+ position: sticky;
146
+ left: 0;
147
+ z-index: 1;
148
+ min-width: 190px;
149
+ max-width: 260px;
150
+ white-space: normal;
151
+ overflow-wrap: anywhere;
152
+ background: #f3f8fa;
153
+ color: #344d61;
154
+ text-align: left;
155
+ }
156
+
157
+ .score-heatmap td {
158
+ font-weight: 700;
159
+ text-align: center;
160
+ font-variant-numeric: tabular-nums;
161
+ }
162
+
163
+ .heatmap-key {
164
+ display: flex;
165
+ justify-content: flex-end;
166
+ align-items: center;
167
+ gap: 8px;
168
+ margin-top: 12px;
169
+ font-size: 11px;
170
+ color: #627487;
171
+ }
172
+
173
+ .heatmap-key i {
174
+ width: 120px;
175
+ height: 9px;
176
+ border-radius: 8px;
177
+ background: linear-gradient(to right, #f5f8fc, #1b718f);
178
+ }
179
+
180
+ .chart-dot-row {
181
+ padding: 10px 0;
182
+ }
183
+
184
+ .chart-dot-track {
185
+ position: relative;
186
+ height: 20px;
187
+ margin: 9px 7px 0;
188
+ border-bottom: 1px solid #d9e5ec;
189
+ background: repeating-linear-gradient(to right, #dce8ef 0 1px, transparent 1px 20%);
190
+ }
191
+
192
+ .chart-dot {
193
+ position: absolute;
194
+ top: 3px;
195
+ width: 13px;
196
+ height: 13px;
197
+ border-radius: 50%;
198
+ transform: translateX(-50%);
199
+ box-shadow: 0 0 0 3px #fff;
200
+ }
201
+
202
+ .class-dot-group {
203
+ padding: 20px 0;
204
+ border-bottom: 1px solid #dce6ec;
205
+ }
206
+
207
+ .class-dot-group h3 {
208
+ font-size: 14px;
209
+ color: #344d61;
210
+ overflow-wrap: anywhere;
211
+ }
212
+
213
+ @media (max-width: 1050px) {
214
+ .chart-grid {
215
+ grid-template-columns: minmax(0, 1fr);
216
+ }
217
+ }
218
+
219
+ @media (max-width: 700px) {
220
+ .chart-options {
221
+ width: 100%;
222
+ justify-content: flex-start;
223
+ }
224
+
225
+ .chart-options label {
226
+ flex: 1;
227
+ }
228
+
229
+ .chart-options select {
230
+ max-width: 100%;
231
+ width: 100%;
232
+ }
233
+
234
+ .chart-radar {
235
+ overflow-x: auto;
236
+ }
237
+
238
+ .chart-radar svg {
239
+ min-width: 650px;
240
+ }
241
+ }
charts.js ADDED
@@ -0,0 +1,193 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /* Additional chart views for the existing PaddyLiteX benchmark interface. */
2
+ (function () {
3
+ 'use strict';
4
+
5
+ const metricNames = {
6
+ precision: 'Precision',
7
+ recall: 'Recall',
8
+ map50: 'mAP@0.5',
9
+ map5095: 'mAP@0.5:0.95',
10
+ };
11
+ const hiddenSeries = new Map();
12
+ const $ = (id) => document.getElementById(id);
13
+ const escape = (value) => String(value).replace(/[&<>"']/g, (character) => ({
14
+ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;',
15
+ }[character]));
16
+ const hasScore = (value) => typeof value === 'number' && Number.isFinite(value);
17
+ const percent = (value) => hasScore(value) ? `${value.toFixed(2)}%` : 'N/A';
18
+ const classLabel = (name) => name.replace(/^Rice__/, '').replace(/([a-z])([A-Z])/g, '$1 $2');
19
+ const modelName = (model) => model.label.replace(/\bDenseNet121\b/g, 'Yolov8n_DenseNet121');
20
+
21
+ function legend(models, key, redraw) {
22
+ if (!hiddenSeries.has(key)) hiddenSeries.set(key, new Set());
23
+ const hidden = hiddenSeries.get(key);
24
+ const markup = models.map((model) => `
25
+ <button type="button" class="chart-series-toggle" data-series="${escape(model.id)}"
26
+ aria-pressed="${!hidden.has(model.id)}" title="Show or hide ${escape(modelName(model))}">
27
+ <i style="background:${model.color}"></i>${escape(modelName(model))}
28
+ </button>`).join('');
29
+ return { hidden, markup, bind(container) {
30
+ container.querySelectorAll('[data-series]').forEach((button) => {
31
+ button.addEventListener('click', () => {
32
+ const id = button.dataset.series;
33
+ if (hidden.has(id)) hidden.delete(id);
34
+ else hidden.add(id);
35
+ redraw();
36
+ });
37
+ });
38
+ } };
39
+ }
40
+
41
+ function radar(axes, models, getScore, key, redraw) {
42
+ const width = 900;
43
+ const height = 590;
44
+ const center = { x: 450, y: 285 };
45
+ const radius = 195;
46
+ const point = (index, score, padding = 0) => {
47
+ const angle = -Math.PI / 2 + index * 2 * Math.PI / axes.length;
48
+ const distance = radius * score / 100 + padding;
49
+ return { x: center.x + Math.cos(angle) * distance,
50
+ y: center.y + Math.sin(angle) * distance };
51
+ };
52
+ const points = (score) => axes.map((_, index) => {
53
+ const p = point(index, score);
54
+ return `${p.x},${p.y}`;
55
+ }).join(' ');
56
+ const controls = legend(models, key, redraw);
57
+ const rings = [20, 40, 60, 80, 100].map((score) => `
58
+ <polygon points="${points(score)}" fill="none" stroke="#d6e3ea" />
59
+ <text x="${center.x + 7}" y="${center.y - radius * score / 100 + 14}"
60
+ fill="#617587" font-size="12">${score}%</text>`).join('');
61
+ const spokes = axes.map((axis, index) => {
62
+ const edge = point(index, 100);
63
+ const label = point(index, 100, 29);
64
+ const anchor = Math.abs(label.x - center.x) < 15 ? 'middle'
65
+ : label.x < center.x ? 'end' : 'start';
66
+ const words = axis.label.split(' ');
67
+ const lines = words.length > 3
68
+ ? [words.slice(0, Math.ceil(words.length / 2)).join(' '), words.slice(Math.ceil(words.length / 2)).join(' ')]
69
+ : [axis.label];
70
+ return `
71
+ <line x1="${center.x}" y1="${center.y}" x2="${edge.x}" y2="${edge.y}" stroke="#d6e3ea" />
72
+ <text x="${label.x}" y="${label.y}" text-anchor="${anchor}" fill="#344d61" font-size="14">
73
+ ${lines.map((line, i) => `<tspan x="${label.x}" dy="${i ? 17 : 0}">${i ? ' ' : ''}${escape(line)}</tspan>`).join('')}
74
+ </text>`;
75
+ }).join('');
76
+ let incomplete = 0;
77
+ const polygons = models.map((model, modelIndex) => {
78
+ if (controls.hidden.has(model.id)) return '';
79
+ const scores = axes.map((axis) => getScore(model, axis));
80
+ if (!scores.every(hasScore)) { incomplete += 1; return ''; }
81
+ const vertices = scores.map((score, i) => point(i, score));
82
+ const dash = ['', '7 3', '2 3', '10 3 2 3'][modelIndex % 4];
83
+ const description = `${modelName(model)}: ${axes.map((axis, i) => `${axis.label} ${percent(scores[i])}`).join('; ')}`;
84
+ return `
85
+ <polygon data-model-id="${escape(model.id)}" points="${vertices.map((p) => `${p.x},${p.y}`).join(' ')}"
86
+ fill="${model.color}" fill-opacity="${models.length > 4 ? 0.025 : 0.055}"
87
+ stroke="${model.color}" stroke-width="2.5" stroke-dasharray="${dash}">
88
+ <title>${escape(description)}</title>
89
+ </polygon>
90
+ ${vertices.map((p, i) => `<circle cx="${p.x}" cy="${p.y}" r="4" fill="${model.color}" stroke="white" stroke-width="1.5"><title>${escape(modelName(model))} · ${escape(axes[i].label)}: ${percent(scores[i])}</title></circle>`).join('')}`;
91
+ }).join('');
92
+ return { controls, markup: `
93
+ <div class="chart-radar">
94
+ <svg viewBox="0 0 ${width} ${height}" role="img" aria-label="Radar chart, percentage scores on a shared zero to one hundred scale">
95
+ ${rings}${spokes}${polygons}
96
+ </svg>
97
+ </div>
98
+ <div class="chart-series-controls">${controls.markup}</div>
99
+ <p class="chart-tip">Select model names to show or hide their series. All axes use 0–100%.${incomplete ? ' Models with missing scores are omitted from the radar.' : ''}</p>` };
100
+ }
101
+
102
+ function scoreColor(value) {
103
+ if (!hasScore(value)) return '#edf1f5';
104
+ // A fixed 0–100% scale keeps heatmaps comparable across metrics and studies.
105
+ const ratio = Math.max(0, Math.min(100, value)) / 100;
106
+ const start = [245, 248, 252];
107
+ const end = [27, 113, 143];
108
+ return `rgb(${start.map((channel, i) => Math.round(channel + (end[i] - channel) * ratio)).join(',')})`;
109
+ }
110
+
111
+ function heatmap(rowLabels, columnLabels, getScore, caption) {
112
+ return `
113
+ <div class="chart-table-scroll">
114
+ <table class="score-heatmap">
115
+ <caption>${escape(caption)}</caption>
116
+ <thead><tr><th scope="col">${escape(rowLabels.heading)}</th>
117
+ ${columnLabels.map((column) => `<th scope="col">${escape(column.label).replace(/_/g, '_<wbr>')}</th>`).join('')}
118
+ </tr></thead>
119
+ <tbody>${rowLabels.items.map((row) => `
120
+ <tr><th scope="row">${escape(row.label)}</th>
121
+ ${columnLabels.map((column) => {
122
+ const value = getScore(row, column);
123
+ return `<td style="background:${scoreColor(value)};color:${hasScore(value) && value >= 65 ? '#fff' : '#304859'}"
124
+ title="${escape(row.label)} · ${escape(column.label)}: ${percent(value)}">${percent(value)}</td>`;
125
+ }).join('')}
126
+ </tr>`).join('')}
127
+ </tbody>
128
+ </table>
129
+ </div>
130
+ <div class="heatmap-key"><span>0%</span><i></i><span>100%</span></div>`;
131
+ }
132
+
133
+ function dotRows(models, getScore, format, limit) {
134
+ return models.map((model) => {
135
+ const value = getScore(model);
136
+ const label = hasScore(value) ? format(value) : 'N/A';
137
+ const position = hasScore(value) ? Math.max(0, Math.min(100, value / limit * 100)) : 0;
138
+ return `<div class="chart-dot-row" data-model-id="${escape(model.id)}">
139
+ <div class="bar-label"><span>${escape(modelName(model))}</span><strong>${label}</strong></div>
140
+ <div class="chart-dot-track" role="img" aria-label="${escape(modelName(model))}: ${label}">
141
+ ${hasScore(value) ? `<i class="chart-dot" style="left:${position}%;background:${model.color}"></i>` : ''}
142
+ </div>
143
+ </div>`;
144
+ }).join('');
145
+ }
146
+
147
+ function renderComparison(containerId, models, metric, options) {
148
+ const container = $(containerId);
149
+ const redraw = () => renderComparison(containerId, models, metric, options);
150
+ if (options.type === 'radar') {
151
+ const axes = Object.entries(metricNames).map(([id, label]) => ({ id, label }));
152
+ const chart = radar(axes, models, (model, axis) => model[axis.id], `quality-${options.study}`, redraw);
153
+ container.innerHTML = chart.markup;
154
+ chart.controls.bind(container);
155
+ } else if (options.type === 'heatmap') {
156
+ container.innerHTML = heatmap(
157
+ { heading: 'Model', items: models.map((model) => ({ id: model.id, label: modelName(model), model })) },
158
+ Object.entries(metricNames).map(([id, label]) => ({ id, label })),
159
+ (row, column) => row.model[column.id], 'Detection performance across all four metrics (%)',
160
+ );
161
+ } else {
162
+ container.innerHTML = dotRows(models, (model) => model[metric], options.format, options.limit)
163
+ + `<div class="axis"><span>0</span><span>${options.format(options.limit)}</span></div>`;
164
+ }
165
+ }
166
+
167
+ function renderClasses(models, rows, metric, options) {
168
+ const container = $('class-chart');
169
+ const getScore = (row, model) => row.models[model.id]?.[metric];
170
+ if (options.type === 'radar') {
171
+ // Keep class axes in dataset order, even if bar/heatmap sorting was changed.
172
+ const axes = [...rows].sort((a, b) => a.id - b.id)
173
+ .map((row) => ({ id: row.id, label: `${row.id} · ${classLabel(row.name)}`, row }));
174
+ const redraw = () => renderClasses(models, rows, metric, options);
175
+ const chart = radar(axes, models, (model, axis) => getScore(axis.row, model), `classes-${options.study}`, redraw);
176
+ container.innerHTML = chart.markup;
177
+ chart.controls.bind(container);
178
+ } else if (options.type === 'heatmap') {
179
+ container.innerHTML = heatmap(
180
+ { heading: 'Disease class', items: rows.map((row) => ({ label: `${row.id} · ${row.name}`, row })) },
181
+ models.map((model) => ({ label: modelName(model), model })),
182
+ (row, column) => getScore(row.row, column.model), `${options.label} by disease class (%)`,
183
+ );
184
+ } else {
185
+ container.innerHTML = rows.map((row) => `<section class="class-dot-group" data-class-id="${row.id}">
186
+ <h3>${row.id} · ${escape(row.name)}</h3>
187
+ ${dotRows(models, (model) => getScore(row, model), percent, 100)}
188
+ </section>`).join('') + '<div class="axis"><span>0%</span><span>100%</span></div>';
189
+ }
190
+ }
191
+
192
+ window.PaddyLiteXCharts = { renderComparison, renderClasses };
193
+ })();
index.html CHANGED
@@ -1,459 +1,496 @@
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" />
6
- <meta
7
- name="description"
8
- content="Paddy disease detection: compare YOLOv8n, Yolov8n_DenseNet121 and DenseLiteX predictions and research benchmarks."
9
- />
10
- <title>PaddyLiteX · Thesis II</title>
11
- <link rel="icon" href="./favicon.svg" />
12
- <link rel="stylesheet" href="./style.css" />
13
- <script src="./browser-runtime.js?v=clean-model-names-20261012" defer></script>
14
- <script src="./app.js?v=hf-root-sync-20261012" defer></script>
15
- </head>
16
- <body>
17
- <header class="topbar">
18
- <a class="brand" href="/" aria-label="PaddyLiteX home">
19
- <span class="brandmark"><img src="./favicon.svg" alt="" aria-hidden="true" /></span>
20
- <span>
21
- PaddyLiteX
22
- <small>THESIS II</small>
23
- </span>
24
- </a>
25
- <nav class="tabs" aria-label="Main views">
26
- <button class="tab active" data-view="workspace" aria-selected="true">
27
- Detection studio
28
- </button>
29
- <button class="tab" data-view="benchmarks" aria-selected="false">Model benchmarks</button>
30
- </nav>
31
- <span class="research">XMUM · AI Engineering</span>
32
- </header>
33
- <main>
34
- <section id="workspace" class="view active">
35
- <section class="studio-hero intro-playing" id="studio-hero" aria-labelledby="hero-title">
36
- <div class="hero-glow hero-glow-mint" aria-hidden="true"></div>
37
- <div class="hero-glow hero-glow-blue" aria-hidden="true"></div>
38
- <div class="hero-copy">
39
- <p class="hero-eyebrow">
40
- <span></span>
41
- COMPUTER VISION · PADDY DISEASE DETECTION
42
- </p>
43
- <h1 id="hero-title">
44
- A closer look.
45
- <br />
46
- <span>A clearer comparison.</span>
47
- </h1>
48
- <p class="hero-description">
49
- From a paddy image to three model perspectives. Explore disease detections and the
50
- balance between model performance and complexity.
51
- </p>
52
- <div class="hero-actions">
53
- <button class="hero-primary" id="start-comparison">
54
- Start comparison
55
- <span aria-hidden="true">↗</span>
56
- </button>
57
- <button class="hero-secondary" data-view="benchmarks">Explore benchmarks</button>
58
- </div>
59
- <div class="hero-facts" aria-label="Demonstration workflow">
60
- <span>
61
- <b>01</b>
62
- Upload an image
63
- </span>
64
- <span>
65
- <b>02</b>
66
- Compare three models
67
- </span>
68
- <span>
69
- <b>03</b>
70
- Explore the results
71
- </span>
72
- </div>
73
- </div>
74
- <div class="hero-art" aria-label="Animated illustration of a paddy leaf">
75
- <div class="leaf-orbit orbit-one" aria-hidden="true"></div>
76
- <div class="leaf-orbit orbit-two" aria-hidden="true"></div>
77
- <div class="leaf-visual">
78
- <div class="leaf-visual-header">
79
- <span class="visual-dot"></span>
80
- PADDY LEAF STUDY
81
- <span>01 / 03</span>
82
- </div>
83
- <svg class="paddy-illustration" viewBox="0 0 360 360" aria-hidden="true">
84
- <defs>
85
- <linearGradient id="blade-main" x1="0" y1="1" x2="1" y2="0">
86
- <stop offset="0" stop-color="#176c51" />
87
- <stop offset="0.5" stop-color="#73ca73" />
88
- <stop offset="1" stop-color="#d5eea2" />
89
- </linearGradient>
90
- <linearGradient id="blade-side" x1="0" y1="1" x2="1" y2="0">
91
- <stop offset="0" stop-color="#277e61" />
92
- <stop offset="1" stop-color="#a0ddb7" />
93
- </linearGradient>
94
- </defs>
95
- <g fill="none" stroke="#a1d9cb" stroke-opacity="0.18">
96
- <circle cx="180" cy="180" r="130" />
97
- <circle cx="180" cy="180" r="95" stroke-dasharray="3 9" />
98
- <path d="M180 22v316M22 180h316" />
99
- </g>
100
- <path
101
- d="M133 303C132 202 182 109 287 35c-12 106-51 212-154 268Z"
102
- fill="url(#blade-main)"
103
- />
104
- <path
105
- d="M139 297C180 192 232 105 278 49"
106
- fill="none"
107
- stroke="#e5f3b0"
108
- stroke-width="3"
109
- />
110
- <g fill="none" stroke="#cff0ad" stroke-opacity="0.55" stroke-width="1.4">
111
- <path d="m160 246 51-11m-36-23 51-12m-34-19 50-14m-33-17 43-17m-30-8 36-21" />
112
- <path d="m160 246-7-49m22 15-3-48m20 17 1-42m16 11 5-38m8 13 10-31" />
113
- </g>
114
- <path
115
- d="M132 303C85 254 61 179 64 92c58 65 97 136 68 211Z"
116
- fill="url(#blade-side)"
117
- />
118
- <path
119
- d="M133 304C107 222 82 153 67 105"
120
- fill="none"
121
- stroke="#c4edc8"
122
- stroke-width="2"
123
- />
124
- <path
125
- d="M124 331c11-27 19-54 23-76"
126
- fill="none"
127
- stroke="#d5efa9"
128
- stroke-width="4"
129
- stroke-linecap="round"
130
- />
131
- <g fill="none" stroke="#adebcf" stroke-width="2" stroke-linecap="round">
132
- <path d="M103 125v-18h18M257 125v-18h-18M103 256v18h18M257 256v18h-18" />
133
- </g>
134
- </svg>
135
- <div class="leaf-scan" aria-hidden="true"></div>
136
- <div class="leaf-visual-footer">
137
- <span>Leaf illustration</span>
138
- <span>Research visualization</span>
139
- </div>
140
- </div>
141
- <div class="floating-tag tag-quality">
142
- <span class="tag-icon">◈</span>
143
- <div>
144
- <small>THE QUESTION</small>
145
- <strong>How well does it detect?</strong>
146
- </div>
147
- </div>
148
- <div class="floating-tag tag-complexity">
149
- <span class="tag-icon">◎</span>
150
- <div>
151
- <small>THE TRADE-OFF</small>
152
- <strong>How much does it cost?</strong>
153
- </div>
154
- </div>
155
- </div>
156
- <div class="hero-bottom">
157
- <span class="hero-signature">
158
- PaddyLiteX
159
- <span>RESEARCH IN ACTION</span>
160
- </span>
161
- <div class="hero-motion-controls">
162
- <button id="replay-intro">
163
- <span aria-hidden="true">↻</span>
164
- Replay intro
165
- </button>
166
- <button id="toggle-motion" aria-pressed="false">Pause motion</button>
167
- </div>
168
- </div>
169
- </section>
170
-
171
- <div class="heading">
172
- <div>
173
- <p class="eyebrow">PADDY DISEASE DETECTION</p>
174
- <h2 class="studio-title">Detection studio</h2>
175
- <p class="muted">Compare your baselines and proposed model on the same image.</p>
176
- </div>
177
- <span class="pill" id="service-status">Checking model availability</span>
178
- </div>
179
- <div class="studio-grid">
180
- <section class="panel upload-panel" id="studio-input">
181
- <div class="panel-heading">
182
- <h2>Input image</h2>
183
- <span class="micro">JPG / JPEG · PNG · WebP</span>
184
- </div>
185
- <label class="dropzone" id="dropzone" tabindex="0">
186
- <input id="file" type="file" accept=".jpg,.jpeg,.png,.webp,image/jpeg,image/png,image/webp" hidden />
187
- <span id="upload-hint">
188
- <span class="upload-icon" aria-hidden="true">+</span>
189
- <strong>Choose a paddy image</strong>
190
- <span>or drag it here</span>
191
- <small>Up to 10 MB · 12 megapixels</small>
192
- </span>
193
- <img id="input-preview" alt="Selected paddy image" hidden />
194
- </label>
195
- <div class="fileline">
196
- <span id="filename">No image selected</span>
197
- <button id="clear" class="text-button" hidden>Clear</button>
198
- </div>
199
- <!-- Dataset source -->
200
- <aside class="dataset-source" aria-labelledby="dataset-source-title">
201
- <span class="dataset-source-icon" aria-hidden="true">
202
- <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7">
203
- <path d="M20 4c0 9-4 14-10 14a6 6 0 0 1-6-6C4 6 11 4 20 4Z" />
204
- <path d="M4 20 15 9" />
205
- </svg>
206
- </span>
207
- <div class="dataset-source-copy">
208
- <h3 id="dataset-source-title">Try a dataset image</h3>
209
- <p>Download a rice leaf image from Kaggle, then upload it here to compare the models.</p>
210
- <a
211
- href="https://www.kaggle.com/datasets/yusufmurtaza01/rice-leaf-diseases"
212
- target="_blank"
213
- rel="noopener noreferrer"
214
- >
215
- RICE Leaf Diseases · Kaggle
216
- <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" aria-hidden="true">
217
- <path d="M14 4h6v6M20 4 10 14M10 4H5a1 1 0 0 0-1 1v14a1 1 0 0 0 1 1h14a1 1 0 0 0 1-1v-5" />
218
- </svg>
219
- <span class="dataset-source-sr">(opens in a new tab)</span>
220
- </a>
221
- </div>
222
- </aside>
223
- <!-- End dataset source -->
224
- <div class="settings">
225
- <div>
226
- <label for="confidence">
227
- Confidence threshold
228
- <output id="confidence-value">0.25</output>
229
- </label>
230
- <input
231
- type="range"
232
- id="confidence"
233
- min="0.05"
234
- max="0.95"
235
- step="0.05"
236
- value="0.25"
237
- />
238
- </div>
239
- <div>
240
- <label for="image-size">Model input size</label>
241
- <select id="image-size">
242
- <option value="320">320 x 320</option>
243
- <option value="512" selected>512 x 512</option>
244
- <option value="640">640 x 640</option>
245
- </select>
246
- </div>
247
- </div>
248
- <button class="primary" id="compare" disabled>Compare all three models</button>
249
- <p class="privacy">Images are processed on your device and are not uploaded to a detection server.</p>
250
- <p class="message" id="message" role="status" aria-live="polite"></p>
251
- </section>
252
- <section class="panel guidance">
253
- <div>
254
- <p class="eyebrow">CONTROLLED COMPARISON</p>
255
- <h2>
256
- The same input.
257
- <br />
258
- The same settings.
259
- </h2>
260
- <p>
261
- Each model receives the same image, confidence threshold and input size. Explore
262
- bounding boxes, predicted classes and confidence side by side.
263
- </p>
264
- </div>
265
- <div class="model-list" id="model-list"></div>
266
- <div class="note">
267
- <strong>Prediction confidence ≠ accuracy</strong>
268
- <p>
269
- An uploaded image has no ground-truth annotation. Dataset-level performance is
270
- available in Model benchmarks.
271
- </p>
272
- </div>
273
- </section>
274
- </div>
275
- <div class="section-heading">
276
- <div>
277
- <h2>Detection results</h2>
278
- <p class="muted">Real model predictions appear here after comparison.</p>
279
- </div>
280
- <span class="micro">P3 / P4 / P5 · 9 classes</span>
281
- </div>
282
- <div class="result-grid" id="results"></div>
283
- </section>
284
- <section id="benchmarks" class="view" hidden>
285
- <div class="heading">
286
- <div>
287
- <p class="eyebrow">RECORDED EXPERIMENTAL RESULTS</p>
288
- <h1>Performance, with perspective.</h1>
289
- <p class="muted">
290
- Detection quality and computational complexity across the evaluated models.
291
- </p>
292
- </div>
293
- <div class="segmented">
294
- <button data-scope="main" class="selected">Main comparison</button>
295
- <button data-scope="ablation">Ablation study</button>
296
- </div>
297
- </div>
298
- <div class="stat-grid">
299
- <div class="stat">
300
- <small>Test images</small>
301
- <strong id="test-images">803</strong>
302
- </div>
303
- <div class="stat">
304
- <small>Annotated objects</small>
305
- <strong id="test-objects">2,466</strong>
306
- </div>
307
- <div class="stat">
308
- <small>Detection classes</small>
309
- <strong id="test-classes">9</strong>
310
- </div>
311
- <div class="stat">
312
- <small>Proposed mAP@0.5:0.95</small>
313
- <strong class="green" id="proposed-map5095">
314
- 55.83
315
- <span>%</span>
316
- </strong>
317
- </div>
318
- <div class="stat">
319
- <small>Proposed mAP@0.5</small>
320
- <strong class="green" id="proposed-map50">
321
- 81.67
322
- <span>%</span>
323
- </strong>
324
- </div>
325
- </div>
326
- <div class="chart-grid">
327
- <section class="panel">
328
- <div class="panel-heading">
329
- <div>
330
- <p class="eyebrow">DETECTION PERFORMANCE</p>
331
- <h2>Quality across models</h2>
332
- </div>
333
- <select id="metric" aria-label="Detection metric">
334
- <option value="map5095">mAP@0.5:0.95</option>
335
- <option value="map50">mAP@0.5</option>
336
- <option value="precision">Precision</option>
337
- <option value="recall">Recall</option>
338
- </select>
339
- </div>
340
- <div id="quality-chart" class="bars"></div>
341
- <p class="chart-note">Percent · higher is better · axis starts at zero</p>
342
- </section>
343
- <section class="panel">
344
- <div class="panel-heading">
345
- <div>
346
- <p class="eyebrow">COMPUTATIONAL COMPLEXITY</p>
347
- <h2>Cost across models</h2>
348
- </div>
349
- <select id="cost-metric" aria-label="Complexity metric">
350
- <option value="parameters">Parameters</option>
351
- <option value="gflops">GFLOPs</option>
352
- </select>
353
- </div>
354
- <div id="cost-chart" class="bars"></div>
355
- <p class="chart-note">Lower is lighter · axis starts at zero</p>
356
- </section>
357
- </div>
358
- <section class="panel scatter-panel">
359
- <div class="panel-heading">
360
- <div>
361
- <p class="eyebrow">PERFORMANCE-COMPLEXITY TRADE-OFF</p>
362
- <h2>Where each model stands</h2>
363
- </div>
364
- <span class="micro" id="scatter-metrics">Parameters vs. mAP@0.5:0.95</span>
365
- </div>
366
- <div id="scatter"></div>
367
- <div id="legend" class="legend"></div>
368
- </section>
369
- <div class="insight-grid">
370
- <div class="insight">
371
- <small>Against Yolov8n_DenseNet121 baseline</small>
372
- <strong id="map-improvement">-0.25%</strong>
373
- <p>mAP@0.5:0.95 score difference</p>
374
- <p class="insight-values" id="map5095-change">56.08% → 55.83%</p>
375
- </div>
376
- <div class="insight">
377
- <small>Against Yolov8n_DenseNet121 baseline</small>
378
- <strong id="map50-improvement">+0.98%</strong>
379
- <p>mAP@0.5 score difference</p>
380
- <p class="insight-values" id="map50-change">80.69% → 81.67%</p>
381
- </div>
382
- <div class="insight">
383
- <small>Against Yolov8n_DenseNet121 baseline</small>
384
- <strong id="parameter-reduction">68.01% fewer</strong>
385
- <p>parameters</p>
386
- </div>
387
- <div class="insight">
388
- <small>Against Yolov8n_DenseNet121 baseline</small>
389
- <strong id="flops-reduction">72.59% fewer</strong>
390
- <p>reported GFLOPs</p>
391
- </div>
392
- </div>
393
- <section class="panel class-panel" id="class-panel" aria-labelledby="class-heading">
394
- <div class="panel-heading">
395
- <div>
396
- <p class="eyebrow">CLASS-LEVEL DETECTION PERFORMANCE</p>
397
- <h2 id="class-heading">Every class, three perspectives.</h2>
398
- <p class="muted">
399
- Compare your two baselines and final proposed model for each class.
400
- </p>
401
- </div>
402
- <button class="secondary" id="download-class-csv" disabled>Export class CSV</button>
403
- </div>
404
- <div class="class-toolbar">
405
- <label>
406
- Performance metric
407
- <select id="class-metric">
408
- <option value="map5095">AP@0.5:0.95</option>
409
- <option value="map50">AP@0.5</option>
410
- <option value="precision">Precision</option>
411
- <option value="recall">Recall</option>
412
- </select>
413
- </label>
414
- <label>
415
- Class order
416
- <select id="class-order">
417
- <option value="dataset">Dataset order</option>
418
- <option value="lowest">Proposed: lowest first</option>
419
- <option value="highest">Proposed: highest first</option>
420
- </select>
421
- </label>
422
- <span class="micro" id="class-context"></span>
423
- </div>
424
- <div id="class-legend" class="legend"></div>
425
- <div id="class-chart" aria-live="polite"></div>
426
- <p class="chart-note" id="class-note">
427
- Test-set performance · percent · higher is better · all bars use a 0-100% scale.
428
- </p>
429
- </section>
430
- <section class="panel table-panel">
431
- <div class="panel-heading">
432
- <h2>Exact measurements</h2>
433
- <button class="secondary" id="download-csv">Export CSV</button>
434
- </div>
435
- <div class="table-wrap">
436
- <table>
437
- <thead>
438
- <tr>
439
- <th>Model</th>
440
- <th>Precision</th>
441
- <th>Recall</th>
442
- <th>mAP@0.5</th>
443
- <th>mAP@0.5:0.95</th>
444
- <th>Parameters</th>
445
- <th>GFLOPs</th>
446
- </tr>
447
- </thead>
448
- <tbody id="metrics-table"></tbody>
449
- </table>
450
- </div>
451
- </section>
452
- </section>
453
- <footer>
454
- <span>PaddyLiteX · Thesis II demonstration</span>
455
- <span>AIT2309246</span>
456
- </footer>
457
- </main>
458
- </body>
459
- </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" />
6
+ <meta
7
+ name="description"
8
+ content="Paddy disease detection: compare YOLOv8n, Yolov8n_DenseNet121 and DenseLiteX predictions and research benchmarks."
9
+ />
10
+ <title>PaddyLiteX · Thesis II</title>
11
+ <link rel="icon" href="./favicon.svg" />
12
+ <link rel="stylesheet" href="./style.css" />
13
+ <link rel="stylesheet" href="./charts.css?v=baseline1-charts-20261013" />
14
+ <script src="./browser-runtime.js?v=baseline1-charts-20261013" defer></script>
15
+ <script src="./charts.js?v=baseline1-charts-20261013" defer></script>
16
+ <script src="./app.js?v=baseline1-charts-20261013" defer></script>
17
+ </head>
18
+ <body>
19
+ <header class="topbar">
20
+ <a class="brand" href="/" aria-label="PaddyLiteX home">
21
+ <span class="brandmark"><img src="./favicon.svg" alt="" aria-hidden="true" /></span>
22
+ <span>
23
+ PaddyLiteX
24
+ <small>THESIS II</small>
25
+ </span>
26
+ </a>
27
+ <nav class="tabs" aria-label="Main views">
28
+ <button class="tab active" data-view="workspace" aria-selected="true">
29
+ Detection studio
30
+ </button>
31
+ <button class="tab" data-view="benchmarks" aria-selected="false">Model benchmarks</button>
32
+ </nav>
33
+ <span class="research">XMUM · AI Engineering</span>
34
+ </header>
35
+ <main>
36
+ <section id="workspace" class="view active">
37
+ <section class="studio-hero intro-playing" id="studio-hero" aria-labelledby="hero-title">
38
+ <div class="hero-glow hero-glow-mint" aria-hidden="true"></div>
39
+ <div class="hero-glow hero-glow-blue" aria-hidden="true"></div>
40
+ <div class="hero-copy">
41
+ <p class="hero-eyebrow">
42
+ <span></span>
43
+ COMPUTER VISION · PADDY DISEASE DETECTION
44
+ </p>
45
+ <h1 id="hero-title">
46
+ A closer look.
47
+ <br />
48
+ <span>A clearer comparison.</span>
49
+ </h1>
50
+ <p class="hero-description">
51
+ From a paddy image to three model perspectives. Explore disease detections and the
52
+ balance between model performance and complexity.
53
+ </p>
54
+ <div class="hero-actions">
55
+ <button class="hero-primary" id="start-comparison">
56
+ Start comparison
57
+ <span aria-hidden="true">↗</span>
58
+ </button>
59
+ <button class="hero-secondary" data-view="benchmarks">Explore benchmarks</button>
60
+ </div>
61
+ <div class="hero-facts" aria-label="Demonstration workflow">
62
+ <span>
63
+ <b>01</b>
64
+ Upload an image
65
+ </span>
66
+ <span>
67
+ <b>02</b>
68
+ Compare three models
69
+ </span>
70
+ <span>
71
+ <b>03</b>
72
+ Explore the results
73
+ </span>
74
+ </div>
75
+ </div>
76
+ <div class="hero-art" aria-label="Animated illustration of a paddy leaf">
77
+ <div class="leaf-orbit orbit-one" aria-hidden="true"></div>
78
+ <div class="leaf-orbit orbit-two" aria-hidden="true"></div>
79
+ <div class="leaf-visual">
80
+ <div class="leaf-visual-header">
81
+ <span class="visual-dot"></span>
82
+ PADDY LEAF STUDY
83
+ <span>01 / 03</span>
84
+ </div>
85
+ <svg class="paddy-illustration" viewBox="0 0 360 360" aria-hidden="true">
86
+ <defs>
87
+ <linearGradient id="blade-main" x1="0" y1="1" x2="1" y2="0">
88
+ <stop offset="0" stop-color="#176c51" />
89
+ <stop offset="0.5" stop-color="#73ca73" />
90
+ <stop offset="1" stop-color="#d5eea2" />
91
+ </linearGradient>
92
+ <linearGradient id="blade-side" x1="0" y1="1" x2="1" y2="0">
93
+ <stop offset="0" stop-color="#277e61" />
94
+ <stop offset="1" stop-color="#a0ddb7" />
95
+ </linearGradient>
96
+ </defs>
97
+ <g fill="none" stroke="#a1d9cb" stroke-opacity="0.18">
98
+ <circle cx="180" cy="180" r="130" />
99
+ <circle cx="180" cy="180" r="95" stroke-dasharray="3 9" />
100
+ <path d="M180 22v316M22 180h316" />
101
+ </g>
102
+ <path
103
+ d="M133 303C132 202 182 109 287 35c-12 106-51 212-154 268Z"
104
+ fill="url(#blade-main)"
105
+ />
106
+ <path
107
+ d="M139 297C180 192 232 105 278 49"
108
+ fill="none"
109
+ stroke="#e5f3b0"
110
+ stroke-width="3"
111
+ />
112
+ <g fill="none" stroke="#cff0ad" stroke-opacity="0.55" stroke-width="1.4">
113
+ <path d="m160 246 51-11m-36-23 51-12m-34-19 50-14m-33-17 43-17m-30-8 36-21" />
114
+ <path d="m160 246-7-49m22 15-3-48m20 17 1-42m16 11 5-38m8 13 10-31" />
115
+ </g>
116
+ <path
117
+ d="M132 303C85 254 61 179 64 92c58 65 97 136 68 211Z"
118
+ fill="url(#blade-side)"
119
+ />
120
+ <path
121
+ d="M133 304C107 222 82 153 67 105"
122
+ fill="none"
123
+ stroke="#c4edc8"
124
+ stroke-width="2"
125
+ />
126
+ <path
127
+ d="M124 331c11-27 19-54 23-76"
128
+ fill="none"
129
+ stroke="#d5efa9"
130
+ stroke-width="4"
131
+ stroke-linecap="round"
132
+ />
133
+ <g fill="none" stroke="#adebcf" stroke-width="2" stroke-linecap="round">
134
+ <path d="M103 125v-18h18M257 125v-18h-18M103 256v18h18M257 256v18h-18" />
135
+ </g>
136
+ </svg>
137
+ <div class="leaf-scan" aria-hidden="true"></div>
138
+ <div class="leaf-visual-footer">
139
+ <span>Leaf illustration</span>
140
+ <span>Research visualization</span>
141
+ </div>
142
+ </div>
143
+ <div class="floating-tag tag-quality">
144
+ <span class="tag-icon">◈</span>
145
+ <div>
146
+ <small>THE QUESTION</small>
147
+ <strong>How well does it detect?</strong>
148
+ </div>
149
+ </div>
150
+ <div class="floating-tag tag-complexity">
151
+ <span class="tag-icon">◎</span>
152
+ <div>
153
+ <small>THE TRADE-OFF</small>
154
+ <strong>How much does it cost?</strong>
155
+ </div>
156
+ </div>
157
+ </div>
158
+ <div class="hero-bottom">
159
+ <span class="hero-signature">
160
+ PaddyLiteX
161
+ <span>RESEARCH IN ACTION</span>
162
+ </span>
163
+ <div class="hero-motion-controls">
164
+ <button id="replay-intro">
165
+ <span aria-hidden="true">↻</span>
166
+ Replay intro
167
+ </button>
168
+ <button id="toggle-motion" aria-pressed="false">Pause motion</button>
169
+ </div>
170
+ </div>
171
+ </section>
172
+
173
+ <div class="heading">
174
+ <div>
175
+ <p class="eyebrow">PADDY DISEASE DETECTION</p>
176
+ <h2 class="studio-title">Detection studio</h2>
177
+ <p class="muted">Compare your baselines and proposed model on the same image.</p>
178
+ </div>
179
+ <span class="pill" id="service-status">Checking model availability</span>
180
+ </div>
181
+ <div class="studio-grid">
182
+ <section class="panel upload-panel" id="studio-input">
183
+ <div class="panel-heading">
184
+ <h2>Input image</h2>
185
+ <span class="micro">JPG / JPEG · PNG · WebP</span>
186
+ </div>
187
+ <label class="dropzone" id="dropzone" tabindex="0">
188
+ <input id="file" type="file" accept=".jpg,.jpeg,.png,.webp,image/jpeg,image/png,image/webp" hidden />
189
+ <span id="upload-hint">
190
+ <span class="upload-icon" aria-hidden="true">+</span>
191
+ <strong>Choose a paddy image</strong>
192
+ <span>or drag it here</span>
193
+ <small>Up to 10 MB · 12 megapixels</small>
194
+ </span>
195
+ <img id="input-preview" alt="Selected paddy image" hidden />
196
+ </label>
197
+ <div class="fileline">
198
+ <span id="filename">No image selected</span>
199
+ <button id="clear" class="text-button" hidden>Clear</button>
200
+ </div>
201
+ <!-- Dataset source -->
202
+ <aside class="dataset-source" aria-labelledby="dataset-source-title">
203
+ <span class="dataset-source-icon" aria-hidden="true">
204
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7">
205
+ <path d="M20 4c0 9-4 14-10 14a6 6 0 0 1-6-6C4 6 11 4 20 4Z" />
206
+ <path d="M4 20 15 9" />
207
+ </svg>
208
+ </span>
209
+ <div class="dataset-source-copy">
210
+ <h3 id="dataset-source-title">Try a dataset image</h3>
211
+ <p>Download a rice leaf image from Kaggle, then upload it here to compare the models.</p>
212
+ <a
213
+ href="https://www.kaggle.com/datasets/yusufmurtaza01/rice-leaf-diseases"
214
+ target="_blank"
215
+ rel="noopener noreferrer"
216
+ >
217
+ RICE Leaf Diseases · Kaggle
218
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" aria-hidden="true">
219
+ <path d="M14 4h6v6M20 4 10 14M10 4H5a1 1 0 0 0-1 1v14a1 1 0 0 0 1 1h14a1 1 0 0 0 1-1v-5" />
220
+ </svg>
221
+ <span class="dataset-source-sr">(opens in a new tab)</span>
222
+ </a>
223
+ </div>
224
+ </aside>
225
+ <!-- End dataset source -->
226
+ <div class="settings">
227
+ <div>
228
+ <label for="confidence">
229
+ Confidence threshold
230
+ <output id="confidence-value">0.25</output>
231
+ </label>
232
+ <input
233
+ type="range"
234
+ id="confidence"
235
+ min="0.05"
236
+ max="0.95"
237
+ step="0.05"
238
+ value="0.25"
239
+ />
240
+ </div>
241
+ <div>
242
+ <label for="image-size">Model input size</label>
243
+ <select id="image-size">
244
+ <option value="320">320 x 320</option>
245
+ <option value="512" selected>512 x 512</option>
246
+ <option value="640">640 x 640</option>
247
+ </select>
248
+ </div>
249
+ </div>
250
+ <button class="primary" id="compare" disabled>Compare all three models</button>
251
+ <p class="privacy">Images are processed on your device and are not uploaded to a detection server.</p>
252
+ <p class="message" id="message" role="status" aria-live="polite"></p>
253
+ </section>
254
+ <section class="panel guidance">
255
+ <div>
256
+ <p class="eyebrow">CONTROLLED COMPARISON</p>
257
+ <h2>
258
+ The same input.
259
+ <br />
260
+ The same settings.
261
+ </h2>
262
+ <p>
263
+ Each model receives the same image, confidence threshold and input size. Explore
264
+ bounding boxes, predicted classes and confidence side by side.
265
+ </p>
266
+ </div>
267
+ <div class="model-list" id="model-list"></div>
268
+ <div class="note">
269
+ <strong>Prediction confidence ≠ accuracy</strong>
270
+ <p>
271
+ An uploaded image has no ground-truth annotation. Dataset-level performance is
272
+ available in Model benchmarks.
273
+ </p>
274
+ </div>
275
+ </section>
276
+ </div>
277
+ <div class="section-heading">
278
+ <div>
279
+ <h2>Detection results</h2>
280
+ <p class="muted">Real model predictions appear here after comparison.</p>
281
+ </div>
282
+ <span class="micro">P3 / P4 / P5 · 9 classes</span>
283
+ </div>
284
+ <div class="result-grid" id="results"></div>
285
+ </section>
286
+ <section id="benchmarks" class="view" hidden>
287
+ <div class="heading">
288
+ <div>
289
+ <p class="eyebrow">RECORDED EXPERIMENTAL RESULTS</p>
290
+ <h1>Performance, with perspective.</h1>
291
+ <p class="muted">
292
+ Detection quality and computational complexity across the evaluated models.
293
+ </p>
294
+ </div>
295
+ <div class="segmented">
296
+ <button data-scope="main" class="selected">Main comparison</button>
297
+ <button data-scope="ablation">Ablation study</button>
298
+ </div>
299
+ </div>
300
+ <div class="stat-grid">
301
+ <div class="stat">
302
+ <small>Test images</small>
303
+ <strong id="test-images">803</strong>
304
+ </div>
305
+ <div class="stat">
306
+ <small>Annotated objects</small>
307
+ <strong id="test-objects">2,466</strong>
308
+ </div>
309
+ <div class="stat">
310
+ <small>Detection classes</small>
311
+ <strong id="test-classes">9</strong>
312
+ </div>
313
+ <div class="stat">
314
+ <small>Proposed mAP@0.5:0.95</small>
315
+ <strong class="green" id="proposed-map5095">
316
+ 55.83
317
+ <span>%</span>
318
+ </strong>
319
+ </div>
320
+ <div class="stat">
321
+ <small>Proposed mAP@0.5</small>
322
+ <strong class="green" id="proposed-map50">
323
+ 81.67
324
+ <span>%</span>
325
+ </strong>
326
+ </div>
327
+ </div>
328
+ <div class="chart-grid">
329
+ <section class="panel">
330
+ <div class="panel-heading">
331
+ <div>
332
+ <p class="eyebrow">DETECTION PERFORMANCE</p>
333
+ <h2>Quality across models</h2>
334
+ </div>
335
+ <div class="chart-options">
336
+ <label>
337
+ Detection metric
338
+ <select id="metric" aria-label="Detection metric">
339
+ <option value="map5095">mAP@0.5:0.95</option>
340
+ <option value="map50">mAP@0.5</option>
341
+ <option value="precision">Precision</option>
342
+ <option value="recall">Recall</option>
343
+ </select>
344
+ </label>
345
+ <label>
346
+ Chart view
347
+ <select id="quality-chart-type">
348
+ <option value="bar">Bar chart</option>
349
+ <option value="dot">Dot plot</option>
350
+ <option value="radar">Radar · all metrics</option>
351
+ <option value="heatmap">Heatmap · all metrics</option>
352
+ </select>
353
+ </label>
354
+ </div>
355
+ </div>
356
+ <div id="quality-chart" class="bars"></div>
357
+ <p class="chart-note" id="quality-chart-note">Percent · higher is better · axis starts at zero</p>
358
+ </section>
359
+ <section class="panel">
360
+ <div class="panel-heading">
361
+ <div>
362
+ <p class="eyebrow">COMPUTATIONAL COMPLEXITY</p>
363
+ <h2>Cost across models</h2>
364
+ </div>
365
+ <div class="chart-options">
366
+ <label>
367
+ Complexity metric
368
+ <select id="cost-metric" aria-label="Complexity metric">
369
+ <option value="parameters">Parameters</option>
370
+ <option value="gflops">GFLOPs</option>
371
+ </select>
372
+ </label>
373
+ <label>
374
+ Chart view
375
+ <select id="cost-chart-type">
376
+ <option value="bar">Bar chart</option>
377
+ <option value="dot">Dot plot</option>
378
+ </select>
379
+ </label>
380
+ </div>
381
+ </div>
382
+ <div id="cost-chart" class="bars"></div>
383
+ <p class="chart-note">Lower is lighter · axis starts at zero</p>
384
+ </section>
385
+ </div>
386
+ <section class="panel scatter-panel">
387
+ <div class="panel-heading">
388
+ <div>
389
+ <p class="eyebrow">PERFORMANCE-COMPLEXITY TRADE-OFF</p>
390
+ <h2>Where each model stands</h2>
391
+ </div>
392
+ <span class="micro" id="scatter-metrics">Parameters vs. mAP@0.5:0.95</span>
393
+ </div>
394
+ <div id="scatter"></div>
395
+ <div id="legend" class="legend"></div>
396
+ </section>
397
+ <div class="insight-grid">
398
+ <div class="insight">
399
+ <small>Against Yolov8n_DenseNet121 baseline</small>
400
+ <strong id="map-improvement">-0.25%</strong>
401
+ <p>mAP@0.5:0.95 score difference</p>
402
+ <p class="insight-values" id="map5095-change">56.08% → 55.83%</p>
403
+ </div>
404
+ <div class="insight">
405
+ <small>Against Yolov8n_DenseNet121 baseline</small>
406
+ <strong id="map50-improvement">+0.98%</strong>
407
+ <p>mAP@0.5 score difference</p>
408
+ <p class="insight-values" id="map50-change">80.69% → 81.67%</p>
409
+ </div>
410
+ <div class="insight">
411
+ <small>Against Yolov8n_DenseNet121 baseline</small>
412
+ <strong id="parameter-reduction">68.01% fewer</strong>
413
+ <p>parameters</p>
414
+ </div>
415
+ <div class="insight">
416
+ <small>Against Yolov8n_DenseNet121 baseline</small>
417
+ <strong id="flops-reduction">72.59% fewer</strong>
418
+ <p>reported GFLOPs</p>
419
+ </div>
420
+ </div>
421
+ <section class="panel class-panel" id="class-panel" aria-labelledby="class-heading">
422
+ <div class="panel-heading">
423
+ <div>
424
+ <p class="eyebrow">CLASS-LEVEL DETECTION PERFORMANCE</p>
425
+ <h2 id="class-heading">Every class, three perspectives.</h2>
426
+ <p class="muted">
427
+ Compare your two baselines and final proposed model for each class.
428
+ </p>
429
+ </div>
430
+ <button class="secondary" id="download-class-csv" disabled>Export class CSV</button>
431
+ </div>
432
+ <div class="class-toolbar">
433
+ <label>
434
+ Chart view
435
+ <select id="class-chart-type">
436
+ <option value="bar">Grouped bars</option>
437
+ <option value="dot">Dot plot</option>
438
+ <option value="heatmap">Heatmap</option>
439
+ <option value="radar">Radar</option>
440
+ </select>
441
+ </label>
442
+ <label>
443
+ Performance metric
444
+ <select id="class-metric">
445
+ <option value="map5095">AP@0.5:0.95</option>
446
+ <option value="map50">AP@0.5</option>
447
+ <option value="precision">Precision</option>
448
+ <option value="recall">Recall</option>
449
+ </select>
450
+ </label>
451
+ <label>
452
+ Class order
453
+ <select id="class-order">
454
+ <option value="dataset">Dataset order</option>
455
+ <option value="lowest">Proposed: lowest first</option>
456
+ <option value="highest">Proposed: highest first</option>
457
+ </select>
458
+ </label>
459
+ <span class="micro" id="class-context"></span>
460
+ </div>
461
+ <div id="class-legend" class="legend"></div>
462
+ <div id="class-chart" aria-live="polite"></div>
463
+ <p class="chart-note" id="class-note">
464
+ Test-set performance · percent · higher is better · all bars use a 0-100% scale.
465
+ </p>
466
+ </section>
467
+ <section class="panel table-panel">
468
+ <div class="panel-heading">
469
+ <h2>Exact measurements</h2>
470
+ <button class="secondary" id="download-csv">Export CSV</button>
471
+ </div>
472
+ <div class="table-wrap">
473
+ <table>
474
+ <thead>
475
+ <tr>
476
+ <th>Model</th>
477
+ <th>Precision</th>
478
+ <th>Recall</th>
479
+ <th>mAP@0.5</th>
480
+ <th>mAP@0.5:0.95</th>
481
+ <th>Parameters</th>
482
+ <th>GFLOPs</th>
483
+ </tr>
484
+ </thead>
485
+ <tbody id="metrics-table"></tbody>
486
+ </table>
487
+ </div>
488
+ </section>
489
+ </section>
490
+ <footer>
491
+ <span>PaddyLiteX · Thesis II demonstration</span>
492
+ <span>AIT2309246</span>
493
+ </footer>
494
+ </main>
495
+ </body>
496
+ </html>
per_class.json CHANGED
@@ -1,646 +1,660 @@
1
- {
2
- "schema_version": 1,
3
- "unit": "percent",
4
- "context": {
5
- "source": "Class-level test-set results from my paddy disease detection experiments.",
6
- "split": "test",
7
- "images": 803,
8
- "objects": 2466,
9
- "precision_note": "Transcribed from the displayed validation tables. Values use the precision shown in the screenshots; full-precision exports may differ slightly.",
10
- "model_evaluations": {
11
- "yolov8n_densenet121": {
12
- "c2f_repeats": 1,
13
- "split": "test",
14
- "run_name": "yolov8n_densenet121_fs",
15
- "source_screenshot": "image(20261011-154943).png",
16
- "aggregate_precision_note": "Percent metrics transcribed from the four-decimal Evaluation Summary.",
17
- "class_precision_note": "Percent metrics transcribed from the displayed per-class table; rounding follows the screenshot.",
18
- "complexity_note": "Parameters and GFLOPs transcribed from the displayed model summary; profiling convention awaits confirmation."
19
- },
20
- "yolov8n_denselitex_sppf_c2psa": {
21
- "c2f_repeats": 1,
22
- "split": "test",
23
- "run_name": "yolov8n_denselitex_sppf_c2psa_fs",
24
- "source_screenshot": "image(20261011-155020).png",
25
- "aggregate_precision_note": "Percent metrics transcribed from the four-decimal Evaluation Summary.",
26
- "class_precision_note": "Percent metrics transcribed from the displayed per-class table; rounding follows the screenshot.",
27
- "complexity_note": "Parameters and GFLOPs transcribed from the displayed model summary; profiling convention awaits confirmation."
28
- },
29
- "yolov8n_denselitex": {
30
- "c2f_repeats": 1,
31
- "split": "test",
32
- "run_name": "yolov8n_denselitex_fs",
33
- "source_screenshot": "image(20261011-155244).png",
34
- "aggregate_precision_note": "Percent metrics transcribed from the four-decimal Evaluation Summary.",
35
- "class_precision_note": "Percent metrics transcribed from the displayed per-class table; rounding follows the screenshot.",
36
- "complexity_note": "Parameters and GFLOPs transcribed from the displayed model summary; profiling convention awaits confirmation."
37
- },
38
- "yolov8n_denselitex_sppf": {
39
- "c2f_repeats": 1,
40
- "split": "test",
41
- "run_name": "yolov8n_denselitex_sppf_fs",
42
- "source_screenshot": "image(20261011-155432).png",
43
- "aggregate_precision_note": "Percent metrics transcribed from the four-decimal Evaluation Summary.",
44
- "class_precision_note": "Percent metrics transcribed from the displayed per-class table; rounding follows the screenshot.",
45
- "complexity_note": "Parameters and GFLOPs transcribed from the displayed model summary; profiling convention awaits confirmation."
46
- },
47
- "yolov8n_denselitex_c2psa": {
48
- "c2f_repeats": 1,
49
- "split": "test",
50
- "run_name": "yolov8n_denselitex_c2psa_fs",
51
- "source_screenshot": "image(20261011-155504).png",
52
- "aggregate_precision_note": "Percent metrics transcribed from the four-decimal Evaluation Summary.",
53
- "class_precision_note": "Percent metrics transcribed from the displayed per-class table; rounding follows the screenshot.",
54
- "complexity_note": "Parameters and GFLOPs transcribed from the displayed model summary; profiling convention awaits confirmation."
55
- },
56
- "yolov8n_densenet121_sppf": {
57
- "c2f_repeats": 1,
58
- "split": "test",
59
- "run_name": "yolov8n_densenet121_sppf_fs",
60
- "source_screenshot": "image(20261011-155804).png",
61
- "aggregate_precision_note": "Percent metrics transcribed from the four-decimal Evaluation Summary.",
62
- "class_precision_note": "Percent metrics transcribed from the displayed per-class table; rounding follows the screenshot.",
63
- "complexity_note": "Parameters and GFLOPs transcribed from the displayed model summary; profiling convention awaits confirmation."
64
- },
65
- "yolov8n_densenet121_c2psa": {
66
- "c2f_repeats": 1,
67
- "split": "test",
68
- "run_name": "yolov8n_densenet121_c2psa_fs",
69
- "source_screenshot": "image(20261011-155818).png",
70
- "aggregate_precision_note": "Percent metrics transcribed from the four-decimal Evaluation Summary.",
71
- "class_precision_note": "Percent metrics transcribed from the displayed per-class table; rounding follows the screenshot.",
72
- "complexity_note": "Parameters and GFLOPs transcribed from the displayed model summary; profiling convention awaits confirmation."
73
- },
74
- "yolov8n_densenet121_sppf_c2psa": {
75
- "c2f_repeats": 1,
76
- "split": "test",
77
- "run_name": "yolov8n_densenet121_sppf_c2psa_fs",
78
- "source_screenshot": "image(20261011-160157).png",
79
- "aggregate_precision_note": "Percent metrics transcribed from the four-decimal Evaluation Summary.",
80
- "class_precision_note": "Percent metrics transcribed from the displayed per-class table; rounding follows the screenshot.",
81
- "complexity_note": "Parameters and GFLOPs transcribed from the displayed model summary; profiling convention awaits confirmation."
82
- }
83
- },
84
- "last_results_update": "c2f1-results-20261012-batch2"
85
- },
86
- "classes": [
87
- {
88
- "id": 0,
89
- "name": "Rice__BacterialLeafBlight",
90
- "images": 183,
91
- "instances": 209,
92
- "models": {
93
- "yolov8n": {
94
- "precision": 93.3,
95
- "recall": 90.9,
96
- "map50": 93.9,
97
- "map5095": 71.4
98
- },
99
- "yolov8n_densenet121": {
100
- "precision": 97.2,
101
- "recall": 90.0,
102
- "map50": 92.6,
103
- "map5095": 74.8
104
- },
105
- "yolov8n_denselitex_sppf_c2psa": {
106
- "precision": 95.7,
107
- "recall": 90.4,
108
- "map50": 94.3,
109
- "map5095": 72.9
110
- },
111
- "yolov8n_densenet121_c2psa": {
112
- "precision": 99.0,
113
- "recall": 90.3,
114
- "map50": 93.6,
115
- "map5095": 79.4
116
- },
117
- "yolov8n_densenet121_sppf_c2psa": {
118
- "precision": 97.5,
119
- "recall": 92.8,
120
- "map50": 93.1,
121
- "map5095": 79.0
122
- },
123
- "yolov8n_denselitex": {
124
- "precision": 90.6,
125
- "recall": 88.0,
126
- "map50": 91.8,
127
- "map5095": 65.2
128
- },
129
- "yolov8n_denselitex_sppf": {
130
- "precision": 94.5,
131
- "recall": 90.0,
132
- "map50": 93.6,
133
- "map5095": 70.0
134
- },
135
- "yolov8n_denselitex_c2psa": {
136
- "precision": 94.0,
137
- "recall": 90.0,
138
- "map50": 93.5,
139
- "map5095": 68.5
140
- },
141
- "yolov8n_densenet121_sppf": {
142
- "precision": 96.8,
143
- "recall": 90.0,
144
- "map50": 92.9,
145
- "map5095": 78.0
146
- }
147
- }
148
- },
149
- {
150
- "id": 1,
151
- "name": "Rice__BrownSpot",
152
- "images": 174,
153
- "instances": 1280,
154
- "models": {
155
- "yolov8n": {
156
- "precision": 89.7,
157
- "recall": 83.8,
158
- "map50": 92.1,
159
- "map5095": 57.7
160
- },
161
- "yolov8n_densenet121": {
162
- "precision": 96.4,
163
- "recall": 86.5,
164
- "map50": 94.6,
165
- "map5095": 62.9
166
- },
167
- "yolov8n_denselitex_sppf_c2psa": {
168
- "precision": 90.6,
169
- "recall": 85.3,
170
- "map50": 93.1,
171
- "map5095": 59.9
172
- },
173
- "yolov8n_densenet121_c2psa": {
174
- "precision": 96.9,
175
- "recall": 86.6,
176
- "map50": 95.6,
177
- "map5095": 65.7
178
- },
179
- "yolov8n_densenet121_sppf_c2psa": {
180
- "precision": 93.7,
181
- "recall": 90.7,
182
- "map50": 95.3,
183
- "map5095": 66.4
184
- },
185
- "yolov8n_denselitex": {
186
- "precision": 90.5,
187
- "recall": 76.7,
188
- "map50": 88.8,
189
- "map5095": 50.7
190
- },
191
- "yolov8n_denselitex_sppf": {
192
- "precision": 91.5,
193
- "recall": 78.5,
194
- "map50": 91.0,
195
- "map5095": 55.9
196
- },
197
- "yolov8n_denselitex_c2psa": {
198
- "precision": 92.1,
199
- "recall": 79.9,
200
- "map50": 91.2,
201
- "map5095": 54.7
202
- },
203
- "yolov8n_densenet121_sppf": {
204
- "precision": 93.4,
205
- "recall": 89.1,
206
- "map50": 94.8,
207
- "map5095": 64.9
208
- }
209
- }
210
- },
211
- {
212
- "id": 2,
213
- "name": "Rice__Healthy",
214
- "images": 44,
215
- "instances": 69,
216
- "models": {
217
- "yolov8n": {
218
- "precision": 75.4,
219
- "recall": 44.9,
220
- "map50": 52.1,
221
- "map5095": 33.6
222
- },
223
- "yolov8n_densenet121": {
224
- "precision": 68.0,
225
- "recall": 44.9,
226
- "map50": 49.4,
227
- "map5095": 31.7
228
- },
229
- "yolov8n_denselitex_sppf_c2psa": {
230
- "precision": 66.9,
231
- "recall": 42.0,
232
- "map50": 50.0,
233
- "map5095": 32.0
234
- },
235
- "yolov8n_densenet121_c2psa": {
236
- "precision": 70.6,
237
- "recall": 49.3,
238
- "map50": 51.1,
239
- "map5095": 32.5
240
- },
241
- "yolov8n_densenet121_sppf_c2psa": {
242
- "precision": 71.4,
243
- "recall": 47.1,
244
- "map50": 51.4,
245
- "map5095": 35.5
246
- },
247
- "yolov8n_denselitex": {
248
- "precision": 65.6,
249
- "recall": 40.6,
250
- "map50": 51.5,
251
- "map5095": 31.4
252
- },
253
- "yolov8n_denselitex_sppf": {
254
- "precision": 70.1,
255
- "recall": 40.7,
256
- "map50": 48.6,
257
- "map5095": 32.8
258
- },
259
- "yolov8n_denselitex_c2psa": {
260
- "precision": 61.1,
261
- "recall": 40.6,
262
- "map50": 47.9,
263
- "map5095": 30.6
264
- },
265
- "yolov8n_densenet121_sppf": {
266
- "precision": 68.6,
267
- "recall": 43.5,
268
- "map50": 51.2,
269
- "map5095": 31.0
270
- }
271
- }
272
- },
273
- {
274
- "id": 3,
275
- "name": "Rice__Hispa",
276
- "images": 44,
277
- "instances": 89,
278
- "models": {
279
- "yolov8n": {
280
- "precision": 85.4,
281
- "recall": 84.3,
282
- "map50": 83.0,
283
- "map5095": 54.3
284
- },
285
- "yolov8n_densenet121": {
286
- "precision": 85.0,
287
- "recall": 80.9,
288
- "map50": 82.2,
289
- "map5095": 52.7
290
- },
291
- "yolov8n_denselitex_sppf_c2psa": {
292
- "precision": 84.9,
293
- "recall": 79.8,
294
- "map50": 79.6,
295
- "map5095": 52.2
296
- },
297
- "yolov8n_densenet121_c2psa": {
298
- "precision": 88.5,
299
- "recall": 78.7,
300
- "map50": 84.1,
301
- "map5095": 55.1
302
- },
303
- "yolov8n_densenet121_sppf_c2psa": {
304
- "precision": 86.7,
305
- "recall": 80.9,
306
- "map50": 81.6,
307
- "map5095": 53.0
308
- },
309
- "yolov8n_denselitex": {
310
- "precision": 79.3,
311
- "recall": 77.5,
312
- "map50": 77.8,
313
- "map5095": 48.9
314
- },
315
- "yolov8n_denselitex_sppf": {
316
- "precision": 83.2,
317
- "recall": 76.4,
318
- "map50": 79.3,
319
- "map5095": 50.7
320
- },
321
- "yolov8n_denselitex_c2psa": {
322
- "precision": 77.1,
323
- "recall": 78.7,
324
- "map50": 80.1,
325
- "map5095": 51.9
326
- },
327
- "yolov8n_densenet121_sppf": {
328
- "precision": 85.6,
329
- "recall": 80.4,
330
- "map50": 81.0,
331
- "map5095": 52.2
332
- }
333
- }
334
- },
335
- {
336
- "id": 4,
337
- "name": "Rice__LeafBlast",
338
- "images": 46,
339
- "instances": 70,
340
- "models": {
341
- "yolov8n": {
342
- "precision": 65.8,
343
- "recall": 55.7,
344
- "map50": 63.7,
345
- "map5095": 39.5
346
- },
347
- "yolov8n_densenet121": {
348
- "precision": 68.4,
349
- "recall": 51.4,
350
- "map50": 63.9,
351
- "map5095": 40.9
352
- },
353
- "yolov8n_denselitex_sppf_c2psa": {
354
- "precision": 71.7,
355
- "recall": 57.8,
356
- "map50": 67.0,
357
- "map5095": 41.9
358
- },
359
- "yolov8n_densenet121_c2psa": {
360
- "precision": 78.8,
361
- "recall": 61.4,
362
- "map50": 69.2,
363
- "map5095": 43.0
364
- },
365
- "yolov8n_densenet121_sppf_c2psa": {
366
- "precision": 64.9,
367
- "recall": 63.4,
368
- "map50": 66.2,
369
- "map5095": 41.3
370
- },
371
- "yolov8n_denselitex": {
372
- "precision": 75.1,
373
- "recall": 50.0,
374
- "map50": 62.0,
375
- "map5095": 34.6
376
- },
377
- "yolov8n_denselitex_sppf": {
378
- "precision": 67.3,
379
- "recall": 52.9,
380
- "map50": 60.3,
381
- "map5095": 38.7
382
- },
383
- "yolov8n_denselitex_c2psa": {
384
- "precision": 68.6,
385
- "recall": 58.6,
386
- "map50": 65.8,
387
- "map5095": 37.6
388
- },
389
- "yolov8n_densenet121_sppf": {
390
- "precision": 71.4,
391
- "recall": 65.7,
392
- "map50": 65.6,
393
- "map5095": 42.2
394
- }
395
- }
396
- },
397
- {
398
- "id": 5,
399
- "name": "Rice__LeafScald",
400
- "images": 46,
401
- "instances": 51,
402
- "models": {
403
- "yolov8n": {
404
- "precision": 83.8,
405
- "recall": 86.3,
406
- "map50": 86.7,
407
- "map5095": 56.7
408
- },
409
- "yolov8n_densenet121": {
410
- "precision": 84.1,
411
- "recall": 72.6,
412
- "map50": 80.6,
413
- "map5095": 56.0
414
- },
415
- "yolov8n_denselitex_sppf_c2psa": {
416
- "precision": 81.5,
417
- "recall": 80.4,
418
- "map50": 89.0,
419
- "map5095": 60.6
420
- },
421
- "yolov8n_densenet121_c2psa": {
422
- "precision": 88.3,
423
- "recall": 76.5,
424
- "map50": 86.6,
425
- "map5095": 55.3
426
- },
427
- "yolov8n_densenet121_sppf_c2psa": {
428
- "precision": 80.9,
429
- "recall": 80.4,
430
- "map50": 85.0,
431
- "map5095": 56.5
432
- },
433
- "yolov8n_denselitex": {
434
- "precision": 84.5,
435
- "recall": 74.5,
436
- "map50": 83.9,
437
- "map5095": 48.2
438
- },
439
- "yolov8n_denselitex_sppf": {
440
- "precision": 87.3,
441
- "recall": 82.4,
442
- "map50": 87.3,
443
- "map5095": 58.2
444
- },
445
- "yolov8n_denselitex_c2psa": {
446
- "precision": 78.9,
447
- "recall": 74.5,
448
- "map50": 78.1,
449
- "map5095": 51.1
450
- },
451
- "yolov8n_densenet121_sppf": {
452
- "precision": 83.9,
453
- "recall": 81.9,
454
- "map50": 87.9,
455
- "map5095": 56.4
456
- }
457
- }
458
- },
459
- {
460
- "id": 6,
461
- "name": "Rice__LeafSmut",
462
- "images": 175,
463
- "instances": 535,
464
- "models": {
465
- "yolov8n": {
466
- "precision": 96.4,
467
- "recall": 95.4,
468
- "map50": 98.0,
469
- "map5095": 75.2
470
- },
471
- "yolov8n_densenet121": {
472
- "precision": 97.4,
473
- "recall": 96.2,
474
- "map50": 98.0,
475
- "map5095": 77.5
476
- },
477
- "yolov8n_denselitex_sppf_c2psa": {
478
- "precision": 95.8,
479
- "recall": 96.1,
480
- "map50": 98.2,
481
- "map5095": 76.4
482
- },
483
- "yolov8n_densenet121_c2psa": {
484
- "precision": 98.4,
485
- "recall": 95.7,
486
- "map50": 97.7,
487
- "map5095": 81.9
488
- },
489
- "yolov8n_densenet121_sppf_c2psa": {
490
- "precision": 97.7,
491
- "recall": 96.3,
492
- "map50": 98.0,
493
- "map5095": 81.8
494
- },
495
- "yolov8n_denselitex": {
496
- "precision": 95.0,
497
- "recall": 94.0,
498
- "map50": 96.7,
499
- "map5095": 67.2
500
- },
501
- "yolov8n_denselitex_sppf": {
502
- "precision": 96.5,
503
- "recall": 95.0,
504
- "map50": 97.8,
505
- "map5095": 73.8
506
- },
507
- "yolov8n_denselitex_c2psa": {
508
- "precision": 94.6,
509
- "recall": 93.8,
510
- "map50": 97.1,
511
- "map5095": 73.0
512
- },
513
- "yolov8n_densenet121_sppf": {
514
- "precision": 97.0,
515
- "recall": 95.8,
516
- "map50": 97.7,
517
- "map5095": 80.5
518
- }
519
- }
520
- },
521
- {
522
- "id": 7,
523
- "name": "Rice__NarrowBrownLeafSpot",
524
- "images": 45,
525
- "instances": 66,
526
- "models": {
527
- "yolov8n": {
528
- "precision": 76.5,
529
- "recall": 66.7,
530
- "map50": 70.1,
531
- "map5095": 45.7
532
- },
533
- "yolov8n_densenet121": {
534
- "precision": 76.5,
535
- "recall": 65.2,
536
- "map50": 73.2,
537
- "map5095": 48.0
538
- },
539
- "yolov8n_denselitex_sppf_c2psa": {
540
- "precision": 74.5,
541
- "recall": 63.6,
542
- "map50": 73.0,
543
- "map5095": 45.4
544
- },
545
- "yolov8n_densenet121_c2psa": {
546
- "precision": 75.7,
547
- "recall": 66.7,
548
- "map50": 75.5,
549
- "map5095": 48.6
550
- },
551
- "yolov8n_densenet121_sppf_c2psa": {
552
- "precision": 71.2,
553
- "recall": 74.2,
554
- "map50": 79.0,
555
- "map5095": 48.9
556
- },
557
- "yolov8n_denselitex": {
558
- "precision": 77.3,
559
- "recall": 60.6,
560
- "map50": 70.7,
561
- "map5095": 41.7
562
- },
563
- "yolov8n_denselitex_sppf": {
564
- "precision": 73.7,
565
- "recall": 62.1,
566
- "map50": 70.3,
567
- "map5095": 43.3
568
- },
569
- "yolov8n_denselitex_c2psa": {
570
- "precision": 65.6,
571
- "recall": 60.6,
572
- "map50": 65.0,
573
- "map5095": 37.8
574
- },
575
- "yolov8n_densenet121_sppf": {
576
- "precision": 79.5,
577
- "recall": 70.4,
578
- "map50": 76.1,
579
- "map5095": 48.3
580
- }
581
- }
582
- },
583
- {
584
- "id": 8,
585
- "name": "Rice__NeckBlast",
586
- "images": 46,
587
- "instances": 97,
588
- "models": {
589
- "yolov8n": {
590
- "precision": 86.7,
591
- "recall": 81.4,
592
- "map50": 87.6,
593
- "map5095": 57.7
594
- },
595
- "yolov8n_densenet121": {
596
- "precision": 89.4,
597
- "recall": 85.6,
598
- "map50": 91.7,
599
- "map5095": 60.3
600
- },
601
- "yolov8n_denselitex_sppf_c2psa": {
602
- "precision": 88.0,
603
- "recall": 85.6,
604
- "map50": 90.7,
605
- "map5095": 61.1
606
- },
607
- "yolov8n_densenet121_c2psa": {
608
- "precision": 88.6,
609
- "recall": 83.5,
610
- "map50": 89.7,
611
- "map5095": 58.3
612
- },
613
- "yolov8n_densenet121_sppf_c2psa": {
614
- "precision": 90.2,
615
- "recall": 85.1,
616
- "map50": 90.5,
617
- "map5095": 62.3
618
- },
619
- "yolov8n_denselitex": {
620
- "precision": 86.1,
621
- "recall": 84.5,
622
- "map50": 86.1,
623
- "map5095": 54.7
624
- },
625
- "yolov8n_denselitex_sppf": {
626
- "precision": 88.9,
627
- "recall": 86.6,
628
- "map50": 91.3,
629
- "map5095": 61.9
630
- },
631
- "yolov8n_denselitex_c2psa": {
632
- "precision": 78.5,
633
- "recall": 84.5,
634
- "map50": 86.7,
635
- "map5095": 52.2
636
- },
637
- "yolov8n_densenet121_sppf": {
638
- "precision": 93.0,
639
- "recall": 83.5,
640
- "map50": 90.1,
641
- "map5095": 61.0
642
- }
643
- }
644
- }
645
- ]
646
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "schema_version": 1,
3
+ "unit": "percent",
4
+ "context": {
5
+ "source": "Class-level test-set results from my paddy disease detection experiments.",
6
+ "split": "test",
7
+ "images": 803,
8
+ "objects": 2466,
9
+ "precision_note": "Transcribed from the displayed validation tables. Values use the precision shown in the screenshots; full-precision exports may differ slightly.",
10
+ "model_evaluations": {
11
+ "yolov8n_densenet121": {
12
+ "c2f_repeats": 1,
13
+ "split": "test",
14
+ "run_name": "yolov8n_densenet121_fs",
15
+ "source_screenshot": "image(20261011-154943).png",
16
+ "aggregate_precision_note": "Percent metrics transcribed from the four-decimal Evaluation Summary.",
17
+ "class_precision_note": "Percent metrics transcribed from the displayed per-class table; rounding follows the screenshot.",
18
+ "complexity_note": "Parameters and GFLOPs transcribed from the displayed model summary; profiling convention awaits confirmation."
19
+ },
20
+ "yolov8n_denselitex_sppf_c2psa": {
21
+ "c2f_repeats": 1,
22
+ "split": "test",
23
+ "run_name": "yolov8n_denselitex_sppf_c2psa_fs",
24
+ "source_screenshot": "image(20261011-155020).png",
25
+ "aggregate_precision_note": "Percent metrics transcribed from the four-decimal Evaluation Summary.",
26
+ "class_precision_note": "Percent metrics transcribed from the displayed per-class table; rounding follows the screenshot.",
27
+ "complexity_note": "Parameters and GFLOPs transcribed from the displayed model summary; profiling convention awaits confirmation."
28
+ },
29
+ "yolov8n_denselitex": {
30
+ "c2f_repeats": 1,
31
+ "split": "test",
32
+ "run_name": "yolov8n_denselitex_fs",
33
+ "source_screenshot": "image(20261011-155244).png",
34
+ "aggregate_precision_note": "Percent metrics transcribed from the four-decimal Evaluation Summary.",
35
+ "class_precision_note": "Percent metrics transcribed from the displayed per-class table; rounding follows the screenshot.",
36
+ "complexity_note": "Parameters and GFLOPs transcribed from the displayed model summary; profiling convention awaits confirmation."
37
+ },
38
+ "yolov8n_denselitex_sppf": {
39
+ "c2f_repeats": 1,
40
+ "split": "test",
41
+ "run_name": "yolov8n_denselitex_sppf_fs",
42
+ "source_screenshot": "image(20261011-155432).png",
43
+ "aggregate_precision_note": "Percent metrics transcribed from the four-decimal Evaluation Summary.",
44
+ "class_precision_note": "Percent metrics transcribed from the displayed per-class table; rounding follows the screenshot.",
45
+ "complexity_note": "Parameters and GFLOPs transcribed from the displayed model summary; profiling convention awaits confirmation."
46
+ },
47
+ "yolov8n_denselitex_c2psa": {
48
+ "c2f_repeats": 1,
49
+ "split": "test",
50
+ "run_name": "yolov8n_denselitex_c2psa_fs",
51
+ "source_screenshot": "image(20261011-155504).png",
52
+ "aggregate_precision_note": "Percent metrics transcribed from the four-decimal Evaluation Summary.",
53
+ "class_precision_note": "Percent metrics transcribed from the displayed per-class table; rounding follows the screenshot.",
54
+ "complexity_note": "Parameters and GFLOPs transcribed from the displayed model summary; profiling convention awaits confirmation."
55
+ },
56
+ "yolov8n_densenet121_sppf": {
57
+ "c2f_repeats": 1,
58
+ "split": "test",
59
+ "run_name": "yolov8n_densenet121_sppf_fs",
60
+ "source_screenshot": "image(20261011-155804).png",
61
+ "aggregate_precision_note": "Percent metrics transcribed from the four-decimal Evaluation Summary.",
62
+ "class_precision_note": "Percent metrics transcribed from the displayed per-class table; rounding follows the screenshot.",
63
+ "complexity_note": "Parameters and GFLOPs transcribed from the displayed model summary; profiling convention awaits confirmation."
64
+ },
65
+ "yolov8n_densenet121_c2psa": {
66
+ "c2f_repeats": 1,
67
+ "split": "test",
68
+ "run_name": "yolov8n_densenet121_c2psa_fs",
69
+ "source_screenshot": "image(20261011-155818).png",
70
+ "aggregate_precision_note": "Percent metrics transcribed from the four-decimal Evaluation Summary.",
71
+ "class_precision_note": "Percent metrics transcribed from the displayed per-class table; rounding follows the screenshot.",
72
+ "complexity_note": "Parameters and GFLOPs transcribed from the displayed model summary; profiling convention awaits confirmation."
73
+ },
74
+ "yolov8n_densenet121_sppf_c2psa": {
75
+ "c2f_repeats": 1,
76
+ "split": "test",
77
+ "run_name": "yolov8n_densenet121_sppf_c2psa_fs",
78
+ "source_screenshot": "image(20261011-160157).png",
79
+ "aggregate_precision_note": "Percent metrics transcribed from the four-decimal Evaluation Summary.",
80
+ "class_precision_note": "Percent metrics transcribed from the displayed per-class table; rounding follows the screenshot.",
81
+ "complexity_note": "Parameters and GFLOPs transcribed from the displayed model summary; profiling convention awaits confirmation."
82
+ },
83
+ "yolov8n": {
84
+ "split": "test",
85
+ "run_name": "yolov8n_fs1",
86
+ "source_screenshot": "image(20261013-075958).png",
87
+ "aggregate_precision_note": "Percent metrics transcribed from the four-decimal Evaluation Summary.",
88
+ "class_precision_note": "Percent metrics transcribed from the three-decimal displayed per-class table.",
89
+ "complexity_note": "Fused model summary: 3,007,403 parameters and 8.1 GFLOPs.",
90
+ "training_runtime": {
91
+ "python": "3.10.20",
92
+ "ultralytics": "8.4.70",
93
+ "torch": "2.5.1+cu121",
94
+ "device": "NVIDIA GeForce RTX 4060 Laptop GPU"
95
+ }
96
+ }
97
+ },
98
+ "last_results_update": "baseline1-charts-20261013"
99
+ },
100
+ "classes": [
101
+ {
102
+ "id": 0,
103
+ "name": "Rice__BacterialLeafBlight",
104
+ "images": 183,
105
+ "instances": 209,
106
+ "models": {
107
+ "yolov8n": {
108
+ "precision": 92.8,
109
+ "recall": 93.3,
110
+ "map50": 95.2,
111
+ "map5095": 70.6
112
+ },
113
+ "yolov8n_densenet121": {
114
+ "precision": 97.2,
115
+ "recall": 90.0,
116
+ "map50": 92.6,
117
+ "map5095": 74.8
118
+ },
119
+ "yolov8n_denselitex_sppf_c2psa": {
120
+ "precision": 95.7,
121
+ "recall": 90.4,
122
+ "map50": 94.3,
123
+ "map5095": 72.9
124
+ },
125
+ "yolov8n_densenet121_c2psa": {
126
+ "precision": 99.0,
127
+ "recall": 90.3,
128
+ "map50": 93.6,
129
+ "map5095": 79.4
130
+ },
131
+ "yolov8n_densenet121_sppf_c2psa": {
132
+ "precision": 97.5,
133
+ "recall": 92.8,
134
+ "map50": 93.1,
135
+ "map5095": 79.0
136
+ },
137
+ "yolov8n_denselitex": {
138
+ "precision": 90.6,
139
+ "recall": 88.0,
140
+ "map50": 91.8,
141
+ "map5095": 65.2
142
+ },
143
+ "yolov8n_denselitex_sppf": {
144
+ "precision": 94.5,
145
+ "recall": 90.0,
146
+ "map50": 93.6,
147
+ "map5095": 70.0
148
+ },
149
+ "yolov8n_denselitex_c2psa": {
150
+ "precision": 94.0,
151
+ "recall": 90.0,
152
+ "map50": 93.5,
153
+ "map5095": 68.5
154
+ },
155
+ "yolov8n_densenet121_sppf": {
156
+ "precision": 96.8,
157
+ "recall": 90.0,
158
+ "map50": 92.9,
159
+ "map5095": 78.0
160
+ }
161
+ }
162
+ },
163
+ {
164
+ "id": 1,
165
+ "name": "Rice__BrownSpot",
166
+ "images": 174,
167
+ "instances": 1280,
168
+ "models": {
169
+ "yolov8n": {
170
+ "precision": 88.6,
171
+ "recall": 84.7,
172
+ "map50": 91.2,
173
+ "map5095": 58.1
174
+ },
175
+ "yolov8n_densenet121": {
176
+ "precision": 96.4,
177
+ "recall": 86.5,
178
+ "map50": 94.6,
179
+ "map5095": 62.9
180
+ },
181
+ "yolov8n_denselitex_sppf_c2psa": {
182
+ "precision": 90.6,
183
+ "recall": 85.3,
184
+ "map50": 93.1,
185
+ "map5095": 59.9
186
+ },
187
+ "yolov8n_densenet121_c2psa": {
188
+ "precision": 96.9,
189
+ "recall": 86.6,
190
+ "map50": 95.6,
191
+ "map5095": 65.7
192
+ },
193
+ "yolov8n_densenet121_sppf_c2psa": {
194
+ "precision": 93.7,
195
+ "recall": 90.7,
196
+ "map50": 95.3,
197
+ "map5095": 66.4
198
+ },
199
+ "yolov8n_denselitex": {
200
+ "precision": 90.5,
201
+ "recall": 76.7,
202
+ "map50": 88.8,
203
+ "map5095": 50.7
204
+ },
205
+ "yolov8n_denselitex_sppf": {
206
+ "precision": 91.5,
207
+ "recall": 78.5,
208
+ "map50": 91.0,
209
+ "map5095": 55.9
210
+ },
211
+ "yolov8n_denselitex_c2psa": {
212
+ "precision": 92.1,
213
+ "recall": 79.9,
214
+ "map50": 91.2,
215
+ "map5095": 54.7
216
+ },
217
+ "yolov8n_densenet121_sppf": {
218
+ "precision": 93.4,
219
+ "recall": 89.1,
220
+ "map50": 94.8,
221
+ "map5095": 64.9
222
+ }
223
+ }
224
+ },
225
+ {
226
+ "id": 2,
227
+ "name": "Rice__Healthy",
228
+ "images": 44,
229
+ "instances": 69,
230
+ "models": {
231
+ "yolov8n": {
232
+ "precision": 72.6,
233
+ "recall": 52.2,
234
+ "map50": 53.4,
235
+ "map5095": 33.2
236
+ },
237
+ "yolov8n_densenet121": {
238
+ "precision": 68.0,
239
+ "recall": 44.9,
240
+ "map50": 49.4,
241
+ "map5095": 31.7
242
+ },
243
+ "yolov8n_denselitex_sppf_c2psa": {
244
+ "precision": 66.9,
245
+ "recall": 42.0,
246
+ "map50": 50.0,
247
+ "map5095": 32.0
248
+ },
249
+ "yolov8n_densenet121_c2psa": {
250
+ "precision": 70.6,
251
+ "recall": 49.3,
252
+ "map50": 51.1,
253
+ "map5095": 32.5
254
+ },
255
+ "yolov8n_densenet121_sppf_c2psa": {
256
+ "precision": 71.4,
257
+ "recall": 47.1,
258
+ "map50": 51.4,
259
+ "map5095": 35.5
260
+ },
261
+ "yolov8n_denselitex": {
262
+ "precision": 65.6,
263
+ "recall": 40.6,
264
+ "map50": 51.5,
265
+ "map5095": 31.4
266
+ },
267
+ "yolov8n_denselitex_sppf": {
268
+ "precision": 70.1,
269
+ "recall": 40.7,
270
+ "map50": 48.6,
271
+ "map5095": 32.8
272
+ },
273
+ "yolov8n_denselitex_c2psa": {
274
+ "precision": 61.1,
275
+ "recall": 40.6,
276
+ "map50": 47.9,
277
+ "map5095": 30.6
278
+ },
279
+ "yolov8n_densenet121_sppf": {
280
+ "precision": 68.6,
281
+ "recall": 43.5,
282
+ "map50": 51.2,
283
+ "map5095": 31.0
284
+ }
285
+ }
286
+ },
287
+ {
288
+ "id": 3,
289
+ "name": "Rice__Hispa",
290
+ "images": 44,
291
+ "instances": 89,
292
+ "models": {
293
+ "yolov8n": {
294
+ "precision": 78.4,
295
+ "recall": 84.3,
296
+ "map50": 80.3,
297
+ "map5095": 53.0
298
+ },
299
+ "yolov8n_densenet121": {
300
+ "precision": 85.0,
301
+ "recall": 80.9,
302
+ "map50": 82.2,
303
+ "map5095": 52.7
304
+ },
305
+ "yolov8n_denselitex_sppf_c2psa": {
306
+ "precision": 84.9,
307
+ "recall": 79.8,
308
+ "map50": 79.6,
309
+ "map5095": 52.2
310
+ },
311
+ "yolov8n_densenet121_c2psa": {
312
+ "precision": 88.5,
313
+ "recall": 78.7,
314
+ "map50": 84.1,
315
+ "map5095": 55.1
316
+ },
317
+ "yolov8n_densenet121_sppf_c2psa": {
318
+ "precision": 86.7,
319
+ "recall": 80.9,
320
+ "map50": 81.6,
321
+ "map5095": 53.0
322
+ },
323
+ "yolov8n_denselitex": {
324
+ "precision": 79.3,
325
+ "recall": 77.5,
326
+ "map50": 77.8,
327
+ "map5095": 48.9
328
+ },
329
+ "yolov8n_denselitex_sppf": {
330
+ "precision": 83.2,
331
+ "recall": 76.4,
332
+ "map50": 79.3,
333
+ "map5095": 50.7
334
+ },
335
+ "yolov8n_denselitex_c2psa": {
336
+ "precision": 77.1,
337
+ "recall": 78.7,
338
+ "map50": 80.1,
339
+ "map5095": 51.9
340
+ },
341
+ "yolov8n_densenet121_sppf": {
342
+ "precision": 85.6,
343
+ "recall": 80.4,
344
+ "map50": 81.0,
345
+ "map5095": 52.2
346
+ }
347
+ }
348
+ },
349
+ {
350
+ "id": 4,
351
+ "name": "Rice__LeafBlast",
352
+ "images": 46,
353
+ "instances": 70,
354
+ "models": {
355
+ "yolov8n": {
356
+ "precision": 65.0,
357
+ "recall": 64.3,
358
+ "map50": 64.0,
359
+ "map5095": 40.3
360
+ },
361
+ "yolov8n_densenet121": {
362
+ "precision": 68.4,
363
+ "recall": 51.4,
364
+ "map50": 63.9,
365
+ "map5095": 40.9
366
+ },
367
+ "yolov8n_denselitex_sppf_c2psa": {
368
+ "precision": 71.7,
369
+ "recall": 57.8,
370
+ "map50": 67.0,
371
+ "map5095": 41.9
372
+ },
373
+ "yolov8n_densenet121_c2psa": {
374
+ "precision": 78.8,
375
+ "recall": 61.4,
376
+ "map50": 69.2,
377
+ "map5095": 43.0
378
+ },
379
+ "yolov8n_densenet121_sppf_c2psa": {
380
+ "precision": 64.9,
381
+ "recall": 63.4,
382
+ "map50": 66.2,
383
+ "map5095": 41.3
384
+ },
385
+ "yolov8n_denselitex": {
386
+ "precision": 75.1,
387
+ "recall": 50.0,
388
+ "map50": 62.0,
389
+ "map5095": 34.6
390
+ },
391
+ "yolov8n_denselitex_sppf": {
392
+ "precision": 67.3,
393
+ "recall": 52.9,
394
+ "map50": 60.3,
395
+ "map5095": 38.7
396
+ },
397
+ "yolov8n_denselitex_c2psa": {
398
+ "precision": 68.6,
399
+ "recall": 58.6,
400
+ "map50": 65.8,
401
+ "map5095": 37.6
402
+ },
403
+ "yolov8n_densenet121_sppf": {
404
+ "precision": 71.4,
405
+ "recall": 65.7,
406
+ "map50": 65.6,
407
+ "map5095": 42.2
408
+ }
409
+ }
410
+ },
411
+ {
412
+ "id": 5,
413
+ "name": "Rice__LeafScald",
414
+ "images": 46,
415
+ "instances": 51,
416
+ "models": {
417
+ "yolov8n": {
418
+ "precision": 80.3,
419
+ "recall": 79.9,
420
+ "map50": 85.6,
421
+ "map5095": 53.4
422
+ },
423
+ "yolov8n_densenet121": {
424
+ "precision": 84.1,
425
+ "recall": 72.6,
426
+ "map50": 80.6,
427
+ "map5095": 56.0
428
+ },
429
+ "yolov8n_denselitex_sppf_c2psa": {
430
+ "precision": 81.5,
431
+ "recall": 80.4,
432
+ "map50": 89.0,
433
+ "map5095": 60.6
434
+ },
435
+ "yolov8n_densenet121_c2psa": {
436
+ "precision": 88.3,
437
+ "recall": 76.5,
438
+ "map50": 86.6,
439
+ "map5095": 55.3
440
+ },
441
+ "yolov8n_densenet121_sppf_c2psa": {
442
+ "precision": 80.9,
443
+ "recall": 80.4,
444
+ "map50": 85.0,
445
+ "map5095": 56.5
446
+ },
447
+ "yolov8n_denselitex": {
448
+ "precision": 84.5,
449
+ "recall": 74.5,
450
+ "map50": 83.9,
451
+ "map5095": 48.2
452
+ },
453
+ "yolov8n_denselitex_sppf": {
454
+ "precision": 87.3,
455
+ "recall": 82.4,
456
+ "map50": 87.3,
457
+ "map5095": 58.2
458
+ },
459
+ "yolov8n_denselitex_c2psa": {
460
+ "precision": 78.9,
461
+ "recall": 74.5,
462
+ "map50": 78.1,
463
+ "map5095": 51.1
464
+ },
465
+ "yolov8n_densenet121_sppf": {
466
+ "precision": 83.9,
467
+ "recall": 81.9,
468
+ "map50": 87.9,
469
+ "map5095": 56.4
470
+ }
471
+ }
472
+ },
473
+ {
474
+ "id": 6,
475
+ "name": "Rice__LeafSmut",
476
+ "images": 175,
477
+ "instances": 535,
478
+ "models": {
479
+ "yolov8n": {
480
+ "precision": 94.4,
481
+ "recall": 93.8,
482
+ "map50": 97.7,
483
+ "map5095": 74.4
484
+ },
485
+ "yolov8n_densenet121": {
486
+ "precision": 97.4,
487
+ "recall": 96.2,
488
+ "map50": 98.0,
489
+ "map5095": 77.5
490
+ },
491
+ "yolov8n_denselitex_sppf_c2psa": {
492
+ "precision": 95.8,
493
+ "recall": 96.1,
494
+ "map50": 98.2,
495
+ "map5095": 76.4
496
+ },
497
+ "yolov8n_densenet121_c2psa": {
498
+ "precision": 98.4,
499
+ "recall": 95.7,
500
+ "map50": 97.7,
501
+ "map5095": 81.9
502
+ },
503
+ "yolov8n_densenet121_sppf_c2psa": {
504
+ "precision": 97.7,
505
+ "recall": 96.3,
506
+ "map50": 98.0,
507
+ "map5095": 81.8
508
+ },
509
+ "yolov8n_denselitex": {
510
+ "precision": 95.0,
511
+ "recall": 94.0,
512
+ "map50": 96.7,
513
+ "map5095": 67.2
514
+ },
515
+ "yolov8n_denselitex_sppf": {
516
+ "precision": 96.5,
517
+ "recall": 95.0,
518
+ "map50": 97.8,
519
+ "map5095": 73.8
520
+ },
521
+ "yolov8n_denselitex_c2psa": {
522
+ "precision": 94.6,
523
+ "recall": 93.8,
524
+ "map50": 97.1,
525
+ "map5095": 73.0
526
+ },
527
+ "yolov8n_densenet121_sppf": {
528
+ "precision": 97.0,
529
+ "recall": 95.8,
530
+ "map50": 97.7,
531
+ "map5095": 80.5
532
+ }
533
+ }
534
+ },
535
+ {
536
+ "id": 7,
537
+ "name": "Rice__NarrowBrownLeafSpot",
538
+ "images": 45,
539
+ "instances": 66,
540
+ "models": {
541
+ "yolov8n": {
542
+ "precision": 71.9,
543
+ "recall": 69.7,
544
+ "map50": 73.2,
545
+ "map5095": 49.5
546
+ },
547
+ "yolov8n_densenet121": {
548
+ "precision": 76.5,
549
+ "recall": 65.2,
550
+ "map50": 73.2,
551
+ "map5095": 48.0
552
+ },
553
+ "yolov8n_denselitex_sppf_c2psa": {
554
+ "precision": 74.5,
555
+ "recall": 63.6,
556
+ "map50": 73.0,
557
+ "map5095": 45.4
558
+ },
559
+ "yolov8n_densenet121_c2psa": {
560
+ "precision": 75.7,
561
+ "recall": 66.7,
562
+ "map50": 75.5,
563
+ "map5095": 48.6
564
+ },
565
+ "yolov8n_densenet121_sppf_c2psa": {
566
+ "precision": 71.2,
567
+ "recall": 74.2,
568
+ "map50": 79.0,
569
+ "map5095": 48.9
570
+ },
571
+ "yolov8n_denselitex": {
572
+ "precision": 77.3,
573
+ "recall": 60.6,
574
+ "map50": 70.7,
575
+ "map5095": 41.7
576
+ },
577
+ "yolov8n_denselitex_sppf": {
578
+ "precision": 73.7,
579
+ "recall": 62.1,
580
+ "map50": 70.3,
581
+ "map5095": 43.3
582
+ },
583
+ "yolov8n_denselitex_c2psa": {
584
+ "precision": 65.6,
585
+ "recall": 60.6,
586
+ "map50": 65.0,
587
+ "map5095": 37.8
588
+ },
589
+ "yolov8n_densenet121_sppf": {
590
+ "precision": 79.5,
591
+ "recall": 70.4,
592
+ "map50": 76.1,
593
+ "map5095": 48.3
594
+ }
595
+ }
596
+ },
597
+ {
598
+ "id": 8,
599
+ "name": "Rice__NeckBlast",
600
+ "images": 46,
601
+ "instances": 97,
602
+ "models": {
603
+ "yolov8n": {
604
+ "precision": 87.1,
605
+ "recall": 83.9,
606
+ "map50": 87.7,
607
+ "map5095": 60.0
608
+ },
609
+ "yolov8n_densenet121": {
610
+ "precision": 89.4,
611
+ "recall": 85.6,
612
+ "map50": 91.7,
613
+ "map5095": 60.3
614
+ },
615
+ "yolov8n_denselitex_sppf_c2psa": {
616
+ "precision": 88.0,
617
+ "recall": 85.6,
618
+ "map50": 90.7,
619
+ "map5095": 61.1
620
+ },
621
+ "yolov8n_densenet121_c2psa": {
622
+ "precision": 88.6,
623
+ "recall": 83.5,
624
+ "map50": 89.7,
625
+ "map5095": 58.3
626
+ },
627
+ "yolov8n_densenet121_sppf_c2psa": {
628
+ "precision": 90.2,
629
+ "recall": 85.1,
630
+ "map50": 90.5,
631
+ "map5095": 62.3
632
+ },
633
+ "yolov8n_denselitex": {
634
+ "precision": 86.1,
635
+ "recall": 84.5,
636
+ "map50": 86.1,
637
+ "map5095": 54.7
638
+ },
639
+ "yolov8n_denselitex_sppf": {
640
+ "precision": 88.9,
641
+ "recall": 86.6,
642
+ "map50": 91.3,
643
+ "map5095": 61.9
644
+ },
645
+ "yolov8n_denselitex_c2psa": {
646
+ "precision": 78.5,
647
+ "recall": 84.5,
648
+ "map50": 86.7,
649
+ "map5095": 52.2
650
+ },
651
+ "yolov8n_densenet121_sppf": {
652
+ "precision": 93.0,
653
+ "recall": 83.5,
654
+ "map50": 90.1,
655
+ "map5095": 61.0
656
+ }
657
+ }
658
+ }
659
+ ]
660
+ }
results.json CHANGED
@@ -1,210 +1,225 @@
1
- {
2
- "context": {
3
- "source": "Test-set evaluation results from my paddy disease detection experiments.",
4
- "images": 803,
5
- "objects": 2466,
6
- "classes": 9,
7
- "runs": "Each model was evaluated using one experimental run.",
8
- "flops_note": "GFLOPs are taken from the model summaries.",
9
- "selection_note": "The charts summarize performance on the held-out test set. They do not represent the performance on an individually uploaded image.",
10
- "model_evaluations": {
11
- "yolov8n_densenet121": {
12
- "c2f_repeats": 1,
13
- "split": "test",
14
- "run_name": "yolov8n_densenet121_fs",
15
- "source_screenshot": "image(20261011-154943).png",
16
- "aggregate_precision_note": "Percent metrics transcribed from the four-decimal Evaluation Summary.",
17
- "class_precision_note": "Percent metrics transcribed from the displayed per-class table; rounding follows the screenshot.",
18
- "complexity_note": "Parameters and GFLOPs transcribed from the displayed model summary; profiling convention awaits confirmation."
19
- },
20
- "yolov8n_denselitex_sppf_c2psa": {
21
- "c2f_repeats": 1,
22
- "split": "test",
23
- "run_name": "yolov8n_denselitex_sppf_c2psa_fs",
24
- "source_screenshot": "image(20261011-155020).png",
25
- "aggregate_precision_note": "Percent metrics transcribed from the four-decimal Evaluation Summary.",
26
- "class_precision_note": "Percent metrics transcribed from the displayed per-class table; rounding follows the screenshot.",
27
- "complexity_note": "Parameters and GFLOPs transcribed from the displayed model summary; profiling convention awaits confirmation."
28
- },
29
- "yolov8n_denselitex": {
30
- "c2f_repeats": 1,
31
- "split": "test",
32
- "run_name": "yolov8n_denselitex_fs",
33
- "source_screenshot": "image(20261011-155244).png",
34
- "aggregate_precision_note": "Percent metrics transcribed from the four-decimal Evaluation Summary.",
35
- "class_precision_note": "Percent metrics transcribed from the displayed per-class table; rounding follows the screenshot.",
36
- "complexity_note": "Parameters and GFLOPs transcribed from the displayed model summary; profiling convention awaits confirmation."
37
- },
38
- "yolov8n_denselitex_sppf": {
39
- "c2f_repeats": 1,
40
- "split": "test",
41
- "run_name": "yolov8n_denselitex_sppf_fs",
42
- "source_screenshot": "image(20261011-155432).png",
43
- "aggregate_precision_note": "Percent metrics transcribed from the four-decimal Evaluation Summary.",
44
- "class_precision_note": "Percent metrics transcribed from the displayed per-class table; rounding follows the screenshot.",
45
- "complexity_note": "Parameters and GFLOPs transcribed from the displayed model summary; profiling convention awaits confirmation."
46
- },
47
- "yolov8n_denselitex_c2psa": {
48
- "c2f_repeats": 1,
49
- "split": "test",
50
- "run_name": "yolov8n_denselitex_c2psa_fs",
51
- "source_screenshot": "image(20261011-155504).png",
52
- "aggregate_precision_note": "Percent metrics transcribed from the four-decimal Evaluation Summary.",
53
- "class_precision_note": "Percent metrics transcribed from the displayed per-class table; rounding follows the screenshot.",
54
- "complexity_note": "Parameters and GFLOPs transcribed from the displayed model summary; profiling convention awaits confirmation."
55
- },
56
- "yolov8n_densenet121_sppf": {
57
- "c2f_repeats": 1,
58
- "split": "test",
59
- "run_name": "yolov8n_densenet121_sppf_fs",
60
- "source_screenshot": "image(20261011-155804).png",
61
- "aggregate_precision_note": "Percent metrics transcribed from the four-decimal Evaluation Summary.",
62
- "class_precision_note": "Percent metrics transcribed from the displayed per-class table; rounding follows the screenshot.",
63
- "complexity_note": "Parameters and GFLOPs transcribed from the displayed model summary; profiling convention awaits confirmation."
64
- },
65
- "yolov8n_densenet121_c2psa": {
66
- "c2f_repeats": 1,
67
- "split": "test",
68
- "run_name": "yolov8n_densenet121_c2psa_fs",
69
- "source_screenshot": "image(20261011-155818).png",
70
- "aggregate_precision_note": "Percent metrics transcribed from the four-decimal Evaluation Summary.",
71
- "class_precision_note": "Percent metrics transcribed from the displayed per-class table; rounding follows the screenshot.",
72
- "complexity_note": "Parameters and GFLOPs transcribed from the displayed model summary; profiling convention awaits confirmation."
73
- },
74
- "yolov8n_densenet121_sppf_c2psa": {
75
- "c2f_repeats": 1,
76
- "split": "test",
77
- "run_name": "yolov8n_densenet121_sppf_c2psa_fs",
78
- "source_screenshot": "image(20261011-160157).png",
79
- "aggregate_precision_note": "Percent metrics transcribed from the four-decimal Evaluation Summary.",
80
- "class_precision_note": "Percent metrics transcribed from the displayed per-class table; rounding follows the screenshot.",
81
- "complexity_note": "Parameters and GFLOPs transcribed from the displayed model summary; profiling convention awaits confirmation."
82
- }
83
- },
84
- "last_results_update": "c2f1-results-20261012-batch2"
85
- },
86
- "main": [
87
- {
88
- "id": "yolov8n",
89
- "label": "YOLOv8n",
90
- "role": "Baseline 1",
91
- "precision": 83.68,
92
- "recall": 76.61,
93
- "map50": 80.79,
94
- "map5095": 54.66,
95
- "parameters": 3007403,
96
- "gflops": 8.1,
97
- "color": "#4277ef"
98
- },
99
- {
100
- "id": "yolov8n_densenet121",
101
- "label": "Yolov8n_DenseNet121",
102
- "role": "Baseline 2",
103
- "precision": 84.71,
104
- "recall": 74.79,
105
- "map50": 80.69,
106
- "map5095": 56.08,
107
- "parameters": 9002123,
108
- "gflops": 51.8,
109
- "color": "#8265d5"
110
- },
111
- {
112
- "id": "yolov8n_denselitex_sppf_c2psa",
113
- "label": "DenseLiteX + SPPF + C2PSA",
114
- "role": "Proposed",
115
- "precision": 83.29,
116
- "recall": 75.67,
117
- "map50": 81.67,
118
- "map5095": 55.83,
119
- "parameters": 2879917,
120
- "gflops": 14.2,
121
- "color": "#129a70"
122
- }
123
- ],
124
- "ablation": [
125
- {
126
- "id": "yolov8n_densenet121_sppf",
127
- "label": "Yolov8n_DenseNet121 + SPPF",
128
- "role": "Ablation",
129
- "precision": 85.46,
130
- "recall": 77.8,
131
- "map50": 81.92,
132
- "map5095": 57.18,
133
- "parameters": 9166347,
134
- "gflops": 51.9,
135
- "color": "#8265d5"
136
- },
137
- {
138
- "id": "yolov8n_densenet121_c2psa",
139
- "label": "Yolov8n_DenseNet121 + C2PSA",
140
- "role": "Ablation",
141
- "precision": 87.19,
142
- "recall": 76.51,
143
- "map50": 82.55,
144
- "map5095": 57.75,
145
- "parameters": 9250443,
146
- "gflops": 52.0,
147
- "color": "#597bc8"
148
- },
149
- {
150
- "id": "yolov8n_densenet121_sppf_c2psa",
151
- "label": "Yolov8n_DenseNet121 + SPPF + C2PSA",
152
- "role": "Ablation",
153
- "precision": 83.81,
154
- "recall": 79.01,
155
- "map50": 82.25,
156
- "map5095": 58.3,
157
- "parameters": 9414667,
158
- "gflops": 52.1,
159
- "color": "#af63b7"
160
- },
161
- {
162
- "id": "yolov8n_denselitex",
163
- "label": "DenseLiteX",
164
- "role": "Ablation",
165
- "precision": 82.67,
166
- "recall": 71.84,
167
- "map50": 78.82,
168
- "map5095": 49.16,
169
- "parameters": 2467373,
170
- "gflops": 13.9,
171
- "color": "#558da5"
172
- },
173
- {
174
- "id": "yolov8n_denselitex_sppf",
175
- "label": "DenseLiteX + SPPF",
176
- "role": "Ablation",
177
- "precision": 83.67,
178
- "recall": 73.82,
179
- "map50": 79.94,
180
- "map5095": 53.92,
181
- "parameters": 2631597,
182
- "gflops": 14.0,
183
- "color": "#e09f43"
184
- },
185
- {
186
- "id": "yolov8n_denselitex_c2psa",
187
- "label": "DenseLiteX + C2PSA",
188
- "role": "Ablation",
189
- "precision": 78.95,
190
- "recall": 73.47,
191
- "map50": 78.39,
192
- "map5095": 50.82,
193
- "parameters": 2715693,
194
- "gflops": 14.1,
195
- "color": "#d36586"
196
- },
197
- {
198
- "id": "yolov8n_denselitex_sppf_c2psa",
199
- "label": "DenseLiteX + SPPF + C2PSA",
200
- "role": "Proposed",
201
- "precision": 83.29,
202
- "recall": 75.67,
203
- "map50": 81.67,
204
- "map5095": 55.83,
205
- "parameters": 2879917,
206
- "gflops": 14.2,
207
- "color": "#129a70"
208
- }
209
- ]
210
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "context": {
3
+ "source": "Test-set evaluation results from my paddy disease detection experiments.",
4
+ "images": 803,
5
+ "objects": 2466,
6
+ "classes": 9,
7
+ "runs": "Each model was evaluated using one experimental run.",
8
+ "flops_note": "GFLOPs are taken from the model summaries.",
9
+ "selection_note": "The charts summarize performance on the held-out test set. They do not represent the performance on an individually uploaded image.",
10
+ "model_evaluations": {
11
+ "yolov8n_densenet121": {
12
+ "c2f_repeats": 1,
13
+ "split": "test",
14
+ "run_name": "yolov8n_densenet121_fs",
15
+ "source_screenshot": "image(20261011-154943).png",
16
+ "aggregate_precision_note": "Percent metrics transcribed from the four-decimal Evaluation Summary.",
17
+ "class_precision_note": "Percent metrics transcribed from the displayed per-class table; rounding follows the screenshot.",
18
+ "complexity_note": "Parameters and GFLOPs transcribed from the displayed model summary; profiling convention awaits confirmation."
19
+ },
20
+ "yolov8n_denselitex_sppf_c2psa": {
21
+ "c2f_repeats": 1,
22
+ "split": "test",
23
+ "run_name": "yolov8n_denselitex_sppf_c2psa_fs",
24
+ "source_screenshot": "image(20261011-155020).png",
25
+ "aggregate_precision_note": "Percent metrics transcribed from the four-decimal Evaluation Summary.",
26
+ "class_precision_note": "Percent metrics transcribed from the displayed per-class table; rounding follows the screenshot.",
27
+ "complexity_note": "Parameters and GFLOPs transcribed from the displayed model summary; profiling convention awaits confirmation."
28
+ },
29
+ "yolov8n_denselitex": {
30
+ "c2f_repeats": 1,
31
+ "split": "test",
32
+ "run_name": "yolov8n_denselitex_fs",
33
+ "source_screenshot": "image(20261011-155244).png",
34
+ "aggregate_precision_note": "Percent metrics transcribed from the four-decimal Evaluation Summary.",
35
+ "class_precision_note": "Percent metrics transcribed from the displayed per-class table; rounding follows the screenshot.",
36
+ "complexity_note": "Parameters and GFLOPs transcribed from the displayed model summary; profiling convention awaits confirmation."
37
+ },
38
+ "yolov8n_denselitex_sppf": {
39
+ "c2f_repeats": 1,
40
+ "split": "test",
41
+ "run_name": "yolov8n_denselitex_sppf_fs",
42
+ "source_screenshot": "image(20261011-155432).png",
43
+ "aggregate_precision_note": "Percent metrics transcribed from the four-decimal Evaluation Summary.",
44
+ "class_precision_note": "Percent metrics transcribed from the displayed per-class table; rounding follows the screenshot.",
45
+ "complexity_note": "Parameters and GFLOPs transcribed from the displayed model summary; profiling convention awaits confirmation."
46
+ },
47
+ "yolov8n_denselitex_c2psa": {
48
+ "c2f_repeats": 1,
49
+ "split": "test",
50
+ "run_name": "yolov8n_denselitex_c2psa_fs",
51
+ "source_screenshot": "image(20261011-155504).png",
52
+ "aggregate_precision_note": "Percent metrics transcribed from the four-decimal Evaluation Summary.",
53
+ "class_precision_note": "Percent metrics transcribed from the displayed per-class table; rounding follows the screenshot.",
54
+ "complexity_note": "Parameters and GFLOPs transcribed from the displayed model summary; profiling convention awaits confirmation."
55
+ },
56
+ "yolov8n_densenet121_sppf": {
57
+ "c2f_repeats": 1,
58
+ "split": "test",
59
+ "run_name": "yolov8n_densenet121_sppf_fs",
60
+ "source_screenshot": "image(20261011-155804).png",
61
+ "aggregate_precision_note": "Percent metrics transcribed from the four-decimal Evaluation Summary.",
62
+ "class_precision_note": "Percent metrics transcribed from the displayed per-class table; rounding follows the screenshot.",
63
+ "complexity_note": "Parameters and GFLOPs transcribed from the displayed model summary; profiling convention awaits confirmation."
64
+ },
65
+ "yolov8n_densenet121_c2psa": {
66
+ "c2f_repeats": 1,
67
+ "split": "test",
68
+ "run_name": "yolov8n_densenet121_c2psa_fs",
69
+ "source_screenshot": "image(20261011-155818).png",
70
+ "aggregate_precision_note": "Percent metrics transcribed from the four-decimal Evaluation Summary.",
71
+ "class_precision_note": "Percent metrics transcribed from the displayed per-class table; rounding follows the screenshot.",
72
+ "complexity_note": "Parameters and GFLOPs transcribed from the displayed model summary; profiling convention awaits confirmation."
73
+ },
74
+ "yolov8n_densenet121_sppf_c2psa": {
75
+ "c2f_repeats": 1,
76
+ "split": "test",
77
+ "run_name": "yolov8n_densenet121_sppf_c2psa_fs",
78
+ "source_screenshot": "image(20261011-160157).png",
79
+ "aggregate_precision_note": "Percent metrics transcribed from the four-decimal Evaluation Summary.",
80
+ "class_precision_note": "Percent metrics transcribed from the displayed per-class table; rounding follows the screenshot.",
81
+ "complexity_note": "Parameters and GFLOPs transcribed from the displayed model summary; profiling convention awaits confirmation."
82
+ },
83
+ "yolov8n": {
84
+ "split": "test",
85
+ "run_name": "yolov8n_fs1",
86
+ "source_screenshot": "image(20261013-075958).png",
87
+ "aggregate_precision_note": "Percent metrics transcribed from the four-decimal Evaluation Summary.",
88
+ "class_precision_note": "Percent metrics transcribed from the three-decimal displayed per-class table.",
89
+ "complexity_note": "Fused model summary: 3,007,403 parameters and 8.1 GFLOPs.",
90
+ "training_runtime": {
91
+ "python": "3.10.20",
92
+ "ultralytics": "8.4.70",
93
+ "torch": "2.5.1+cu121",
94
+ "device": "NVIDIA GeForce RTX 4060 Laptop GPU"
95
+ }
96
+ }
97
+ },
98
+ "last_results_update": "baseline1-charts-20261013"
99
+ },
100
+ "main": [
101
+ {
102
+ "id": "yolov8n",
103
+ "label": "YOLOv8n",
104
+ "role": "Baseline 1",
105
+ "precision": 81.24,
106
+ "recall": 78.45,
107
+ "map50": 80.94,
108
+ "map5095": 54.72,
109
+ "parameters": 3007403,
110
+ "gflops": 8.1,
111
+ "color": "#4277ef",
112
+ "checkpoint": "runs/train/yolov8n/weights/best.pt"
113
+ },
114
+ {
115
+ "id": "yolov8n_densenet121",
116
+ "label": "Yolov8n_DenseNet121",
117
+ "role": "Baseline 2",
118
+ "precision": 84.71,
119
+ "recall": 74.79,
120
+ "map50": 80.69,
121
+ "map5095": 56.08,
122
+ "parameters": 9002123,
123
+ "gflops": 51.8,
124
+ "color": "#8265d5"
125
+ },
126
+ {
127
+ "id": "yolov8n_denselitex_sppf_c2psa",
128
+ "label": "DenseLiteX + SPPF + C2PSA",
129
+ "role": "Proposed",
130
+ "precision": 83.29,
131
+ "recall": 75.67,
132
+ "map50": 81.67,
133
+ "map5095": 55.83,
134
+ "parameters": 2879917,
135
+ "gflops": 14.2,
136
+ "color": "#129a70"
137
+ }
138
+ ],
139
+ "ablation": [
140
+ {
141
+ "id": "yolov8n_densenet121_sppf",
142
+ "label": "Yolov8n_DenseNet121 + SPPF",
143
+ "role": "Ablation",
144
+ "precision": 85.46,
145
+ "recall": 77.8,
146
+ "map50": 81.92,
147
+ "map5095": 57.18,
148
+ "parameters": 9166347,
149
+ "gflops": 51.9,
150
+ "color": "#8265d5"
151
+ },
152
+ {
153
+ "id": "yolov8n_densenet121_c2psa",
154
+ "label": "Yolov8n_DenseNet121 + C2PSA",
155
+ "role": "Ablation",
156
+ "precision": 87.19,
157
+ "recall": 76.51,
158
+ "map50": 82.55,
159
+ "map5095": 57.75,
160
+ "parameters": 9250443,
161
+ "gflops": 52.0,
162
+ "color": "#597bc8"
163
+ },
164
+ {
165
+ "id": "yolov8n_densenet121_sppf_c2psa",
166
+ "label": "Yolov8n_DenseNet121 + SPPF + C2PSA",
167
+ "role": "Ablation",
168
+ "precision": 83.81,
169
+ "recall": 79.01,
170
+ "map50": 82.25,
171
+ "map5095": 58.3,
172
+ "parameters": 9414667,
173
+ "gflops": 52.1,
174
+ "color": "#af63b7"
175
+ },
176
+ {
177
+ "id": "yolov8n_denselitex",
178
+ "label": "DenseLiteX",
179
+ "role": "Ablation",
180
+ "precision": 82.67,
181
+ "recall": 71.84,
182
+ "map50": 78.82,
183
+ "map5095": 49.16,
184
+ "parameters": 2467373,
185
+ "gflops": 13.9,
186
+ "color": "#558da5"
187
+ },
188
+ {
189
+ "id": "yolov8n_denselitex_sppf",
190
+ "label": "DenseLiteX + SPPF",
191
+ "role": "Ablation",
192
+ "precision": 83.67,
193
+ "recall": 73.82,
194
+ "map50": 79.94,
195
+ "map5095": 53.92,
196
+ "parameters": 2631597,
197
+ "gflops": 14.0,
198
+ "color": "#e09f43"
199
+ },
200
+ {
201
+ "id": "yolov8n_denselitex_c2psa",
202
+ "label": "DenseLiteX + C2PSA",
203
+ "role": "Ablation",
204
+ "precision": 78.95,
205
+ "recall": 73.47,
206
+ "map50": 78.39,
207
+ "map5095": 50.82,
208
+ "parameters": 2715693,
209
+ "gflops": 14.1,
210
+ "color": "#d36586"
211
+ },
212
+ {
213
+ "id": "yolov8n_denselitex_sppf_c2psa",
214
+ "label": "DenseLiteX + SPPF + C2PSA",
215
+ "role": "Proposed",
216
+ "precision": 83.29,
217
+ "recall": 75.67,
218
+ "map50": 81.67,
219
+ "map5095": 55.83,
220
+ "parameters": 2879917,
221
+ "gflops": 14.2,
222
+ "color": "#129a70"
223
+ }
224
+ ]
225
+ }