From 014bf98ea24da9e3d309f5d6e192bbccfc13a9b7 Mon Sep 17 00:00:00 2001 From: Reuven Date: Wed, 11 Mar 2026 19:31:23 -0400 Subject: [PATCH] feat(demo): add Self-Learning tab with 6 interactive training demos - Live Classifier: Train custom classes with labeled examples, test classification - Few-Shot Learning: 3-class system (A/B/C) with drag-drop training - Incremental Learning: Positive/negative examples with prototype visualization - Feedback Learning: Track predictions and accuracy over time - Memory Bank: View stored embeddings, export/import as JSON - Camera Training: Train using webcam with single/auto-capture modes All demos use real CNN embeddings (512-dim) with prototypical networks for classification. Includes cosine similarity scoring and confidence bars. Co-Authored-By: claude-flow --- docs/cnn/index.html | 707 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 707 insertions(+) diff --git a/docs/cnn/index.html b/docs/cnn/index.html index 256fcab90..e37ec25b7 100644 --- a/docs/cnn/index.html +++ b/docs/cnn/index.html @@ -706,6 +706,7 @@ + @@ -1009,6 +1010,246 @@ + +
+
+ +
+
+
🧠
+
+
Live Classifier
+
Train a classifier in real-time
+
+
+
+

+ 1. Add labeled examples   2. Test on new images   3. Correct mistakes to improve +

+ +
+ + +
+ +
+ +
+

Test Classification:

+
+ 🎯 + Drop image to classify +
+ +
+
+
+
+ + +
+
+
🎯
+
+
Few-Shot Learning
+
Learn from 1-5 examples per class
+
+
+
+

+ Register classes with just a few examples, then classify new images instantly. +

+ +
+
+
+
Class A
+
Drop images
+
+ +
0 examples
+
+
+
+
Class B
+
Drop images
+
+ +
0 examples
+
+
+
+
Class C
+
Drop images
+
+ +
0 examples
+
+
+ +
+
+ 🔮 Drop to classify +
+ +
+
+
+
+ + +
+
+
📈
+
+
Incremental Learning
+
Watch accuracy improve over time
+
+
+
+

+ Add examples one by one and watch the prototype centroids evolve. +

+ +
+ +
+ 📥 Drop to add example +
+ +
+ +
+
+
0
+
Positive examples
+
+
+
0
+
Negative examples
+
+
+ +
+
+ ⚖️ Drop to test +
+ +
+
+ + +
+
+ + +
+
+
🔄
+
+
Feedback Learning
+
Correct mistakes to improve
+
+
+
+

+ The model learns from corrections. Wrong prediction? Tell it the right answer! +

+ +
+
+ Train the Live Classifier above, then test images here to provide feedback +
+
+ +
+
+
0
+
Total
+
+
+
0
+
Correct
+
+
+
+
Accuracy
+
+
+
+
+ + +
+
+
💾
+
+
Memory Bank
+
Persistent embedding storage
+
+
+
+

+ View all stored embeddings and their class assignments. +

