Преглед изворни кода

face recognition implementation

Dr-Swopt пре 1 недеља
родитељ
комит
ae5e4b403a

+ 17 - 0
src/app/components/enroll-dialog/enroll-dialog.component.css

@@ -0,0 +1,17 @@
+video {
+  width: 100%;
+  max-height: 300px;
+  border-radius: 8px;
+  border: 1px solid #ccc;
+  background: #000;
+}
+
+mat-dialog-content {
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+}
+
+mat-form-field {
+  margin-top: 10px;
+}

+ 17 - 0
src/app/components/enroll-dialog/enroll-dialog.component.html

@@ -0,0 +1,17 @@
+<h2 mat-dialog-title>Enroll Face</h2>
+<mat-dialog-content>
+  <video #video autoplay></video>
+  <canvas #canvas style="display:none;"></canvas>
+
+  <mat-form-field appearance="fill" style="width:100%; margin-top:10px;">
+    <mat-label>Name</mat-label>
+    <input matInput [(ngModel)]="name" />
+  </mat-form-field>
+</mat-dialog-content>
+
+<mat-dialog-actions align="end">
+  <button mat-button (click)="cancel()" [disabled]="loading">Cancel</button>
+  <button mat-raised-button color="primary" (click)="enroll()" [disabled]="loading">
+    {{ loading ? 'Enrolling...' : 'Enroll' }}
+  </button>
+</mat-dialog-actions>

+ 88 - 0
src/app/components/enroll-dialog/enroll-dialog.component.ts

@@ -0,0 +1,88 @@
+import { Component, ViewChild, ElementRef, AfterViewInit, Inject } from '@angular/core';
+import { MatDialogRef, MAT_DIALOG_DATA, MatDialogModule } from '@angular/material/dialog';
+import { CommonModule } from '@angular/common';
+import { FormsModule } from '@angular/forms';
+import { HttpClient, HttpClientModule } from '@angular/common/http';
+import { MatButtonModule } from '@angular/material/button';
+import { MatInputModule } from '@angular/material/input';
+import { MatSnackBar, MatSnackBarModule } from '@angular/material/snack-bar';
+import { webConfig } from '../../config';
+
+@Component({
+  selector: 'app-enroll-dialog',
+  standalone: true,
+  imports: [
+    CommonModule,
+    FormsModule,
+    HttpClientModule,
+    MatDialogModule,
+    MatButtonModule,
+    MatInputModule,
+    MatSnackBarModule
+  ],
+  templateUrl: './enroll-dialog.component.html',
+  styleUrls: ['./enroll-dialog.component.css']
+})
+export class EnrollDialogComponent implements AfterViewInit {
+  @ViewChild('video') videoRef!: ElementRef<HTMLVideoElement>;
+  @ViewChild('canvas') canvasRef!: ElementRef<HTMLCanvasElement>;
+
+  name: string = '';
+  loading: boolean = false;
+
+  constructor(
+    private http: HttpClient,
+    private snack: MatSnackBar,
+    private dialogRef: MatDialogRef<EnrollDialogComponent>,
+    @Inject(MAT_DIALOG_DATA) public data: any
+  ) {
+    if (data?.defaultName) {
+      this.name = data.defaultName;
+    }
+  }
+
+  ngAfterViewInit() {
+    navigator.mediaDevices.getUserMedia({ video: true })
+      .then(stream => this.videoRef.nativeElement.srcObject = stream)
+      .catch(err => console.error('Webcam error:', err));
+  }
+
+  private captureImage(): string {
+    const canvas = this.canvasRef.nativeElement;
+    const video = this.videoRef.nativeElement;
+    canvas.width = video.videoWidth;
+    canvas.height = video.videoHeight;
+    canvas.getContext('2d')?.drawImage(video, 0, 0);
+    return canvas.toDataURL('image/jpeg').split(',')[1]; // base64 only
+  }
+
+  enroll() {
+    if (!this.name.trim()) {
+      this.snack.open('Please enter a name', 'Close', { duration: 2000 });
+      return;
+    }
+
+    this.loading = true;
+    const base64Image = this.captureImage();
+
+    this.http.post<any>(`${webConfig.exposedUrl}/api/face/enroll`, {
+      imageBase64: base64Image,
+      name: this.name
+    }).subscribe({
+      next: res => {
+        this.loading = false;
+        this.snack.open(`Enrolled ${this.name} successfully!`, 'Close', { duration: 3000 });
+        this.dialogRef.close(res);
+      },
+      error: err => {
+        console.error(err);
+        this.loading = false;
+        this.snack.open('Failed to enroll face', 'Close', { duration: 3000 });
+      }
+    });
+  }
+
+  cancel() {
+    this.dialogRef.close();
+  }
+}

