Compare commits
4 Commits
f3c4c55c55
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 90cbc7c506 | |||
| 794fc621ff | |||
| d426ab1e78 | |||
| 6ddc6be910 |
@@ -6,28 +6,15 @@ namespace DahuaIPCRename
|
|||||||
{
|
{
|
||||||
public static Dictionary<string, string> listIPC = new Dictionary<string, string>
|
public static Dictionary<string, string> listIPC = new Dictionary<string, string>
|
||||||
{
|
{
|
||||||
{"172.16.54.18","Ворота 35"},
|
{"172.22.10.122","Этаж 2|Пом. 247"},
|
||||||
{"172.16.54.20","Ворота 36"},
|
{"172.22.10.123","Этаж 2|Пом. 248-4"},
|
||||||
{"172.16.54.21","Ворота 31"},
|
{"172.22.10.124","Этаж 2|Пом. 248-5"},
|
||||||
{"172.16.54.22","Ворота 32"},
|
{"172.22.10.125","Этаж 2|Пом. 248-2"},
|
||||||
{"172.16.54.23","Ворота 33"},
|
{"172.22.10.126","Этаж 2|Пом. 248-6"},
|
||||||
{"172.16.54.24","Ворота 34"},
|
{"172.22.10.127","Этаж 2|Пом. 248-3"},
|
||||||
{"172.16.54.28","Ворота 16"},
|
{"172.22.10.128","Этаж 2|Пом. 273-1"},
|
||||||
{"172.16.54.29","Ворота 26"},
|
{"172.22.10.129","Этаж 2|Пом. 268-1"},
|
||||||
{"172.16.54.30","Ворота 20"},
|
{"172.22.10.130","Этаж 2|Пом. 273-2"}
|
||||||
{"172.16.54.31","Ворота 22"},
|
|
||||||
{"172.16.54.32","Ворота 18"},
|
|
||||||
{"172.16.54.33","Ворота 21"},
|
|
||||||
{"172.16.54.34","Ворота 24"},
|
|
||||||
{"172.16.54.35","Ворота 27"},
|
|
||||||
{"172.16.54.36","Ворота 29"},
|
|
||||||
{"172.16.54.37","Ворота 25"},
|
|
||||||
{"172.16.54.38","Ворота 23"},
|
|
||||||
{"172.16.54.39","Ворота 15"},
|
|
||||||
{"172.16.54.40","Ворота 17"},
|
|
||||||
{"172.16.54.41","Ворота 37"},
|
|
||||||
{"172.16.54.42","Ворота 19"},
|
|
||||||
{"172.16.54.43","Ворота 28"}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
static async Task Main(string[] args)
|
static async Task Main(string[] args)
|
||||||
|
|||||||
@@ -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>
|
||||||
@@ -8,6 +8,16 @@
|
|||||||
|
|
||||||
<PageTitle>База данных</PageTitle>
|
<PageTitle>База данных</PageTitle>
|
||||||
|
|
||||||
|
<ul class="nav nav-tabs">
|
||||||
|
<li class="nav-item">
|
||||||
|
<a class="nav-link" aria-current="page" href="database">Сводные данные</a>
|
||||||
|
</li>
|
||||||
|
<li class="nav-item">
|
||||||
|
<a class="nav-link active" href="database/type">Типы устройств</a>
|
||||||
|
</li>
|
||||||
|
|
||||||
|
</ul>
|
||||||
|
|
||||||
<div class="modal fade modal1" id="addtype" data-bs-backdrop="static" data-bs-keyboard="false" tabindex="-1" aria-labelledby="addtypeLabel" aria-hidden="true">
|
<div class="modal fade modal1" id="addtype" data-bs-backdrop="static" data-bs-keyboard="false" tabindex="-1" aria-labelledby="addtypeLabel" aria-hidden="true">
|
||||||
<div class="modal-dialog">
|
<div class="modal-dialog">
|
||||||
<div class="modal-content">
|
<div class="modal-content">
|
||||||
@@ -40,7 +50,7 @@
|
|||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
<ul class="nav nav-tabs">
|
@* <ul class="nav nav-tabs">
|
||||||
<li class="nav-item" role="presentation">
|
<li class="nav-item" role="presentation">
|
||||||
<a class="nav-link" aria-current="page" href="database">Сводные данные</a>
|
<a class="nav-link" aria-current="page" href="database">Сводные данные</a>
|
||||||
</li>
|
</li>
|
||||||
@@ -55,7 +65,14 @@ else
|
|||||||
href="database/addtype">Добавить Тип</a>
|
href="database/addtype">Добавить Тип</a>
|
||||||
</li>
|
</li>
|
||||||
|
|
||||||
</ul>
|
</ul> *@
|
||||||
|
<br>
|
||||||
|
<button type="button" class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#addtype">
|
||||||
|
Добавить тип
|
||||||
|
</button>
|
||||||
|
<button type="button" class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#deltype">
|
||||||
|
Удалить тип
|
||||||
|
</button>
|
||||||
|
|
||||||
<div class="card-body p-0">
|
<div class="card-body p-0">
|
||||||
<div class="tab-content" id="myTabContent">
|
<div class="tab-content" id="myTabContent">
|
||||||
@@ -83,9 +100,7 @@ else
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
<button type="button" class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#deltype">
|
|
||||||
Удалить тип
|
|
||||||
</button>
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -114,9 +129,7 @@ else
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
<button type="button" class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#addtype">
|
|
||||||
Добавить тип
|
|
||||||
</button>
|
|
||||||
|
|
||||||
@code {
|
@code {
|
||||||
//HttpClient http = new();
|
//HttpClient http = new();
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ public class DeviceController : ControllerBase
|
|||||||
{
|
{
|
||||||
_logger = logger;
|
_logger = logger;
|
||||||
}
|
}
|
||||||
|
|
||||||
//=========================== GET =================================//
|
//=========================== GET =================================//
|
||||||
[HttpGet("GetTypes")]
|
[HttpGet("GetTypes")]
|
||||||
[ProducesResponseType<List<DeviceType>>(StatusCodes.Status200OK)]
|
[ProducesResponseType<List<DeviceType>>(StatusCodes.Status200OK)]
|
||||||
|
|||||||
@@ -13,10 +13,10 @@
|
|||||||
public int ConnectedToSwitchId { get; set; }
|
public int ConnectedToSwitchId { get; set; }
|
||||||
public int ConnectedToPortOnSwitch { get; set; }
|
public int ConnectedToPortOnSwitch { get; set; }
|
||||||
|
|
||||||
//public DeviceItem(int type, string ip, string mac, string location)
|
public DeviceItem(string ip, string mac, string location)
|
||||||
//{
|
{
|
||||||
|
|
||||||
//}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
//Unknown = 0,
|
//Unknown = 0,
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -1,4 +1,7 @@
|
|||||||
<Solution>
|
<Solution>
|
||||||
|
<Folder Name="/Faces/">
|
||||||
|
<Project Path="FaceRecognitionApp/FaceRecognitionApp.csproj" Id="401c1b89-2e49-4ad0-8e88-0aff3edb4c6c" />
|
||||||
|
</Folder>
|
||||||
<Folder Name="/LC/">
|
<Folder Name="/LC/">
|
||||||
<Project Path="Ministreliy/Ministreliy.csproj" Id="ffd1e86c-d678-413c-a0fd-dd75b4ec1b2e" />
|
<Project Path="Ministreliy/Ministreliy.csproj" Id="ffd1e86c-d678-413c-a0fd-dd75b4ec1b2e" />
|
||||||
<Project Path="Strela/Strela.csproj" />
|
<Project Path="Strela/Strela.csproj" />
|
||||||
|
|||||||
Reference in New Issue
Block a user