+ +
+
+ No memories yet. Add examples to the classifiers above. +
+
+ +
+ + + +
+ +
+ 0 classes • 0 embeddings • 0 KB +
+
+
+ + +
+
+
📹
+
+
Camera Training
+
Train using your webcam
+
+
+
+
+ +
+ Ready +
+
+ +
+ + +
+ +
+ + + +
+ +
+
+
+
+
+
@@ -3114,6 +3355,472 @@ async function gestureLoop() { anomalyResult.innerHTML = `Found ${anomalyCount} potential anomaly${anomalyCount !== 1 ? 's' : ''} (red border)`; } + // ==================== SELF-LEARNING DEMOS ==================== + + // Shared classifier state + const classifier = { + classes: new Map(), // className -> { embeddings: [], color: string } + colors: ['hsl(262, 83%, 58%)', 'hsl(142, 76%, 36%)', 'hsl(38, 92%, 50%)', 'hsl(0, 62%, 50%)', 'hsl(200, 70%, 50%)', 'hsl(280, 60%, 50%)'], + colorIndex: 0, + feedbackLog: [], + totalTests: 0, + correctTests: 0 + }; + + // Helper: Extract embedding from image file + async function extractEmbeddingFromFile(file) { + return new Promise((resolve, reject) => { + const reader = new FileReader(); + reader.onload = e => { + const img = new Image(); + img.onload = () => { + const canvas = document.createElement('canvas'); + canvas.width = 224; canvas.height = 224; + canvas.getContext('2d').drawImage(img, 0, 0, 224, 224); + const imageData = canvas.getContext('2d').getImageData(0, 0, 224, 224); + const rgb = new Uint8Array(224 * 224 * 3); + for (let i = 0, j = 0; i < imageData.data.length; i += 4, j += 3) { + rgb[j] = imageData.data[i]; rgb[j+1] = imageData.data[i+1]; rgb[j+2] = imageData.data[i+2]; + } + resolve({ embedding: embedder.extract(rgb, 224, 224), dataUrl: e.target.result }); + }; + img.onerror = reject; + img.src = e.target.result; + }; + reader.onerror = reject; + reader.readAsDataURL(file); + }); + } + + // Helper: Classify using stored prototypes + function classifyEmbedding(embedding) { + if (classifier.classes.size === 0) return null; + const scores = []; + for (const [className, data] of classifier.classes) { + if (data.embeddings.length === 0) continue; + // Compute prototype (mean embedding) + const proto = computePrototype(data.embeddings); + const sim = embedder.cosine_similarity(embedding, proto); + scores.push({ className, similarity: sim, color: data.color, count: data.embeddings.length }); + } + return scores.sort((a, b) => b.similarity - a.similarity); + } + + function computePrototype(embeddings) { + const dim = embeddings[0].length; + const proto = new Float32Array(dim); + for (const emb of embeddings) { + for (let i = 0; i < dim; i++) proto[i] += emb[i]; + } + for (let i = 0; i < dim; i++) proto[i] /= embeddings.length; + const norm = Math.sqrt(proto.reduce((s, v) => s + v*v, 0)); + for (let i = 0; i < dim; i++) proto[i] /= norm; + return proto; + } + + // Live Classifier + const addClassBtn = document.getElementById('add-class-btn'); + const classNameInput = document.getElementById('class-name-input'); + const classContainers = document.getElementById('class-containers'); + const classifyDrop = document.getElementById('classify-drop'); + const classifyInput = document.getElementById('classify-input'); + const classifyResult = document.getElementById('classify-result'); + + if (addClassBtn) { + addClassBtn.onclick = () => { + const name = classNameInput.value.trim(); + if (!name || classifier.classes.has(name)) return; + const color = classifier.colors[classifier.colorIndex++ % classifier.colors.length]; + classifier.classes.set(name, { embeddings: [], color, thumbnails: [] }); + renderClassContainers(); + classNameInput.value = ''; + updateMemoryBank(); + }; + } + + function renderClassContainers() { + if (!classContainers) return; + classContainers.innerHTML = ''; + for (const [name, data] of classifier.classes) { + const container = document.createElement('div'); + container.style.cssText = `background: hsl(var(--secondary)); border: 2px solid ${data.color}; border-radius: 8px; padding: 0.5rem; min-width: 120px;`; + container.innerHTML = ` +
${name} (${data.embeddings.length})
+
+ + `; + const thumbsDiv = container.querySelector('.class-thumbs'); + const fileInput = container.querySelector('.class-file-input'); + + // Show thumbnails + data.thumbnails.slice(-6).forEach(thumb => { + const img = document.createElement('img'); + img.src = thumb; + img.style.cssText = 'width: 24px; height: 24px; object-fit: cover; border-radius: 3px;'; + thumbsDiv.appendChild(img); + }); + + // Drop zone behavior + container.ondragover = e => { e.preventDefault(); container.style.borderStyle = 'dashed'; }; + container.ondragleave = () => { container.style.borderStyle = 'solid'; }; + container.ondrop = async e => { + e.preventDefault(); + container.style.borderStyle = 'solid'; + for (const file of e.dataTransfer.files) { + if (!file.type.startsWith('image/')) continue; + const result = await extractEmbeddingFromFile(file); + data.embeddings.push(result.embedding); + data.thumbnails.push(result.dataUrl); + } + renderClassContainers(); + updateMemoryBank(); + }; + + container.onclick = () => fileInput.click(); + fileInput.onchange = async e => { + for (const file of e.target.files) { + const result = await extractEmbeddingFromFile(file); + data.embeddings.push(result.embedding); + data.thumbnails.push(result.dataUrl); + } + renderClassContainers(); + updateMemoryBank(); + }; + + classContainers.appendChild(container); + } + } + + // Classification drop zone + if (classifyDrop && classifyInput) { + classifyDrop.onclick = () => classifyInput.click(); + classifyDrop.ondragover = e => { e.preventDefault(); classifyDrop.classList.add('drag-over'); }; + classifyDrop.ondragleave = () => classifyDrop.classList.remove('drag-over'); + classifyDrop.ondrop = async e => { e.preventDefault(); classifyDrop.classList.remove('drag-over'); await doClassify(e.dataTransfer.files[0]); }; + classifyInput.onchange = async e => await doClassify(e.target.files[0]); + } + + async function doClassify(file) { + if (!file || !embedder) return; + const result = await extractEmbeddingFromFile(file); + const scores = classifyEmbedding(result.embedding); + + if (!scores || scores.length === 0) { + classifyResult.innerHTML = 'Add classes first!'; + return; + } + + const top = scores[0]; + classifyResult.innerHTML = ` +
+ +
+
${top.className}
+
Confidence: ${(top.similarity * 100).toFixed(1)}%
+
+
+
+ ${scores.slice(0, 4).map(s => `${s.className}: ${(s.similarity * 100).toFixed(0)}%`).join('')} +
+ `; + + // Add to feedback log + classifier.totalTests++; + updateFeedbackStats(); + } + + // Few-Shot Learning + const fewshotClasses = { A: [], B: [], C: [] }; + const fewshotEmbeddings = { A: [], B: [], C: [] }; + + ['a', 'b', 'c'].forEach(letter => { + const dropEl = document.getElementById(`fewshot-class-${letter}`); + const inputEl = document.getElementById(`fewshot-input-${letter}`); + const countEl = document.getElementById(`fewshot-count-${letter}`); + const classKey = letter.toUpperCase(); + + if (dropEl && inputEl) { + dropEl.onclick = () => inputEl.click(); + dropEl.ondragover = e => { e.preventDefault(); dropEl.classList.add('drag-over'); }; + dropEl.ondragleave = () => dropEl.classList.remove('drag-over'); + dropEl.ondrop = async e => { + e.preventDefault(); + dropEl.classList.remove('drag-over'); + for (const file of e.dataTransfer.files) { + if (!file.type.startsWith('image/')) continue; + const result = await extractEmbeddingFromFile(file); + fewshotEmbeddings[classKey].push(result.embedding); + } + countEl.textContent = `${fewshotEmbeddings[classKey].length} examples`; + }; + inputEl.onchange = async e => { + for (const file of e.target.files) { + const result = await extractEmbeddingFromFile(file); + fewshotEmbeddings[classKey].push(result.embedding); + } + countEl.textContent = `${fewshotEmbeddings[classKey].length} examples`; + }; + } + }); + + const fewshotTest = document.getElementById('fewshot-test'); + const fewshotTestInput = document.getElementById('fewshot-test-input'); + const fewshotResult = document.getElementById('fewshot-result'); + + if (fewshotTest && fewshotTestInput) { + fewshotTest.onclick = () => fewshotTestInput.click(); + fewshotTest.ondragover = e => { e.preventDefault(); fewshotTest.classList.add('drag-over'); }; + fewshotTest.ondragleave = () => fewshotTest.classList.remove('drag-over'); + fewshotTest.ondrop = async e => { e.preventDefault(); fewshotTest.classList.remove('drag-over'); await fewshotClassify(e.dataTransfer.files[0]); }; + fewshotTestInput.onchange = async e => await fewshotClassify(e.target.files[0]); + } + + async function fewshotClassify(file) { + if (!file || !embedder) return; + const result = await extractEmbeddingFromFile(file); + const scores = []; + const classColors = { A: 'hsl(262, 83%, 58%)', B: 'hsl(142, 76%, 36%)', C: 'hsl(38, 92%, 50%)' }; + + for (const [cls, embs] of Object.entries(fewshotEmbeddings)) { + if (embs.length === 0) continue; + const proto = computePrototype(embs); + const sim = embedder.cosine_similarity(result.embedding, proto); + scores.push({ cls, similarity: sim, color: classColors[cls] }); + } + + if (scores.length === 0) { + fewshotResult.innerHTML = 'Add examples to classes first!'; + return; + } + + scores.sort((a, b) => b.similarity - a.similarity); + fewshotResult.innerHTML = `Class ${scores[0].cls} (${(scores[0].similarity * 100).toFixed(1)}%) ` + + scores.slice(1).map(s => `${s.cls}: ${(s.similarity * 100).toFixed(0)}%`).join(' '); + } + + // Incremental Learning + const incrementalModel = { positive: [], negative: [] }; + const incPosCount = document.getElementById('inc-pos-count'); + const incNegCount = document.getElementById('inc-neg-count'); + const incrementalAdd = document.getElementById('incremental-add'); + const incrementalInput = document.getElementById('incremental-input'); + const incrementalClass = document.getElementById('incremental-class'); + const incrementalTest = document.getElementById('incremental-test'); + const incrementalTestInput = document.getElementById('incremental-test-input'); + const incrementalResult = document.getElementById('incremental-result'); + const incrementalReset = document.getElementById('incremental-reset'); + + if (incrementalAdd && incrementalInput) { + incrementalAdd.onclick = () => incrementalInput.click(); + incrementalAdd.ondragover = e => { e.preventDefault(); incrementalAdd.classList.add('drag-over'); }; + incrementalAdd.ondragleave = () => incrementalAdd.classList.remove('drag-over'); + incrementalAdd.ondrop = async e => { e.preventDefault(); incrementalAdd.classList.remove('drag-over'); await addIncrementalExample(e.dataTransfer.files[0]); }; + incrementalInput.onchange = async e => await addIncrementalExample(e.target.files[0]); + } + + async function addIncrementalExample(file) { + if (!file || !embedder) return; + const cls = incrementalClass.value; + const result = await extractEmbeddingFromFile(file); + incrementalModel[cls].push(result.embedding); + incPosCount.textContent = incrementalModel.positive.length; + incNegCount.textContent = incrementalModel.negative.length; + } + + if (incrementalTest && incrementalTestInput) { + incrementalTest.onclick = () => incrementalTestInput.click(); + incrementalTest.ondragover = e => { e.preventDefault(); incrementalTest.classList.add('drag-over'); }; + incrementalTest.ondragleave = () => incrementalTest.classList.remove('drag-over'); + incrementalTest.ondrop = async e => { e.preventDefault(); incrementalTest.classList.remove('drag-over'); await testIncremental(e.dataTransfer.files[0]); }; + incrementalTestInput.onchange = async e => await testIncremental(e.target.files[0]); + } + + async function testIncremental(file) { + if (!file || !embedder) return; + const result = await extractEmbeddingFromFile(file); + let posSim = 0, negSim = 0; + + if (incrementalModel.positive.length > 0) { + const posProto = computePrototype(incrementalModel.positive); + posSim = embedder.cosine_similarity(result.embedding, posProto); + } + if (incrementalModel.negative.length > 0) { + const negProto = computePrototype(incrementalModel.negative); + negSim = embedder.cosine_similarity(result.embedding, negProto); + } + + const isPositive = posSim > negSim; + incrementalResult.innerHTML = ` +
+
${isPositive ? 'Positive (+)' : 'Negative (-)'}
+
+: ${(posSim * 100).toFixed(1)}% | -: ${(negSim * 100).toFixed(1)}%
+
`; + } + + if (incrementalReset) { + incrementalReset.onclick = () => { + incrementalModel.positive = []; + incrementalModel.negative = []; + incPosCount.textContent = '0'; + incNegCount.textContent = '0'; + incrementalResult.innerHTML = ''; + }; + } + + // Memory Bank + function updateMemoryBank() { + const grid = document.getElementById('memory-bank-grid'); + const stats = document.getElementById('memory-stats'); + if (!grid) return; + + let totalEmbs = 0; + grid.innerHTML = ''; + + for (const [name, data] of classifier.classes) { + totalEmbs += data.embeddings.length; + data.thumbnails.slice(-12).forEach(thumb => { + const div = document.createElement('div'); + div.style.cssText = `aspect-ratio: 1; border-radius: 4px; overflow: hidden; border: 2px solid ${data.color};`; + div.innerHTML = ``; + div.title = name; + grid.appendChild(div); + }); + } + + if (totalEmbs === 0) { + grid.innerHTML = '
No memories yet.
'; + } + + const sizeKB = (totalEmbs * 512 * 4 / 1024).toFixed(1); + stats.textContent = `${classifier.classes.size} classes • ${totalEmbs} embeddings • ${sizeKB} KB`; + } + + // Export/Import Memory + const exportMemoryBtn = document.getElementById('export-memory-btn'); + const importMemoryBtn = document.getElementById('import-memory-btn'); + const importMemoryInput = document.getElementById('import-memory-input'); + + if (exportMemoryBtn) { + exportMemoryBtn.onclick = () => { + const data = {}; + for (const [name, d] of classifier.classes) { + data[name] = { embeddings: d.embeddings.map(e => Array.from(e)), color: d.color }; + } + const blob = new Blob([JSON.stringify(data)], { type: 'application/json' }); + const url = URL.createObjectURL(blob); + const a = document.createElement('a'); a.href = url; a.download = 'classifier-memory.json'; a.click(); + }; + } + + if (importMemoryBtn && importMemoryInput) { + importMemoryBtn.onclick = () => importMemoryInput.click(); + importMemoryInput.onchange = async e => { + const file = e.target.files[0]; + if (!file) return; + const text = await file.text(); + const data = JSON.parse(text); + for (const [name, d] of Object.entries(data)) { + classifier.classes.set(name, { + embeddings: d.embeddings.map(e => new Float32Array(e)), + color: d.color, + thumbnails: [] + }); + } + renderClassContainers(); + updateMemoryBank(); + }; + } + + // Feedback Stats + function updateFeedbackStats() { + const totalEl = document.getElementById('feedback-total'); + const correctEl = document.getElementById('feedback-correct'); + const accuracyEl = document.getElementById('feedback-accuracy'); + if (totalEl) totalEl.textContent = classifier.totalTests; + if (correctEl) correctEl.textContent = classifier.correctTests; + if (accuracyEl) accuracyEl.textContent = classifier.totalTests > 0 ? `${(classifier.correctTests / classifier.totalTests * 100).toFixed(0)}%` : '—'; + } + + // Camera Training + let trainingStream = null; + const trainingVideo = document.getElementById('training-video'); + const startTrainingCamBtn = document.getElementById('start-training-cam-btn'); + const stopTrainingCamBtn = document.getElementById('stop-training-cam-btn'); + const captureTrainingBtn = document.getElementById('capture-training-btn'); + const autoCaptureBtna = document.getElementById('auto-capture-btn'); + const camClassInput = document.getElementById('cam-class-input'); + const camTrainingLog = document.getElementById('cam-training-log'); + const trainingClassBadge = document.getElementById('training-class-badge'); + + if (startTrainingCamBtn) { + startTrainingCamBtn.onclick = async () => { + try { + trainingStream = await navigator.mediaDevices.getUserMedia({ video: { width: 320, height: 240 } }); + trainingVideo.srcObject = trainingStream; + startTrainingCamBtn.classList.add('hidden'); + stopTrainingCamBtn.classList.remove('hidden'); + captureTrainingBtn.disabled = false; + autoCaptureBtna.disabled = false; + } catch (e) { console.error(e); } + }; + } + + if (stopTrainingCamBtn) { + stopTrainingCamBtn.onclick = () => { + if (trainingStream) { trainingStream.getTracks().forEach(t => t.stop()); trainingStream = null; } + trainingVideo.srcObject = null; + startTrainingCamBtn.classList.remove('hidden'); + stopTrainingCamBtn.classList.add('hidden'); + captureTrainingBtn.disabled = true; + autoCaptureBtna.disabled = true; + }; + } + + if (captureTrainingBtn) { + captureTrainingBtn.onclick = () => captureTrainingFrame(); + } + + if (autoCaptureBtna) { + autoCaptureBtna.onclick = async () => { + for (let i = 0; i < 5; i++) { + await captureTrainingFrame(); + await new Promise(r => setTimeout(r, 500)); + } + }; + } + + async function captureTrainingFrame() { + if (!embedder || !trainingStream) return; + const className = camClassInput.value.trim() || 'default'; + + const canvas = document.createElement('canvas'); + canvas.width = 224; canvas.height = 224; + canvas.getContext('2d').drawImage(trainingVideo, 0, 0, 224, 224); + const imageData = canvas.getContext('2d').getImageData(0, 0, 224, 224); + const rgb = new Uint8Array(224 * 224 * 3); + for (let i = 0, j = 0; i < imageData.data.length; i += 4, j += 3) { + rgb[j] = imageData.data[i]; rgb[j+1] = imageData.data[i+1]; rgb[j+2] = imageData.data[i+2]; + } + const embedding = embedder.extract(rgb, 224, 224); + const dataUrl = canvas.toDataURL(); + + if (!classifier.classes.has(className)) { + const color = classifier.colors[classifier.colorIndex++ % classifier.colors.length]; + classifier.classes.set(className, { embeddings: [], color, thumbnails: [] }); + } + + const data = classifier.classes.get(className); + data.embeddings.push(embedding); + data.thumbnails.push(dataUrl); + + trainingClassBadge.textContent = `${className} (${data.embeddings.length})`; + trainingClassBadge.style.background = data.color; + + camTrainingLog.innerHTML = `
+ Added to "${className}" (${data.embeddings.length} total)
` + camTrainingLog.innerHTML; + renderClassContainers(); + updateMemoryBank(); + } + // Initialize similarity search when WASM loads setTimeout(initSimilaritySearch, 2000);