Claude Code Plugins

Community-maintained marketplace

Feedback

face-recognition

@omanjaya/attendancedev
0
0

Face recognition system patterns for attendance. Use when working with face detection, verification, enrollment, liveness detection, or any biometric authentication features.

Install Skill

1Download skill
2Enable skills in Claude

Open claude.ai/settings/capabilities and find the "Skills" section

3Upload to Claude

Click "Upload skill" and select the downloaded ZIP file

Note: Please verify skill by going through its instructions before using it.

SKILL.md

name face-recognition
description Face recognition system patterns for attendance. Use when working with face detection, verification, enrollment, liveness detection, or any biometric authentication features.
allowed-tools Read, Grep, Glob, Edit, Write

Face Recognition Skill

This skill provides guidance for implementing and maintaining the face recognition system used for attendance tracking.

Architecture Overview

Frontend (Vue 3 + TypeScript)
├── components/           # UI Components
│   ├── FaceRecognition.vue
│   ├── FaceEnrollment.vue
│   └── FaceAnalytics.vue
├── services/             # Detection Libraries
│   ├── FaceDetectionService.js    # Face-API.js wrapper
│   └── MediaPipeFaceService.js    # MediaPipe wrapper
├── composables/
│   └── useFaceDetection.js        # Composable logic
├── stores/
│   └── faceDetection.ts           # Pinia state
└── types/
    └── face-recognition.ts        # TypeScript interfaces

Backend (Laravel)
├── Services/
│   └── FaceRecognitionService.php # Core business logic
├── Controllers/
│   └── FaceDetectionController.php
└── routes/
    └── api_face_recognition.php

Face Descriptor Format

Face descriptors are 128-dimensional float arrays generated by the face recognition neural network:

// TypeScript
type FaceDescriptor = number[]  // Length: 128, Range: -1 to 1

// PHP
/** @var float[] $descriptor 128 elements */

Frontend Implementation

Using Face-API.js (Primary)

// resources/js/services/FaceDetectionService.js
import * as faceapi from 'face-api.js'

// Load models (call once at app init)
async function loadModels() {
  const modelPath = '/models'
  await Promise.all([
    faceapi.nets.tinyFaceDetector.loadFromUri(modelPath),
    faceapi.nets.faceLandmark68Net.loadFromUri(modelPath),
    faceapi.nets.faceRecognitionNet.loadFromUri(modelPath),
    faceapi.nets.faceExpressionNet.loadFromUri(modelPath),
  ])
}

// Detect face and get descriptor
async function detectFace(video: HTMLVideoElement): Promise<FaceDetectionResult | null> {
  const detection = await faceapi
    .detectSingleFace(video, new faceapi.TinyFaceDetectorOptions({
      inputSize: 416,
      scoreThreshold: 0.5
    }))
    .withFaceLandmarks()
    .withFaceDescriptor()
    .withFaceExpressions()

  if (!detection) return null

  return {
    confidence: detection.detection.score,
    boundingBox: detection.detection.box,
    landmarks: detection.landmarks.positions,
    descriptor: Array.from(detection.descriptor),  // Float32Array → number[]
    expressions: detection.expressions
  }
}

Using MediaPipe (Lightweight Alternative)

// resources/js/services/MediaPipeFaceService.js
import { FaceDetection } from '@mediapipe/face_detection'

const faceDetection = new FaceDetection({
  locateFile: (file) => `/mediapipe/${file}`
})

faceDetection.setOptions({
  model: 'short',           // 'short' (fast) or 'full' (accurate)
  minDetectionConfidence: 0.5,
  maxNumFaces: 1
})

// Note: MediaPipe doesn't generate 128-dim descriptors natively
// Use Face-API.js for descriptor generation when matching is needed

Composable Pattern

// resources/js/composables/useFaceDetection.js
import { ref, onMounted, onUnmounted } from 'vue'
import { useFaceDetectionStore } from '@/stores/faceDetection'

export function useFaceDetection() {
  const store = useFaceDetectionStore()
  const videoRef = ref<HTMLVideoElement | null>(null)
  const canvasRef = ref<HTMLCanvasElement | null>(null)

  async function startCamera() {
    const stream = await navigator.mediaDevices.getUserMedia({
      video: {
        width: { ideal: 640 },
        height: { ideal: 480 },
        facingMode: 'user'
      }
    })
    if (videoRef.value) {
      videoRef.value.srcObject = stream
    }
    store.setCameraActive(true)
  }

  function stopCamera() {
    const stream = videoRef.value?.srcObject as MediaStream
    stream?.getTracks().forEach(track => track.stop())
    store.setCameraActive(false)
  }

  async function captureAndVerify() {
    if (!videoRef.value) return null

    const result = await detectFace(videoRef.value)
    if (!result || result.confidence < 0.7) {
      return { success: false, error: 'No face detected or low confidence' }
    }

    // Send to backend for verification
    const response = await faceRecognitionAPI.verifyFace({
      descriptor: result.descriptor,
      confidence: result.confidence,
      liveness: result.liveness
    })

    return response
  }

  onUnmounted(() => stopCamera())

  return {
    videoRef,
    canvasRef,
    startCamera,
    stopCamera,
    captureAndVerify,
    isReady: computed(() => store.isInitialized),
    isProcessing: computed(() => store.processing)
  }
}

