File size: 14,237 Bytes
e9daf74 b7a3808 e9daf74 b7a3fa4 e9daf74 b7a3808 e9daf74 b7a3808 e9daf74 b7a3808 b7a3fa4 e9daf74 b7a3808 204addf |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 |
import os
# Ensure all caches and user data write to writable dirs on HF Spaces
os.environ.setdefault("MPLCONFIGDIR", "/tmp/matplotlib")
os.environ.setdefault("XDG_CACHE_HOME", "/tmp/.cache")
os.environ.setdefault("HF_HOME", "/tmp/hf_cache")
os.environ.setdefault("INSIGHTFACE_HOME", "/tmp/.insightface")
# Force HOME to a writable directory so expanduser doesn't resolve to '/'
os.environ.setdefault("HOME", "/tmp")
import cv2
import numpy as np
import urllib.request
from flask import Flask, request, jsonify, send_file
from io import BytesIO
import base64
import json
import re
import insightface
from insightface.app import FaceAnalysis
# URL đến mô hình hoán đổi khuôn mặt
model_url = "https://huggingface.co/ezioruan/inswapper_128.onnx/resolve/main/inswapper_128.onnx"
CACHE_DIR = os.environ.get("INSIGHTFACE_HOME", "/tmp/.insightface")
model_path = os.path.join(CACHE_DIR, "models", "inswapper_128.onnx")
# Tải mô hình nếu chưa tồn tại
os.makedirs(os.path.dirname(model_path), exist_ok=True)
if not os.path.exists(model_path):
print("Đang tải mô hình từ URL...")
urllib.request.urlretrieve(model_url, model_path)
print("Hoàn tất tải mô hình!")
# Khởi tạo đối tượng INSwapper
swapper = insightface.model_zoo.get_model(model_path)
# Khởi tạo mô hình nhận diện khuôn mặt
app = FaceAnalysis(name='buffalo_l', root=CACHE_DIR)
app.prepare(ctx_id=-1, det_size=(640, 640), det_thresh=0.3)
# Khởi tạo Flask app
flask_app = Flask(__name__)
def get_gender(face):
"""Xác định giới tính của khuôn mặt."""
return "male" if face.gender > 0.5 else "female"
def process_image(file):
"""Chuyển đổi tệp tin ảnh thành mảng NumPy."""
return cv2.imdecode(np.frombuffer(file.read(), np.uint8), cv2.IMREAD_COLOR)
def swap_faces(source_img, target_img):
"""Hoán đổi khuôn mặt từ ảnh nguồn sang ảnh đích."""
source_img = process_image(source_img)
target_img = process_image(target_img)
source_faces = app.get(source_img)
target_faces = app.get(target_img)
if not source_faces:
return {"error": "Không tìm thấy khuôn mặt trong ảnh nguồn."}, 400
if not target_faces:
return {"error": "Không tìm thấy khuôn mặt trong ảnh đích."}, 400
source_face = source_faces[0]
result_img = target_img.copy()
for target_face in target_faces:
result_img = swapper.get(result_img, target_face, source_face, paste_back=True)
_, buffer = cv2.imencode('.jpg', result_img)
img_base64 = base64.b64encode(buffer).decode('utf-8')
return img_base64
def swap_faces_gender_based(source_male, source_female, target_img):
"""Hoán đổi khuôn mặt theo giới tính."""
source_male = process_image(source_male)
source_female = process_image(source_female)
target_img = process_image(target_img)
source_male_faces = app.get(source_male)
source_female_faces = app.get(source_female)
target_faces = app.get(target_img)
if not source_male_faces or not source_female_faces:
return {"error": "Không tìm thấy khuôn mặt trong ảnh nguồn."}, 400
if not target_faces:
return {"error": "Không tìm thấy khuôn mặt trong ảnh đích."}, 400
source_male_face = source_male_faces[0]
source_female_face = source_female_faces[0]
result_img = target_img.copy()
for target_face in target_faces:
gender = get_gender(target_face)
swap_face = source_male_face if gender == "male" else source_female_face
result_img = swapper.get(result_img, target_face, swap_face, paste_back=True)
_, buffer = cv2.imencode('.jpg', result_img)
img_base64 = base64.b64encode(buffer).decode('utf-8')
return img_base64
def swap_faces_multi(source_files, target_file, mapping=None):
"""Hoán đổi nhiều khuôn mặt: mỗi khuôn mặt đích dùng 1 khuôn mặt nguồn riêng.
- source_files: list các FileStorage cho ảnh nguồn (mỗi ảnh nguồn nên chỉ có 1 mặt)
- target_file: FileStorage cho ảnh đích (có nhiều mặt)
- mapping: list ánh xạ index mặt đích -> index ảnh nguồn
* Cho phép phần tử là số nguyên (chỉ mục ảnh nguồn), None/null, hoặc -1 để "bỏ qua" mặt đích tương ứng.
* Nếu mapping ngắn hơn số mặt đích, các mặt còn lại sẽ bị bỏ qua.
* Nếu mapping dài hơn, phần dư sẽ bị cắt bỏ.
* Nếu mapping=None và số nguồn == số mặt đích, tự map trái→phải; nếu khác, tự map tối đa có thể và bỏ qua phần còn lại.
"""
# Decode images
source_imgs = [process_image(f) for f in source_files]
target_img = process_image(target_file)
if target_img is None:
return {"error": "Ảnh đích không hợp lệ."}, 400
if any(img is None for img in source_imgs):
return {"error": "Ít nhất một ảnh nguồn không hợp lệ."}, 400
# Detect faces
source_faces_list = [app.get(img) for img in source_imgs]
target_faces = app.get(target_img)
if not target_faces:
return {"error": "Không tìm thấy khuôn mặt trong ảnh đích."}, 400
# Chuẩn hoá danh sách nguồn: lấy khuôn mặt đầu tiên của mỗi ảnh nếu có, nếu không để None
source_single_faces = []
for faces in source_faces_list:
if faces and len(faces) > 0:
source_single_faces.append(faces[0])
else:
source_single_faces.append(None) # Không có mặt trong ảnh nguồn này
# Sắp xếp mặt đích theo toạ độ x (trái → phải) để ổn định thứ tự
def face_center_x(face):
x1, y1, x2, y2 = face.bbox.astype(int).tolist()
return (x1 + x2) / 2.0
target_faces_sorted = sorted(target_faces, key=face_center_x)
# Xây mapping mặc định nếu không cung cấp
if mapping is None:
if len(source_single_faces) == len(target_faces_sorted):
mapping = list(range(len(source_single_faces)))
else:
limit = min(len(source_single_faces), len(target_faces_sorted))
mapping = list(range(limit)) + [None] * (len(target_faces_sorted) - limit)
# Chuẩn hoá mapping về đúng độ dài số mặt đích
if len(mapping) < len(target_faces_sorted):
mapping = list(mapping) + [None] * (len(target_faces_sorted) - len(mapping))
elif len(mapping) > len(target_faces_sorted):
mapping = list(mapping)[:len(target_faces_sorted)]
# Thực hiện swap theo ánh xạ; bỏ qua nếu chỉ mục không hợp lệ hoặc nguồn không có mặt
result_img = target_img.copy()
for t_idx, target_face in enumerate(target_faces_sorted):
s_idx = mapping[t_idx]
# Cho phép None hoặc -1 để bỏ qua
if s_idx is None or s_idx == -1:
continue
# Nếu s_idx vượt phạm vi, bỏ qua
if not isinstance(s_idx, int) or s_idx < 0 or s_idx >= len(source_single_faces):
continue
src_face = source_single_faces[s_idx]
if src_face is None:
# Ảnh nguồn ở vị trí này không có mặt -> bỏ qua
continue
result_img = swapper.get(result_img, target_face, src_face, paste_back=True)
_, buffer = cv2.imencode('.jpg', result_img)
img_base64 = base64.b64encode(buffer).decode('utf-8')
return img_base64
@flask_app.route('/swap-multi', methods=['POST'])
def swap_multi():
"""API hoán đổi nhiều khuôn mặt.
Cách gọi 1 (tự map): gửi nhiều file nguồn cùng key `sources` (hoặc `sources[]`), và 1 file `target`.
Nếu số ảnh nguồn == số mặt đích, hệ thống tự map trái→phải. Nếu khác, tự map tối đa có thể; phần còn lại bỏ qua.
Cách gọi 2 (map tuỳ chỉnh): ngoài `sources` và `target`, gửi thêm field form `map` là JSON array
(ví dụ: `[2,0,null,-1]`). Độ dài sẽ được chuẩn hoá bằng với số mặt đích. Mỗi phần tử:
- số nguyên: chỉ mục ảnh nguồn tương ứng;
- `null` hoặc `-1`: bỏ qua khuôn mặt đích ở vị trí đó.
Cách gọi 3: gửi file nguồn với key dạng `sources[<index>]` (ví dụ: `sources[2]`, `sources[3]`).
Chỉ mục trong key sẽ được sử dụng để gán file vào vị trí tương ứng trong danh sách nguồn.
"""
if 'target' not in request.files:
return jsonify({"error": "Thiếu ảnh đích (field 'target')."}), 400
# Thu thập tất cả file nguồn từ request.files
source_files = []
indexed_sources = {}
# Regex để phát hiện key dạng sources[<index>]
index_pattern = re.compile(r'sources\[(\d+)\]')
# Duyệt qua tất cả key trong request.files
for key in request.files:
if key == 'sources' or key == 'sources[]':
# Thêm các file từ sources hoặc sources[] vào danh sách
source_files.extend(request.files.getlist(key))
elif index_pattern.match(key):
# Xử lý các key dạng sources[<index>]
match = index_pattern.match(key)
index = int(match.group(1))
indexed_sources[index] = request.files[key]
# Nếu có file với chỉ mục, gán chúng vào danh sách theo chỉ mục
if indexed_sources:
# Tìm chỉ mục lớn nhất để tạo danh sách đủ lớn
max_index = max(indexed_sources.keys())
# Khởi tạo danh sách với None
source_files_with_index = [None] * (max_index + 1)
# Gán file vào vị trí tương ứng
for index, file in indexed_sources.items():
source_files_with_index[index] = file
# Loại bỏ các phần tử None ở cuối nếu cần
while source_files_with_index and source_files_with_index[-1] is None:
source_files_with_index.pop()
# Kết hợp danh sách từ sources/sources[] và sources[<index>]
source_files = source_files_with_index + source_files
else:
# Nếu không có sources[<index>], chỉ sử dụng sources hoặc sources[]
source_files = source_files or request.files.getlist('sources[]')
if not source_files:
return jsonify({"error": "Thiếu ảnh nguồn (field 'sources', 'sources[]', hoặc 'sources[<index>]')."}), 400
target_file = request.files['target']
# Đọc mapping nếu có
mapping = None
map_text = request.form.get('map')
if map_text:
try:
mapping = json.loads(map_text)
except Exception:
return jsonify({"error": "Giá trị 'map' không phải JSON hợp lệ."}), 400
if not isinstance(mapping, list) or not all((x is None) or isinstance(x, int) for x in mapping):
return jsonify({"error": "'map' phải là mảng, mỗi phần tử là số nguyên, -1 hoặc null để bỏ qua."}), 400
# Giả sử swap_faces_multi là hàm xử lý logic hoán đổi khuôn mặt
result = swap_faces_multi(source_files, target_file, mapping)
if isinstance(result, tuple):
return jsonify(result[0]), result[1]
return jsonify({"image_base64": result})
@flask_app.route('/swap', methods=['POST'])
def swap():
"""API hoán đổi khuôn mặt cơ bản."""
if 'source' not in request.files or 'target' not in request.files:
return jsonify({"error": "Thiếu ảnh nguồn hoặc ảnh đích."}), 400
source_img = request.files['source']
target_img = request.files['target']
result = swap_faces(source_img, target_img)
if isinstance(result, tuple):
return jsonify(result[0]), result[1]
return jsonify({"image_base64": result})
@flask_app.route('/swap-2', methods=['POST'])
def swap_2():
"""API hoán đổi khuôn mặt dựa trên giới tính."""
if 'source-male' not in request.files or 'source-female' not in request.files or 'target' not in request.files:
return jsonify({"error": "Thiếu ảnh nguồn nam, nữ hoặc ảnh đích."}), 400
source_male = request.files['source-male']
source_female = request.files['source-female']
target_img = request.files['target']
result = swap_faces_gender_based(source_male, source_female, target_img)
if isinstance(result, tuple):
return jsonify(result[0]), result[1]
return jsonify({"image_base64": result})
# New endpoint for analyzing faces in a base64 image
@flask_app.route('/analyze', methods=['POST'])
def analyze_face():
if 'image' not in request.files:
return jsonify({'error': 'Thiếu file ảnh'}), 400
image_file = request.files['image']
img = cv2.imdecode(np.frombuffer(image_file.read(), np.uint8), cv2.IMREAD_COLOR)
if img is None:
return jsonify({'error': 'Ảnh không hợp lệ'}), 400
# Load DNN face detector
modelFile = "models/res10_300x300_ssd_iter_140000.caffemodel"
configFile = "models/deploy.prototxt"
net = cv2.dnn.readNetFromCaffe(configFile, modelFile)
h, w = img.shape[:2]
blob = cv2.dnn.blobFromImage(img, 1.0, (300, 300), [104, 117, 123], False, False)
net.setInput(blob)
detections = net.forward()
face_data = []
for i in range(detections.shape[2]):
confidence = detections[0, 0, i, 2]
if confidence > 0.5:
box = detections[0, 0, i, 3:7] * np.array([w, h, w, h])
(x1, y1, x2, y2) = box.astype("int")
face_data.append({
'face_rectangle': {
'left': x1 / w,
'top': y1 / h,
'width': (x2 - x1) / w,
'height': (y2 - y1) / h
}
})
return jsonify({
'media_info_list': [{
'media_extra': {
'faces': face_data
}
}]
})
if __name__ == '__main__':
port = int(os.environ.get('PORT', 7860))
flask_app.run(host='0.0.0.0', port=port, debug=False) |