+ 3 - 3
src/app/dashboard/dashboard.component.html

@@ -5,17 +5,17 @@
 
 <mat-tab-group>
   <mat-tab label="Dashboard">
-    <app-dashboard-home></app-dashboard-home>
+    <app-webcam></app-webcam>
   </mat-tab>
 
   <mat-tab label="Activities">
     <app-activity></app-activity>
   </mat-tab>
-  
+
   <mat-tab label="FFB Production">
     <app-ffb-production></app-ffb-production>
   </mat-tab>
-    
+
   <mat-tab label="Plantation">
     <!-- Integrate PlantationTreeComponent here -->
     <app-plantation-tree></app-plantation-tree>

+ 3 - 3
src/app/dashboard/dashboard.component.ts

@@ -4,10 +4,10 @@ import { CommonModule } from '@angular/common';
 import { MatTabsModule } from '@angular/material/tabs';
 import { MatToolbarModule } from '@angular/material/toolbar';
 import { MatButtonModule } from '@angular/material/button';
-import { DashboardHomeComponent } from './dashboard.home.component';
 import { PlantationTreeComponent } from "../plantation/plantation-tree.component";
 import { ActivityComponent } from '../activity/activity.component';
 import { FfbProductionComponent } from "../ffb/ffb-production.component";
