anh-khoa-nguyen commited on
Commit
8bf9be8
·
1 Parent(s): 481efa8

hotfix...

Browse files
.gitattributes CHANGED
@@ -33,3 +33,5 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
 
 
 
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
36
+ *.pdiparams filter=lfs diff=lfs merge=lfs -text
37
+ *.pdmodel filter=lfs diff=lfs merge=lfs -text
app.py CHANGED
@@ -1,49 +1,73 @@
 
 
1
  import base64
2
- import datetime
3
  import os
4
  import re
5
  import time
6
  import uuid
 
 
7
 
8
  import cv2
 
9
  from fastapi import FastAPI, File, UploadFile, HTTPException
10
  from pydantic import BaseModel
11
- from typing import Optional
12
 
13
- # Import lớp Extractor từ thư mục core
14
- from core.extractor import Extractor
15
 
16
- # --- Khởi tạo ---
17
-
18
- # Khởi tạo ứng dụng FastAPI
19
  app = FastAPI(
20
- title="CCCD Extraction API",
21
- description="Một microservice để trích xuất thông tin từ Căn cước công dân Việt Nam.",
22
- version="1.0.0"
23
  )
24
 
25
- # Đường dẫn để lưu trữ file upload
26
- UPLOAD_DIR = "uploads"
27
- os.makedirs(UPLOAD_DIR, exist_ok=True)
28
-
29
- # Khởi tạo một lần duy nhất đối tượng Extractor để tái sử dụng
30
- # Điều này giúp load model một lần và tăng tốc độ xử lý cho các request sau
31
- try:
32
- idcard_extractor = Extractor()
33
- print("CCCD Extractor loaded successfully.")
34
- except Exception as e:
35
- print(f"Error loading CCCD Extractor: {e}")
36
- idcard_extractor = None
37
 
38
 
39
- # --- Định nghĩa Model cho Request và Response ---
40
-
41
- # Model cho request nếu gửi ảnh dạng base64
42
- class ImageRequest(BaseModel):
43
- image_base64: str
44
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
45
 
46
- # Model cho response trả về
47
  class ExtractionResponse(BaseModel):
48
  ID_number: Optional[str] = None
49
  Name: Optional[str] = None
@@ -52,91 +76,98 @@ class ExtractionResponse(BaseModel):
52
  Nationality: Optional[str] = None
53
  Place_of_origin: Optional[str] = None
54
  Place_of_residence: Optional[str] = None
 
55
  elapsed: float
56
 
57
 
58
- # --- Xây dựng API Endpoint ---
59
 
60
  @app.get("/")
61
  def read_root():
62
- return {"message": "Welcome to the CCCD Extraction API. Use the /extract/ endpoint to process an image."}
63
 
64
 
65
  @app.post("/extract/", response_model=ExtractionResponse, tags=["CCCD Extraction"])
66
  async def extract_id_card_info(file: UploadFile = File(...)):
67
  """
68
- Nhận một file ảnh CCCD, trích xuất thông tin và trả về.
 
69
  """
70
- if not idcard_extractor:
71
- raise HTTPException(status_code=500, detail="OCR Extractor is not available.")
 
72
 
73
- # --- 1. Lưu file ảnh được upload ---
74
- # Tạo tên file ngẫu nhiên và an toàn để tránh trùng lặp
75
- file_extension = os.path.splitext(file.filename)[1]
76
- random_filename = f"{uuid.uuid4()}{file_extension}"
77
- file_path = os.path.join(UPLOAD_DIR, random_filename)
78
 
 
 
 
 
 
 
79
  try:
80
- # Đọc nội dung file lưu lại
81
  with open(file_path, "wb") as buffer:
82
  buffer.write(await file.read())
83
- except Exception as e:
84
- raise HTTPException(status_code=500, detail=f"Could not save uploaded file: {e}")
85
 
86
- # --- 2. Xử lý ảnh và trích xuất thông tin (logic từ Django view) ---
87
- start_time = time.time()
88
- try:
89
  frame = cv2.imread(file_path)
90
  if frame is None:
91
  raise HTTPException(status_code=400, detail="Invalid image file.")
92
 
93
- # Bước 1: Dùng PaddleOCR để phát hiện các vùng văn bản
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
94
  annotations = idcard_extractor.Detection(frame)
95
-
96
  info = {}
97
-
98
- # Tìm số CCCD trước tiên
99
  for box in annotations:
100
  text_detected = box[1][0]
101
- if re.search(r'\d{9,12}', text_detected):
102
- # Tách số ra khỏi chuỗi nhiễu
103
- id_number = re.search(r'\d{9,12}', text_detected).group(0)
104
- info['ID_number'] = id_number
105
  info['ID_number_box'] = box[0]
