This commit is contained in:
@@ -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();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user