Spaces:
Sleeping
Sleeping
File size: 7,968 Bytes
039104b 0184e25 6d5ba45 0184e25 039104b f51ce5a fffbe66 4aaa514 61d29c5 815e104 f51ce5a 4aaa514 fffbe66 039104b 0184e25 039104b f51ce5a 039104b 0184e25 8364a01 39ad460 8364a01 815e104 4aaa514 61d29c5 b6787a3 815e104 288e886 b6787a3 815e104 288e886 b6787a3 815e104 288e886 815e104 288e886 f2b919c 8364a01 f2b919c 6b6fcb1 f2b919c 288e886 039104b 0184e25 039104b 85d8b66 039104b 0184e25 039104b efec4b6 39ad460 85d8b66 6d5ba45 85d8b66 6d5ba45 39ad460 85d8b66 39ad460 2b4fe74 39ad460 efec4b6 8364a01 efec4b6 039104b |
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 |
from flask import Flask, request, jsonify
from ultralytics import YOLO
import cv2
import numpy as np
from shapely.geometry import box as shapely_box, Polygon
from shapely.ops import unary_union
import mediapipe as mp
from collections import defaultdict
from concurrent.futures import ThreadPoolExecutor, as_completed
# -------------------- CONFIG --------------------
model_paths = {
"pores": "pores.pt",
"pig1": "pig1.pt", # β
pigmentation model
"combine": "combine.pt", # β
combine model
"wrinkle": "wrinkle.pt",
}
default_conf_threshold = 0.05
special_conf_threshold = 0.08 # β
for combine & pig1
pores2_conf_threshold = 0.02 # β
special for pores2
imgsz = 1024
# -------------------- INIT --------------------
app = Flask(__name__)
models = {name: YOLO(path) for name, path in model_paths.items()}
mp_face_mesh = mp.solutions.face_mesh
face_mesh = mp_face_mesh.FaceMesh(static_image_mode=True)
# -------------------- SKIN TYPE DETECTOR --------------------
def detect_skin_type_from_image(img):
img = cv2.resize(img, (400, 400))
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
gray = cv2.equalizeHist(gray)
# Oiliness
_, highlights = cv2.threshold(gray, 220, 255, cv2.THRESH_BINARY)
oily_score = np.sum(highlights > 0) / highlights.size
# Dryness
laplacian = cv2.Laplacian(gray, cv2.CV_64F)
texture_score = laplacian.var()
# Combination (different facial regions)
h, w = gray.shape
regions = {
"forehead": gray[0:int(h*0.3), :],
"nose": gray[int(h*0.3):int(h*0.6), int(w*0.4):int(w*0.6)],
"cheeks": gray[int(h*0.3):int(h*0.7), int(w*0.1):int(w*0.9)]
}
region_oiliness = []
for r in regions.values():
_, r_highlights = cv2.threshold(r, 220, 255, cv2.THRESH_BINARY)
r_score = np.sum(r_highlights > 0) / r_highlights.size
region_oiliness.append(np.float64(r_score))
combo_score = np.std(region_oiliness)
# Normalize scores
oily_norm = min(oily_score / 0.30, 1.0)
dry_norm = min(texture_score / 6000.0, 1.0)
combo_norm = min(combo_score * 5, 1.0)
normal_norm = max(1.0 - (oily_norm + dry_norm + combo_norm) / 3, 0.0)
# Percentages
total = oily_norm + dry_norm + combo_norm + normal_norm + 1e-6
percentages = {
"Oily": round(100 * oily_norm / total, 2),
"Dry": round(100 * dry_norm / total, 2),
"Combination": round(100 * combo_norm / total, 2),
"Normal": round(100 * normal_norm / total, 2)
}
# Final type
final_type = max(percentages, key=percentages.get)
final_value = percentages[final_type]
return percentages, f"{final_type} ({final_value}%)"
# -------------------- HELPERS --------------------
def run_model(model_name, model, img, face_polygon, face_area):
if model_name == "pores2":
conf = pores2_conf_threshold
elif model_name in ["combine", "pig1"]:
conf = special_conf_threshold
else:
conf = default_conf_threshold
results = model(img, conf=conf, imgsz=imgsz)
boxes_xy = results[0].boxes.xyxy.cpu().numpy()
boxes_cls = results[0].boxes.cls.cpu().numpy().astype(int)
class_polygons = defaultdict(list)
for i, cls_id in enumerate(boxes_cls):
cls_name = model.names.get(cls_id, str(cls_id)).lower()
if model_name == "combine" and cls_name == "wrinkle":
continue
x1, y1, x2, y2 = boxes_xy[i].astype(int)
det_poly = shapely_box(x1, y1, x2, y2)
if face_polygon.intersects(det_poly):
intersection = det_poly.intersection(face_polygon)
if intersection.area > 0:
class_polygons[cls_id].append(intersection)
skin_percentages = {name.lower(): 0.0 for name in model.names.values()}
if model_name == "combine":
if "wrinkle" in skin_percentages:
skin_percentages.pop("wrinkle")
for cls_id, polys in class_polygons.items():
union_poly = unary_union(polys)
pixels = union_poly.area
percentage = (pixels / face_area) * 100 if face_area > 0 else 0.0
cls_name = model.names.get(cls_id, str(cls_id)).lower()
skin_percentages[cls_name] = round(percentage, 2)
return skin_percentages
def normalize_and_merge(percentages):
normalized = {}
for cls_name, value in percentages.items():
name = cls_name.lower()
if name == "pore":
name = "pores"
elif name == "wrinkle":
name = "wrinkles"
elif name == "forehead":
name = "forehead"
elif name == "dark_circle":
name = "dark circles"
elif name == "acne_scar":
name = "scar"
if name in ["pigmentation", "melasma"]:
normalized["pigmentation"] = normalized.get("pigmentation", 0.0) + value
else:
normalized[name] = value
return normalized
# -------------------- ROUTES --------------------
@app.route("/", methods=["GET"])
def home():
return jsonify({
"message": "β
Skin API is running",
"usage": "POST one image (form-data key 'file') to /analyze"
})
@app.route("/analyze", methods=["POST"])
def analyze():
try:
files = request.files.getlist("file")
if not files or len(files) != 1:
return jsonify({
"success": False,
"analysis": [],
"error": "You must upload exactly 1 image."
}), 400
file = files[0]
file_bytes = np.frombuffer(file.read(), np.uint8)
img = cv2.imdecode(file_bytes, cv2.IMREAD_COLOR)
if img is None:
return jsonify({"success": False, "analysis": [], "error": "Invalid image."}), 400
img_h, img_w = img.shape[:2]
rgb_img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)
result = face_mesh.process(rgb_img)
if not result.multi_face_landmarks:
return jsonify({"success": False, "analysis": [], "error": "No face detected."}), 400
# Face polygon
for landmarks in result.multi_face_landmarks:
points = np.array([[int(lm.x * img_w), int(lm.y * img_h)] for lm in landmarks.landmark])
hull = cv2.convexHull(points)
face_area = cv2.contourArea(hull)
face_polygon = Polygon(hull.reshape(-1, 2))
break
# Run YOLO models in parallel
combined_percentages = {}
with ThreadPoolExecutor() as executor:
futures = {
executor.submit(run_model, model_name, model, img, face_polygon, face_area): model_name
for model_name, model in models.items()
}
for future in as_completed(futures):
skin_percentages = future.result()
combined_percentages.update(skin_percentages)
final_percentages = normalize_and_merge(combined_percentages)
# Merge wrinkles + forehead
wrinkle_value = final_percentages.get("wrinkles", 0.0) + final_percentages.get("forehead", 0.0)
final_percentages["wrinkles"] = round(wrinkle_value, 2)
if "forehead" in final_percentages:
final_percentages.pop("forehead")
# Skin type
skin_type_percentages, _ = detect_skin_type_from_image(img)
final_skin_type = max(skin_type_percentages, key=skin_type_percentages.get)
final_skin_type_str = f"{final_skin_type} ({skin_type_percentages[final_skin_type]}%)"
# Format response
analysis_list = [f"{cls_name.upper()}: {value}%" for cls_name, value in final_percentages.items()]
analysis_list.append(f"SKIN TYPE: {final_skin_type_str}")
return jsonify({"success": True, "analysis": ["\n".join(analysis_list)]})
except Exception as e:
return jsonify({"success": False, "analysis": [], "error": str(e)}), 500
# -------------------- RUN --------------------
if __name__ == "__main__":
app.run(host="0.0.0.0", port=7860)
|