106
  break
107
 
108
  if 'ID_number' not in info:
109
- raise HTTPException(status_code=400, detail="Could not detect an ID number in the image.")
110
 
111
- # Bước 2: Dùng VietOCR để nhận dạng các trường thông tin còn lại
112
  extracted_result = []
113
  for box in annotations:
114
- # Bỏ qua vùng chứa số ID đã xử lý
115
- if re.search(r'\d{9,12}', box[1][0]):
116
- continue
117
-
118
- top_left = (int(box[0][0][0]), int(box[0][0][1]))
119
- top_right = (int(box[0][1][0]), int(box[0][1][1]))
120
- bottom_right = (int(box[0][2][0]), int(box[0][2][1]))
121
- bottom_left = (int(box[0][3][0]), int(box[0][3][1]))
122
-
123
- # Warp và nhận dạng
124
  result_text, _ = idcard_extractor.WarpAndRec(frame, top_left, top_right, bottom_right, bottom_left)
125
- extracted_result.append((result_text, box[0])) # Lưu cả text và bounding box
126
 
127
- # Bước 3: Tổng hợp thông tin
128
  final_info = idcard_extractor.GetInformationAndSave(extracted_result, info['ID_number'], info['ID_number_box'])
129
-
130
  elapsed = time.time() - start_time
131
  final_info["elapsed"] = round(elapsed, 2)
132
-
133
- # Xóa file tạm sau khi xử lý xong
134
- os.remove(file_path)
135
 
136
  return final_info
137
 
138
  except Exception as e:
139
- # Nếu c�� lỗi, cũng xóa file tạm
 
 
 
 
140
  if os.path.exists(file_path):
141
- os.remove(file_path)
142
- raise HTTPException(status_code=500, detail=f"An error occurred during processing: {str(e)}")
 
1
+ # app.py
2
+
3
  import base64
 
4
  import os
5
  import re
6
  import time
7
  import uuid
8
+ import threading
9
+ from typing import Optional
10
 
11
  import cv2
12
+ import numpy as np
13
  from fastapi import FastAPI, File, UploadFile, HTTPException
14
  from pydantic import BaseModel
 
15
 
16
+ # --- KHỞI TẠO ỨNG DỤNG CÁC BIẾN TOÀN CỤC ---
 
17
 
 
 
 
18
  app = FastAPI(
19
+ title="Vietnamese Citizen ID OCR & Face Extraction API",
20
+ description="Một microservice để trích xuất thông tin và cắt ảnh chân dung từ CCCD. Sử dụng Lazy Loading cho model.",
21
+ version="1.3.0-lazyload-packaged"
22
  )
23
 
24
+ # Khởi tạo các biến model toàn cục là None. Chúng sẽ được tải sau.
25
+ idcard_extractor = None
26
+ face_cascade = None
27
+ model_lock = threading.Lock() # Lock để đảm bảo model chỉ được tải 1 lần trong môi trường đa luồng
 
 
 
 
 
 
 
 
28
 
29
 
30
+ # --- HÀM TẢI MODEL (LAZY LOADING) ---
 
 
 
 
31
 
32
+ def load_models():
33
+ """
34
+ Hàm này chỉ được gọi một lần duy nhất khi có request đầu tiên.
35
+ Nó tải tất cả các model AI nặng vào bộ nhớ.
36
+ """
37
+ global idcard_extractor, face_cascade
38
+
39
+ # Sử dụng lock để ngăn chặn nhiều request cùng lúc cố gắng tải model (race condition)
40
+ with model_lock:
41
+ # Kiểm tra lại một lần nữa bên trong lock, nếu một luồng khác đã tải xong thì bỏ qua.
42
+ if idcard_extractor is None:
43
+ print("--- LAZY LOADING MODELS (FIRST REQUEST) ---")
44
+ try:
45
+ # Import Extractor ngay tại đây, không import ở đầu file
46
+ from core.extractor import Extractor
47
+
48
+ # 1. Tải model OCR (sẽ đọc từ các file cục bộ trong thư mục /models)
49
+ print("Loading OCR models...")
50
+ idcard_extractor = Extractor()
51
+ print("CCCD Text Extractor loaded successfully.")
52
+
53
+ # 2. Tải model nhận diện khuôn mặt
54
+ print("Loading face detection model...")
55
+ face_cascade_path = os.path.join(cv2.data.haarcascades, 'haarcascade_frontalface_default.xml')
56
+ if not os.path.exists(face_cascade_path):
57
+ raise FileNotFoundError("Không tìm thấy file haarcascade.")
58
+ face_cascade = cv2.CascadeClassifier(face_cascade_path)
59
+ print("Face cascade classifier loaded successfully.")
60
+
61
+ except Exception as e:
62
+ print(f"FATAL: Error during model loading: {e}")
63
+ # Đặt lại thành None để các request sau biết rằng model đã tải thất bại
64
+ idcard_extractor = None
65
+ face_cascade = None
66
+ print("--- MODEL LOADING COMPLETE ---")
67
+
68
+
69
+ # --- ĐỊNH NGHĨA MODEL CHO RESPONSE ---
70
 
 
71
  class ExtractionResponse(BaseModel):
