new components added
This commit is contained in:
parent
a0d6b7a73a
commit
75c8c99481
@ -4,10 +4,18 @@ import Register from "./register/Register";
|
||||
import Search from "./search/Search";
|
||||
import "./MainForm.css";
|
||||
import RealtimeFaceDetection from "./realtimeFaceDetection/RealtimeFaceDetection";
|
||||
import FaceLiveness from "./faceLivelinessCheck/FaceLivelinessCheck";
|
||||
import FaceMovementDetection from "./faceMovementDetection/FaceMovementDetection";
|
||||
import RealtimeCount from "./realtimeCount/RealtimeCount";
|
||||
|
||||
const MainForm: React.FC = () => {
|
||||
const [activeTab, setActiveTab] = useState<
|
||||
"register" | "search" | "realtime"
|
||||
| "register"
|
||||
| "search"
|
||||
| "realtime"
|
||||
| "liveliness"
|
||||
| "realtime-count"
|
||||
| "facemovement"
|
||||
>("register");
|
||||
|
||||
return (
|
||||
@ -31,11 +39,36 @@ const MainForm: React.FC = () => {
|
||||
>
|
||||
Realtime Detection
|
||||
</button>
|
||||
{/* <button
|
||||
className={`tab-button ${activeTab === "liveliness" ? "active" : ""}`}
|
||||
onClick={() => setActiveTab("liveliness")}
|
||||
>
|
||||
Liveliness Detection
|
||||
</button> */}
|
||||
<button
|
||||
className={`tab-button ${
|
||||
activeTab === "realtime-count" ? "active" : ""
|
||||
}`}
|
||||
onClick={() => setActiveTab("realtime-count")}
|
||||
>
|
||||
Realtime Count
|
||||
</button>
|
||||
<button
|
||||
className={`tab-button ${
|
||||
activeTab === "facemovement" ? "active" : ""
|
||||
}`}
|
||||
onClick={() => setActiveTab("facemovement")}
|
||||
>
|
||||
Face Movement Detection
|
||||
</button>
|
||||
</div>
|
||||
<div className="tab-content">
|
||||
{activeTab === "register" && <Register />}
|
||||
{activeTab === "search" && <Search />}
|
||||
{activeTab === "realtime" && <RealtimeFaceDetection />}
|
||||
{activeTab === "liveliness" && <FaceLiveness />}
|
||||
{activeTab === "realtime-count" && <RealtimeCount />}
|
||||
{activeTab === "facemovement" && <FaceMovementDetection />}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
192
components/faceLivelinessCheck/FaceLivelinessCheck.tsx
Normal file
192
components/faceLivelinessCheck/FaceLivelinessCheck.tsx
Normal file
@ -0,0 +1,192 @@
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
"use client";
|
||||
|
||||
import { useState, useRef, useEffect } from "react";
|
||||
import Webcam from "react-webcam";
|
||||
import { Card, CardContent } from "@/components/ui/card";
|
||||
import { Loader2 } from "lucide-react";
|
||||
import { useToast } from "@/hooks/use-toast";
|
||||
import * as faceapi from "face-api.js";
|
||||
|
||||
export default function FaceLiveness() {
|
||||
const webcamRef = useRef<Webcam>(null);
|
||||
const [isModelLoading, setIsModelLoading] = useState(true);
|
||||
const [isProcessing, setIsProcessing] = useState(false);
|
||||
const [previousExpressions, setPreviousExpressions] = useState<any>(null);
|
||||
const processingTimeoutRef = useRef<NodeJS.Timeout | null>(null);
|
||||
const { toast } = useToast();
|
||||
|
||||
useEffect(() => {
|
||||
const loadModels = async () => {
|
||||
try {
|
||||
const MODEL_URL =
|
||||
"https://justadudewhohacks.github.io/face-api.js/models";
|
||||
await Promise.all([
|
||||
faceapi.nets.tinyFaceDetector.loadFromUri(MODEL_URL),
|
||||
faceapi.nets.faceLandmark68Net.loadFromUri(MODEL_URL),
|
||||
faceapi.nets.faceExpressionNet.loadFromUri(MODEL_URL),
|
||||
]);
|
||||
setIsModelLoading(false);
|
||||
} catch (error) {
|
||||
console.error("Error loading models:", error);
|
||||
toast({
|
||||
title: "Error",
|
||||
description:
|
||||
"Failed to load face detection models. Please refresh the page.",
|
||||
variant: "destructive",
|
||||
});
|
||||
}
|
||||
};
|
||||
loadModels();
|
||||
}, [toast]);
|
||||
|
||||
const checkLiveness = (expressions: any, landmarks: any) => {
|
||||
if (!previousExpressions) {
|
||||
setPreviousExpressions(expressions);
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check for expression changes
|
||||
const expressionThreshold = 0.1;
|
||||
let hasExpressionChange = false;
|
||||
for (const expression in expressions) {
|
||||
const diff = Math.abs(
|
||||
expressions[expression] - previousExpressions[expression]
|
||||
);
|
||||
if (diff > expressionThreshold) {
|
||||
hasExpressionChange = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Check for natural facial movement using landmarks
|
||||
const eyeBlinkDetected = detectEyeBlink(landmarks);
|
||||
|
||||
setPreviousExpressions(expressions);
|
||||
return hasExpressionChange || eyeBlinkDetected;
|
||||
};
|
||||
|
||||
const detectEyeBlink = (landmarks: any) => {
|
||||
const leftEye = landmarks.getLeftEye();
|
||||
const rightEye = landmarks.getRightEye();
|
||||
|
||||
// Calculate eye aspect ratio
|
||||
const leftEAR = getEyeAspectRatio(leftEye);
|
||||
const rightEAR = getEyeAspectRatio(rightEye);
|
||||
|
||||
// If either eye is closed (low aspect ratio), consider it a blink
|
||||
const blinkThreshold = 0.2;
|
||||
return leftEAR < blinkThreshold || rightEAR < blinkThreshold;
|
||||
};
|
||||
|
||||
const getEyeAspectRatio = (eye: any) => {
|
||||
// Calculate the eye aspect ratio using the landmark points
|
||||
const height1 = distance(eye[1], eye[5]);
|
||||
const height2 = distance(eye[2], eye[4]);
|
||||
const width = distance(eye[0], eye[3]);
|
||||
return (height1 + height2) / (2.0 * width);
|
||||
};
|
||||
|
||||
const distance = (point1: any, point2: any) => {
|
||||
return Math.sqrt(
|
||||
Math.pow(point1.x - point2.x, 2) + Math.pow(point1.y - point2.y, 2)
|
||||
);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const processFrame = async () => {
|
||||
if (!webcamRef.current || isProcessing || isModelLoading) return;
|
||||
|
||||
setIsProcessing(true);
|
||||
try {
|
||||
const imageSrc = webcamRef.current.getScreenshot();
|
||||
if (!imageSrc) return;
|
||||
|
||||
const img = new Image();
|
||||
img.src = imageSrc;
|
||||
await new Promise((resolve) => (img.onload = resolve));
|
||||
|
||||
const detections = await faceapi
|
||||
.detectAllFaces(img, new faceapi.TinyFaceDetectorOptions())
|
||||
.withFaceLandmarks()
|
||||
.withFaceExpressions();
|
||||
|
||||
if (detections.length > 0) {
|
||||
// Process each detected face with high confidence
|
||||
detections
|
||||
.filter((detection) => detection.detection.score > 0.7)
|
||||
.forEach((detection) => {
|
||||
const isLive = checkLiveness(
|
||||
detection.expressions,
|
||||
detection.landmarks
|
||||
);
|
||||
if (isLive) {
|
||||
toast({
|
||||
title: "Liveness Detected",
|
||||
description: "Real face detected with natural movements",
|
||||
});
|
||||
} else {
|
||||
toast({
|
||||
title: "Liveness Check",
|
||||
description: "Please move or blink naturally",
|
||||
variant: "destructive",
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Processing error:", error);
|
||||
} finally {
|
||||
setIsProcessing(false);
|
||||
// Schedule next frame processing
|
||||
processingTimeoutRef.current = setTimeout(processFrame, 1000); // Process every second
|
||||
}
|
||||
};
|
||||
|
||||
if (!isModelLoading) {
|
||||
processFrame();
|
||||
}
|
||||
|
||||
return () => {
|
||||
if (processingTimeoutRef.current) {
|
||||
clearTimeout(processingTimeoutRef.current);
|
||||
}
|
||||
};
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [isModelLoading, isProcessing, toast]);
|
||||
|
||||
if (isModelLoading) {
|
||||
return (
|
||||
<Card>
|
||||
<CardContent className="p-6 text-center">
|
||||
<Loader2 className="h-8 w-8 animate-spin mx-auto" />
|
||||
<p className="mt-2">Loading face detection models...</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<CardContent className="p-6">
|
||||
<div className="space-y-6">
|
||||
<div className="relative rounded-lg overflow-hidden bg-black">
|
||||
<Webcam
|
||||
ref={webcamRef}
|
||||
screenshotFormat="image/jpeg"
|
||||
className="w-full"
|
||||
videoConstraints={{
|
||||
width: 640,
|
||||
height: 480,
|
||||
facingMode: "user",
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<p className="text-center text-sm text-muted-foreground">
|
||||
Move your face naturally or blink to verify liveness
|
||||
</p>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
28
components/faceMovementDetection/FaceMovementDetection.css
Normal file
28
components/faceMovementDetection/FaceMovementDetection.css
Normal file
@ -0,0 +1,28 @@
|
||||
.video-container {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
max-width: 768px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.webcam {
|
||||
width: 100%;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.alert-warning {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
background-color: #facc15; /* Yellow */
|
||||
color: #000;
|
||||
padding: 12px;
|
||||
border-radius: 6px;
|
||||
margin-top: 16px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.button-container {
|
||||
margin-top: 24px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
112
components/faceMovementDetection/FaceMovementDetection.tsx
Normal file
112
components/faceMovementDetection/FaceMovementDetection.tsx
Normal file
@ -0,0 +1,112 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import Webcam from "react-webcam";
|
||||
import * as faceapi from "face-api.js";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { AlertTriangle, Camera } from "lucide-react";
|
||||
import { useToast } from "@/hooks/use-toast";
|
||||
|
||||
const MODEL_URL = "https://cdn.jsdelivr.net/npm/@vladmandic/face-api/model";
|
||||
const CHECK_INTERVAL = 500;
|
||||
|
||||
const FaceMovementDetection = () => {
|
||||
const webcamRef = useRef<Webcam>(null);
|
||||
const [isModelLoaded, setIsModelLoaded] = useState(false);
|
||||
const [isDetecting, setIsDetecting] = useState(false);
|
||||
const prevBoxSizeRef = useRef<number | null>(null);
|
||||
const [movingForward, setMovingForward] = useState(false);
|
||||
const { toast } = useToast();
|
||||
|
||||
useEffect(() => {
|
||||
const loadModels = async () => {
|
||||
try {
|
||||
await Promise.all([
|
||||
faceapi.nets.tinyFaceDetector.loadFromUri(MODEL_URL),
|
||||
faceapi.nets.faceLandmark68Net.loadFromUri(MODEL_URL),
|
||||
]);
|
||||
setIsModelLoaded(true);
|
||||
toast({
|
||||
title: "Models Loaded",
|
||||
description: "Face detection models ready.",
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("Error loading models:", error);
|
||||
toast({
|
||||
title: "Error",
|
||||
description: "Failed to load models.",
|
||||
variant: "destructive",
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
loadModels();
|
||||
}, [toast]);
|
||||
|
||||
const detectMovement = async () => {
|
||||
if (!webcamRef.current?.video) return;
|
||||
|
||||
const video = webcamRef.current.video;
|
||||
const detections = await faceapi.detectSingleFace(
|
||||
video,
|
||||
new faceapi.TinyFaceDetectorOptions()
|
||||
);
|
||||
|
||||
console.log("prevBoxSizeRef:", prevBoxSizeRef.current);
|
||||
|
||||
if (detections) {
|
||||
const { width, height } = detections.box;
|
||||
const currentBoxSize = width * height;
|
||||
console.log("currentBoxSize:", currentBoxSize);
|
||||
|
||||
if (prevBoxSizeRef.current !== null) {
|
||||
const sizeIncrease = currentBoxSize - prevBoxSizeRef.current;
|
||||
console.log("Size Increase:", sizeIncrease);
|
||||
|
||||
if (sizeIncrease > 3000) {
|
||||
setMovingForward(true);
|
||||
// toast({
|
||||
// title: "Movement Detected",
|
||||
// description: "The person is moving closer!",
|
||||
// variant: "destructive",
|
||||
// });
|
||||
} else {
|
||||
setMovingForward(false);
|
||||
}
|
||||
}
|
||||
|
||||
prevBoxSizeRef.current = currentBoxSize; // Update ref directly
|
||||
}
|
||||
};
|
||||
|
||||
const startDetection = () => {
|
||||
if (!isModelLoaded) return;
|
||||
setIsDetecting(true);
|
||||
setInterval(detectMovement, CHECK_INTERVAL);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="max-w-3xl mx-auto">
|
||||
<div className="relative">
|
||||
<Webcam ref={webcamRef} mirrored className="w-full rounded-lg" />
|
||||
</div>
|
||||
{movingForward && (
|
||||
<div className="mt-4 p-3 bg-yellow-300 text-black rounded-md flex items-center">
|
||||
<AlertTriangle className="mr-2 h-5 w-5" />
|
||||
Person is moving forward!
|
||||
</div>
|
||||
)}
|
||||
<div className="mt-6 flex justify-center">
|
||||
<Button
|
||||
onClick={startDetection}
|
||||
disabled={!isModelLoaded || isDetecting}
|
||||
>
|
||||
<Camera className="mr-2 h-4 w-4" />
|
||||
{isDetecting ? "Detecting..." : "Start Movement Detection"}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default FaceMovementDetection;
|
39
components/realtimeCount/RealtimeCount.tsx
Normal file
39
components/realtimeCount/RealtimeCount.tsx
Normal file
@ -0,0 +1,39 @@
|
||||
"use client";
|
||||
import React, { useState } from "react";
|
||||
import RegisterFaceCount from "./registerFaceCount/RegisterFaceCount";
|
||||
import HeadCount from "./headCount/HeadCount";
|
||||
|
||||
const RealtimeCount: React.FC = () => {
|
||||
const [activeTab, setActiveTab] = useState<
|
||||
"register-face-count" | "get-face-count"
|
||||
>("register-face-count");
|
||||
|
||||
return (
|
||||
<div className="main-container">
|
||||
<div className="tabs">
|
||||
<button
|
||||
className={`tab-button ${
|
||||
activeTab === "register-face-count" ? "active" : ""
|
||||
}`}
|
||||
onClick={() => setActiveTab("register-face-count")}
|
||||
>
|
||||
Register Face Count
|
||||
</button>
|
||||
<button
|
||||
className={`tab-button ${
|
||||
activeTab === "get-face-count" ? "active" : ""
|
||||
}`}
|
||||
onClick={() => setActiveTab("get-face-count")}
|
||||
>
|
||||
Get Face Count
|
||||
</button>
|
||||
</div>
|
||||
<div className="tab-content">
|
||||
{activeTab === "register-face-count" && <RegisterFaceCount />}
|
||||
{activeTab === "get-face-count" && <HeadCount />}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default RealtimeCount;
|
120
components/realtimeCount/headCount/HeadCount.css
Normal file
120
components/realtimeCount/headCount/HeadCount.css
Normal file
@ -0,0 +1,120 @@
|
||||
.headcount-container {
|
||||
max-width: 500px;
|
||||
margin: 0 auto;
|
||||
padding: 20px;
|
||||
font-family: Arial, sans-serif;
|
||||
background-color: #f9f9f9;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.heading {
|
||||
text-align: center;
|
||||
color: #333;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.form {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 15px;
|
||||
}
|
||||
|
||||
.input-group {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 5px;
|
||||
}
|
||||
|
||||
.label {
|
||||
font-size: 14px;
|
||||
color: #555;
|
||||
}
|
||||
|
||||
.input {
|
||||
padding: 10px;
|
||||
font-size: 16px;
|
||||
border-radius: 4px;
|
||||
border: 1px solid #ccc;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.button {
|
||||
padding: 10px;
|
||||
font-size: 16px;
|
||||
background-color: #007bff;
|
||||
color: #fff;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.3s ease;
|
||||
}
|
||||
|
||||
.button:disabled {
|
||||
background-color: #ccc;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.error {
|
||||
color: red;
|
||||
text-align: center;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.names-container {
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.sub-heading {
|
||||
color: #333;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.list {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.list-item {
|
||||
padding: 10px;
|
||||
background-color: #fff;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 4px;
|
||||
margin-bottom: 5px;
|
||||
font-size: 14px;
|
||||
}
|
||||
.daily-counts-container {
|
||||
margin-top: 20px;
|
||||
padding: 15px;
|
||||
border-radius: 8px;
|
||||
background: #f8f9fa;
|
||||
box-shadow: 0px 4px 6px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.sub-heading {
|
||||
font-size: 1.5rem;
|
||||
margin-bottom: 10px;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.list {
|
||||
list-style-type: none;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.list-item {
|
||||
font-size: 1.1rem;
|
||||
padding: 8px 0;
|
||||
border-bottom: 1px solid #ddd;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.date {
|
||||
font-weight: bold;
|
||||
color: #007bff;
|
||||
}
|
||||
|
||||
.count {
|
||||
color: #555;
|
||||
}
|
183
components/realtimeCount/headCount/HeadCount.tsx
Normal file
183
components/realtimeCount/headCount/HeadCount.tsx
Normal file
@ -0,0 +1,183 @@
|
||||
import React, { useState } from "react";
|
||||
import "./HeadCount.css";
|
||||
|
||||
interface ApiResponse {
|
||||
total_unique_faces: number;
|
||||
daily_counts: { date: string; unique_faces: number }[];
|
||||
}
|
||||
|
||||
interface DateTimeRange {
|
||||
date: string;
|
||||
time: string;
|
||||
}
|
||||
|
||||
const HeadCount: React.FC = () => {
|
||||
const [from, setFrom] = useState<DateTimeRange>({ date: "", time: "" });
|
||||
const [to, setTo] = useState<DateTimeRange>({ date: "", time: "" });
|
||||
const [count, setCount] = useState<number | null>(null);
|
||||
const [loading, setLoading] = useState<boolean>(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [dailyCounts, setDailyCounts] = useState<
|
||||
{ date: string; count: number }[]
|
||||
>([]);
|
||||
|
||||
const handleInputChange = (
|
||||
e: React.ChangeEvent<HTMLInputElement>,
|
||||
field: "from" | "to"
|
||||
) => {
|
||||
const { name, value } = e.target;
|
||||
if (field === "from") {
|
||||
setFrom((prev) => ({ ...prev, [name]: value }));
|
||||
} else {
|
||||
setTo((prev) => ({ ...prev, [name]: value }));
|
||||
}
|
||||
};
|
||||
|
||||
const formatDateTime = (date: string, time: string) => {
|
||||
return `${date}T${time}:00+00:00`;
|
||||
};
|
||||
|
||||
const handleSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
|
||||
if (!from.date || !from.time || !to.date || !to.time) {
|
||||
setError("Please fill in all date and time fields.");
|
||||
return;
|
||||
}
|
||||
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
|
||||
const start = formatDateTime(from.date, from.time);
|
||||
const end = formatDateTime(to.date, to.time);
|
||||
console.log(start, end);
|
||||
try {
|
||||
const response = await fetch(
|
||||
`${
|
||||
process.env.NEXT_PUBLIC_BASE_URL
|
||||
}/face/headcount?start_time=${encodeURIComponent(
|
||||
start
|
||||
)}&end_time=${encodeURIComponent(end)}`,
|
||||
{
|
||||
method: "GET",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error("Failed to fetch data");
|
||||
}
|
||||
|
||||
const data: ApiResponse = await response.json();
|
||||
setCount(data.total_unique_faces);
|
||||
if (data?.daily_counts) {
|
||||
setDailyCounts(
|
||||
data.daily_counts.map((d) => ({
|
||||
date: d.date,
|
||||
count: d.unique_faces,
|
||||
}))
|
||||
);
|
||||
}
|
||||
} catch (err) {
|
||||
setError("An error occurred while fetching data.");
|
||||
console.error(err);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="headcount-container">
|
||||
<h1 className="heading">Head Count</h1>
|
||||
<form onSubmit={handleSubmit} className="form">
|
||||
<div className="input-group">
|
||||
<label htmlFor="from-date" className="label">
|
||||
From Date:
|
||||
</label>
|
||||
<input
|
||||
type="date"
|
||||
id="from-date"
|
||||
name="date"
|
||||
value={from.date}
|
||||
onChange={(e) => handleInputChange(e, "from")}
|
||||
required
|
||||
className="input"
|
||||
/>
|
||||
</div>
|
||||
<div className="input-group">
|
||||
<label htmlFor="from-time" className="label">
|
||||
From Time:
|
||||
</label>
|
||||
<input
|
||||
type="time"
|
||||
id="from-time"
|
||||
name="time"
|
||||
value={from.time}
|
||||
onChange={(e) => handleInputChange(e, "from")}
|
||||
required
|
||||
className="input"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="input-group">
|
||||
<label htmlFor="to-date" className="label">
|
||||
To Date:
|
||||
</label>
|
||||
<input
|
||||
type="date"
|
||||
id="to-date"
|
||||
name="date"
|
||||
value={to.date}
|
||||
onChange={(e) => handleInputChange(e, "to")}
|
||||
required
|
||||
className="input"
|
||||
/>
|
||||
</div>
|
||||
<div className="input-group">
|
||||
<label htmlFor="to-time" className="label">
|
||||
To Time:
|
||||
</label>
|
||||
<input
|
||||
type="time"
|
||||
id="to-time"
|
||||
name="time"
|
||||
value={to.time}
|
||||
onChange={(e) => handleInputChange(e, "to")}
|
||||
required
|
||||
className="input"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<button type="submit" disabled={loading} className="button">
|
||||
{loading ? "Submitting..." : "Submit"}
|
||||
</button>
|
||||
</form>
|
||||
|
||||
{error && <p className="error">{error}</p>}
|
||||
|
||||
{count && (
|
||||
<div className="names-container">
|
||||
<h2 className="sub-heading">Total Unique Face Count:</h2>
|
||||
<ul className="list">{count}</ul>
|
||||
</div>
|
||||
)}
|
||||
{dailyCounts?.length > 0 && (
|
||||
<div className="daily-counts-container">
|
||||
<h2 className="sub-heading">Daily Counts:</h2>
|
||||
<ul className="list">
|
||||
{dailyCounts.map((item, index) => (
|
||||
<li key={index} className="list-item">
|
||||
<span className="date">{item.date}:</span>
|
||||
<span className="count"> {item.count}</span>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default HeadCount;
|
166
components/realtimeCount/registerFaceCount/RegisterFaceCount.tsx
Normal file
166
components/realtimeCount/registerFaceCount/RegisterFaceCount.tsx
Normal file
@ -0,0 +1,166 @@
|
||||
import React from "react";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import Webcam from "react-webcam";
|
||||
import * as faceapi from "face-api.js";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Camera } from "lucide-react";
|
||||
import { useToast } from "@/hooks/use-toast";
|
||||
|
||||
const MODEL_URL = "https://cdn.jsdelivr.net/npm/@vladmandic/face-api/model";
|
||||
const PADDING = 60;
|
||||
|
||||
const RegisterFaceCount = () => {
|
||||
const webcamRef = useRef<Webcam>(null);
|
||||
const canvasRef = useRef<HTMLCanvasElement>(null);
|
||||
const [isModelLoaded, setIsModelLoaded] = useState(false);
|
||||
const [isDetecting, setIsDetecting] = useState(false);
|
||||
const { toast } = useToast();
|
||||
|
||||
useEffect(() => {
|
||||
const loadModels = async () => {
|
||||
try {
|
||||
await faceapi.nets.tinyFaceDetector.loadFromUri(MODEL_URL);
|
||||
await faceapi.nets.faceLandmark68Net.loadFromUri(MODEL_URL);
|
||||
await faceapi.nets.faceRecognitionNet.loadFromUri(MODEL_URL);
|
||||
setIsModelLoaded(true);
|
||||
} catch (error) {
|
||||
console.error("Error loading models:", error);
|
||||
toast({
|
||||
title: "Error",
|
||||
description: "Failed to load face detection models.",
|
||||
variant: "destructive",
|
||||
});
|
||||
}
|
||||
};
|
||||
loadModels();
|
||||
}, [toast]);
|
||||
|
||||
const extractFaceWithPadding = (
|
||||
video: HTMLVideoElement,
|
||||
box: faceapi.Box
|
||||
): HTMLCanvasElement => {
|
||||
const canvas = document.createElement("canvas");
|
||||
const context = canvas.getContext("2d");
|
||||
|
||||
// Calculate padded dimensions
|
||||
const x = Math.max(0, box.x - PADDING);
|
||||
const y = Math.max(0, box.y - PADDING);
|
||||
const width = Math.min(video.videoWidth - x, box.width + 2 * PADDING);
|
||||
const height = Math.min(video.videoHeight - y, box.height + 2 * PADDING);
|
||||
|
||||
canvas.width = width;
|
||||
canvas.height = height;
|
||||
|
||||
if (context) {
|
||||
context.drawImage(video, x, y, width, height, 0, 0, width, height);
|
||||
}
|
||||
|
||||
return canvas;
|
||||
};
|
||||
|
||||
const detectFace = async () => {
|
||||
if (!webcamRef.current?.video || !canvasRef.current) return;
|
||||
|
||||
const video = webcamRef.current.video;
|
||||
const canvas = canvasRef.current;
|
||||
const context = canvas.getContext("2d");
|
||||
|
||||
if (!context) return;
|
||||
|
||||
canvas.width = video.videoWidth;
|
||||
canvas.height = video.videoHeight;
|
||||
context.clearRect(0, 0, canvas.width, canvas.height);
|
||||
context.translate(canvas.width, 0);
|
||||
context.scale(-1, 1);
|
||||
|
||||
const detections = await faceapi
|
||||
.detectAllFaces(video, new faceapi.TinyFaceDetectorOptions())
|
||||
.withFaceLandmarks()
|
||||
.withFaceDescriptors();
|
||||
|
||||
if (detections.length > 0) {
|
||||
const highConfidenceDetections = detections.filter(
|
||||
(detection) => detection.detection.score > 0.5
|
||||
);
|
||||
|
||||
for (const detection of highConfidenceDetections) {
|
||||
const { box } = detection.detection;
|
||||
context.strokeStyle = "#00FF00";
|
||||
context.lineWidth = 2;
|
||||
context.strokeRect(box.x, box.y, box.width, box.height);
|
||||
context.save();
|
||||
context.scale(-1, 1);
|
||||
context.fillStyle = "#00FF00";
|
||||
context.font = "16px Arial";
|
||||
context.fillText(
|
||||
`Confidence: ${Math.round(detection.detection.score * 100)}%`,
|
||||
-box.x - box.width,
|
||||
box.y - 5
|
||||
);
|
||||
context.restore();
|
||||
|
||||
const faceCanvas = extractFaceWithPadding(video, box);
|
||||
faceCanvas.toBlob(
|
||||
(blob) => {
|
||||
if (blob) sendFaceDataToAPI(blob);
|
||||
},
|
||||
"image/jpeg",
|
||||
0.95
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const sendFaceDataToAPI = async (imageBlob: Blob) => {
|
||||
try {
|
||||
const formData = new FormData();
|
||||
formData.append("image", imageBlob, "face.jpg");
|
||||
|
||||
const response = await fetch(
|
||||
`${process.env.NEXT_PUBLIC_BASE_URL}/face/search`,
|
||||
{
|
||||
method: "POST",
|
||||
body: formData,
|
||||
}
|
||||
);
|
||||
|
||||
const data = await response.json();
|
||||
toast({ title: data?.name, description: data.message });
|
||||
} catch (error) {
|
||||
console.error("Error sending face data:", error);
|
||||
toast({
|
||||
title: "Error",
|
||||
description: "Failed to send face data.",
|
||||
variant: "destructive",
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const startDetection = () => {
|
||||
if (!isModelLoaded) return;
|
||||
setIsDetecting(true);
|
||||
setInterval(detectFace, 1000);
|
||||
};
|
||||
return (
|
||||
<div className="max-w-3xl mx-auto">
|
||||
<div className="relative">
|
||||
<Webcam ref={webcamRef} mirrored className="w-full rounded-lg" />
|
||||
<canvas
|
||||
ref={canvasRef}
|
||||
className="absolute top-0 left-0 w-full h-full"
|
||||
/>
|
||||
</div>
|
||||
<div className="mt-6 flex justify-center">
|
||||
<Button
|
||||
onClick={startDetection}
|
||||
disabled={!isModelLoaded || isDetecting}
|
||||
>
|
||||
<Camera className="mr-2 h-4 w-4" />
|
||||
{isDetecting ? "Detecting..." : "Start Realtime Detection"}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default RegisterFaceCount;
|
@ -81,7 +81,7 @@ const RealtimeFaceDetection = () => {
|
||||
|
||||
if (detections.length > 0) {
|
||||
const highConfidenceDetections = detections.filter(
|
||||
(detection) => detection.detection.score > 0.7
|
||||
(detection) => detection.detection.score > 0.5
|
||||
);
|
||||
|
||||
for (const detection of highConfidenceDetections) {
|
||||
|
79
components/ui/card.tsx
Normal file
79
components/ui/card.tsx
Normal file
@ -0,0 +1,79 @@
|
||||
import * as React from "react"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const Card = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
React.HTMLAttributes<HTMLDivElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"rounded-lg border bg-card text-card-foreground shadow-sm",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
Card.displayName = "Card"
|
||||
|
||||
const CardHeader = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
React.HTMLAttributes<HTMLDivElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn("flex flex-col space-y-1.5 p-6", className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
CardHeader.displayName = "CardHeader"
|
||||
|
||||
const CardTitle = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
React.HTMLAttributes<HTMLDivElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"text-2xl font-semibold leading-none tracking-tight",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
CardTitle.displayName = "CardTitle"
|
||||
|
||||
const CardDescription = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
React.HTMLAttributes<HTMLDivElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn("text-sm text-muted-foreground", className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
CardDescription.displayName = "CardDescription"
|
||||
|
||||
const CardContent = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
React.HTMLAttributes<HTMLDivElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<div ref={ref} className={cn("p-6 pt-0", className)} {...props} />
|
||||
))
|
||||
CardContent.displayName = "CardContent"
|
||||
|
||||
const CardFooter = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
React.HTMLAttributes<HTMLDivElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn("flex items-center p-6 pt-0", className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
CardFooter.displayName = "CardFooter"
|
||||
|
||||
export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent }
|
Loading…
x
Reference in New Issue
Block a user