DonJadeRoy commited on
Commit
13bd96e
·
1 Parent(s): bc0b671

vfdhbvjfn

Browse files
controllers/main.py CHANGED
@@ -4149,20 +4149,26 @@ async def register(
4149
  raise Exception("Số CCCD này đã được đăng ký trong hệ thống!")
4150
 
4151
  user_descriptor = None
 
 
4152
  for i, img_file in enumerate(images):
4153
  img_bytes = await img_file.read()
4154
  detections = face_ai_service.extract_faces(img_bytes)
4155
 
4156
  if len(detections) == 0:
 
 
4157
  raise Exception(f"Không tìm thấy khuôn mặt trong ảnh mẫu thứ {i + 1}.")
4158
  if len(detections) > 1:
 
 
4159
  raise Exception(f"Ảnh mẫu thứ {i + 1} có nhiều hơn 1 khuôn mặt.")
4160
 
4161
  descriptor = detections[0]["descriptor"]
4162
  emb_id = str(uuid.uuid4())
4163
  img_b64 = face_ai_service.bytes_to_base64(img_bytes)
4164
 
4165
- if i == 0:
4166
  user_descriptor = descriptor
4167
  avatar_b64 = img_b64
4168
  cursor.execute(
@@ -4178,6 +4184,10 @@ async def register(
4178
  )
4179
  new_encodings.append((person_id, name, role, avatar_b64, expiry_val, descriptor))
4180
 
 
 
 
 
4181
  front_b64 = back_b64 = ""
4182
 
4183
  if cccd_front:
 
4149
  raise Exception("Số CCCD này đã được đăng ký trong hệ thống!")
4150
 
4151
  user_descriptor = None
4152
+ avatar_b64 = ""
4153
+ sample_errors = []
4154
  for i, img_file in enumerate(images):
4155
  img_bytes = await img_file.read()
4156
  detections = face_ai_service.extract_faces(img_bytes)
4157
 
4158
  if len(detections) == 0:
4159
+ sample_errors.append(f"Anh mau {i + 1}: khong tim thay khuon mat.")
4160
+ continue
4161
  raise Exception(f"Không tìm thấy khuôn mặt trong ảnh mẫu thứ {i + 1}.")
4162
  if len(detections) > 1:
4163
+ sample_errors.append(f"Anh mau {i + 1}: co nhieu hon 1 khuon mat.")
4164
+ continue
4165
  raise Exception(f"Ảnh mẫu thứ {i + 1} có nhiều hơn 1 khuôn mặt.")
4166
 
4167
  descriptor = detections[0]["descriptor"]
4168
  emb_id = str(uuid.uuid4())
4169
  img_b64 = face_ai_service.bytes_to_base64(img_bytes)
4170
 
4171
+ if user_descriptor is None:
4172
  user_descriptor = descriptor
4173
  avatar_b64 = img_b64
4174
  cursor.execute(
 
4184
  )
4185
  new_encodings.append((person_id, name, role, avatar_b64, expiry_val, descriptor))
4186
 
4187
+ if user_descriptor is None:
4188
+ detail = " ".join(sample_errors[:3])
4189
+ raise Exception(f"Khong tim thay khuon mat hop le trong cac anh mau. {detail}".strip())
4190
+
4191
  front_b64 = back_b64 = ""
4192
 
4193
  if cccd_front:
database/database.py CHANGED
@@ -117,6 +117,14 @@ def _ensure_recognition_log_columns(cursor):
117
  if not _column_exists(cursor, "recognition_logs", column_name):
118
  cursor.execute(statement)
119
 
 
 
 
 
 
 
 
 
120
  def _ensure_attendance_notification_columns(cursor):
121
  additions = [
122
  ("recognition_log_id", "ALTER TABLE attendance_notifications ADD COLUMN recognition_log_id VARCHAR(36) NULL AFTER person_id"),
@@ -144,11 +152,25 @@ def _ensure_attendance_notification_columns(cursor):
144
  indexes = [
145
  ("idx_notification_person_status", "CREATE INDEX idx_notification_person_status ON attendance_notifications (person_id, status)"),
146
  ("idx_notification_attendance_time", "CREATE INDEX idx_notification_attendance_time ON attendance_notifications (attendance_time)"),
 
 
 
147
  ]
148
  for index_name, statement in indexes:
149
  if not _index_exists(cursor, "attendance_notifications", index_name):
150
  cursor.execute(statement)
151
 
 
 
 
 
 
 
 
 
 
 
 
152
  def init_database():
153
  """Tu dong tao Schema theo chuan MySQL ban cung cap"""
154
 
@@ -318,6 +340,7 @@ def init_database():
318
  _ensure_employee_account_columns(cursor)
319
  _ensure_recognition_log_columns(cursor)
320
  _ensure_attendance_notification_columns(cursor)
 
321
  _ensure_default_admin(cursor)
322
  conn.commit()
323
  print("[OK] Da kiem tra va khoi tao cau truc CSDL thanh cong tren Railway!")
 
117
  if not _column_exists(cursor, "recognition_logs", column_name):
118
  cursor.execute(statement)
119
 
120
+ indexes = [
121
+ ("idx_recognition_person_status_created", "CREATE INDEX idx_recognition_person_status_created ON recognition_logs (person_id, status, created_at)"),
122
+ ("idx_recognition_created", "CREATE INDEX idx_recognition_created ON recognition_logs (created_at)"),
123
+ ]
124
+ for index_name, statement in indexes:
125
+ if not _index_exists(cursor, "recognition_logs", index_name):
126
+ cursor.execute(statement)
127
+
128
  def _ensure_attendance_notification_columns(cursor):
129
  additions = [
130
  ("recognition_log_id", "ALTER TABLE attendance_notifications ADD COLUMN recognition_log_id VARCHAR(36) NULL AFTER person_id"),
 
152
  indexes = [
153
  ("idx_notification_person_status", "CREATE INDEX idx_notification_person_status ON attendance_notifications (person_id, status)"),
154
  ("idx_notification_attendance_time", "CREATE INDEX idx_notification_attendance_time ON attendance_notifications (attendance_time)"),
155
+ ("idx_notification_person_time", "CREATE INDEX idx_notification_person_time ON attendance_notifications (person_id, attendance_time)"),
156
+ ("idx_notification_log", "CREATE INDEX idx_notification_log ON attendance_notifications (recognition_log_id)"),
157
+ ("idx_notification_person_status_time", "CREATE INDEX idx_notification_person_status_time ON attendance_notifications (person_id, status, attendance_time)"),
158
  ]
159
  for index_name, statement in indexes:
160
  if not _index_exists(cursor, "attendance_notifications", index_name):
161
  cursor.execute(statement)
162
 
163
+ def _ensure_performance_indexes(cursor):
164
+ indexes = [
165
+ ("persons", "idx_person_status_name", "CREATE INDEX idx_person_status_name ON persons (status, name)"),
166
+ ("persons", "idx_person_status_registered", "CREATE INDEX idx_person_status_registered ON persons (status, registered_at)"),
167
+ ("face_embeddings", "idx_face_person_created", "CREATE INDEX idx_face_person_created ON face_embeddings (person_id, created_at)"),
168
+ ("citizen_ids", "idx_citizen_person_id_number", "CREATE INDEX idx_citizen_person_id_number ON citizen_ids (person_id, id_number)"),
169
+ ]
170
+ for table_name, index_name, statement in indexes:
171
+ if not _index_exists(cursor, table_name, index_name):
172
+ cursor.execute(statement)
173
+
174
  def init_database():
175
  """Tu dong tao Schema theo chuan MySQL ban cung cap"""
176
 
 
340
  _ensure_employee_account_columns(cursor)
341
  _ensure_recognition_log_columns(cursor)
342
  _ensure_attendance_notification_columns(cursor)
343
+ _ensure_performance_indexes(cursor)
344
  _ensure_default_admin(cursor)
345
  conn.commit()
346
  print("[OK] Da kiem tra va khoi tao cau truc CSDL thanh cong tren Railway!")
database/migrate_db_v3_employee_login.py CHANGED
@@ -77,6 +77,14 @@ def ensure_recognition_log_columns(cursor) -> None:
77
  if not column_exists(cursor, "recognition_logs", column_name):
78
  cursor.execute(statement)
79
 
 
 
 
 
 
 
 
 
80
 
81
  def ensure_attendance_notification_columns(cursor) -> None:
82
  additions = [
@@ -105,12 +113,27 @@ def ensure_attendance_notification_columns(cursor) -> None:
105
  indexes = [
106
  ("idx_notification_person_status", "CREATE INDEX idx_notification_person_status ON attendance_notifications (person_id, status)"),
107
  ("idx_notification_attendance_time", "CREATE INDEX idx_notification_attendance_time ON attendance_notifications (attendance_time)"),
 
 
 
108
  ]
109
  for index_name, statement in indexes:
110
  if not index_exists(cursor, "attendance_notifications", index_name):
111
  cursor.execute(statement)
112
 
113
 
 
 
 
 
 
 
 
 
 
 
 
 
114
  def migrate():
115
  conn = get_db_connection()
116
  cursor = conn.cursor(dictionary=True)
@@ -201,6 +224,7 @@ def migrate():
201
  ensure_employee_columns(cursor)
202
  ensure_recognition_log_columns(cursor)
203
  ensure_attendance_notification_columns(cursor)
 
204
 
205
  admin_username = DEFAULT_ADMIN_USERNAME
206
  admin_password = DEFAULT_ADMIN_PASSWORD
 
77
  if not column_exists(cursor, "recognition_logs", column_name):
78
  cursor.execute(statement)
79
 
80
+ indexes = [
81
+ ("idx_recognition_person_status_created", "CREATE INDEX idx_recognition_person_status_created ON recognition_logs (person_id, status, created_at)"),
82
+ ("idx_recognition_created", "CREATE INDEX idx_recognition_created ON recognition_logs (created_at)"),
83
+ ]
84
+ for index_name, statement in indexes:
85
+ if not index_exists(cursor, "recognition_logs", index_name):
86
+ cursor.execute(statement)
87
+
88
 
89
  def ensure_attendance_notification_columns(cursor) -> None:
90
  additions = [
 
113
  indexes = [
114
  ("idx_notification_person_status", "CREATE INDEX idx_notification_person_status ON attendance_notifications (person_id, status)"),
115
  ("idx_notification_attendance_time", "CREATE INDEX idx_notification_attendance_time ON attendance_notifications (attendance_time)"),
116
+ ("idx_notification_person_time", "CREATE INDEX idx_notification_person_time ON attendance_notifications (person_id, attendance_time)"),
117
+ ("idx_notification_log", "CREATE INDEX idx_notification_log ON attendance_notifications (recognition_log_id)"),
118
+ ("idx_notification_person_status_time", "CREATE INDEX idx_notification_person_status_time ON attendance_notifications (person_id, status, attendance_time)"),
119
  ]
120
  for index_name, statement in indexes:
121
  if not index_exists(cursor, "attendance_notifications", index_name):
122
  cursor.execute(statement)
123
 
124
 
125
+ def ensure_performance_indexes(cursor) -> None:
126
+ indexes = [
127
+ ("persons", "idx_person_status_name", "CREATE INDEX idx_person_status_name ON persons (status, name)"),
128
+ ("persons", "idx_person_status_registered", "CREATE INDEX idx_person_status_registered ON persons (status, registered_at)"),
129
+ ("face_embeddings", "idx_face_person_created", "CREATE INDEX idx_face_person_created ON face_embeddings (person_id, created_at)"),
130
+ ("citizen_ids", "idx_citizen_person_id_number", "CREATE INDEX idx_citizen_person_id_number ON citizen_ids (person_id, id_number)"),
131
+ ]
132
+ for table_name, index_name, statement in indexes:
133
+ if not index_exists(cursor, table_name, index_name):
134
+ cursor.execute(statement)
135
+
136
+
137
  def migrate():
138
  conn = get_db_connection()
139
  cursor = conn.cursor(dictionary=True)
 
224
  ensure_employee_columns(cursor)
225
  ensure_recognition_log_columns(cursor)
226
  ensure_attendance_notification_columns(cursor)
227
+ ensure_performance_indexes(cursor)
228
 
229
  admin_username = DEFAULT_ADMIN_USERNAME
230
  admin_password = DEFAULT_ADMIN_PASSWORD
service/face_service.py CHANGED
@@ -264,7 +264,7 @@
264
  import cv2, numpy as np, io, os, threading, logging, urllib.request
265
  from dataclasses import dataclass, field
266
  from typing import Optional
267
- from PIL import Image
268
 
269
  logger = logging.getLogger(__name__)
270
 
@@ -379,35 +379,80 @@ class FaceAiService:
379
  _download_model(YUNET_URL, YUNET_PATH, "YuNet")
380
  _download_model(SFACE_URL, SFACE_PATH, "SFace")
381
  logger.info("[AI] Khởi tạo YuNet + SFace...")
382
- self._detector = cv2.FaceDetectorYN.create(YUNET_PATH, "", (320,240), score_threshold=0.6, nms_threshold=0.3, top_k=5)
383
  self._recognizer = cv2.FaceRecognizerSF.create(SFACE_PATH, "")
384
  logger.info("[AI] Sẵn sàng")
385
 
386
  @staticmethod
387
  def _decode(file_bytes: bytes):
 
 
 
 
 
388
  try:
389
  arr = np.frombuffer(file_bytes, np.uint8)
390
  img = cv2.imdecode(arr, cv2.IMREAD_COLOR)
391
  if img is not None: return img
392
- except Exception: pass
393
- try:
394
- pil = Image.open(io.BytesIO(file_bytes)).convert("RGB")
395
- return cv2.cvtColor(np.array(pil), cv2.COLOR_RGB2BGR)
396
  except Exception as e:
397
  logger.error(f"[AI] Không đọc ảnh: {e}"); return None
398
 
399
- def extract_faces(self, file_bytes: bytes) -> list[dict]:
400
- img = self._decode(file_bytes)
401
- if img is None: return []
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
402
  h, w = img.shape[:2]
403
  self._detector.setInputSize((w, h))
404
  _, faces_raw = self._detector.detect(img)
405
- if faces_raw is None or len(faces_raw) == 0: return []
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
406
  results = []
407
  for fd in faces_raw:
408
  x,y,fw,fh = [int(v) for v in fd[:4]]
409
  x=max(0,x); y=max(0,y); fw=min(fw,w-x); fh=min(fh,h-y)
410
- aligned = self._recognizer.alignCrop(img, fd)
411
  feature = self._recognizer.feature(aligned)
412
  results.append({
413
  "box": {"x":x,"y":y,"width":fw,"height":fh},
@@ -438,4 +483,4 @@ class FaceAiService:
438
 
439
 
440
  face_ai_service = FaceAiService()
441
- face_memory_store = FaceMemoryStore()
 
264
  import cv2, numpy as np, io, os, threading, logging, urllib.request
265
  from dataclasses import dataclass, field
266
  from typing import Optional
267
+ from PIL import Image, ImageOps
268
 
269
  logger = logging.getLogger(__name__)
270
 
 
379
  _download_model(YUNET_URL, YUNET_PATH, "YuNet")
380
  _download_model(SFACE_URL, SFACE_PATH, "SFace")
381
  logger.info("[AI] Khởi tạo YuNet + SFace...")
382
+ self._detector = cv2.FaceDetectorYN.create(YUNET_PATH, "", (320,240), score_threshold=0.45, nms_threshold=0.3, top_k=5)
383
  self._recognizer = cv2.FaceRecognizerSF.create(SFACE_PATH, "")
384
  logger.info("[AI] Sẵn sàng")
385
 
386
  @staticmethod
387
  def _decode(file_bytes: bytes):
388
+ try:
389
+ pil = Image.open(io.BytesIO(file_bytes))
390
+ pil = ImageOps.exif_transpose(pil).convert("RGB")
391
+ return cv2.cvtColor(np.array(pil), cv2.COLOR_RGB2BGR)
392
+ except Exception: pass
393
  try:
394
  arr = np.frombuffer(file_bytes, np.uint8)
395
  img = cv2.imdecode(arr, cv2.IMREAD_COLOR)
396
  if img is not None: return img
 
 
 
 
397
  except Exception as e:
398
  logger.error(f"[AI] Không đọc ảnh: {e}"); return None
399
 
400
+ @staticmethod
401
+ def _image_variants(img):
402
+ variants = [("original", img)]
403
+ h, w = img.shape[:2]
404
+ max_side = max(h, w)
405
+ min_side = min(h, w)
406
+
407
+ if max_side > 1600:
408
+ scale = 1600 / max_side
409
+ variants.append(("downscale", cv2.resize(img, (int(w * scale), int(h * scale)), interpolation=cv2.INTER_AREA)))
410
+ if min_side < 480:
411
+ scale = 480 / min_side
412
+ variants.append(("upscale", cv2.resize(img, (int(w * scale), int(h * scale)), interpolation=cv2.INTER_CUBIC)))
413
+
414
+ try:
415
+ lab = cv2.cvtColor(img, cv2.COLOR_BGR2LAB)
416
+ l, a, b = cv2.split(lab)
417
+ l = cv2.createCLAHE(clipLimit=2.0, tileGridSize=(8, 8)).apply(l)
418
+ variants.append(("contrast", cv2.cvtColor(cv2.merge((l, a, b)), cv2.COLOR_LAB2BGR)))
419
+ except Exception:
420
+ pass
421
+
422
+ variants.extend([
423
+ ("rotate90", cv2.rotate(img, cv2.ROTATE_90_CLOCKWISE)),
424
+ ("rotate270", cv2.rotate(img, cv2.ROTATE_90_COUNTERCLOCKWISE)),
425
+ ("rotate180", cv2.rotate(img, cv2.ROTATE_180)),
426
+ ])
427
+ return variants
428
+
429
+ def _detect_raw(self, img):
430
  h, w = img.shape[:2]
431
  self._detector.setInputSize((w, h))
432
  _, faces_raw = self._detector.detect(img)
433
+ if faces_raw is None or len(faces_raw) == 0:
434
+ return None
435
+ return sorted(faces_raw, key=lambda fd: float(fd[-1]), reverse=True)
436
+
437
+ def extract_faces(self, file_bytes: bytes) -> list[dict]:
438
+ img = self._decode(file_bytes)
439
+ if img is None: return []
440
+ selected_img = img
441
+ faces_raw = None
442
+ used_variant = "original"
443
+ for variant_name, candidate in self._image_variants(img):
444
+ faces_raw = self._detect_raw(candidate)
445
+ if faces_raw is not None:
446
+ selected_img = candidate
447
+ used_variant = variant_name
448
+ break
449
+ if faces_raw is None: return []
450
+ h, w = selected_img.shape[:2]
451
  results = []
452
  for fd in faces_raw:
453
  x,y,fw,fh = [int(v) for v in fd[:4]]
454
  x=max(0,x); y=max(0,y); fw=min(fw,w-x); fh=min(fh,h-y)
455
+ aligned = self._recognizer.alignCrop(selected_img, fd)
456
  feature = self._recognizer.feature(aligned)
457
  results.append({
458
  "box": {"x":x,"y":y,"width":fw,"height":fh},
 
483
 
484
 
485
  face_ai_service = FaceAiService()
486
+ face_memory_store = FaceMemoryStore()