72
  ID_number: Optional[str] = None
73
  Name: Optional[str] = None
 
76
  Nationality: Optional[str] = None
77
  Place_of_origin: Optional[str] = None
78
  Place_of_residence: Optional[str] = None
79
+ portrait_image_base64: Optional[str] = None
80
  elapsed: float
81
 
82
 
83
+ # --- API ENDPOINT ---
84
 
85
  @app.get("/")
86
  def read_root():
87
+ return {"message": "Welcome to the CCCD Extraction API. POST to /extract/ to process an image."}
88
 
89
 
90
  @app.post("/extract/", response_model=ExtractionResponse, tags=["CCCD Extraction"])
91
  async def extract_id_card_info(file: UploadFile = File(...)):
92
  """
93
+ Nhận ảnh CCCD, trích xuất thông tin và cắt ảnh chân dung.
94
+ Tải các model AI nếu đây là request đầu tiên.
95
  """
96
+ # Bước 1: Tải model nếu chưa có
97
+ # Nếu model đã được tải, hàm này sẽ bỏ qua rất nhanh.
98
+ load_models()
99
 
100
+ # Kiểm tra xem model đã được tải thành công chưa
101
+ if not idcard_extractor or not face_cascade:
102
+ raise HTTPException(status_code=503,
103
+ detail="Server is starting or models failed to load. Please try again in a moment.")
 
104
 
105
+ # Bước 2: Tạo thư mục upload tạm thời trong /tmp và xác định đường dẫn file
106
+ upload_dir = "/tmp/uploads"
107
+ os.makedirs(upload_dir, exist_ok=True)
108
+ file_path = os.path.join(upload_dir, f"{uuid.uuid4()}{os.path.splitext(file.filename)[1]}")
109
+
110
+ start_time = time.time()
111
  try:
112
+ # Bước 3: Lưu file ảnh được upload
113
  with open(file_path, "wb") as buffer:
114
  buffer.write(await file.read())
 
 
115
 
 
 
 
116
  frame = cv2.imread(file_path)
117
  if frame is None:
118
  raise HTTPException(status_code=400, detail="Invalid image file.")
119
 
120
+ # Bước 4: Nhận diện cắt ảnh chân dung
121
+ gray_image = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)
122
+ gray_image = cv2.equalizeHist(gray_image)
123
+ faces = face_cascade.detectMultiScale(gray_image, scaleFactor=1.1, minNeighbors=5, minSize=(80, 80))
124
+
125
+ portrait_base64 = None
126
+ if len(faces) > 0:
127
+ faces = sorted(faces, key=lambda f: f[2] * f[3], reverse=True)
128
+ (x, y, w, h) = faces[0]
129
+ padding_y, padding_x = int(h * 0.2), int(w * 0.2)
130
+ portrait_img = frame[max(0, y - padding_y):min(frame.shape[0], y + h + padding_y),
131
+ max(0, x - padding_x):min(frame.shape[1], x + w + padding_x)]
132
+ _, buffer = cv2.imencode('.jpg', portrait_img)
133
+ portrait_base64 = base64.b64encode(buffer).decode('utf-8')
134
+
135
+ # Bước 5: Trích xuất thông tin văn bản
136
  annotations = idcard_extractor.Detection(frame)
 
137
  info = {}
 
 
138
  for box in annotations:
139
  text_detected = box[1][0]
140
+ id_match = re.search(r'\d{9,12}', text_detected)
141
+ if id_match:
142
+ info['ID_number'] = id_match.group(0)
 
143
  info['ID_number_box'] = box[0]
144
  break
145
 
146
  if 'ID_number' not in info:
147
+ raise HTTPException(status_code=400, detail="Could not detect ID number.")
148
 
 
149
  extracted_result = []
150
  for box in annotations:
151
+ if re.search(r'\d{9,12}', box[1][0]): continue
152
+ top_left, top_right, bottom_right, bottom_left = (
153
+ tuple(map(int, box[0][0])), tuple(map(int, box[0][1])), tuple(map(int, box[0][2])),
154
+ tuple(map(int, box[0][3])))
 
 
 
 
 
 
155
  result_text, _ = idcard_extractor.WarpAndRec(frame, top_left, top_right, bottom_right, bottom_left)
