candle-wasm-examples/yolo/lib-example.html (512 lines of code) (raw):

<html> <head> <meta content="text/html;charset=utf-8" http-equiv="Content-Type" /> <title>Candle YOLOv8 Rust/WASM</title> </head> <body></body> </html> <!DOCTYPE html> <html> <head> <meta charset="UTF-8" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <style> @import url("https://fonts.googleapis.com/css2?family=Source+Code+Pro:wght@200;300;400&family=Source+Sans+3:wght@100;200;300;400;500;600;700;800;900&display=swap"); html, body { font-family: "Source Sans 3", sans-serif; } code, output, select, pre { font-family: "Source Code Pro", monospace; } </style> <script src="https://cdn.tailwindcss.com"></script> <script src="https://cdn.jsdelivr.net/gh/huggingface/hub-js-utils/share-canvas.js" type="module" ></script> <script type="module"> const MODEL_BASEURL = "https://huggingface.co/lmz/candle-yolo-v8/resolve/main/"; const MODELS = { yolov8n: { model_size: "n", url: "yolov8n.safetensors", }, yolov8s: { model_size: "s", url: "yolov8s.safetensors", }, yolov8m: { model_size: "m", url: "yolov8m.safetensors", }, yolov8l: { model_size: "l", url: "yolov8l.safetensors", }, yolov8x: { model_size: "x", url: "yolov8x.safetensors", }, yolov8n_pose: { model_size: "n", url: "yolov8n-pose.safetensors", }, yolov8s_pose: { model_size: "s", url: "yolov8s-pose.safetensors", }, yolov8m_pose: { model_size: "m", url: "yolov8m-pose.safetensors", }, yolov8l_pose: { model_size: "l", url: "yolov8l-pose.safetensors", }, yolov8x_pose: { model_size: "x", url: "yolov8x-pose.safetensors", }, }; const COCO_PERSON_SKELETON = [ [4, 0], // head [3, 0], [16, 14], // left lower leg [14, 12], // left upper leg [6, 12], // left torso [6, 5], // top torso [6, 8], // upper arm [8, 10], // lower arm [1, 2], // head [1, 3], // right head [2, 4], // left head [3, 5], // right neck [4, 6], // left neck [5, 7], // right upper arm [7, 9], // right lower arm [5, 11], // right torso [11, 12], // bottom torso [11, 13], // right upper leg [13, 15], // right lower leg ]; // init web worker const yoloWorker = new Worker("./yoloWorker.js", { type: "module" }); let hasImage = false; //add event listener to image examples document.querySelector("#image-select").addEventListener("click", (e) => { const target = e.target; if (target.nodeName === "IMG") { const href = target.src; drawImageCanvas(href); } }); //add event listener to file input document.querySelector("#file-upload").addEventListener("change", (e) => { const target = e.target; if (target.files.length > 0) { const href = URL.createObjectURL(target.files[0]); drawImageCanvas(href); } }); // add event listener to drop-area const dropArea = document.querySelector("#drop-area"); dropArea.addEventListener("dragenter", (e) => { e.preventDefault(); dropArea.classList.add("border-blue-700"); }); dropArea.addEventListener("dragleave", (e) => { e.preventDefault(); dropArea.classList.remove("border-blue-700"); }); dropArea.addEventListener("dragover", (e) => { e.preventDefault(); }); dropArea.addEventListener("drop", (e) => { e.preventDefault(); dropArea.classList.remove("border-blue-700"); const url = e.dataTransfer.getData("text/uri-list"); const files = e.dataTransfer.files; if (files.length > 0) { const href = URL.createObjectURL(files[0]); drawImageCanvas(href); } else if (url) { drawImageCanvas(url); } }); document.querySelector("#clear-btn").addEventListener("click", () => { drawImageCanvas(); }); function drawImageCanvas(imgURL) { const canvas = document.querySelector("#canvas"); const canvasResult = document.querySelector("#canvas-result"); canvasResult .getContext("2d") .clearRect(0, 0, canvas.width, canvas.height); const ctx = canvas.getContext("2d"); ctx.clearRect(0, 0, canvas.width, canvas.height); document.querySelector("#share-btn").classList.add("invisible"); document.querySelector("#clear-btn").classList.add("invisible"); document.querySelector("#detect").disabled = true; hasImage = false; canvas.parentElement.style.height = "auto"; if (imgURL && imgURL !== "") { const img = new Image(); img.crossOrigin = "anonymous"; img.onload = () => { canvas.width = img.width; canvas.height = img.height; ctx.drawImage(img, 0, 0); canvas.parentElement.style.height = canvas.offsetHeight + "px"; hasImage = true; document.querySelector("#detect").disabled = false; document.querySelector("#clear-btn").classList.remove("invisible"); }; img.src = imgURL; } } async function classifyImage( imageURL, // URL of image to classify modelID, // ID of model to use modelURL, // URL to model file modelSize, // size of model confidence, // confidence threshold iou_threshold, // IoU threshold updateStatus // function receives status updates ) { return new Promise((resolve, reject) => { yoloWorker.postMessage({ imageURL, modelID, modelURL, modelSize, confidence, iou_threshold, }); function handleMessage(event) { console.log("message", event.data); if ("status" in event.data) { updateStatus(event.data.status); } if ("error" in event.data) { yoloWorker.removeEventListener("message", handleMessage); reject(new Error(event.data.error)); } if (event.data.status === "complete") { yoloWorker.removeEventListener("message", handleMessage); resolve(event.data); } } yoloWorker.addEventListener("message", handleMessage); }); } // add event listener to detect button document.querySelector("#detect").addEventListener("click", async () => { if (!hasImage) { return; } const modelID = document.querySelector("#model").value; const modelURL = MODEL_BASEURL + MODELS[modelID].url; const modelSize = MODELS[modelID].model_size; const confidence = parseFloat( document.querySelector("#confidence").value ); const iou_threshold = parseFloat( document.querySelector("#iou_threshold").value ); const canvasInput = document.querySelector("#canvas"); const canvas = document.querySelector("#canvas-result"); canvas.width = canvasInput.width; canvas.height = canvasInput.height; const scale = canvas.width / canvas.offsetWidth; const ctx = canvas.getContext("2d"); ctx.drawImage(canvasInput, 0, 0); const imageURL = canvas.toDataURL(); const results = await await classifyImage( imageURL, modelID, modelURL, modelSize, confidence, iou_threshold, updateStatus ); const { output } = results; ctx.lineWidth = 1 + 2 * scale; ctx.strokeStyle = "#3c8566"; ctx.fillStyle = "#0dff9a"; const fontSize = 14 * scale; ctx.font = `${fontSize}px sans-serif`; for (const detection of output) { // check keypoint for pose model data let xmin, xmax, ymin, ymax, label, confidence, keypoints; if ("keypoints" in detection) { xmin = detection.xmin; xmax = detection.xmax; ymin = detection.ymin; ymax = detection.ymax; confidence = detection.confidence; keypoints = detection.keypoints; } else { const [_label, bbox] = detection; label = _label; xmin = bbox.xmin; xmax = bbox.xmax; ymin = bbox.ymin; ymax = bbox.ymax; confidence = bbox.confidence; } const [x, y, w, h] = [xmin, ymin, xmax - xmin, ymax - ymin]; const text = `${label ? label + " " : ""}${confidence.toFixed(2)}`; const width = ctx.measureText(text).width; ctx.fillStyle = "#3c8566"; ctx.fillRect(x - 2, y - fontSize, width + 4, fontSize); ctx.fillStyle = "#e3fff3"; ctx.strokeRect(x, y, w, h); ctx.fillText(text, x, y - 2); if (keypoints) { ctx.save(); ctx.fillStyle = "magenta"; ctx.strokeStyle = "yellow"; for (const keypoint of keypoints) { const { x, y } = keypoint; ctx.beginPath(); ctx.arc(x, y, 3, 0, 2 * Math.PI); ctx.fill(); } ctx.beginPath(); for (const [xid, yid] of COCO_PERSON_SKELETON) { //draw line between skeleton keypoitns if (keypoints[xid] && keypoints[yid]) { ctx.moveTo(keypoints[xid].x, keypoints[xid].y); ctx.lineTo(keypoints[yid].x, keypoints[yid].y); } } ctx.stroke(); ctx.restore(); } } }); function updateStatus(statusMessage) { const button = document.querySelector("#detect"); if (statusMessage === "detecting") { button.disabled = true; button.classList.add("bg-blue-700"); button.classList.remove("bg-blue-950"); button.textContent = "Predicting..."; } else if (statusMessage === "complete") { button.disabled = false; button.classList.add("bg-blue-950"); button.classList.remove("bg-blue-700"); button.textContent = "Predict"; document.querySelector("#share-btn").classList.remove("invisible"); } } document.querySelector("#share-btn").addEventListener("click", () => { shareToCommunity( "lmz/candle-yolo", "Candle + YOLOv8", "YOLOv8 with [Candle](https://github.com/huggingface/candle)", "canvas-result", "share-btn" ); }); </script> </head> <body class="container max-w-4xl mx-auto p-4"> <main class="grid grid-cols-1 gap-8 relative"> <span class="absolute text-5xl -ml-[1em]"> 🕯️ </span> <div> <h1 class="text-5xl font-bold">Candle YOLOv8</h1> <h2 class="text-2xl font-bold">Rust/WASM Demo</h2> <p class="max-w-lg"> This demo showcases object detection and pose estimation models in your browser using Rust/WASM. It utilizes <a href="https://huggingface.co/lmz/candle-yolo-v8" target="_blank" class="underline hover:text-blue-500 hover:no-underline" > safetensor's YOLOv8 models </a> and a WASM runtime built with <a href="https://github.com/huggingface/candle/" target="_blank" class="underline hover:text-blue-500 hover:no-underline" >Candle </a >. </p> <p> To run pose estimation, select a yolo pose model from the dropdown </p> </div> <div> <label for="model" class="font-medium">Models Options: </label> <select id="model" class="border-2 border-gray-500 rounded-md font-light" > <option value="yolov8n" selected>yolov8n (6.37 MB)</option> <option value="yolov8s">yolov8s (22.4 MB)</option> <option value="yolov8m">yolov8m (51.9 MB)</option> <option value="yolov8l">yolov8l (87.5 MB)</option> <option value="yolov8x">yolov8x (137 MB)</option> <!-- Pose models --> <option value="yolov8n_pose">yolov8n_pose (6.65 MB)</option> <option value="yolov8s_pose">yolov8s_pose (23.3 MB)</option> <option value="yolov8m_pose">yolov8m_pose (53 MB)</option> <option value="yolov8l_pose">yolov8l_pose (89.1 MB)</option> <option value="yolov8x_pose">yolov8x_pose (139 MB)</option> </select> </div> <div> <button id="detect" disabled class="bg-gray-700 hover:bg-gray-800 text-white font-normal py-2 px-4 rounded disabled:bg-gray-300 disabled:cursor-not-allowed" > Predict </button> </div> <!-- drag and drop area --> <div class="relative max-w-lg"> <div class="py-1"> <button id="clear-btn" class="text-xs bg-white rounded-md disabled:opacity-50 flex gap-1 items-center ml-auto invisible" > <svg class="" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 13 12" height="1em" > <path d="M1.6.7 12 11.1M12 .7 1.6 11.1" stroke="#2E3036" stroke-width="2" /> </svg> Clear image </button> </div> <div id="drop-area" class="flex flex-col items-center justify-center border-2 border-gray-300 border-dashed rounded-xl relative aspect-video w-full overflow-hidden" > <div class="flex flex-col items-center justify-center space-y-1 text-center" > <svg width="25" height="25" viewBox="0 0 25 25" fill="none" xmlns="http://www.w3.org/2000/svg" > <path d="M3.5 24.3a3 3 0 0 1-1.9-.8c-.5-.5-.8-1.2-.8-1.9V2.9c0-.7.3-1.3.8-1.9.6-.5 1.2-.7 2-.7h18.6c.7 0 1.3.2 1.9.7.5.6.7 1.2.7 2v18.6c0 .7-.2 1.4-.7 1.9a3 3 0 0 1-2 .8H3.6Zm0-2.7h18.7V2.9H3.5v18.7Zm2.7-2.7h13.3c.3 0 .5 0 .6-.3v-.7l-3.7-5a.6.6 0 0 0-.6-.2c-.2 0-.4 0-.5.3l-3.5 4.6-2.4-3.3a.6.6 0 0 0-.6-.3c-.2 0-.4.1-.5.3l-2.7 3.6c-.1.2-.2.4 0 .7.1.2.3.3.6.3Z" fill="#000" /> </svg> <div class="flex text-sm text-gray-600"> <label for="file-upload" class="relative cursor-pointer bg-white rounded-md font-medium text-blue-950 hover:text-blue-700" > <span>Drag and drop your image here</span> <span class="block text-xs">or</span> <span class="block text-xs">Click to upload</span> </label> </div> <input id="file-upload" name="file-upload" type="file" class="sr-only" /> </div> <canvas id="canvas" class="absolute pointer-events-none w-full" ></canvas> <canvas id="canvas-result" class="absolute pointer-events-none w-full" ></canvas> </div> <div class="text-right py-2"> <button id="share-btn" class="bg-white rounded-md hover:outline outline-orange-200 disabled:opacity-50 invisible" > <img src="https://huggingface.co/datasets/huggingface/badges/raw/main/share-to-community-sm.svg" /> </button> </div> </div> <div> <div class="flex gap-3 items-center overflow-x-scroll" id="image-select" > <h3 class="font-medium">Examples:</h3> <img src="https://huggingface.co/datasets/huggingface/documentation-images/resolve/main/candle/examples/sf.jpg" class="cursor-pointer w-24 h-24 object-cover" /> <img src="https://huggingface.co/datasets/huggingface/documentation-images/resolve/main/candle/examples/bike.jpeg" class="cursor-pointer w-24 h-24 object-cover" /> <img src="https://huggingface.co/datasets/huggingface/documentation-images/resolve/main/candle/examples/000000000077.jpg" class="cursor-pointer w-24 h-24 object-cover" /> </div> </div> <div> <div class="grid grid-cols-3 max-w-md items-center gap-3"> <label class="text-sm font-medium" for="confidence" >Confidence Threshold</label > <input type="range" id="confidence" name="confidence" min="0" max="1" step="0.01" value="0.25" oninput="this.nextElementSibling.value = Number(this.value).toFixed(2)" /> <output class="text-xs font-light px-1 py-1 border border-gray-700 rounded-md w-min" >0.25</output > <label class="text-sm font-medium" for="iou_threshold" >IoU Threshold</label > <input type="range" id="iou_threshold" name="iou_threshold" min="0" max="1" step="0.01" value="0.45" oninput="this.nextElementSibling.value = Number(this.value).toFixed(2)" /> <output class="font-extralight text-xs px-1 py-1 border border-gray-700 rounded-md w-min" >0.45</output > </div> </div> </main> </body> </html>