Pinia Store Structure

// resources/js/stores/faceDetection.ts
import { defineStore } from 'pinia'

interface FaceDetectionState {
  cameraActive: boolean
  isInitialized: boolean
  processing: boolean
  currentDetection: FaceDetectionResult | null
  settings: {
    minConfidence: number      // Default: 0.7
    enableLiveness: boolean    // Default: true
    detectionMethod: 'face-api' | 'mediapipe'
  }
}

export const useFaceDetectionStore = defineStore('faceDetection', () => {
  const state = reactive<FaceDetectionState>({
    cameraActive: false,
    isInitialized: false,
    processing: false,
    currentDetection: null,
    settings: {
      minConfidence: 0.7,
      enableLiveness: true,
      detectionMethod: 'face-api'
    }
  })

  // Getters
  const canCapture = computed(() =>
    state.cameraActive && state.isInitialized && !state.processing
  )

  const detectionQuality = computed(() => {
    const conf = state.currentDetection?.confidence ?? 0
    if (conf >= 0.9) return 'excellent'
    if (conf >= 0.7) return 'good'
    if (conf >= 0.5) return 'fair'
    return 'poor'
  })

  return { ...toRefs(state), canCapture, detectionQuality }
})

Backend Implementation

Service Layer

// app/Services/FaceRecognitionService.php

class FaceRecognitionService
{
    // Configuration constants
    const SIMILARITY_THRESHOLD = 0.6;      // Minimum match score
    const MIN_CONFIDENCE = 0.7;            // Minimum detection confidence
    const QUALITY_THRESHOLD = 0.7;         // Minimum quality for registration
    const CACHE_TTL = 3600;                // Face data cache (1 hour)

    /**
     * Register a new face for an employee
     */
    public function registerFace(int $employeeId, array $data): array
    {
        // Validate descriptor
        if (count($data['descriptor']) !== 128) {
            throw new InvalidArgumentException('Descriptor must be 128-dimensional');
        }

        if ($data['confidence'] < self::MIN_CONFIDENCE) {
            return [
                'success' => false,
                'message' => 'Detection confidence too low'
            ];
        }

        $employee = Employee::findOrFail($employeeId);

        // Store face image securely
        $imagePath = $this->storeFaceImage($employeeId, $data['image']);

        // Calculate quality score
        $quality = $this->calculateQualityScore($data);

        // Save to employee metadata
        $employee->update([
            'face_descriptor' => json_encode($data['descriptor']),
            'face_image_path' => $imagePath,
            'face_quality_score' => $quality,
            'face_registered_at' => now(),
        ]);

        // Clear cache
        Cache::forget("face_data_{$employeeId}");

        return [
            'success' => true,
            'quality' => $quality,
            'message' => 'Face registered successfully'
        ];
    }

    /**
     * Verify a face against registered employees
     */
    public function verifyFace(array $data): array
    {
        $descriptor = $data['descriptor'];
        $registeredFaces = $this->getRegisteredFaces();

        $bestMatch = null;
        $highestSimilarity = 0;

        foreach ($registeredFaces as $face) {
            $similarity = $this->cosineSimilarity($descriptor, $face['descriptor']);

            if ($similarity > $highestSimilarity && $similarity >= self::SIMILARITY_THRESHOLD) {
                $highestSimilarity = $similarity;
                $bestMatch = $face;
            }
        }

        if (!$bestMatch) {
            return [
                'success' => false,
                'message' => 'No matching face found'
            ];
        }

        return [
            'success' => true,
            'employee_id' => $bestMatch['employee_id'],
            'employee_name' => $bestMatch['employee_name'],
            'similarity' => $highestSimilarity,
            'confidence' => $data['confidence']
        ];
    }

    /**
     * Calculate cosine similarity between two descriptors
     */
    private function cosineSimilarity(array $a, array $b): float
    {
        $dotProduct = 0;
        $normA = 0;
        $normB = 0;

        for ($i = 0; $i < 128; $i++) {
            $dotProduct += $a[$i] * $b[$i];
            $normA += $a[$i] * $a[$i];
            $normB += $b[$i] * $b[$i];
        }

        $denominator = sqrt($normA) * sqrt($normB);

        return $denominator > 0 ? $dotProduct / $denominator : 0;
    }

    /**
     * Calculate face quality score (multi-factor)
     */
    private function calculateQualityScore(array $data): float
    {
        $weights = [
            'confidence' => 0.30,
            'face_size' => 0.20,
            'pose' => 0.20,
            'lighting' => 0.15,
            'blur' => 0.15,
        ];

        $scores = [
            'confidence' => $data['confidence'] ?? 0,
            'face_size' => $this->calculateFaceSizeScore($data['boundingBox'] ?? null),
            'pose' => $data['pose_score'] ?? 0.8,
            'lighting' => $data['lighting_score'] ?? 0.8,
            'blur' => $data['blur_score'] ?? 0.8,
        ];

        $totalScore = 0;
        foreach ($weights as $factor => $weight) {
            $totalScore += ($scores[$factor] ?? 0) * $weight;
        }

        return round($totalScore, 4);
    }

