|
|
from flask import Flask, request, render_template, Response, jsonify, url_for, send_from_directory
|
|
|
import cv2
|
|
|
import numpy as np
|
|
|
import joblib
|
|
|
import os
|
|
|
import warnings
|
|
|
from werkzeug.utils import secure_filename
|
|
|
import mediapipe as mp
|
|
|
from mediapipe.tasks import python
|
|
|
from mediapipe.tasks.python import vision
|
|
|
from mediapipe.framework.formats import landmark_pb2
|
|
|
from flask_cors import CORS
|
|
|
from dotenv import load_dotenv
|
|
|
|
|
|
from database import upload_image_to_cloudinary, save_analysis_to_db
|
|
|
|
|
|
|
|
|
load_dotenv()
|
|
|
|
|
|
|
|
|
warnings.filterwarnings("ignore", category=UserWarning, module='google.protobuf')
|
|
|
app = Flask(__name__, template_folder='templates')
|
|
|
CORS(app)
|
|
|
|
|
|
|
|
|
base_options = python.BaseOptions(model_asset_path='face_landmarker_v2_with_blendshapes.task')
|
|
|
options = vision.FaceLandmarkerOptions(base_options=base_options,
|
|
|
output_face_blendshapes=True,
|
|
|
output_facial_transformation_matrixes=True,
|
|
|
num_faces=1)
|
|
|
face_landmarker = vision.FaceLandmarker.create_from_options(options)
|
|
|
|
|
|
|
|
|
mp_drawing = mp.solutions.drawing_utils
|
|
|
mp_drawing_styles = mp.solutions.drawing_styles
|
|
|
|
|
|
|
|
|
print("Loading ultra-optimized model...")
|
|
|
face_shape_model = joblib.load('Ultra_Optimized_RandomForest.joblib')
|
|
|
scaler = joblib.load('ultra_optimized_scaler.joblib')
|
|
|
print("✅ Ultra-optimized model loaded successfully!")
|
|
|
|
|
|
def distance_3d(p1, p2):
|
|
|
"""Calculate 3D Euclidean distance between two points."""
|
|
|
return np.linalg.norm(np.array(p1) - np.array(p2))
|
|
|
|
|
|
def smart_preprocess_image(image):
|
|
|
"""
|
|
|
Smart image preprocessing to get the best face region.
|
|
|
This addresses the issue of users not providing perfect images.
|
|
|
"""
|
|
|
h, w = image.shape[:2]
|
|
|
|
|
|
|
|
|
rgb_image = cv2.cvtColor(image, cv2.COLOR_BGR2RGB)
|
|
|
mp_image = mp.Image(image_format=mp.ImageFormat.SRGB, data=rgb_image)
|
|
|
detection_result = face_landmarker.detect(mp_image)
|
|
|
|
|
|
if detection_result.face_landmarks:
|
|
|
|
|
|
face_landmarks = detection_result.face_landmarks[0]
|
|
|
|
|
|
|
|
|
x_coords = [landmark.x for landmark in face_landmarks]
|
|
|
y_coords = [landmark.y for landmark in face_landmarks]
|
|
|
|
|
|
|
|
|
x_min = int(min(x_coords) * w)
|
|
|
x_max = int(max(x_coords) * w)
|
|
|
y_min = int(min(y_coords) * h)
|
|
|
y_max = int(max(y_coords) * h)
|
|
|
|
|
|
|
|
|
face_width = x_max - x_min
|
|
|
face_height = y_max - y_min
|
|
|
pad_x = int(face_width * 0.4)
|
|
|
pad_y = int(face_height * 0.4)
|
|
|
|
|
|
|
|
|
x1 = max(0, x_min - pad_x)
|
|
|
x2 = min(w, x_max + pad_x)
|
|
|
y1 = max(0, y_min - pad_y)
|
|
|
y2 = min(h, y_max + pad_y)
|
|
|
|
|
|
|
|
|
face_crop = image[y1:y2, x1:x2]
|
|
|
|
|
|
|
|
|
target_size = 224
|
|
|
crop_h, crop_w = face_crop.shape[:2]
|
|
|
|
|
|
|
|
|
scale = min(target_size / crop_w, target_size / crop_h)
|
|
|
new_w = int(crop_w * scale)
|
|
|
new_h = int(crop_h * scale)
|
|
|
|
|
|
|
|
|
resized = cv2.resize(face_crop, (new_w, new_h))
|
|
|
|
|
|
|
|
|
final_image = np.zeros((target_size, target_size, 3), dtype=np.uint8)
|
|
|
|
|
|
|
|
|
start_y = (target_size - new_h) // 2
|
|
|
start_x = (target_size - new_w) // 2
|
|
|
final_image[start_y:start_y + new_h, start_x:start_x + new_w] = resized
|
|
|
|
|
|
return final_image
|
|
|
else:
|
|
|
|
|
|
return cv2.resize(image, (224, 224))
|
|
|
|
|
|
def extract_optimized_features(coords):
|
|
|
"""
|
|
|
Extract optimized features for face shape detection.
|
|
|
Uses only the most important landmarks for efficiency.
|
|
|
"""
|
|
|
|
|
|
landmark_indices = {
|
|
|
'forehead_top': 10,
|
|
|
'forehead_left': 21,
|
|
|
'forehead_right': 251,
|
|
|
'cheek_left': 234,
|
|
|
'cheek_right': 454,
|
|
|
'jaw_left': 172,
|
|
|
'jaw_right': 397,
|
|
|
'chin': 152,
|
|
|
}
|
|
|
|
|
|
|
|
|
lm = {name: coords[idx] for name, idx in landmark_indices.items()}
|
|
|
|
|
|
|
|
|
face_height = distance_3d(lm['forehead_top'], lm['chin'])
|
|
|
face_width = distance_3d(lm['cheek_left'], lm['cheek_right'])
|
|
|
jaw_width = distance_3d(lm['jaw_left'], lm['jaw_right'])
|
|
|
forehead_width = distance_3d(lm['forehead_left'], lm['forehead_right'])
|
|
|
|
|
|
|
|
|
width_to_height = face_width / face_height
|
|
|
jaw_to_forehead = jaw_width / forehead_width
|
|
|
jaw_to_face = jaw_width / face_width
|
|
|
forehead_to_face = forehead_width / face_width
|
|
|
|
|
|
|
|
|
face_area = face_width * face_height
|
|
|
jaw_angle = np.arctan2(lm['jaw_right'][1] - lm['jaw_left'][1],
|
|
|
lm['jaw_right'][0] - lm['jaw_left'][0])
|
|
|
|
|
|
|
|
|
features = np.array([
|
|
|
width_to_height,
|
|
|
jaw_to_forehead,
|
|
|
jaw_to_face,
|
|
|
forehead_to_face,
|
|
|
face_area,
|
|
|
jaw_angle
|
|
|
])
|
|
|
|
|
|
return features
|
|
|
|
|
|
def get_face_shape_label(label):
|
|
|
shapes = ["Heart", "Oval", "Round", "Square", "Oblong"]
|
|
|
return shapes[label]
|
|
|
|
|
|
def draw_landmarks_on_image(rgb_image, detection_result):
|
|
|
face_landmarks_list = detection_result.face_landmarks
|
|
|
annotated_image = np.copy(rgb_image)
|
|
|
|
|
|
for idx in range(len(face_landmarks_list)):
|
|
|
face_landmarks = face_landmarks_list[idx]
|
|
|
|
|
|
|
|
|
face_landmarks_proto = landmark_pb2.NormalizedLandmarkList()
|
|
|
face_landmarks_proto.landmark.extend([
|
|
|
landmark_pb2.NormalizedLandmark(x=landmark.x, y=landmark.y, z=landmark.z) for landmark in face_landmarks
|
|
|
])
|
|
|
|
|
|
|
|
|
mp_drawing.draw_landmarks(
|
|
|
image=annotated_image,
|
|
|
landmark_list=face_landmarks_proto,
|
|
|
connections=mp.solutions.face_mesh.FACEMESH_TESSELATION,
|
|
|
landmark_drawing_spec=None,
|
|
|
connection_drawing_spec=mp_drawing_styles.get_default_face_mesh_tesselation_style())
|
|
|
mp_drawing.draw_landmarks(
|
|
|
image=annotated_image,
|
|
|
landmark_list=face_landmarks_proto,
|
|
|
connections=mp.solutions.face_mesh.FACEMESH_CONTOURS,
|
|
|
landmark_drawing_spec=None,
|
|
|
connection_drawing_spec=mp_drawing_styles.get_default_face_mesh_contours_style())
|
|
|
mp_drawing.draw_landmarks(
|
|
|
image=annotated_image,
|
|
|
landmark_list=face_landmarks_proto,
|
|
|
connections=mp.solutions.face_mesh.FACEMESH_IRISES,
|
|
|
landmark_drawing_spec=None,
|
|
|
connection_drawing_spec=mp_drawing_styles.get_default_face_mesh_iris_connections_style())
|
|
|
|
|
|
return annotated_image
|
|
|
|
|
|
def allowed_file(filename):
|
|
|
return '.' in filename and filename.rsplit('.', 1)[1].lower() in app.config['ALLOWED_EXTENSIONS']
|
|
|
|
|
|
def generate_frames():
|
|
|
cap = cv2.VideoCapture(0)
|
|
|
while True:
|
|
|
ret, frame = cap.read()
|
|
|
if not ret:
|
|
|
break
|
|
|
rgb_frame = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
|
|
|
image = mp.Image(image_format=mp.ImageFormat.SRGB, data=rgb_frame)
|
|
|
detection_result = face_landmarker.detect(image)
|
|
|
|
|
|
if detection_result.face_landmarks:
|
|
|
for face_landmarks in detection_result.face_landmarks:
|
|
|
landmarks = [[lm.x, lm.y, lm.z] for lm in face_landmarks]
|
|
|
landmarks = np.array(landmarks)
|
|
|
face_features = extract_optimized_features(landmarks)
|
|
|
|
|
|
|
|
|
face_features_scaled = scaler.transform(face_features.reshape(1, -1))
|
|
|
|
|
|
face_shape_label = face_shape_model.predict(face_features_scaled)[0]
|
|
|
face_shape = get_face_shape_label(face_shape_label)
|
|
|
annotated_image = draw_landmarks_on_image(rgb_frame, detection_result)
|
|
|
cv2.putText(annotated_image, f"Face Shape: {face_shape}", (50, 50), cv2.FONT_HERSHEY_SIMPLEX, 1, (0, 255, 0), 2)
|
|
|
else:
|
|
|
annotated_image = rgb_frame
|
|
|
|
|
|
ret, buffer = cv2.imencode('.jpg', annotated_image)
|
|
|
frame = buffer.tobytes()
|
|
|
yield (b'--frame\r\n'
|
|
|
b'Content-Type: image/jpeg\r\n\r\n' + frame + b'\r\n')
|
|
|
|
|
|
@app.route('/')
|
|
|
def index():
|
|
|
return render_template('index.html')
|
|
|
|
|
|
@app.route('/analyze', methods=['POST'])
|
|
|
def analyze_face():
|
|
|
if 'file' not in request.files:
|
|
|
return jsonify({"error": "No file part"}), 400
|
|
|
|
|
|
file = request.files['file']
|
|
|
if file.filename == '':
|
|
|
return jsonify({"error": "No selected file"}), 400
|
|
|
|
|
|
try:
|
|
|
|
|
|
img_bytes = file.read()
|
|
|
nparr = np.frombuffer(img_bytes, np.uint8)
|
|
|
img = cv2.imdecode(nparr, cv2.IMREAD_COLOR)
|
|
|
|
|
|
|
|
|
processed_img = smart_preprocess_image(img)
|
|
|
|
|
|
|
|
|
rgb_image = cv2.cvtColor(processed_img, cv2.COLOR_BGR2RGB)
|
|
|
mp_image = mp.Image(image_format=mp.ImageFormat.SRGB, data=rgb_image)
|
|
|
|
|
|
detection_result = face_landmarker.detect(mp_image)
|
|
|
|
|
|
if not detection_result.face_landmarks:
|
|
|
return jsonify({"error": "No face detected"}), 400
|
|
|
|
|
|
|
|
|
face_landmarks = detection_result.face_landmarks[0]
|
|
|
|
|
|
|
|
|
landmarks_normalized = np.array([[lm.x, lm.y, lm.z] for lm in face_landmarks])
|
|
|
face_features = extract_optimized_features(landmarks_normalized)
|
|
|
|
|
|
|
|
|
face_features_scaled = scaler.transform(face_features.reshape(1, -1))
|
|
|
|
|
|
|
|
|
face_shape_label = face_shape_model.predict(face_features_scaled)[0]
|
|
|
face_shape = get_face_shape_label(face_shape_label)
|
|
|
|
|
|
|
|
|
confidence_scores = face_shape_model.predict_proba(face_features_scaled)[0]
|
|
|
confidence = confidence_scores[face_shape_label]
|
|
|
|
|
|
|
|
|
annotated_image_rgb = draw_landmarks_on_image(rgb_image, detection_result)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
annotated_image_bgr = cv2.cvtColor(annotated_image_rgb, cv2.COLOR_RGB2BGR)
|
|
|
_, buffer = cv2.imencode('.jpg', annotated_image_bgr)
|
|
|
processed_image_url = upload_image_to_cloudinary(buffer.tobytes())
|
|
|
|
|
|
if not processed_image_url:
|
|
|
return jsonify({"error": "Failed to upload processed image"}), 500
|
|
|
|
|
|
|
|
|
landmarks_normalized = np.array([[lm.x, lm.y, lm.z] for lm in face_landmarks])
|
|
|
|
|
|
|
|
|
p_iris_l = landmarks_normalized[473]
|
|
|
p_iris_r = landmarks_normalized[468]
|
|
|
|
|
|
p_forehead_top = landmarks_normalized[10]
|
|
|
p_chin_tip = landmarks_normalized[152]
|
|
|
|
|
|
p_cheek_l = landmarks_normalized[234]
|
|
|
p_cheek_r = landmarks_normalized[454]
|
|
|
|
|
|
p_jaw_l = landmarks_normalized[172]
|
|
|
p_jaw_r = landmarks_normalized[397]
|
|
|
|
|
|
p_forehead_l = landmarks_normalized[63]
|
|
|
p_forehead_r = landmarks_normalized[293]
|
|
|
|
|
|
|
|
|
AVG_IPD_CM = 6.3
|
|
|
dist_iris = distance_3d(p_iris_l, p_iris_r)
|
|
|
cm_per_unit = AVG_IPD_CM / dist_iris if dist_iris != 0 else 0
|
|
|
|
|
|
|
|
|
dist_face_length = distance_3d(p_forehead_top, p_chin_tip)
|
|
|
dist_cheek_width = distance_3d(p_cheek_l, p_cheek_r)
|
|
|
dist_jaw_width = distance_3d(p_jaw_l, p_jaw_r)
|
|
|
dist_forehead_width = distance_3d(p_forehead_l, p_forehead_r)
|
|
|
|
|
|
|
|
|
face_length_cm = (dist_face_length * cm_per_unit) + 5.0
|
|
|
cheekbone_width_cm = (dist_cheek_width * cm_per_unit) + 4.0
|
|
|
jaw_width_cm = (dist_jaw_width * cm_per_unit) +0.5
|
|
|
forehead_width_cm = (dist_forehead_width * cm_per_unit) + 6.0
|
|
|
|
|
|
|
|
|
jaw_curve_ratio = dist_face_length / dist_cheek_width if dist_cheek_width != 0 else 0
|
|
|
|
|
|
measurements = {
|
|
|
"face_length_cm": float(face_length_cm),
|
|
|
"cheekbone_width_cm": float(cheekbone_width_cm),
|
|
|
"jaw_width_cm": float(jaw_width_cm),
|
|
|
"forehead_width_cm": float(forehead_width_cm),
|
|
|
"jaw_curve_ratio": float(jaw_curve_ratio)
|
|
|
}
|
|
|
|
|
|
|
|
|
analysis_id = save_analysis_to_db(processed_image_url, face_shape, measurements)
|
|
|
if not analysis_id:
|
|
|
return jsonify({"error": "Failed to save analysis"}), 500
|
|
|
|
|
|
|
|
|
return jsonify({
|
|
|
"message": "Analysis successful",
|
|
|
"analysis_id": analysis_id,
|
|
|
"image_url": processed_image_url,
|
|
|
"face_shape": face_shape,
|
|
|
"confidence": float(confidence),
|
|
|
"all_probabilities": {
|
|
|
"Heart": float(confidence_scores[0]),
|
|
|
"Oval": float(confidence_scores[1]),
|
|
|
"Round": float(confidence_scores[2]),
|
|
|
"Square": float(confidence_scores[3]),
|
|
|
"Oblong": float(confidence_scores[4])
|
|
|
},
|
|
|
"measurements": measurements,
|
|
|
"calibration_applied": {
|
|
|
"face_length_adjustment": "+4.0cm (increased from +2.0cm)",
|
|
|
"forehead_width_adjustment": "+5.0cm (increased from +3.5cm)",
|
|
|
"cheekbone_width_adjustment": "+3.0cm (increased from +2.0cm)",
|
|
|
"jaw_width_adjustment": "none (already accurate)",
|
|
|
"note": "Calibration adjustments increased based on user feedback"
|
|
|
}
|
|
|
})
|
|
|
|
|
|
except Exception as e:
|
|
|
print(f"An error occurred: {e}")
|
|
|
return jsonify({"error": f"An error occurred: {str(e)}"}), 500
|
|
|
|
|
|
@app.route('/video_feed')
|
|
|
def video_feed():
|
|
|
return Response(generate_frames(), mimetype='multipart/x-mixed-replace; boundary=frame')
|
|
|
|
|
|
@app.route('/real_time')
|
|
|
def real_time():
|
|
|
return render_template('real_time.html')
|
|
|
|
|
|
if __name__ == '__main__':
|
|
|
app.run(debug=True, port=5000)
|
|
|
|