156
+ extracted_result.append((result_text, box[0]))
157
 
158
+ # Bước 6: Tổng hợp kết quả và trả về
159
  final_info = idcard_extractor.GetInformationAndSave(extracted_result, info['ID_number'], info['ID_number_box'])
 
160
  elapsed = time.time() - start_time
161
  final_info["elapsed"] = round(elapsed, 2)
162
+ final_info["portrait_image_base64"] = portrait_base64
 
 
163
 
164
  return final_info
165
 
166
  except Exception as e:
167
+ # Ghi lại lỗi chi tiết vào log của server để gỡ lỗi
168
+ print(f"Error during extraction: {e}")
169
+ raise HTTPException(status_code=500, detail=f"An error occurred during processing: {str(e)}")
170
+ finally:
171
+ # Bước 7: Dọn dẹp file tạm sau khi xử lý xong
172
  if os.path.exists(file_path):
173
+ os.remove(file_path)
 
core/extractor.py CHANGED
@@ -26,10 +26,15 @@ class Extractor:
26
  self.config['cnn']['pretrained'] = False
27
  self.config['device'] = 'cpu'
28
 
29
- if (ocr == None):
30
- self.ocr = PaddleOCR(lang='en', use_gpu=False, ocr_version='PP-OCRv3', det_model_dir='/tmp/.paddleocr/det', rec_model_dir='/tmp/.paddleocr/rec', cls_model_dir='/tmp/.paddleocr/cls')
31
- else:
32
- self.ocr = ocr
 
 
 
 
 
33
  if (detector == None):
34
  self.detector = Predictor(self.config)
35
  else:
 
26
  self.config['cnn']['pretrained'] = False
27
  self.config['device'] = 'cpu'
28
 
29
+ self.ocr = PaddleOCR(
30
+ lang='en',
31
+ use_gpu=False,
32
+ ocr_version='PP-OCRv3',
33
+ det_model_dir='./models/det/en_PP-OCRv3_det_infer/',
34
+ rec_model_dir='./models/rec/en_PP-OCRv3_rec_infer/',
35
+ cls_model_dir='./models/cls/ch_ppocr_mobile_v2.0_cls_infer/'
36
+ )
37
+
38
  if (detector == None):
39
  self.detector = Predictor(self.config)
40
  else:
models/cls/ch_ppocr_mobile_v2.0_cls_infer/._inference.pdmodel ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:d86f5afbfb8cd933a1d0dbbfd8ff2b93ca3eacc6c45f4590a4a2ee107047f6d2
3
+ size 176
models/cls/ch_ppocr_mobile_v2.0_cls_infer/inference.pdiparams ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:d1efda1b80e174b4fcb168a035ac96c1af4938892bd86a55f300a6027105d08c
3
+ size 539978
models/cls/ch_ppocr_mobile_v2.0_cls_infer/inference.pdiparams.info ADDED
Binary file (18.5 kB). View file
 
models/cls/ch_ppocr_mobile_v2.0_cls_infer/inference.pdmodel ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:3c4337ec61722a20b1dca2e5bfaffc313c0592bc89ad6e0d45168224186f6683
3
+ size 1624487
models/det/en_PP-OCRv3_det_infer/inference.pdiparams ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:83676ec730627ab4502f401410a4b6a3ce1c0bb98fa249b71db055b6bddae051
3
+ size 2377917
models/det/en_PP-OCRv3_det_infer/inference.pdiparams.info ADDED
Binary file (26.4 kB). View file
 
models/det/en_PP-OCRv3_det_infer/inference.pdmodel ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:c4bfb1b05d9d1d5a760801eaf6d20180ef7e47bcc675fb17d1f3a89da5fef427
3
+ size 1590133
models/rec/en_PP-OCRv3_rec_infer.tar ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:94402088520ae3938c51eb8782da1cd7c1bc46d5531766de99f77ee9c2f23343
3
+ size 9963520
models/rec/en_PP-OCRv3_rec_infer/inference.pdiparams ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:2cea2de604d8a75fd9151b54ea2dd027659fb8cb777cde377224581110dbff13
3
+ size 8916816
models/rec/en_PP-OCRv3_rec_infer/inference.pdiparams.info ADDED
Binary file (22 kB). View file
 
models/rec/en_PP-OCRv3_rec_infer/inference.pdmodel ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:f0288362f754e815030929477f7b1f96fe2e913e197537288a01bd43f768d0ab
3
+ size 1020915