Add Face
Gitea Actions Demo / Explore-Gitea-Actions (push) Successful in 11s

This commit is contained in:
2026-06-19 18:19:30 +03:00
parent d426ab1e78
commit 3385195796
14 changed files with 33988 additions and 0 deletions
@@ -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();
}
}