Commit ·
082e871
1
Parent(s): 3e0b398
updatevgfdhf
Browse files- controllers/main.py +61 -39
- database/database.py +3 -0
- database/migrate_db_v2.py +44 -0
- service/face_service.py +7 -0
controllers/main.py
CHANGED
|
@@ -805,7 +805,7 @@ class CCCDBackSimpleExtractor:
|
|
| 805 |
|
| 806 |
@staticmethod
|
| 807 |
def _parse_mrz(lines):
|
| 808 |
-
"""Parse 3 dòng MRZ thành dict (
|
| 809 |
result = {
|
| 810 |
"mrz_raw": "", "mrz_doc_type": "", "mrz_country": "",
|
| 811 |
"mrz_id": "", "mrz_dob": "", "mrz_gender": "",
|
|
@@ -814,17 +814,44 @@ class CCCDBackSimpleExtractor:
|
|
| 814 |
if len(lines) < 3:
|
| 815 |
return result
|
| 816 |
|
| 817 |
-
def
|
| 818 |
-
|
| 819 |
-
|
| 820 |
-
|
| 821 |
-
|
| 822 |
-
|
| 823 |
-
|
| 824 |
-
|
| 825 |
-
|
| 826 |
-
|
| 827 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 828 |
name_field = l3.strip("<")
|
| 829 |
if "<<" in name_field:
|
| 830 |
parts = name_field.split("<<", 1)
|
|
@@ -837,6 +864,7 @@ class CCCDBackSimpleExtractor:
|
|
| 837 |
return result
|
| 838 |
|
| 839 |
|
|
|
|
| 840 |
# ═══════════════════════════════════════════════════════════════════════════
|
| 841 |
# KHỞI TẠO AI CHẠY NGẦM
|
| 842 |
# ═══════════════════════════════════════════════════════════════════════════
|
|
@@ -1100,7 +1128,7 @@ def _load_embeddings_to_ram():
|
|
| 1100 |
conn = get_db_connection()
|
| 1101 |
cursor = conn.cursor(dictionary=True)
|
| 1102 |
cursor.execute("""
|
| 1103 |
-
SELECT e.person_id, p.name, p.role, p.
|
| 1104 |
p.work_expiry_date, e.embedding_vector
|
| 1105 |
FROM face_embeddings e
|
| 1106 |
JOIN persons p ON e.person_id = p.id
|
|
@@ -1114,7 +1142,7 @@ def _load_embeddings_to_ram():
|
|
| 1114 |
"person_id": row["person_id"],
|
| 1115 |
"name": row["name"],
|
| 1116 |
"role": row.get("role", ""),
|
| 1117 |
-
"img_path": row.get("
|
| 1118 |
"work_expiry_date": str(row["work_expiry_date"]) if row.get("work_expiry_date") else None,
|
| 1119 |
"embedding_vector": json.loads(row["embedding_vector"]),
|
| 1120 |
})
|
|
@@ -1199,12 +1227,8 @@ async def extract_ocr_local(file: UploadFile = File(...), side: str = Form(...))
|
|
| 1199 |
}
|
| 1200 |
|
| 1201 |
else:
|
| 1202 |
-
|
| 1203 |
-
|
| 1204 |
-
elif read_back is not None:
|
| 1205 |
-
raw = read_back.get_back_info(temp_path)
|
| 1206 |
-
else:
|
| 1207 |
-
raw = read_info.get_back_info(temp_path)
|
| 1208 |
|
| 1209 |
logger.info(f"[OCR] Mặt sau raw: {raw}")
|
| 1210 |
mapped_data = {
|
|
@@ -1348,28 +1372,28 @@ async def register(
|
|
| 1348 |
descriptor = detections[0]["descriptor"]
|
| 1349 |
emb_id = str(uuid.uuid4())
|
| 1350 |
|
| 1351 |
-
|
| 1352 |
-
user_descriptor = descriptor
|
| 1353 |
-
|
| 1354 |
-
saved_path = face_ai_service.save_image(img_bytes, person_id, index=i)
|
| 1355 |
-
saved_files.append(saved_path)
|
| 1356 |
|
| 1357 |
if i == 0:
|
| 1358 |
-
|
|
|
|
|
|
|
|
|
|
| 1359 |
cursor.execute(
|
| 1360 |
"""INSERT INTO persons
|
| 1361 |
-
(id, name, role, department, status, img_path, work_expiry_date)
|
| 1362 |
-
VALUES (%s, %s, %s, %s, 'active', %s, %s)""",
|
| 1363 |
-
(person_id, name, role, department,
|
| 1364 |
)
|
| 1365 |
|
| 1366 |
cursor.execute(
|
| 1367 |
-
"INSERT INTO face_embeddings (id, person_id, embedding_vector) VALUES (%s, %s, %s)",
|
| 1368 |
-
(emb_id, person_id, json.dumps(descriptor)),
|
| 1369 |
)
|
| 1370 |
-
new_encodings.append((person_id, name, role,
|
| 1371 |
|
| 1372 |
front_path, back_path = "", ""
|
|
|
|
| 1373 |
|
| 1374 |
if cccd_front:
|
| 1375 |
fb_bytes = await cccd_front.read()
|
|
@@ -1387,24 +1411,22 @@ async def register(
|
|
| 1387 |
logger.warning(f"Cảnh báo giả mạo: Score {score} < {COSINE_THRESHOLD}")
|
| 1388 |
raise Exception("Cảnh báo: Khuôn mặt trên thẻ CCCD KHÔNG KHỚP với ảnh chụp trực tiếp!")
|
| 1389 |
|
| 1390 |
-
|
| 1391 |
-
saved_files.append(front_path)
|
| 1392 |
|
| 1393 |
if cccd_back:
|
| 1394 |
bb_bytes = await cccd_back.read()
|
| 1395 |
if bb_bytes:
|
| 1396 |
-
|
| 1397 |
-
saved_files.append(back_path)
|
| 1398 |
|
| 1399 |
cursor.execute("""
|
| 1400 |
INSERT INTO citizen_ids
|
| 1401 |
-
(id, person_id, front_img_path, back_img_path,
|
| 1402 |
id_number, full_name, dob, gender, nationality,
|
| 1403 |
hometown, address, expiry_date, issue_date, special_features)
|
| 1404 |
-
VALUES (%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s)
|
| 1405 |
""", (
|
| 1406 |
str(uuid.uuid4()), person_id,
|
| 1407 |
-
|
| 1408 |
cccd.get("id_number"), cccd.get("full_name"),
|
| 1409 |
cccd.get("dob"), cccd.get("gender"),
|
| 1410 |
cccd.get("nationality", "Việt Nam"),
|
|
|
|
| 805 |
|
| 806 |
@staticmethod
|
| 807 |
def _parse_mrz(lines):
|
| 808 |
+
"""Parse 3 dòng MRZ thành dict (ICAO TD1 format)"""
|
| 809 |
result = {
|
| 810 |
"mrz_raw": "", "mrz_doc_type": "", "mrz_country": "",
|
| 811 |
"mrz_id": "", "mrz_dob": "", "mrz_gender": "",
|
|
|
|
| 814 |
if len(lines) < 3:
|
| 815 |
return result
|
| 816 |
|
| 817 |
+
def _clean(raw):
|
| 818 |
+
c = raw.upper().replace(' ', '<').replace('|', '<')
|
| 819 |
+
c = re.sub(r'[^A-Z0-9<]', '<', c)
|
| 820 |
+
return (c + '<' * 30)[:30]
|
| 821 |
+
|
| 822 |
+
l1 = _clean(lines[0])
|
| 823 |
+
l2 = _clean(lines[1])
|
| 824 |
+
l3 = _clean(lines[2])
|
| 825 |
+
result["mrz_raw"] = f"{l1}\n{l2}\n{l3}"
|
| 826 |
+
|
| 827 |
+
# Line 1: doc_type(2) + country(3) + id(9) + check + optional
|
| 828 |
+
if len(l1) >= 5:
|
| 829 |
+
result["mrz_doc_type"] = l1[0:2].replace('<', '').strip()
|
| 830 |
+
result["mrz_country"] = l1[2:5].replace('<', '').strip()
|
| 831 |
+
if len(l1) >= 14:
|
| 832 |
+
id_raw = l1[5:14].replace('<', '')
|
| 833 |
+
if re.match(r'\d{9}', id_raw):
|
| 834 |
+
result["mrz_id"] = id_raw[:9]
|
| 835 |
+
|
| 836 |
+
# Line 2: dob(6) + check(1) + sex(1) + expiry(6) + ...
|
| 837 |
+
if len(l2) >= 14:
|
| 838 |
+
dob_raw = l2[0:6]
|
| 839 |
+
gender_raw = l2[7] if len(l2) > 7 else ''
|
| 840 |
+
exp_raw = l2[8:14]
|
| 841 |
+
|
| 842 |
+
if re.fullmatch(r'\d{6}', dob_raw):
|
| 843 |
+
yy, mm, dd = dob_raw[0:2], dob_raw[2:4], dob_raw[4:6]
|
| 844 |
+
cc = "19" if int(yy) >= 30 else "20"
|
| 845 |
+
result["mrz_dob"] = f"{dd}/{mm}/{cc}{yy}"
|
| 846 |
+
|
| 847 |
+
result["mrz_gender"] = {"M": "Nam", "F": "Nữ"}.get(gender_raw, "")
|
| 848 |
+
|
| 849 |
+
if re.fullmatch(r'\d{6}', exp_raw):
|
| 850 |
+
yy, mm, dd = exp_raw[0:2], exp_raw[2:4], exp_raw[4:6]
|
| 851 |
+
cc = "19" if int(yy) >= 30 else "20"
|
| 852 |
+
result["mrz_expiry"] = f"{dd}/{mm}/{cc}{yy}"
|
| 853 |
+
|
| 854 |
+
# Line 3: name (LAST<<FIRST<MIDDLE)
|
| 855 |
name_field = l3.strip("<")
|
| 856 |
if "<<" in name_field:
|
| 857 |
parts = name_field.split("<<", 1)
|
|
|
|
| 864 |
return result
|
| 865 |
|
| 866 |
|
| 867 |
+
|
| 868 |
# ═══════════════════════════════════════════════════════════════════════════
|
| 869 |
# KHỞI TẠO AI CHẠY NGẦM
|
| 870 |
# ═══════════════════════════════════════════════════════════════════════════
|
|
|
|
| 1128 |
conn = get_db_connection()
|
| 1129 |
cursor = conn.cursor(dictionary=True)
|
| 1130 |
cursor.execute("""
|
| 1131 |
+
SELECT e.person_id, p.name, p.role, p.img_url,
|
| 1132 |
p.work_expiry_date, e.embedding_vector
|
| 1133 |
FROM face_embeddings e
|
| 1134 |
JOIN persons p ON e.person_id = p.id
|
|
|
|
| 1142 |
"person_id": row["person_id"],
|
| 1143 |
"name": row["name"],
|
| 1144 |
"role": row.get("role", ""),
|
| 1145 |
+
"img_path": row.get("img_url", ""),
|
| 1146 |
"work_expiry_date": str(row["work_expiry_date"]) if row.get("work_expiry_date") else None,
|
| 1147 |
"embedding_vector": json.loads(row["embedding_vector"]),
|
| 1148 |
})
|
|
|
|
| 1227 |
}
|
| 1228 |
|
| 1229 |
else:
|
| 1230 |
+
# Dùng scan-based approach (không cần YOLO cho mặt sau)
|
| 1231 |
+
raw = read_info.get_back_info(temp_path)
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1232 |
|
| 1233 |
logger.info(f"[OCR] Mặt sau raw: {raw}")
|
| 1234 |
mapped_data = {
|
|
|
|
| 1372 |
descriptor = detections[0]["descriptor"]
|
| 1373 |
emb_id = str(uuid.uuid4())
|
| 1374 |
|
| 1375 |
+
img_b64 = face_ai_service.bytes_to_base64(img_bytes)
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1376 |
|
| 1377 |
if i == 0:
|
| 1378 |
+
user_descriptor = descriptor
|
| 1379 |
+
avatar_path = "" # keeping this variable so API responses don't break immediately
|
| 1380 |
+
avatar_b64 = img_b64
|
| 1381 |
+
|
| 1382 |
cursor.execute(
|
| 1383 |
"""INSERT INTO persons
|
| 1384 |
+
(id, name, role, department, status, img_url, img_path, work_expiry_date)
|
| 1385 |
+
VALUES (%s, %s, %s, %s, 'active', %s, '', %s)""",
|
| 1386 |
+
(person_id, name, role, department, avatar_b64, expiry_val),
|
| 1387 |
)
|
| 1388 |
|
| 1389 |
cursor.execute(
|
| 1390 |
+
"INSERT INTO face_embeddings (id, person_id, embedding_vector, img_base64) VALUES (%s, %s, %s, %s)",
|
| 1391 |
+
(emb_id, person_id, json.dumps(descriptor), img_b64),
|
| 1392 |
)
|
| 1393 |
+
new_encodings.append((person_id, name, role, avatar_b64, expiry_val, descriptor))
|
| 1394 |
|
| 1395 |
front_path, back_path = "", ""
|
| 1396 |
+
front_b64, back_b64 = "", ""
|
| 1397 |
|
| 1398 |
if cccd_front:
|
| 1399 |
fb_bytes = await cccd_front.read()
|
|
|
|
| 1411 |
logger.warning(f"Cảnh báo giả mạo: Score {score} < {COSINE_THRESHOLD}")
|
| 1412 |
raise Exception("Cảnh báo: Khuôn mặt trên thẻ CCCD KHÔNG KHỚP với ảnh chụp trực tiếp!")
|
| 1413 |
|
| 1414 |
+
front_b64 = face_ai_service.bytes_to_base64(fb_bytes)
|
|
|
|
| 1415 |
|
| 1416 |
if cccd_back:
|
| 1417 |
bb_bytes = await cccd_back.read()
|
| 1418 |
if bb_bytes:
|
| 1419 |
+
back_b64 = face_ai_service.bytes_to_base64(bb_bytes)
|
|
|
|
| 1420 |
|
| 1421 |
cursor.execute("""
|
| 1422 |
INSERT INTO citizen_ids
|
| 1423 |
+
(id, person_id, front_img_path, back_img_path, front_img_base64, back_img_base64,
|
| 1424 |
id_number, full_name, dob, gender, nationality,
|
| 1425 |
hometown, address, expiry_date, issue_date, special_features)
|
| 1426 |
+
VALUES (%s,%s,'','',%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s)
|
| 1427 |
""", (
|
| 1428 |
str(uuid.uuid4()), person_id,
|
| 1429 |
+
front_b64 or None, back_b64 or None,
|
| 1430 |
cccd.get("id_number"), cccd.get("full_name"),
|
| 1431 |
cccd.get("dob"), cccd.get("gender"),
|
| 1432 |
cccd.get("nationality", "Việt Nam"),
|
database/database.py
CHANGED
|
@@ -54,6 +54,8 @@ def init_database():
|
|
| 54 |
person_id VARCHAR(36) NOT NULL,
|
| 55 |
front_img_path VARCHAR(255),
|
| 56 |
back_img_path VARCHAR(255),
|
|
|
|
|
|
|
| 57 |
id_number VARCHAR(20),
|
| 58 |
full_name VARCHAR(255),
|
| 59 |
dob VARCHAR(20),
|
|
@@ -77,6 +79,7 @@ def init_database():
|
|
| 77 |
id VARCHAR(36) PRIMARY KEY,
|
| 78 |
person_id VARCHAR(36) NOT NULL,
|
| 79 |
embedding_vector LONGTEXT NOT NULL,
|
|
|
|
| 80 |
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
| 81 |
FOREIGN KEY (person_id) REFERENCES persons(id) ON DELETE CASCADE,
|
| 82 |
KEY idx_person_id (person_id)
|
|
|
|
| 54 |
person_id VARCHAR(36) NOT NULL,
|
| 55 |
front_img_path VARCHAR(255),
|
| 56 |
back_img_path VARCHAR(255),
|
| 57 |
+
front_img_base64 LONGTEXT COMMENT 'Base64 of front CCCD',
|
| 58 |
+
back_img_base64 LONGTEXT COMMENT 'Base64 of back CCCD',
|
| 59 |
id_number VARCHAR(20),
|
| 60 |
full_name VARCHAR(255),
|
| 61 |
dob VARCHAR(20),
|
|
|
|
| 79 |
id VARCHAR(36) PRIMARY KEY,
|
| 80 |
person_id VARCHAR(36) NOT NULL,
|
| 81 |
embedding_vector LONGTEXT NOT NULL,
|
| 82 |
+
img_base64 LONGTEXT COMMENT 'Base64 of this face angle',
|
| 83 |
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
| 84 |
FOREIGN KEY (person_id) REFERENCES persons(id) ON DELETE CASCADE,
|
| 85 |
KEY idx_person_id (person_id)
|
database/migrate_db_v2.py
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import sys
|
| 2 |
+
sys.path.append('.')
|
| 3 |
+
from database import get_db_connection
|
| 4 |
+
|
| 5 |
+
def migrate():
|
| 6 |
+
conn = get_db_connection()
|
| 7 |
+
cursor = conn.cursor()
|
| 8 |
+
|
| 9 |
+
print("Checking schema for new base64 columns...")
|
| 10 |
+
|
| 11 |
+
# Check and add front_img_base64 to citizen_ids
|
| 12 |
+
cursor.execute("""
|
| 13 |
+
SELECT COUNT(*) as cnt FROM information_schema.COLUMNS
|
| 14 |
+
WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'citizen_ids' AND COLUMN_NAME = 'front_img_base64'
|
| 15 |
+
""")
|
| 16 |
+
if not cursor.fetchone()[0]:
|
| 17 |
+
print(" Adding front_img_base64 to citizen_ids...")
|
| 18 |
+
cursor.execute("ALTER TABLE citizen_ids ADD COLUMN front_img_base64 LONGTEXT")
|
| 19 |
+
|
| 20 |
+
# Check and add back_img_base64 to citizen_ids
|
| 21 |
+
cursor.execute("""
|
| 22 |
+
SELECT COUNT(*) as cnt FROM information_schema.COLUMNS
|
| 23 |
+
WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'citizen_ids' AND COLUMN_NAME = 'back_img_base64'
|
| 24 |
+
""")
|
| 25 |
+
if not cursor.fetchone()[0]:
|
| 26 |
+
print(" Adding back_img_base64 to citizen_ids...")
|
| 27 |
+
cursor.execute("ALTER TABLE citizen_ids ADD COLUMN back_img_base64 LONGTEXT")
|
| 28 |
+
|
| 29 |
+
# Check and add img_base64 to face_embeddings
|
| 30 |
+
cursor.execute("""
|
| 31 |
+
SELECT COUNT(*) as cnt FROM information_schema.COLUMNS
|
| 32 |
+
WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'face_embeddings' AND COLUMN_NAME = 'img_base64'
|
| 33 |
+
""")
|
| 34 |
+
if not cursor.fetchone()[0]:
|
| 35 |
+
print(" Adding img_base64 to face_embeddings...")
|
| 36 |
+
cursor.execute("ALTER TABLE face_embeddings ADD COLUMN img_base64 LONGTEXT")
|
| 37 |
+
|
| 38 |
+
conn.commit()
|
| 39 |
+
cursor.close()
|
| 40 |
+
conn.close()
|
| 41 |
+
print("Migration complete!")
|
| 42 |
+
|
| 43 |
+
if __name__ == "__main__":
|
| 44 |
+
migrate()
|
service/face_service.py
CHANGED
|
@@ -429,6 +429,13 @@ class FaceAiService:
|
|
| 429 |
with open(filepath, "wb") as f: f.write(file_bytes)
|
| 430 |
return filepath
|
| 431 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 432 |
|
| 433 |
face_ai_service = FaceAiService()
|
| 434 |
face_memory_store = FaceMemoryStore()
|
|
|
|
| 429 |
with open(filepath, "wb") as f: f.write(file_bytes)
|
| 430 |
return filepath
|
| 431 |
|
| 432 |
+
@staticmethod
|
| 433 |
+
def bytes_to_base64(file_bytes: bytes) -> str:
|
| 434 |
+
import base64
|
| 435 |
+
# Return a standard base64 data URI format for images
|
| 436 |
+
encoded = base64.b64encode(file_bytes).decode('utf-8')
|
| 437 |
+
return f"data:image/jpeg;base64,{encoded}"
|
| 438 |
+
|
| 439 |
|
| 440 |
face_ai_service = FaceAiService()
|
| 441 |
face_memory_store = FaceMemoryStore()
|