Dal laboratorio a una pagina tua.
Prendi il modello Foto che hai addestrato nel laboratorio e lo fai girare in una pagina web tua, con la fotocamera e senza server. Servono il file esportato, due librerie e un centinaio di righe di JavaScript.
Cosa ti serve
- Un modello addestrato nel laboratorio con l'ingresso Foto, per esempio tazza, chiavi e niente.
- Un computer con un editor di testo. Anche il Blocco note va bene.
- Un modo per aprire la pagina su localhost o su https, perché il browser accende la fotocamera solo lì.
-
Esporta il modello dal laboratorio
Apri il tuo progetto nel laboratorio e vai al passo Usa. In fondo, sotto la scheda del modello, tocca Esporta il modello e la scheda. Il telefono scarica un file .json con il nome del progetto.
Dentro ci sono le classi, la forma della rete, i suoi pesi come liste di numeri, le misure e la scheda. È tutto quello che serve alla pagina, e per un modello con tre classi pesa circa 80 KB.
-
Prepara la cartella
Crea una cartella con due file, index.html e app.js. Puoi scaricarli da questa pagina e cambiarli dopo. La pagina ha un campo per scegliere il file esportato, un pulsante per la fotocamera, il video e il posto per la risposta.
index.html <main> <h1 data-en="My model, in my own page">Il mio modello, in una pagina mia</h1> <p class="soft" data-en="Everything runs in your browser. The photos never leave the phone.">Tutto gira nel tuo browser. Le foto non escono dal telefono.</p> <label class="file"> <span data-en="The file exported from the lab (.json)">Il file esportato dal laboratorio (.json)</span> <input type="file" id="file" accept=".json,application/json"> </label> <button type="button" id="start" disabled></button> <div class="view"><video id="video" muted playsinline></video></div> <p id="answer" aria-live="polite"></p> <ul id="bars"></ul> <p class="soft" id="ms"></p> <p class="soft" id="status" role="status"></p> </main> <script type="module" src="app.js"></script> -
Le due librerie
Il modello del laboratorio ha due parti. Il corpo è MobileNet v3 small, una rete già addestrata da Google che trasforma una foto in 1024 numeri. La testa è la rete piccola che hai addestrato tu, e trasforma quei numeri nella risposta.
Il corpo lo fa girare MediaPipe, la testa TensorFlow.js. Le librerie arrivano da jsDelivr con la versione scritta nell'indirizzo, così non cambiano da sole. Il corpo deve essere lo stesso file del laboratorio, e quello di Google lo è.
app.js import * as tf from "https://cdn.jsdelivr.net/npm/@tensorflow/tfjs@4.22.0/dist/tf.fesm.min.js"; import { FilesetResolver, ImageEmbedder } from "https://cdn.jsdelivr.net/npm/@mediapipe/tasks-vision@1.0.1/vision_bundle.mjs"; const VISION_WASM = "https://cdn.jsdelivr.net/npm/@mediapipe/tasks-vision@1.0.1/wasm"; // the same body as the lab. With a different one the head would get numbers it has never seen const BODY_MODEL = "https://storage.googleapis.com/mediapipe-models/image_embedder/mobilenet_v3_small/float32/latest/mobilenet_v3_small.tflite"; -
Leggi il file esportato
Il file ha le chiavi in italiano o in inglese, a seconda della lingua del laboratorio. La funzione prende l'una o l'altra e controlla che il modello sia dell'ingresso Foto.
app.js // The exported file has Italian or English keys, depending on the lab's language function readExport(json) { const get = (o, it, en) => (o && (o[it] ?? o[en])); if (!/^edge-in-tasca-(laboratorio|lab)\/1$/.test(get(json, "formato", "format") || "")) throw new Error(T.badFile); if (!/^(foto|photo)$/.test(get(json, "ingresso", "input"))) throw new Error(T.notPhoto); const m = get(json, "modello", "model"); return { name: get(json, "nome", "name"), classes: get(json, "classi", "classes") || [], unit: get(json, "unita", "unit") || "", spec: get(m, "architettura", "architecture"), // the kind of head and its sizes norm: get(m, "normalizzazione", "normalization"), // mean and spread of each number, or null weights: get(m, "pesi", "weights"), // [{ shape, data }], one per weight tensor }; } -
Ricostruisci la testa
La testa si ricostruisce con gli stessi strati del laboratorio, nello stesso ordine. Per un modello Foto con le classi è uno strato solo, con 1024 ingressi e un'uscita per classe. Poi setWeights rimette i pesi del file al loro posto, uno dopo l'altro.
app.js // The same layers as the lab's head, in the same order. Dropout has no weights, // so the weights in the file go back into their places one after the other function buildHead(spec) { const m = tf.sequential(), L = tf.layers; const dense = (units, activation, first) => L.dense({ units, activation, ...(first ? { inputShape: [spec.dim] } : {}) }); if (spec.tipo === "lineare") m.add(dense(spec.uscite, "softmax", true)); else if (spec.tipo === "densa") { m.add(dense(32, "relu", true)); m.add(L.dropout({ rate: 0.2 })); m.add(dense(spec.uscite, "softmax")); } else if (spec.tipo === "densa2") { m.add(dense(48, "relu", true)); m.add(L.dropout({ rate: 0.2 })); m.add(dense(24, "relu")); m.add(dense(spec.uscite, "softmax")); } else if (spec.tipo === "numero-lineare") { m.add(dense(16, "relu", true)); m.add(dense(1)); } else if (spec.tipo === "numero") { m.add(dense(16, "relu", true)); m.add(L.dropout({ rate: 0.1 })); m.add(dense(8, "relu")); m.add(dense(1)); } else throw new Error(`Head "${spec.tipo}" isn't supported here`); return m; } function loadHead(model) { const head = buildHead(model.spec); head.setWeights(model.weights.map(w => tf.tensor(w.data, w.shape))); return head; } -
Carica il corpo
MediaPipe scarica il corpo, circa 4 MB, e lo prepara. Prova prima la GPU e, se non c'è, usa la CPU. Le opzioni sono quelle del laboratorio, con i 1024 numeri normalizzati, altrimenti la testa riceve numeri diversi da quelli con cui ha imparato.
app.js async function loadBody() { const files = await FilesetResolver.forVisionTasks(VISION_WASM); const options = { baseOptions: { modelAssetPath: BODY_MODEL }, runningMode: "IMAGE", l2Normalize: true, quantize: false }; // the GPU if there is one, otherwise the CPU try { return await ImageEmbedder.createFromOptions(files, { ...options, baseOptions: { ...options.baseOptions, delegate: "GPU" } }); } catch (e) { return ImageEmbedder.createFromOptions(files, { ...options, baseOptions: { ...options.baseOptions, delegate: "CPU" } }); } } -
Dalla fotocamera al quadrato
Il laboratorio guarda solo il quadrato al centro del fotogramma, ridotto a 256 per 256 pixel. La pagina fa lo stesso, così il modello vede le foto come le vedeva mentre imparava.
app.js const video = $("#video"); const square = document.createElement("canvas"); square.width = square.height = 256; async function startCamera() { const stream = await navigator.mediaDevices.getUserMedia({ audio: false, video: { facingMode: "environment", width: { ideal: 640 }, height: { ideal: 480 } } }); video.srcObject = stream; await video.play(); } // the central square of the frame, 256 by 256 pixels, as in the lab function grabSquare() { const W = video.videoWidth, H = video.videoHeight, s = Math.min(W, H); square.getContext("2d").drawImage(video, (W - s) / 2, (H - s) / 2, s, s, 0, 0, 256, 256); return square; } -
La risposta
Quattro volte al secondo la pagina prende il quadrato e lo passa al corpo, poi alla testa. Esce una probabilità per ogni classe. La pagina mostra la più alta, le barre di tutte e quanti millisecondi ci sono voluti.
app.js function answer(body, head, model) { const t0 = performance.now(); const feat = body.embed(grabSquare()).embeddings[0].floatEmbedding; // 1,024 numbers const out = tf.tidy(() => { const x = model.norm ? Array.from(feat, (v, i) => (v - model.norm.mu[i]) / model.norm.sd[i]) : Array.from(feat); return Array.from(head.predict(tf.tensor2d([x])).dataSync()); }); return { out, ms: performance.now() - t0 }; } function show({ out, ms }, model) { if (out.length === 1) { $("#answer").textContent = `${out[0].toLocaleString(EN ? "en" : "it", { maximumFractionDigits: 1 })} ${model.unit}`.trim(); } else { const best = out.indexOf(Math.max(...out)); $("#answer").textContent = `${model.classes[best]} · ${Math.round(out[best] * 100)}%`; $("#bars").innerHTML = model.classes.map((c, i) => `<li><span>${c}</span><i style="width:${Math.round(out[i] * 100)}%"></i><b>${Math.round(out[i] * 100)}%</b></li>`).join(""); } $("#ms").textContent = `${ms.toLocaleString(EN ? "en" : "it", { maximumFractionDigits: 1 })} ${T.ms}`; } -
Mettila insieme e provala
L'ultimo pezzo collega il campo del file e il pulsante. Poi apri un terminale nella cartella e scrivi npx serve, oppure python -m http.server se hai Python. Apri l'indirizzo che compare, scegli il file esportato e accendi la fotocamera.
Sul telefono serve https. Il modo più semplice è pubblicare la cartella, per esempio su GitHub Pages o su Netlify, e aprire l'indirizzo dal telefono.
app.js let model = null, head = null, body = null; $("#file").addEventListener("change", async e => { const file = e.target.files[0]; if (!file) return; try { say(T.reading); model = readExport(JSON.parse(await file.text())); if (head) head.dispose(); head = loadHead(model); if (!body) { say(T.body); body = await loadBody(); } say(T.ready(model.name)); $("#start").disabled = false; } catch (err) { say(err.message); } }); $("#start").addEventListener("click", async () => { try { await startCamera(); } catch (err) { say(T.denied); return; } $("#start").hidden = true; say(T.on); // four answers a second are enough, and they leave the phone time to breathe setInterval(() => { if (video.readyState >= 2) show(answer(body, head, model), model); }, 250); });
Se qualcosa non va
Questo non è un file esportato dal laboratorio
La pagina legge solo il formato del laboratorio. Esporta di nuovo dal passo Usa e scegli il file .json così com'è, senza aprirlo e salvarlo con un altro programma.
Il modello è stato addestrato su un altro ingresso
Questa pagina ha il corpo dell'ingresso Foto. Suono, Movimento, Mano e Testo hanno un corpo diverso, e arriveranno nelle prossime esercitazioni.
La fotocamera non si accende
Il browser accende la fotocamera solo su https o su localhost. Se apri index.html con un doppio clic, l'indirizzo comincia con file e la fotocamera resta spenta.
Le risposte sono diverse da quelle del laboratorio
Controlla di avere lo stesso corpo e il quadrato al centro a 256 pixel. Con un corpo diverso o un'inquadratura diversa la testa riceve numeri che non conosce.
Ora il modello è tuo. Puoi cambiare la pagina, far suonare il telefono quando vede una classe, o aggiungere un service worker06.05 perché funzioni anche senza rete. Le altre esercitazioni →