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 _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; } /// /// Безопасная конвертация Bitmap → Mat через промежуточный буфер /// 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; } /// /// Безопасная конвертация Mat → Bitmap через промежуточный буфер /// 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(); } }