    /**
     * Get all registered faces (cached)
     */
    private function getRegisteredFaces(): array
    {
        return Cache::remember('registered_faces', self::CACHE_TTL, function () {
            return Employee::whereNotNull('face_descriptor')
                ->where('status', 'active')
                ->get()
                ->map(fn ($e) => [
                    'employee_id' => $e->id,
                    'employee_name' => $e->name,
                    'descriptor' => json_decode($e->face_descriptor, true),
                ])
                ->toArray();
        });
    }
}

Controller

// app/Http/Controllers/FaceDetectionController.php

class FaceDetectionController extends Controller
{
    public function __construct(
        private readonly FaceRecognitionService $faceService
    ) {}

    public function register(Request $request): JsonResponse
    {
        $validated = $request->validate([
            'employee_id' => 'required|exists:employees,id',
            'descriptor' => 'required|array|size:128',
            'descriptor.*' => 'numeric',
            'confidence' => 'required|numeric|min:0.7|max:1',
            'image' => 'required|string',  // Base64
        ]);

        $result = $this->faceService->registerFace(
            $validated['employee_id'],
            $validated
        );

        return response()->json($result, $result['success'] ? 200 : 400);
    }

    public function verify(Request $request): JsonResponse
    {
        $validated = $request->validate([
            'descriptor' => 'required|array|size:128',
            'descriptor.*' => 'numeric',
            'confidence' => 'required|numeric|min:0|max:1',
            'liveness' => 'nullable|numeric|min:0|max:1',
        ]);

        $result = $this->faceService->verifyFace($validated);

        return response()->json($result);
    }
}

TypeScript Interfaces

// resources/js/types/face-recognition.ts

export interface FaceDetectionResult {
  confidence: number           // 0-1 detection score
  liveness: number            // 0-1 liveness score
  boundingBox: BoundingBox | null
  landmarks?: FaceLandmark[]
  descriptor?: number[]       // 128-dim array
  expressions?: FaceExpressions
}

export interface BoundingBox {
  x: number
  y: number
  width: number
  height: number
}

export interface FaceLandmark {
  x: number
  y: number
  z?: number
}

export interface FaceExpressions {
  neutral: number
  happy: number
  sad: number
  angry: number
  fearful: number
  disgusted: number
  surprised: number
}

export interface VerificationResult {
  success: boolean
  employee_id?: string
  employee_name?: string
  similarity?: number
  confidence?: number
  message?: string
}

export interface RegistrationResult {
  success: boolean
  quality?: number
  message: string
}

export type DetectionMethod = 'face-api' | 'mediapipe'

Liveness Detection

Basic liveness checks to prevent photo spoofing:

// Frontend liveness detection
async function checkLiveness(detections: FaceDetectionResult[]): Promise<number> {
  let score = 0.5  // Base score

  // Check for blink (eye aspect ratio change)
  if (detectBlink(detections)) score += 0.15

  // Check for head movement
  if (detectHeadMovement(detections)) score += 0.15

  // Check for expression variation
  if (detectExpressionChange(detections)) score += 0.1

  // Texture analysis (photo vs real face)
  if (analyzeTexture(detections)) score += 0.1

  return Math.min(score, 1.0)
}

API Routes

// routes/api_face_recognition.php

Route::middleware('auth:sanctum')->prefix('face-recognition')->group(function () {
    Route::post('/register', [FaceDetectionController::class, 'register']);
    Route::post('/verify', [FaceDetectionController::class, 'verify']);
    Route::post('/update', [FaceDetectionController::class, 'update']);
    Route::delete('/delete/{employee}', [FaceDetectionController::class, 'delete']);
    Route::get('/statistics', [FaceDetectionController::class, 'statistics']);
});

Quality Thresholds

Score Rating Action
>= 0.9 Excellent Accept
0.7 - 0.9 Good Accept
0.5 - 0.7 Fair Warn user, suggest retry
< 0.5 Poor Reject, require retry

Best Practices

  1. Always validate descriptor length (must be exactly 128)
  2. Use cosine similarity for descriptor comparison (not Euclidean distance)
  3. Cache registered faces to improve verification speed
  4. Require minimum confidence of 0.7 for registration
  5. Store face images privately in non-public storage
  6. Clear cache when face data is updated or deleted
  7. Use transactions when updating face data with related records
  8. Log face operations for audit trail

Common Issues

Low Detection Confidence

  • Ensure adequate lighting
  • Face should be centered and fully visible
  • Avoid extreme angles (> 30 degrees)

Verification Failures

  • Check similarity threshold (default 0.6)
  • Verify descriptor format (128 floats)
  • Confirm employee has registered face

Performance

  • Load models once at app initialization
  • Use tinyFaceDetector for faster detection
  • Cache registered faces (1 hour TTL)