This commit is contained in:
@@ -0,0 +1,23 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Emgu.CV" Version="4.13.0.5924" />
|
||||
<PackageReference Include="Emgu.CV.runtime.windows" Version="4.13.0.5924" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.SignalR" Version="1.2.11" />
|
||||
<PackageReference Include="Microsoft.Data.Sqlite" Version="10.0.9" />
|
||||
<PackageReference Include="System.Drawing.Common" Version="10.0.9" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<None Update="haarcascade_frontalface_default.xml">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</None>
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -0,0 +1,7 @@
|
||||
using Microsoft.AspNetCore.SignalR;
|
||||
|
||||
namespace FaceRecognitionApp.Hubs;
|
||||
|
||||
public class FaceHub : Hub
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
namespace FaceRecognitionApp.Models;
|
||||
|
||||
public class Employee
|
||||
{
|
||||
public int Id { get; set; }
|
||||
public string Name { get; set; } = string.Empty;
|
||||
public string? Department { get; set; }
|
||||
public byte[] FaceDescriptor { get; set; } = Array.Empty<byte>();
|
||||
public byte[]? Photo { get; set; }
|
||||
}
|
||||
|
||||
public class DetectedFace
|
||||
{
|
||||
public byte[] ImageData { get; set; } = Array.Empty<byte>();
|
||||
public bool IsRecognized { get; set; }
|
||||
public Employee? MatchedEmployee { get; set; }
|
||||
public double Confidence { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
using FaceRecognitionApp.Hubs;
|
||||
using FaceRecognitionApp.Services;
|
||||
using Microsoft.AspNetCore.SignalR;
|
||||
|
||||
var builder = WebApplication.CreateBuilder(args);
|
||||
|
||||
// Регистрация сервисов
|
||||
builder.Services.AddSingleton<DatabaseService>();
|
||||
builder.Services.AddSingleton<FaceRecognitionService>();
|
||||
builder.Services.AddSignalR();
|
||||
|
||||
var app = builder.Build();
|
||||
|
||||
// 1. Включаем поиск файлов по умолчанию (index.html, default.html и т.д.)
|
||||
app.UseDefaultFiles();
|
||||
|
||||
// 2. Включаем раздачу статических файлов (css, js, картинки)
|
||||
app.UseStaticFiles();
|
||||
|
||||
// 3. Подключаем SignalR хаб
|
||||
app.MapHub<FaceHub>("/faceHub");
|
||||
|
||||
// Инициализация камеры и распознавания
|
||||
var db = app.Services.GetRequiredService<DatabaseService>();
|
||||
var faceService = app.Services.GetRequiredService<FaceRecognitionService>();
|
||||
|
||||
// URL камеры (замените на свой или используйте тестовый видеофайл)
|
||||
var cameraUrl = Environment.GetEnvironmentVariable("CAMERA_URL") ?? "rtsp://admin:4NUDZhJ7@172.22.10.101:554/cam/realmonitor?channel=1&subtype=1";
|
||||
|
||||
try
|
||||
{
|
||||
Console.WriteLine($"Connecting to camera: {cameraUrl}...");
|
||||
var camera = new CameraService(cameraUrl, faceService, async (face) => {
|
||||
var hub = app.Services.GetRequiredService<IHubContext<FaceHub>>();
|
||||
await hub.Clients.All.SendAsync("NewFace", new
|
||||
{
|
||||
image = Convert.ToBase64String(face.ImageData),
|
||||
isRecognized = face.IsRecognized,
|
||||
name = face.MatchedEmployee?.Name ?? "Unknown"
|
||||
});
|
||||
});
|
||||
camera.Start();
|
||||
Console.WriteLine("✅ Camera service started.");
|
||||
|
||||
// Корректное завершение работы камеры при остановке приложения
|
||||
app.Lifetime.ApplicationStopping.Register(() => {
|
||||
Console.WriteLine("Shutting down camera...");
|
||||
camera.Dispose();
|
||||
});
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine($"⚠️ Camera Error: {ex.Message}");
|
||||
Console.WriteLine("Application will run without camera monitoring.");
|
||||
}
|
||||
|
||||
Console.WriteLine("🚀 Application is ready. Open http://localhost:5290 in your browser.");
|
||||
app.Run();
|
||||
@@ -0,0 +1,29 @@
|
||||
{
|
||||
"$schema": "http://json.schemastore.org/launchsettings.json",
|
||||
"iisSettings": {
|
||||
"windowsAuthentication": false,
|
||||
"anonymousAuthentication": true,
|
||||
"iisExpress": {
|
||||
"applicationUrl": "http://localhost:43065",
|
||||
"sslPort": 0
|
||||
}
|
||||
},
|
||||
"profiles": {
|
||||
"http": {
|
||||
"commandName": "Project",
|
||||
"dotnetRunMessages": true,
|
||||
"launchBrowser": true,
|
||||
"applicationUrl": "http://localhost:5290",
|
||||
"environmentVariables": {
|
||||
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||
}
|
||||
},
|
||||
"IIS Express": {
|
||||
"commandName": "IISExpress",
|
||||
"launchBrowser": true,
|
||||
"environmentVariables": {
|
||||
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,144 @@
|
||||
using Emgu.CV;
|
||||
using FaceRecognitionApp.Models;
|
||||
|
||||
namespace FaceRecognitionApp.Services;
|
||||
|
||||
public class CameraService : IDisposable
|
||||
{
|
||||
private VideoCapture? _capture;
|
||||
private readonly FaceRecognitionService _recognition;
|
||||
private readonly Action<DetectedFace> _onDetected;
|
||||
private CancellationTokenSource? _cts;
|
||||
private Task? _task;
|
||||
private DateTime _lastTime = DateTime.MinValue;
|
||||
private bool _isDisposed = false;
|
||||
|
||||
public CameraService(string url, FaceRecognitionService recognition, Action<DetectedFace> onDetected)
|
||||
{
|
||||
_recognition = recognition;
|
||||
_onDetected = onDetected;
|
||||
|
||||
Console.WriteLine($"Initializing camera with URL: {url}");
|
||||
_capture = new VideoCapture(url);
|
||||
|
||||
// Даем камере время на инициализацию потока
|
||||
Thread.Sleep(1000);
|
||||
|
||||
if (!_capture.IsOpened)
|
||||
throw new Exception("Camera failed to open. Check URL and network connection.");
|
||||
|
||||
Console.WriteLine("Camera hardware initialized.");
|
||||
}
|
||||
|
||||
public void Start()
|
||||
{
|
||||
if (_isDisposed) return;
|
||||
|
||||
_cts = new CancellationTokenSource();
|
||||
_task = Task.Run(() => Loop(_cts.Token));
|
||||
Console.WriteLine("Camera processing loop started.");
|
||||
}
|
||||
|
||||
private async Task Loop(CancellationToken token)
|
||||
{
|
||||
Console.WriteLine("🎥 Frame processing loop active...");
|
||||
|
||||
while (!token.IsCancellationRequested)
|
||||
{
|
||||
try
|
||||
{
|
||||
using var frame = new Mat();
|
||||
|
||||
// Читаем кадр с таймаутом
|
||||
if (_capture!.Read(frame))
|
||||
{
|
||||
if (!frame.IsEmpty && frame.Width > 0 && frame.Height > 0)
|
||||
{
|
||||
var face = _recognition.ProcessFrame(frame);
|
||||
|
||||
if (face != null)
|
||||
{
|
||||
// Лимит частоты отправок (не чаще раза в 2 секунды)
|
||||
if ((DateTime.Now - _lastTime).TotalMilliseconds > 2000)
|
||||
{
|
||||
_lastTime = DateTime.Now;
|
||||
_onDetected(face);
|
||||
|
||||
string status = face.IsRecognized
|
||||
? $"✅ Recognized: {face.MatchedEmployee?.Name}"
|
||||
: "❌ Unknown face";
|
||||
|
||||
Console.WriteLine($"{DateTime.Now:HH:mm:ss} - {status}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// Если Read вернул false, возможно потеря связи
|
||||
Console.WriteLine("⚠️ Failed to read frame, retrying...");
|
||||
await Task.Delay(500, token);
|
||||
}
|
||||
|
||||
// Небольшая задержка, чтобы не грузить процессор на 100%
|
||||
await Task.Delay(50, token);
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
// Это нормальная ситуация при остановке
|
||||
break;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine($"⚠️ Error in camera loop: {ex.Message}");
|
||||
await Task.Delay(1000, token);
|
||||
}
|
||||
}
|
||||
|
||||
Console.WriteLine("🛑 Camera loop finished.");
|
||||
}
|
||||
|
||||
public void Stop()
|
||||
{
|
||||
if (_cts == null) return;
|
||||
|
||||
Console.WriteLine("Stopping camera service...");
|
||||
_cts.Cancel();
|
||||
|
||||
try
|
||||
{
|
||||
// Ждем завершения задачи, но не бесконечно
|
||||
if (_task != null)
|
||||
{
|
||||
_task.Wait(TimeSpan.FromSeconds(5));
|
||||
}
|
||||
}
|
||||
catch (AggregateException ex)
|
||||
{
|
||||
// Игнорируем ошибки отмены задачи
|
||||
foreach (var inner in ex.InnerExceptions)
|
||||
{
|
||||
if (!(inner is TaskCanceledException || inner is OperationCanceledException))
|
||||
{
|
||||
Console.WriteLine($"Unexpected error during stop: {inner.Message}");
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine($"Error stopping task: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
if (_isDisposed) return;
|
||||
|
||||
Stop();
|
||||
_capture?.Dispose();
|
||||
_cts?.Dispose();
|
||||
_isDisposed = true;
|
||||
|
||||
Console.WriteLine("Camera resources released.");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
using Microsoft.Data.Sqlite;
|
||||
using FaceRecognitionApp.Models;
|
||||
|
||||
namespace FaceRecognitionApp.Services;
|
||||
|
||||
public class DatabaseService
|
||||
{
|
||||
private readonly string _connectionString = "Data Source=faces.db";
|
||||
|
||||
public DatabaseService()
|
||||
{
|
||||
InitializeDatabase();
|
||||
}
|
||||
|
||||
private void InitializeDatabase()
|
||||
{
|
||||
using var connection = new SqliteConnection(_connectionString);
|
||||
connection.Open();
|
||||
var cmd = connection.CreateCommand();
|
||||
cmd.CommandText = @"
|
||||
CREATE TABLE IF NOT EXISTS Employees (
|
||||
Id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
Name TEXT NOT NULL,
|
||||
Department TEXT,
|
||||
FaceDescriptor BLOB,
|
||||
Photo BLOB
|
||||
)";
|
||||
cmd.ExecuteNonQuery();
|
||||
}
|
||||
|
||||
public void AddEmployee(string name, string? dept, byte[] descriptor, byte[] photo)
|
||||
{
|
||||
using var connection = new SqliteConnection(_connectionString);
|
||||
connection.Open();
|
||||
var cmd = connection.CreateCommand();
|
||||
cmd.CommandText = "INSERT INTO Employees (Name, Department, FaceDescriptor, Photo) VALUES (@n, @d, @desc, @p)";
|
||||
cmd.Parameters.AddWithValue("@n", name);
|
||||
cmd.Parameters.AddWithValue("@d", (object?)dept ?? DBNull.Value);
|
||||
cmd.Parameters.AddWithValue("@desc", descriptor);
|
||||
cmd.Parameters.AddWithValue("@p", photo);
|
||||
cmd.ExecuteNonQuery();
|
||||
}
|
||||
|
||||
public List<Employee> GetAllEmployees()
|
||||
{
|
||||
var list = new List<Employee>();
|
||||
using var connection = new SqliteConnection(_connectionString);
|
||||
connection.Open();
|
||||
var cmd = connection.CreateCommand();
|
||||
cmd.CommandText = "SELECT * FROM Employees";
|
||||
using var reader = cmd.ExecuteReader();
|
||||
while (reader.Read())
|
||||
{
|
||||
list.Add(new Employee
|
||||
{
|
||||
Id = reader.GetInt32(0),
|
||||
Name = reader.GetString(1),
|
||||
Department = reader.IsDBNull(2) ? null : reader.GetString(2),
|
||||
FaceDescriptor = (byte[])reader[3],
|
||||
Photo = reader.IsDBNull(4) ? null : (byte[])reader[4]
|
||||
});
|
||||
}
|
||||
return list;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,234 @@
|
||||
using Emgu.CV;
|
||||
using Emgu.CV.CvEnum;
|
||||
using Emgu.CV.Face;
|
||||
using Emgu.CV.Structure;
|
||||
using Emgu.CV.Util;
|
||||
using FaceRecognitionApp.Models;
|
||||
using System.Drawing;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace FaceRecognitionApp.Services;
|
||||
|
||||
public class FaceRecognitionService : IDisposable
|
||||
{
|
||||
private readonly LBPHFaceRecognizer _recognizer;
|
||||
private readonly CascadeClassifier _cascade;
|
||||
private readonly DatabaseService _db;
|
||||
private List<Employee> _employees = new();
|
||||
private bool _isTrained = false;
|
||||
|
||||
// Коэффициент уменьшения кадра для ускорения поиска
|
||||
private const double ResizeScale = 0.5;
|
||||
private const int OriginalMinSize = 30;
|
||||
private readonly Size _scaledMinSize;
|
||||
|
||||
public FaceRecognitionService(DatabaseService db)
|
||||
{
|
||||
_db = db;
|
||||
var cascadePath = Path.Combine(AppContext.BaseDirectory, "haarcascade_frontalface_default.xml");
|
||||
if (!File.Exists(cascadePath))
|
||||
throw new FileNotFoundException($"Cascade file missing at: {cascadePath}");
|
||||
|
||||
_cascade = new CascadeClassifier(cascadePath);
|
||||
_recognizer = new LBPHFaceRecognizer(1, 8, 8, 8, 80.0);
|
||||
|
||||
// Вычисляем минимальный размер лица для уменьшенного кадра
|
||||
int scaledMin = (int)(OriginalMinSize * ResizeScale);
|
||||
_scaledMinSize = new Size(Math.Max(scaledMin, 10), Math.Max(scaledMin, 10));
|
||||
|
||||
TrainModel();
|
||||
}
|
||||
|
||||
private void TrainModel()
|
||||
{
|
||||
_employees = _db.GetAllEmployees();
|
||||
if (!_employees.Any()) return;
|
||||
|
||||
using var images = new VectorOfMat();
|
||||
using var labels = new VectorOfInt();
|
||||
|
||||
foreach (var emp in _employees)
|
||||
{
|
||||
if (emp.Photo == null || emp.Photo.Length == 0) continue;
|
||||
|
||||
try
|
||||
{
|
||||
using var ms = new MemoryStream(emp.Photo);
|
||||
using var bmp = new Bitmap(ms);
|
||||
using var mat = BitmapToMat(bmp);
|
||||
using var gray = new Mat();
|
||||
|
||||
CvInvoke.CvtColor(mat, gray, ColorConversion.Bgr2Gray);
|
||||
|
||||
// Ищем лицо на фото сотрудника для обрезки
|
||||
var rects = _cascade.DetectMultiScale(gray, 1.1, 5, new Size(30, 30));
|
||||
|
||||
if (rects.Length > 0)
|
||||
{
|
||||
using var faceMat = new Mat(gray, rects[0]);
|
||||
using var resized = new Mat();
|
||||
CvInvoke.Resize(faceMat, resized, new Size(100, 100));
|
||||
|
||||
images.Push(resized);
|
||||
labels.Push(new int[] { emp.Id });
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine($"Training error for {emp.Name}: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
if (images.Size > 0)
|
||||
{
|
||||
_recognizer.Train(images, labels);
|
||||
_isTrained = true;
|
||||
Console.WriteLine($"✅ Trained on {images.Size} faces.");
|
||||
}
|
||||
else
|
||||
{
|
||||
Console.WriteLine("⚠️ No valid faces found for training.");
|
||||
}
|
||||
}
|
||||
|
||||
public DetectedFace? ProcessFrame(Mat frame)
|
||||
{
|
||||
if (frame == null || frame.IsEmpty) return null;
|
||||
|
||||
// === УСКОРЕНИЕ: Уменьшаем кадр для поиска лиц ===
|
||||
using var smallFrame = new Mat();
|
||||
CvInvoke.Resize(frame, smallFrame, new Size(), ResizeScale, ResizeScale, Inter.Linear);
|
||||
|
||||
using var gray = new Mat();
|
||||
CvInvoke.CvtColor(smallFrame, gray, ColorConversion.Bgr2Gray);
|
||||
|
||||
// Ищем лица на уменьшенном кадре
|
||||
var rects = _cascade.DetectMultiScale(gray, 1.1, 5, _scaledMinSize);
|
||||
|
||||
if (rects.Length == 0)
|
||||
return null;
|
||||
|
||||
var smallRect = rects[0];
|
||||
|
||||
// === ВОССТАНОВЛЕНИЕ координат к оригинальному размеру ===
|
||||
int invScale = (int)(1.0 / ResizeScale);
|
||||
var originalRect = new Rectangle(
|
||||
smallRect.X * invScale,
|
||||
smallRect.Y * invScale,
|
||||
smallRect.Width * invScale,
|
||||
smallRect.Height * invScale
|
||||
);
|
||||
|
||||
// Проверка границ по оригинальному кадру
|
||||
if (originalRect.X < 0 || originalRect.Y < 0 ||
|
||||
originalRect.Right > frame.Width || originalRect.Bottom > frame.Height)
|
||||
return null;
|
||||
|
||||
// Вырезаем лицо из ОРИГИНАЛЬНОГО кадра (лучшее качество для распознавания)
|
||||
using var faceMat = new Mat(frame, originalRect);
|
||||
using var faceGray = new Mat();
|
||||
CvInvoke.CvtColor(faceMat, faceGray, ColorConversion.Bgr2Gray);
|
||||
|
||||
using var resized = new Mat();
|
||||
CvInvoke.Resize(faceGray, resized, new Size(100, 100));
|
||||
|
||||
var result = new DetectedFace();
|
||||
|
||||
if (_isTrained)
|
||||
{
|
||||
try
|
||||
{
|
||||
var prediction = _recognizer.Predict(resized);
|
||||
|
||||
int label = prediction.Label;
|
||||
double confidence = prediction.Distance;
|
||||
|
||||
if (label != -1 && confidence < 80)
|
||||
{
|
||||
result.IsRecognized = true;
|
||||
result.MatchedEmployee = _employees.FirstOrDefault(e => e.Id == label);
|
||||
result.Confidence = confidence;
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine($"Prediction error: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
// Конвертация в JPEG для веб-интерфейса
|
||||
using var ms = new MemoryStream();
|
||||
using var bmp = MatToBitmap(resized);
|
||||
bmp.Save(ms, System.Drawing.Imaging.ImageFormat.Jpeg);
|
||||
result.ImageData = ms.ToArray();
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Безопасная конвертация Bitmap → Mat через промежуточный буфер
|
||||
/// </summary>
|
||||
private static Mat BitmapToMat(Bitmap bitmap)
|
||||
{
|
||||
var mat = new Mat(bitmap.Height, bitmap.Width, DepthType.Cv8U, 3);
|
||||
|
||||
var data = bitmap.LockBits(
|
||||
new Rectangle(0, 0, bitmap.Width, bitmap.Height),
|
||||
System.Drawing.Imaging.ImageLockMode.ReadOnly,
|
||||
System.Drawing.Imaging.PixelFormat.Format24bppRgb);
|
||||
|
||||
try
|
||||
{
|
||||
int length = Math.Abs(data.Stride) * bitmap.Height;
|
||||
byte[] buffer = new byte[length];
|
||||
Marshal.Copy(data.Scan0, buffer, 0, length);
|
||||
Marshal.Copy(buffer, 0, mat.DataPointer, length);
|
||||
}
|
||||
finally
|
||||
{
|
||||
bitmap.UnlockBits(data);
|
||||
}
|
||||
|
||||
return mat;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Безопасная конвертация Mat → Bitmap через промежуточный буфер
|
||||
/// </summary>
|
||||
private static Bitmap MatToBitmap(Mat mat)
|
||||
{
|
||||
var colorMat = new Mat();
|
||||
if (mat.NumberOfChannels == 1)
|
||||
CvInvoke.CvtColor(mat, colorMat, ColorConversion.Gray2Bgr);
|
||||
else
|
||||
mat.CopyTo(colorMat);
|
||||
|
||||
var bmp = new Bitmap(colorMat.Width, colorMat.Height, System.Drawing.Imaging.PixelFormat.Format24bppRgb);
|
||||
|
||||
var data = bmp.LockBits(
|
||||
new Rectangle(0, 0, bmp.Width, bmp.Height),
|
||||
System.Drawing.Imaging.ImageLockMode.WriteOnly,
|
||||
System.Drawing.Imaging.PixelFormat.Format24bppRgb);
|
||||
|
||||
try
|
||||
{
|
||||
int length = Math.Abs(data.Stride) * colorMat.Height;
|
||||
byte[] buffer = new byte[length];
|
||||
Marshal.Copy(colorMat.DataPointer, buffer, 0, length);
|
||||
Marshal.Copy(buffer, 0, data.Scan0, length);
|
||||
}
|
||||
finally
|
||||
{
|
||||
bmp.UnlockBits(data);
|
||||
colorMat.Dispose();
|
||||
}
|
||||
|
||||
return bmp;
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_cascade?.Dispose();
|
||||
_recognizer?.Dispose();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"Logging": {
|
||||
"LogLevel": {
|
||||
"Default": "Information",
|
||||
"Microsoft.AspNetCore": "Warning"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"Logging": {
|
||||
"LogLevel": {
|
||||
"Default": "Information",
|
||||
"Microsoft.AspNetCore": "Warning"
|
||||
}
|
||||
},
|
||||
"AllowedHosts": "*",
|
||||
"Camera": {
|
||||
"Url": "rtsp://admin:4NUDZhJ7@172.22.10.101:554/cam/realmonitor?channel=1&subtype=0"
|
||||
}
|
||||
}
|
||||
Binary file not shown.
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,73 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Face Recognition</title>
|
||||
<style>
|
||||
body {
|
||||
font-family: sans-serif;
|
||||
background: #222;
|
||||
color: #fff;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
#gallery {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: center;
|
||||
gap: 10px;
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.card {
|
||||
background: #333;
|
||||
padding: 10px;
|
||||
border-radius: 8px;
|
||||
width: 200px;
|
||||
}
|
||||
|
||||
img {
|
||||
width: 100%;
|
||||
border: 3px solid red;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.known img {
|
||||
border-color: green;
|
||||
}
|
||||
|
||||
.name {
|
||||
margin-top: 5px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.time {
|
||||
font-size: 0.8em;
|
||||
color: #aaa;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>Live Face Detection</h1>
|
||||
<div id="gallery"></div>
|
||||
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/microsoft-signalr/8.0.0/signalr.min.js"></script>
|
||||
<script>
|
||||
const connection = new signalR.HubConnectionBuilder().withUrl("/faceHub").build();
|
||||
|
||||
connection.on("NewFace", (data) => {
|
||||
const div = document.createElement('div');
|
||||
div.className = data.isRecognized ? 'card known' : 'card';
|
||||
div.innerHTML = `
|
||||
<img src="data:image/jpeg;base64,${data.image}" />
|
||||
<div class="name">${data.isRecognized ? data.name : 'Unknown'}</div>
|
||||
<div class="time">${new Date().toLocaleTimeString()}</div>
|
||||
`;
|
||||
const gallery = document.getElementById('gallery');
|
||||
gallery.insertBefore(div, gallery.firstChild);
|
||||
if (gallery.children.length > 20) gallery.lastChild.remove();
|
||||
});
|
||||
|
||||
connection.start().catch(console.error);
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,4 +1,7 @@
|
||||
<Solution>
|
||||
<Folder Name="/Faces/">
|
||||
<Project Path="FaceRecognitionApp/FaceRecognitionApp.csproj" Id="401c1b89-2e49-4ad0-8e88-0aff3edb4c6c" />
|
||||
</Folder>
|
||||
<Folder Name="/LC/">
|
||||
<Project Path="Ministreliy/Ministreliy.csproj" Id="ffd1e86c-d678-413c-a0fd-dd75b4ec1b2e" />
|
||||
<Project Path="Strela/Strela.csproj" />
|
||||
|
||||
Reference in New Issue
Block a user