+import { WebcamComponent } from "../webcam/webcam.component";
 
 @Component({
   selector: 'app-dashboard',
@@ -17,10 +17,10 @@ import { FfbProductionComponent } from "../ffb/ffb-production.component";
     MatTabsModule,
     MatToolbarModule,
     MatButtonModule,
-    DashboardHomeComponent,
     PlantationTreeComponent,
     ActivityComponent,
-    FfbProductionComponent
+    FfbProductionComponent,
+    WebcamComponent
 ],
   templateUrl: './dashboard.component.html',
   styleUrls: ['./dashboard.component.css']

+ 0 - 2
src/app/dashboard/dashboard.home.component.html

@@ -1,2 +0,0 @@
-<h1>Dashboard</h1>
-<h3>Welcome, {{username}} </h3>

+ 0 - 27
src/app/dashboard/dashboard.home.component.ts

@@ -1,27 +0,0 @@
-// src/app/dashboard/home.component.ts
-
-import { Component } from '@angular/core';
-import { CommonModule } from '@angular/common';
-import { AuthService } from '../services/auth.service';
-
-@Component({
-    standalone: true,
-    selector: 'app-dashboard-home',
-    imports: [CommonModule],
-    templateUrl: './dashboard.home.component.html',
-    styleUrls: ['./dashboard.component.css']
-})
-export class DashboardHomeComponent {
-    username: string = `Guest`
-    token: string = ``
-
-    constructor(private auth: AuthService) {
-
-    }
-
-    ngOnInit(): void {
-        this.username = this.auth.getUsername() || `No name`
-        this.token = this.auth.getToken() || `Null`
-    }
-
-}

+ 77 - 0
src/app/webcam/webcam.component.css

@@ -0,0 +1,77 @@
+.webcam-card {
+    max-width: 480px;
+    margin: 40px auto;
+    padding: 24px;
+    border-radius: 12px;
+    text-align: center;
+    box-shadow: 0 6px 18px rgba(0, 0, 0, 0.12);
+    background: #ffffff;
+}
+
+.title {
+    margin-bottom: 16px;
+    font-weight: 600;
+    color: #333;
+}
+
+.video-container {
+    position: relative;
+    width: 100%;
+    overflow: hidden;
+    border-radius: 8px;
+    border: 6px solid #ddd;
+    /* thicker default border */
+    background: #000;
+    transition: border-color 0.3s ease;
+}
+
+.video-container.success-border {
+    border-color: #4caf50;
+    /* green */
+}
+
+.video-container.fail-border {
+    border-color: #f44336;
+    /* red */
+}
+
+video {
+    width: 100%;
+    height: auto;
+    display: block;
+    border-radius: 8px;
+    object-fit: cover;
+}
+
+.actions {
+    margin-top: 16px;
+    display: flex;
+    justify-content: center;
+    gap: 16px;
+}
+
+.actions button {
+    min-width: 120px;
+    font-weight: 500;
+}
+
+.result-card {
+    margin-top: 24px;
+    padding: 12px;
+    border-radius: 8px;
+    background: #f5f5f5;
+    text-align: left;
+}
+
+.result-card h3 {
+    margin: 0 0 8px 0;
+    font-weight: 600;
+    font-size: 1.1rem;
+    color: #444;
+}
+
+.result-card p {
+    margin: 4px 0;
+    font-size: 0.95rem;
+    color: #555;
+}

+ 20 - 0
src/app/webcam/webcam.component.html

@@ -0,0 +1,20 @@
+<mat-card class="webcam-card">
+    <h2 class="title">Face Recognition</h2>
+
+    <div class="video-container"
+        [ngClass]="{'success-border': scanStatus === 'success', 'fail-border': scanStatus === 'fail'}">
+        <video #video autoplay></video>
+        <canvas #canvas style="display:none;"></canvas>
+    </div>
+
+    <div class="actions">
+        <button mat-raised-button color="primary" (click)="scanFace()">Scan Face</button>
+        <button mat-raised-button color="accent" (click)="openEnrollDialog()">Enroll Face</button>
+    </div>
+
+    <div *ngIf="result" class="result-card">
+        <h3>Scan Result</h3>
+        <p><strong>Name:</strong> {{ result.name }}</p>
+        <p><strong>Confidence:</strong> {{ result.confidence | number:'1.2-2' }}</p>
+    </div>
+</mat-card>

+ 86 - 0
src/app/webcam/webcam.component.ts

@@ -0,0 +1,86 @@
+import { Component, ViewChild, ElementRef, AfterViewInit } from '@angular/core';
+import { CommonModule } from '@angular/common';
+import { HttpClient, HttpClientModule } from '@angular/common/http';
+import { MatButtonModule } from '@angular/material/button';
+import { MatCardModule } from '@angular/material/card';
+import { MatSnackBar, MatSnackBarModule } from '@angular/material/snack-bar';
+import { MatDialog, MatDialogModule } from '@angular/material/dialog';
+import { webConfig } from '../config';
+import { EnrollDialogComponent } from '../components/enroll-dialog/enroll-dialog.component';
+
+@Component({
+    selector: 'app-webcam',
+    standalone: true,
+    imports: [
+        CommonModule,
+        HttpClientModule,
+        MatButtonModule,
+        MatCardModule,
+        MatSnackBarModule,
+        MatDialogModule
+    ],
+    templateUrl: './webcam.component.html',
+    styleUrls: ['./webcam.component.css']
+})
+export class WebcamComponent implements AfterViewInit {
+    @ViewChild('video') videoRef!: ElementRef<HTMLVideoElement>;
+    @ViewChild('canvas') canvasRef!: ElementRef<HTMLCanvasElement>;
+
+    result: any = null;
+    scanStatus: 'success' | 'fail' | null = null;
+
+    constructor(private http: HttpClient, private snack: MatSnackBar, private dialog: MatDialog) { }
+
+    ngAfterViewInit() {
+        navigator.mediaDevices.getUserMedia({ video: true })
+            .then(stream => this.videoRef.nativeElement.srcObject = stream)
+            .catch(err => console.error('Error accessing webcam:', err));
+    }
+
+    private captureImage(): string {
+        const canvas = this.canvasRef.nativeElement;
+        const video = this.videoRef.nativeElement;
+        canvas.width = video.videoWidth;
+        canvas.height = video.videoHeight;
+        canvas.getContext('2d')?.drawImage(video, 0, 0);
+        return canvas.toDataURL('image/jpeg').split(',')[1]; // base64 only
+    }
+
+    scanFace() {
+        const base64Image = this.captureImage();
+
+        this.http.post<any>(`${webConfig.exposedUrl}/api/face/scan`, { imageBase64: base64Image })
+            .subscribe({
+                next: res => {
+                    this.result = res;
+                    // Determine border color based on confidence
+                    this.scanStatus = (res.name && res.name !== 'Unknown') ? 'success' : 'fail';
+
+                    this.snack.open(
+                        this.scanStatus === 'success'
+                            ? `Scan successful: ${res.name} (Confidence: ${res.confidence?.toFixed(2)})`
+                            : 'No match found',
+                        'Close',
+                        { duration: 4000 }
+                    );
+                },
+                error: err => {
+                    console.error(err);
+                    this.scanStatus = 'fail';
+                    this.snack.open('Failed to scan face', 'Close', { duration: 3000 });
+                }
+            });
+    }
+
+    openEnrollDialog() {
+        const dialogRef = this.dialog.open(EnrollDialogComponent, {
+            width: '400px'
+        });
+
+        dialogRef.afterClosed().subscribe(result => {
+            if (result) {
+                this.snack.open(`Enrollment completed for ${result.name}`, 'Close', { duration: 3000 });
+            }
+        });
+    }
+}