<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>
