krave
Task Vision

Face Detection

Configure MediaPipe face detection in a Next.js application.

Installation

npx shadcn@latest add @krave-docs/face-detection

Using Face Detection

src/page.tsx
import HandLandMark from "@/components/HandLandMark";

export default function Home() {
  return (
    <HandLandMark
      position="top-right" //top-left, top-right, bottom-left, bottom-right
      boxColor="#55e6c1"
      labelColor="#f8fafc"
      size={4}
    />
  );
}

Source Code

src/components/FaceDetection.tsx
"use client";

import { useEffect, useRef, useState } from "react";
import { Camera, CameraOff, LoaderCircle } from "lucide-react";
import {
  DrawingUtils,
  FaceDetector,
  FilesetResolver,
} from "@mediapipe/tasks-vision";

type CameraPosition = "top-left" | "top-right" | "bottom-left" | "bottom-right";

interface FaceDetectionProps {
  position: CameraPosition;
  boxColor: string;
  labelColor: string;
  size: number;
}

function FaceDetection({
  position,
  boxColor,
  labelColor,
  size,
}: FaceDetectionProps) {
  const safePosition = position ?? "bottom-right";
  const safeBoxColor = boxColor ?? "#7dd3fc";
  const safeLabelColor = labelColor ?? "#0f172a";

  const videoRef = useRef<HTMLVideoElement>(null);
  const canvasRef = useRef<HTMLCanvasElement>(null);
  const detectorRef = useRef<FaceDetector | null>(null);
  const streamRef = useRef<MediaStream | null>(null);
  const animationFrameRef = useRef<number | null>(null);
  const webcamRunningRef = useRef(false);
  const [isModelReady, setIsModelReady] = useState(false);
  const [isCameraRunning, setIsCameraRunning] = useState(false);
  const [isCameraStarting, setIsCameraStarting] = useState(false);
  const [faceDetected, setFaceDetected] = useState(false);
  const [errorMessage, setErrorMessage] = useState<string | null>(null);

  const cameraPositionClasses: Record<CameraPosition, string> = {
    "top-left": "left-4 top-4 sm:left-6 sm:top-6",
    "top-right": "right-4 top-4 sm:right-6 sm:top-6",
    "bottom-left": "bottom-4 left-4 sm:bottom-6 sm:left-6",
    "bottom-right": "bottom-4 right-4 sm:bottom-6 sm:right-6",
  };

  useEffect(() => {
    let cancelled = false;

    async function createDetector() {
      try {
        const vision = await FilesetResolver.forVisionTasks(
          "https://cdn.jsdelivr.net/npm/@mediapipe/tasks-vision@1.1.0/wasm",
        );

        const detectorOptions = {
          baseOptions: {
            modelAssetPath:
              "https://storage.googleapis.com/mediapipe-models/face_detector/blaze_face_short_range/float16/1/blaze_face_short_range.tflite",
          },
          runningMode: "VIDEO" as const,
          minDetectionConfidence: 0.75,
          minSuppressionThreshold: 0.3,
        };
        let detector: FaceDetector;

        try {
          detector = await FaceDetector.createFromOptions(vision, {
            ...detectorOptions,
            baseOptions: { ...detectorOptions.baseOptions, delegate: "GPU" },
          });
        } catch (gpuError) {
          console.warn("GPU face detection unavailable; retrying with CPU:", gpuError);
          detector = await FaceDetector.createFromOptions(vision, {
            ...detectorOptions,
            baseOptions: { ...detectorOptions.baseOptions, delegate: "CPU" },
          });
        }

        if (cancelled) {
          detector.close();
          return;
        }

        detectorRef.current = detector;
        setIsModelReady(true);
      } catch (error) {
        console.error("Failed to load the face detection model:", error);
        if (!cancelled) {
          setErrorMessage("Could not load the face detector. Check your connection and reload.");
        }
      }
    }

    void createDetector();

    return () => {
      cancelled = true;
      webcamRunningRef.current = false;
      if (animationFrameRef.current !== null) {
        window.cancelAnimationFrame(animationFrameRef.current);
      }
      streamRef.current?.getTracks().forEach((track) => track.stop());
      detectorRef.current?.close();
    };
  }, []);

  async function toggleCamera() {
    if (webcamRunningRef.current) {
      webcamRunningRef.current = false;
      setIsCameraRunning(false);
      if (animationFrameRef.current !== null) {
        window.cancelAnimationFrame(animationFrameRef.current);
        animationFrameRef.current = null;
      }
      streamRef.current?.getTracks().forEach((track) => track.stop());
      streamRef.current = null;

      if (videoRef.current) {
        videoRef.current.srcObject = null;
      }

      const canvas = canvasRef.current;
      const context = canvas?.getContext("2d");
      if (canvas && context) {
        context.clearRect(0, 0, canvas.width, canvas.height);
      }
      setFaceDetected(false);
      return;
    }

    setIsCameraStarting(true);
    setErrorMessage(null);
    try {
      const stream = await navigator.mediaDevices.getUserMedia({
        video: { facingMode: "user" },
      });

      const video = videoRef.current;
      if (!video) {
        stream.getTracks().forEach((track) => track.stop());
        return;
      }

      streamRef.current = stream;
      video.srcObject = stream;
      await video.play();

      webcamRunningRef.current = true;
      setIsCameraRunning(true);
      animationFrameRef.current = window.requestAnimationFrame((timestamp) => {
        void predictWebcam(timestamp);
      });
    } catch (error) {
      streamRef.current?.getTracks().forEach((track) => track.stop());
      streamRef.current = null;
      console.error("Failed to start the camera:", error);
      setErrorMessage(
        error instanceof Error && error.name === "NotAllowedError"
          ? "Camera access was denied. Allow camera access in your browser settings and try again."
          : "Could not start the camera. Check that it is connected and not being used by another app.",
      );
    } finally {
      setIsCameraStarting(false);
    }
  }

  async function predictWebcam(timestamp: number) {
    const video = videoRef.current;
    const canvas = canvasRef.current;
    const detector = detectorRef.current;

    if (!video || !canvas || !webcamRunningRef.current) {
      return;
    }

    if (!detector || video.readyState < HTMLMediaElement.HAVE_CURRENT_DATA || video.videoWidth === 0) {
      animationFrameRef.current = window.requestAnimationFrame((frameTime) => {
        void predictWebcam(frameTime);
      });
      return;
    }

    const canvasContext = canvas.getContext("2d");
    if (!canvasContext) {
      setErrorMessage("Could not create the face detection canvas.");
      webcamRunningRef.current = false;
      setIsCameraRunning(false);
      streamRef.current?.getTracks().forEach((track) => track.stop());
      streamRef.current = null;
      return;
    }

    if (canvas.width !== video.videoWidth || canvas.height !== video.videoHeight) {
      canvas.width = video.videoWidth;
      canvas.height = video.videoHeight;
    }

    let detections;
    try {
      detections = detector.detectForVideo(video, timestamp).detections;
    } catch (error) {
      console.error("Face detection failed:", error);
      setErrorMessage("Face detection failed. Please restart the camera.");
      webcamRunningRef.current = false;
      setIsCameraRunning(false);
      streamRef.current?.getTracks().forEach((track) => track.stop());
      streamRef.current = null;
      return;
    }

    const faceDetections = detections.filter(
      (detection) => (detection.categories?.[0]?.score ?? 0) >= 0.75,
    );

    canvasContext.clearRect(0, 0, canvas.width, canvas.height);
    setFaceDetected(faceDetections.length > 0);

    if (faceDetections.length > 0) {
      const drawingUtils = new DrawingUtils(canvasContext);
      canvasContext.font = "16px sans-serif";
      for (const detection of faceDetections) {
        if (!detection.boundingBox) continue;

        const box = detection.boundingBox;
        const mirroredBox = {
          ...box,
          originX: canvas.width - box.originX - box.width,
        };
        drawingUtils.drawBoundingBox(mirroredBox, {
          color: safeBoxColor,
          lineWidth: size,
          fillColor: "transparent",
        });

        const category = detection.categories?.[0];
        const score = category?.score ? Math.round(category.score * 100) : 0;
        const labelText = `Face - ${score}%`;
        const textWidth = canvasContext.measureText(labelText).width;

        canvasContext.fillStyle = safeLabelColor;
        canvasContext.fillRect(mirroredBox.originX, mirroredBox.originY, textWidth + 12, 26);
        canvasContext.fillStyle = "#ffffff";
        canvasContext.fillText(labelText, mirroredBox.originX + 6, mirroredBox.originY + 18);
      }
    }

    if (webcamRunningRef.current) {
      animationFrameRef.current = window.requestAnimationFrame((frameTime) => {
        void predictWebcam(frameTime);
      });
    }
  }

  return (
    <main className="relative min-h-screen w-full overflow-hidden bg-[#090a0c]">
      <div
        className={`absolute z-10 aspect-video w-[min(24rem,calc(100vw-2rem))] overflow-hidden rounded-2xl border border-white/[0.09] bg-[#0e1012] shadow-2xl shadow-black/50 sm:w-[min(28rem,calc(100vw-3rem))] ${cameraPositionClasses[safePosition] ?? cameraPositionClasses["bottom-right"]}`}
      >
        <video
          ref={videoRef}
          autoPlay
          muted
          playsInline
          aria-label="Camera preview"
          className={`absolute inset-0 size-full object-cover ${isCameraRunning ? "scale-x-[-1]" : "hidden"}`}
        />

        <canvas
          ref={canvasRef}
          aria-hidden="true"
          className={`absolute inset-0 size-full object-cover ${isCameraRunning ? "block" : "hidden"}`}
        />

        {!isCameraRunning && (
          <div className="absolute inset-0 flex flex-col items-center justify-center px-4 text-center">
            <CameraOff className="mb-3 size-7 text-[#59c596]" aria-hidden="true" />
            <p className="text-sm font-medium text-[#f2f3f4]">
              {errorMessage ?? "Camera is off"}
            </p>
            <p className="mt-1 text-xs text-[#92969b]">
              {isModelReady
                ? "Press the camera button to start"
                : errorMessage
                  ? "Face detection could not be initialized"
                  : "Face detection is loading"}
            </p>
          </div>
        )}

        {isCameraRunning && (
          <div
            className="absolute bottom-3 left-3 rounded-full border border-white/10 bg-[#101311]/90 px-2.5 py-1 text-xs text-[#f2f3f4] shadow-lg backdrop-blur"
            role="status"
            aria-live="polite"
          >
            {faceDetected ? "Face detected" : "No face detected"}
          </div>
        )}

        <button
          type="button"
          onClick={() => void toggleCamera()}
          disabled={!isModelReady || isCameraStarting}
          aria-label={
            isCameraStarting
              ? "Starting camera"
              : isCameraRunning
                ? "Turn camera off"
                : "Turn camera on"
          }
          title={
            isCameraStarting
              ? "Starting camera"
              : isCameraRunning
                ? "Turn camera off"
                : "Turn camera on"
          }
          className="absolute right-3 top-3 z-20 flex size-10 items-center justify-center rounded-full border border-white/10 bg-[#101311]/90 text-white shadow-lg backdrop-blur transition hover:border-[#59c596]/40 hover:bg-[#171c19] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[#59c596] disabled:cursor-wait disabled:opacity-70"
        >
          {isCameraStarting ? (
            <LoaderCircle className="size-4 animate-spin text-[#59c596]" aria-hidden="true" />
          ) : isCameraRunning ? (
            <CameraOff className="size-4 text-[#d1d5d8]" aria-hidden="true" />
          ) : (
            <Camera className="size-4 text-[#59c596]" aria-hidden="true" />
          )}
        </button>
      </div>
    </main>
  );
}

export default FaceDetection;

On this page