diff --git a/Visor/Visor/Components/Layout/SideMenu.razor b/Visor/Visor/Components/Layout/SideMenu.razor
index cca6805..31466ef 100644
--- a/Visor/Visor/Components/Layout/SideMenu.razor
+++ b/Visor/Visor/Components/Layout/SideMenu.razor
@@ -29,7 +29,10 @@
Добавить объект
- Simple DataGrid
+ Добавить тип
+
+
+ Добавить устройство
diff --git a/Visor/Visor/Pages/ObjectInfo.razor b/Visor/Visor/Pages/ObjectInfo.razor
index 54bf982..488a9ef 100644
--- a/Visor/Visor/Pages/ObjectInfo.razor
+++ b/Visor/Visor/Pages/ObjectInfo.razor
@@ -6,6 +6,36 @@
{
Название: @_obj.NameObject
Количество устройств: @_devs.Count
+
+
+
+
+ Тип устройства
+
+
+ @foreach (var item in AllPubData.ListDeviceTypeItems)
+ {
+ @item.Name
+ }
+
+
+
+
+
+
+
+ Dark
+ Dark
+ Dark
+ Dark
+ Dark
+ Dark
+ Dark
+ Dark
+
+
+
+
}
else
{
@@ -15,13 +45,22 @@ else
@code {
[Parameter]
public Guid Id { get; set; }
-
+
+ //public Guid IdType { get; set; }
+ private DeviceTypeItems? _SelectedType;
private ObjectItems? _obj;
private List _devs = new();
+ private List _show_devs = new();
protected override void OnInitialized()
{
_obj = AllPubData.ListObjectItems.FirstOrDefault(o => o.Id == Id);
_devs = AllPubData.ListDeviceItems.Where(d => d.DeviceObject == Id).ToList();
}
+
+ private void ChangeType(Guid id)
+ {
+ _SelectedType = AllPubData.ListDeviceTypeItems.First(t => t.Id == id);
+ _show_devs = _devs.Where(d => d.DeviceType == id).ToList();
+ }
}
diff --git a/Visor/Visor/Pages/Setting/Objects/AddDevs.razor b/Visor/Visor/Pages/Setting/Objects/AddDevs.razor
new file mode 100644
index 0000000..627caf4
--- /dev/null
+++ b/Visor/Visor/Pages/Setting/Objects/AddDevs.razor
@@ -0,0 +1,181 @@
+@page "/setting/add_devs"
+@using Visor.Models
+
+@* @inject HttpClient Http *@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ @if (isLoading || listObjects is null)
+ {
+
@MessagePages
+ }
+ else if (listObjects != null && listObjects.Any())
+ {
+
+
+
+ Название
+ Описание
+
+
+
+ @foreach (var item in listObjects)
+ {
+
+ @item.NameObject
+ @item.DescriptionObject
+
+ }
+
+
+ }
+ else
+ {
+
@MessagePages
+
@MessagePages
+ }
+
+
+
+@code {
+ private List? listObjects = AllPubData.ListObjectItems;
+ private Modal modalAdd;
+ private Modal modalDel;
+
+ private Guid select_id { get; set; }
+ private int id_object { get; set; }
+ private string name_object { get; set; }
+ private string decript_object { get; set; }
+
+ private HttpClient Http = AllPubData.http;
+
+
+ private string? MessagePages;
+ bool isLoading = true;
+
+ protected override async Task OnInitializedAsync()
+ {
+ listObjects = AllPubData.ListObjectItems;
+ if (listObjects.Count == 0)
+ MessagePages = "Нет данных";
+ isLoading = false;
+ }
+
+ private Task ShowAddObjetc()
+ {
+ id_object = listObjects.Count;
+ return modalAdd.Show();
+ }
+ private async Task AddObject()
+ {
+ var obj = new ObjectItems()
+ {
+ NameObject = name_object,
+ DescriptionObject = decript_object
+ };
+
+ var post = await Http.PostAsJsonAsync("api/Object/AddObject", obj);
+ if (post.IsSuccessStatusCode)
+ {
+ listObjects.Add(obj);
+ await modalAdd.Hide();
+ //await AllPubData.ReloadObject();
+ }
+
+ }
+ private Task ShowDelObjetc() => modalDel.Show();
+ private async Task DelObject()
+ {
+ var del = await Http.DeleteAsync($"api/Object/DelObject?id={select_id}");
+ if (del.IsSuccessStatusCode)
+ {
+ listObjects.Remove(listObjects.Where(i => i.Id == select_id).First());
+ await HideDelModal();
+ }
+ }
+ private Task HideAddModal() => modalAdd.Hide();
+ private Task HideDelModal() => modalDel.Hide();
+
+}
+
+
+
+ Добавить объект
+
+
+ @*
+
+ Id
+
+
+
+
+ *@
+
+
+ Name
+
+
+
+
+
+
+
+ Описание
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Удалить объект
+
+
+
+
+ Id
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/Visor/Visor/Pages/Setting/Objects/AddTypes.razor b/Visor/Visor/Pages/Setting/Objects/AddTypes.razor
new file mode 100644
index 0000000..b69cbd8
--- /dev/null
+++ b/Visor/Visor/Pages/Setting/Objects/AddTypes.razor
@@ -0,0 +1,179 @@
+@page "/setting/add_types"
+@using Visor.Models
+
+@* @inject HttpClient Http *@
+
+
+
+
+
+
+
+
+
+
+
+
+
+ @if (isLoading || listObjects is null)
+ {
+
@MessagePages
+ }
+ else if (listObjects != null && listObjects.Any())
+ {
+
+
+
+ Название
+ Описание
+
+
+
+ @foreach (var item in listObjects)
+ {
+
+ @item.NameObject
+ @item.DescriptionObject
+
+ }
+
+
+ }
+ else
+ {
+
@MessagePages
+
@MessagePages
+ }
+
+
+
+@code {
+ private List? listObjects = AllPubData.ListObjectItems;
+ private Modal modalAdd;
+ private Modal modalDel;
+
+ private Guid select_id { get; set; }
+ private int id_object { get; set; }
+ private string name_object { get; set; }
+ private string decript_object { get; set; }
+
+ private HttpClient Http = AllPubData.http;
+
+
+ private string? MessagePages;
+ bool isLoading = true;
+
+ protected override async Task OnInitializedAsync()
+ {
+ await AllPubData.GetObjects();
+ listObjects = AllPubData.ListObjectItems;
+ if (listObjects.Count == 0)
+ MessagePages = "Нет данных";
+ isLoading = false;
+ }
+
+ private Task ShowAddObjetc()
+ {
+ id_object = listObjects.Count;
+ return modalAdd.Show();
+ }
+ private async Task AddObject()
+ {
+ var obj = new ObjectItems()
+ {
+ NameObject = name_object,
+ DescriptionObject = decript_object
+ };
+
+ var post = await Http.PostAsJsonAsync("api/Object/AddObject", obj);
+ if (post.IsSuccessStatusCode)
+ {
+ listObjects.Add(obj);
+ await modalAdd.Hide();
+ //await AllPubData.ReloadObject();
+ }
+
+ }
+ private Task ShowDelObjetc() => modalDel.Show();
+ private async Task DelObject()
+ {
+ var del = await Http.DeleteAsync($"api/Object/DelObject?id={select_id}");
+ if (del.IsSuccessStatusCode)
+ {
+ listObjects.Remove(listObjects.Where(i => i.Id == select_id).First());
+ await HideDelModal();
+ }
+ }
+ private Task HideAddModal() => modalAdd.Hide();
+ private Task HideDelModal() => modalDel.Hide();
+
+}
+
+
+
+ Добавить объект
+
+
+ @*
+
+ Id
+
+
+
+
+ *@
+
+
+ Name
+
+
+
+
+
+
+
+ Описание
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Удалить объект
+
+
+
+
+ Id
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app.py b/app.py
new file mode 100644
index 0000000..59b1f53
--- /dev/null
+++ b/app.py
@@ -0,0 +1,597 @@
+import face_recognition
+import cv2
+import os
+import time
+import threading
+import queue
+import base64
+import zipfile
+import json
+import csv
+from datetime import datetime
+from flask import Flask, request, render_template_string, Response, jsonify, send_from_directory, abort
+from flask_socketio import SocketIO, emit
+
+app = Flask(__name__)
+socketio = SocketIO(app, cors_allowed_origins="*", async_mode='threading')
+
+# === КОНФИГУРАЦИЯ ===
+CONFIG_FILE = "config.json"
+DEFAULT_CONFIG = {
+ "camera_ip": "192.168.1.100",
+ "camera_stream": 1, # 1 - низкое качество, 2 - среднее, 0 - высокое
+ "retention_days": 7,
+ "process_interval": 0.2
+}
+
+# === ЖЕСТКИЕ НАСТРОЙКИ КАМЕРЫ (СКРЫТЫ ОТ ОПЕРАТОРА) ===
+CAMERA_USER = "admin"
+CAMERA_PASS = "4NUDZhJ7"
+# Формат URL для Dahua/Hikvision. {stream} заменится на номер потока
+URL_TEMPLATE = "rtsp://{user}:{password}@{ip}/cam/realmonitor?channel=1&subtype={stream}"
+
+def load_config():
+ if not os.path.exists(CONFIG_FILE):
+ with open(CONFIG_FILE, 'w') as f:
+ json.dump(DEFAULT_CONFIG, f, indent=4)
+ return DEFAULT_CONFIG
+ with open(CONFIG_FILE, 'r') as f:
+ return json.load(f)
+
+config = load_config()
+
+# Глобальные переменные
+known_face_encodings = []
+known_face_names = []
+frame_queue = queue.Queue(maxsize=2)
+last_processed_time = 0
+PROCESS_INTERVAL = float(config.get("process_interval", 0.2))
+
+SAVE_DIR = "Лица"
+DATASET_DIR = "dataset"
+LOG_FILE = "access_log.csv"
+ARCHIVE_DIR = "logs_archive"
+RETENTION_DAYS = int(config.get("retention_days", 7))
+
+last_sent_identity = None
+is_person_visible = False
+camera_thread_instance = None
+camera_stop_event = threading.Event()
+
+# === ИНИЦИАЛИЗАЦИЯ ПАПОК ===
+for d in [SAVE_DIR, DATASET_DIR, ARCHIVE_DIR, os.path.join(SAVE_DIR, "Unknown")]:
+ os.makedirs(d, exist_ok=True)
+
+if not os.path.exists(LOG_FILE):
+ with open(LOG_FILE, 'w', newline='', encoding='utf-8') as f:
+ writer = csv.writer(f, delimiter=';')
+ writer.writerow(["Дата", "Время", "Имя", "Скриншот"])
+
+def load_faces():
+ global known_face_encodings, known_face_names
+ print("Loading faces...")
+ known_face_encodings = []
+ known_face_names = []
+ for filename in os.listdir(DATASET_DIR):
+ if filename.endswith((".jpg", ".jpeg", ".png")):
+ try:
+ image = face_recognition.load_image_file(os.path.join(DATASET_DIR, filename))
+ encodings = face_recognition.face_encodings(image)
+ if len(encodings) > 0:
+ known_face_encodings.append(encodings[0])
+ known_face_names.append(os.path.splitext(filename)[0])
+ except Exception as e:
+ print(f"Error loading {filename}: {e}")
+ print(f"✅ Loaded {len(known_face_names)} faces.")
+
+load_faces()
+
+def get_base64_image(path):
+ try:
+ with open(path, "rb") as img_file:
+ return base64.b64encode(img_file.read()).decode('utf-8')
+ except:
+ return ""
+
+def cleanup_old_files():
+ now = time.time()
+ cutoff = now - (RETENTION_DAYS * 86400)
+ for root, dirs, files in os.walk(SAVE_DIR):
+ for file in files:
+ if file.endswith(".jpg"):
+ filepath = os.path.join(root, file)
+ if os.path.getmtime(filepath) < cutoff:
+ try: os.remove(filepath)
+ except: pass
+ if os.path.exists(LOG_FILE) and os.path.getsize(LOG_FILE) > 0:
+ if os.path.getmtime(LOG_FILE) < cutoff:
+ zip_name = os.path.join(ARCHIVE_DIR, f"log_{datetime.now().strftime('%Y%m%d_%H%M%S')}.zip")
+ try:
+ with zipfile.ZipFile(zip_name, 'w', zipfile.ZIP_DEFLATED) as zipf:
+ zipf.write(LOG_FILE)
+ open(LOG_FILE, 'w').close()
+ except: pass
+
+def write_log(name, screenshot_path):
+ timestamp = datetime.now()
+ with open(LOG_FILE, "a", newline='', encoding="utf-8") as f:
+ writer = csv.writer(f, delimiter=';')
+ writer.writerow([
+ timestamp.strftime("%Y-%m-%d"),
+ timestamp.strftime("%H:%M:%S"),
+ name,
+ screenshot_path
+ ])
+
+def process_frame(frame):
+ global last_processed_time, last_sent_identity, is_person_visible
+
+ small_frame = cv2.resize(frame, (0, 0), fx=0.25, fy=0.25)
+ rgb_small_frame = cv2.cvtColor(small_frame, cv2.COLOR_BGR2RGB)
+
+ face_locations = face_recognition.face_locations(rgb_small_frame)
+
+ if not face_locations:
+ if is_person_visible:
+ is_person_visible = False
+ last_sent_identity = None
+ return frame
+
+ top, right, bottom, left = face_locations[0]
+ face_encoding = face_recognition.face_encodings(rgb_small_frame, [face_locations[0]])[0]
+
+ matches = face_recognition.compare_faces(known_face_encodings, face_encoding)
+ name = "Unknown"
+ if True in matches:
+ name = known_face_names[matches.index(True)]
+
+ full_top, full_right = top * 4, right * 4
+ full_bottom, full_left = bottom * 4, left * 4
+
+ should_notify = (name != last_sent_identity) or (not is_person_visible)
+
+ if should_notify:
+ screenshot_path = ""
+ b64_img = ""
+
+ pad_y, pad_x = 60, 40
+ h, w = frame.shape[:2]
+ crop = frame[max(0,full_top-pad_y):min(h,full_bottom+pad_y), max(0,full_left-pad_x):min(w,full_right+pad_x)]
+
+ if crop.size > 0:
+ user_dir = os.path.join(SAVE_DIR, name)
+ os.makedirs(user_dir, exist_ok=True)
+ filename = f"{datetime.now().strftime('%Y-%m-%d_%H-%M-%S')}.jpg"
+ filepath = os.path.join(user_dir, filename)
+
+ cv2.imwrite(filepath, crop, [int(cv2.IMWRITE_JPEG_QUALITY), 100])
+ screenshot_path = filepath
+ b64_img = get_base64_image(filepath)
+
+ write_log(name, filepath)
+ threading.Thread(target=cleanup_old_files).start()
+
+ if name != "Unknown":
+ if not b64_img:
+ emp_photo_path = os.path.join(DATASET_DIR, f"{name}.jpg")
+ b64_img = get_base64_image(emp_photo_path)
+ socketio.emit('new_event', {'type': 'known', 'name': name, 'photo': b64_img, 'time': datetime.now().strftime("%H:%M:%S")})
+ else:
+ if b64_img:
+ socketio.emit('new_event', {'type': 'unknown', 'name': 'НЕИЗВЕСТНЫЙ', 'photo': b64_img, 'path': screenshot_path, 'time': datetime.now().strftime("%H:%M:%S")})
+
+ last_sent_identity = name
+ is_person_visible = True
+
+ color = (0, 255, 0) if name != "Unknown" else (0, 0, 255)
+ cv2.rectangle(frame, (full_left, full_top), (full_right, full_bottom), color, 2)
+ cv2.putText(frame, name, (full_left + 6, full_bottom - 6), cv2.FONT_HERSHEY_DUPLEX, 1.0, (255, 255, 255), 1)
+ return frame
+
+def camera_thread(camera_url):
+ print(f"🔌 Connecting to: {camera_url}")
+ video_capture = cv2.VideoCapture(camera_url)
+ video_capture.set(cv2.CAP_PROP_BUFFERSIZE, 1)
+
+ while not camera_stop_event.is_set():
+ success, frame = video_capture.read()
+ if not success:
+ time.sleep(1)
+ video_capture.release()
+ video_capture = cv2.VideoCapture(camera_url)
+ continue
+ if frame_queue.full():
+ try: frame_queue.get_nowait()
+ except: pass
+ frame_queue.put(frame)
+ time.sleep(0.01)
+ video_capture.release()
+
+def start_camera():
+ global camera_thread_instance, camera_stop_event
+ if camera_thread_instance and camera_thread_instance.is_alive():
+ camera_stop_event.set()
+ camera_thread_instance.join(timeout=5)
+
+ camera_stop_event.clear()
+
+ ip = config.get("camera_ip", "")
+ stream = config.get("camera_stream", 1)
+
+ if ip:
+ # ИСПРАВЛЕНО: pass заменено на password
+ url = URL_TEMPLATE.format(user=CAMERA_USER, password=CAMERA_PASS, ip=ip, stream=stream)
+ camera_thread_instance = threading.Thread(target=camera_thread, args=(url,), daemon=True)
+ camera_thread_instance.start()
+ print(f"📷 Camera started for IP: {ip} (Stream: {stream})")
+ else:
+ print("⚠️ Camera IP not configured")
+
+start_camera()
+
+# === API ENDPOINTS ===
+
+@app.route('/api/config', methods=['GET', 'POST'])
+def api_config():
+ global config, PROCESS_INTERVAL, RETENTION_DAYS
+ if request.method == 'POST':
+ data = request.json
+
+ if 'camera_ip' in data: config['camera_ip'] = data['camera_ip']
+ if 'camera_stream' in data: config['camera_stream'] = int(data['camera_stream'])
+
+ config['retention_days'] = int(data.get('retention_days', 7))
+ config['process_interval'] = float(data.get('process_interval', 0.2))
+
+ with open(CONFIG_FILE, 'w') as f:
+ json.dump(config, f, indent=4)
+
+ PROCESS_INTERVAL = config['process_interval']
+ RETENTION_DAYS = config['retention_days']
+
+ # Перезапускаем камеру при изменении IP или потока
+ if any(k in data for k in ['camera_ip', 'camera_stream']):
+ start_camera()
+
+ return jsonify({"status": "ok"})
+
+ return jsonify(config)
+
+@app.route('/api/log')
+def api_log():
+ entries = []
+ if os.path.exists(LOG_FILE):
+ with open(LOG_FILE, 'r', encoding='utf-8') as f:
+ reader = csv.reader(f, delimiter=';')
+ next(reader, None)
+ for row in reader:
+ if len(row) >= 4:
+ safe_path = row[3].replace("\\", "/")
+ entries.append({
+ 'date': row[0], 'time': row[1],
+ 'name': row[2], 'path': safe_path
+ })
+ entries.reverse()
+ return jsonify(entries)
+
+@app.route('/api/upload', methods=['POST'])
+def api_upload():
+ name = request.form.get('name', '').strip()
+ photo = request.files.get('photo')
+ if not name or not photo:
+ return jsonify({"error": "Name and photo required"}), 400
+ filename = f"{name.replace(' ', '_')}.jpg"
+ path = os.path.join(DATASET_DIR, filename)
+ photo.save(path)
+ load_faces()
+ return jsonify({"status": "ok", "name": name})
+
+@app.route('/api/image/')
+def serve_image(filename):
+ safe_dir = os.path.abspath(SAVE_DIR)
+ requested_path = os.path.abspath(os.path.join(SAVE_DIR, filename))
+
+ if requested_path.startswith(safe_dir) and os.path.exists(requested_path):
+ return send_from_directory(SAVE_DIR, filename)
+ return abort(404)
+
+@app.route('/video_feed')
+def video_feed():
+ def generate():
+ global last_processed_time
+ while True:
+ try:
+ frame = frame_queue.get(timeout=5)
+ if time.time() - last_processed_time >= PROCESS_INTERVAL:
+ frame = process_frame(frame)
+ last_processed_time = time.time()
+ ret, buffer = cv2.imencode('.jpg', frame)
+ yield (b'--frame\r\nContent-Type: image/jpeg\r\n\r\n' + buffer.tobytes() + b'\r\n')
+ except: continue
+ return Response(generate(), mimetype='multipart/x-mixed-replace; boundary=frame')
+
+@app.route('/')
+def index():
+ return render_template_string('''
+
+
+
+
+ Face Recognition System
+
+
+
+
+
+
📹 Монитор
+
📋 Журнал
+
⚙️ Настройки
+
+
+
+
+
+
+

+
+
+
+
+
+
+
Журнал посещений
+
+ | Дата | Время | Имя | Статус | Фото |
+
+
+
+
+
+
+
+
Настройки системы
+
+
+
➕ Добавить сотрудника
+
+
+
+
+
+
📷 Подключение камеры
+
+
+
+
+
+
+
⚙️ Система
+
+
+
+
+
+
+
+
+
+
+
×
+
![]()
+
+
+
+
+
+ ''')
+
+if __name__ == '__main__':
+ socketio.run(app, host='0.0.0.0', port=5000, debug=False)
\ No newline at end of file
diff --git a/face-docker b/face-docker
new file mode 100644
index 0000000..e69de29
diff --git a/lsSoft.slnx b/lsSoft.slnx
index fa5f5e8..2c701c4 100644
--- a/lsSoft.slnx
+++ b/lsSoft.slnx
@@ -4,6 +4,11 @@
+
+
+
+
+