Spaces:
Runtime error
Runtime error
| import cv2 | |
| import numpy as np | |
| import mediapipe as mp | |
| import torch | |
| from flask import Flask, request, jsonify | |
| import torch.nn.functional as F | |
| from flask_cors import CORS | |
| import os | |
| import shutil | |
| # --- Monkey Patch: Override model path before using mediapipe.pose --- | |
| from mediapipe.python.solutions import pose as mp_pose_module | |
| CUSTOM_MODEL_PATH = "/app/models/pose_landmark_heavy.tflite" | |
| def monkey_patch_pose_path(model_complexity): | |
| model_complexity_map = { | |
| 0: "pose_landmark_lite", | |
| 1: "pose_landmark_full", | |
| 2: "pose_landmark_heavy" | |
| } | |
| if model_complexity in model_complexity_map: | |
| file_name = model_complexity_map[model_complexity] + ".tflite" | |
| mp_pose_module.MODEL_PATHS[model_complexity] = CUSTOM_MODEL_PATH | |
| else: | |
| raise ValueError(f"Unsupported model complexity: {model_complexity}") | |
| # Monkey patch before creating Pose instance | |
| monkey_patch_pose_path(model_complexity=2) | |
| app = Flask(__name__) | |
| CORS(app) | |
| mp_pose = mp.solutions.pose | |
| mp_holistic = mp.solutions.holistic | |
| pose = mp_pose.Pose( | |
| model_complexity=2, | |
| static_image_mode=True, | |
| min_detection_confidence=0.5, | |
| min_tracking_confidence=0.5 | |
| ) # Improved accuracy | |
| holistic = mp_holistic.Holistic() # For refining pose | |
| KNOWN_OBJECT_WIDTH_CM = 21.0 # A4 paper width in cm | |
| FOCAL_LENGTH = 600 # Default focal length | |
| DEFAULT_HEIGHT_CM = 152.0 # Default height if not provided | |
| # Load depth estimation model | |
| def load_depth_model(): | |
| model = torch.hub.load("intel-isl/MiDaS", "MiDaS_small") | |
| model.eval() | |
| return model | |
| depth_model = load_depth_model() | |
| def calibrate_focal_length(image, real_width_cm, detected_width_px): | |
| """Dynamically calibrates focal length using a known object.""" | |
| return (detected_width_px * FOCAL_LENGTH) / real_width_cm if detected_width_px else FOCAL_LENGTH | |
| def detect_reference_object(image): | |
| gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY) | |
| edges = cv2.Canny(gray, 50, 150) | |
| contours, _ = cv2.findContours(edges, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) | |
| if contours: | |
| largest_contour = max(contours, key=cv2.contourArea) | |
| x, y, w, h = cv2.boundingRect(largest_contour) | |
| focal_length = calibrate_focal_length(image, KNOWN_OBJECT_WIDTH_CM, w) | |
| scale_factor = KNOWN_OBJECT_WIDTH_CM / w | |
| return scale_factor, focal_length | |
| return 0.05, FOCAL_LENGTH | |
| def estimate_depth(image): | |
| """Uses AI-based depth estimation to improve circumference calculations.""" | |
| input_image = cv2.cvtColor(image, cv2.COLOR_BGR2RGB) / 255.0 | |
| input_tensor = torch.tensor(input_image, dtype=torch.float32).permute(2, 0, 1).unsqueeze(0) | |
| # Resize input to match MiDaS model input size | |
| input_tensor = F.interpolate(input_tensor, size=(384, 384), mode="bilinear", align_corners=False) | |
| with torch.no_grad(): | |
| depth_map = depth_model(input_tensor) | |
| return depth_map.squeeze().numpy() | |
| def calculate_distance_using_height(landmarks, image_height, user_height_cm): | |
| """Calculate distance using the user's known height.""" | |
| top_head = landmarks[mp_pose.PoseLandmark.NOSE.value].y * image_height | |
| bottom_foot = max( | |
| landmarks[mp_pose.PoseLandmark.LEFT_ANKLE.value].y, | |
| landmarks[mp_pose.PoseLandmark.RIGHT_ANKLE.value].y | |
| ) * image_height | |
| person_height_px = abs(bottom_foot - top_head) | |
| # Using the formula: distance = (actual_height_cm * focal_length) / height_in_pixels | |
| distance = (user_height_cm * FOCAL_LENGTH) / person_height_px | |
| # Calculate more accurate scale_factor based on known height | |
| scale_factor = user_height_cm / person_height_px | |
| return distance, scale_factor | |
| def get_body_width_at_height(frame, height_px, center_x): | |
| """Scan horizontally at a specific height to find body edges.""" | |
| # Convert to grayscale and apply threshold | |
| gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY) | |
| blur = cv2.GaussianBlur(gray, (5, 5), 0) | |
| _, thresh = cv2.threshold(blur, 50, 255, cv2.THRESH_BINARY) | |
| # Ensure height_px is within image bounds | |
| if height_px >= frame.shape[0]: | |
| height_px = frame.shape[0] - 1 | |
| # Get horizontal line at the specified height | |
| horizontal_line = thresh[height_px, :] | |
| # Find left and right edges starting from center | |
| center_x = int(center_x * frame.shape[1]) | |
| left_edge, right_edge = center_x, center_x | |
| # Scan from center to left | |
| for i in range(center_x, 0, -1): | |
| if horizontal_line[i] == 0: # Found edge (black pixel) | |
| left_edge = i | |
| break | |
| # Scan from center to right | |
| for i in range(center_x, len(horizontal_line)): | |
| if horizontal_line[i] == 0: # Found edge (black pixel) | |
| right_edge = i | |
| break | |
| width_px = right_edge - left_edge | |
| # If width is unreasonably small, apply a minimum width | |
| min_width = 0.1 * frame.shape[1] # Minimum width as 10% of image width | |
| if width_px < min_width: | |
| width_px = min_width | |
| return width_px | |
| def calculate_measurements(results, scale_factor, image_width, image_height, depth_map, frame=None, user_height_cm=None): | |
| landmarks = results.pose_landmarks.landmark | |
| # If user's height is provided, use it to get a more accurate scale factor | |
| if user_height_cm: | |
| _, scale_factor = calculate_distance_using_height(landmarks, image_height, user_height_cm) | |
| def pixel_to_cm(value): | |
| return round(value * scale_factor, 2) | |
| def calculate_circumference(width_px, depth_ratio=1.0): | |
| """Estimate circumference using width and depth adjustment.""" | |
| # Using a simplified elliptical approximation: C ≈ 2π * sqrt((a² + b²)/2) | |
| # where a is half the width and b is estimated depth | |
| width_cm = width_px * scale_factor | |
| estimated_depth_cm = width_cm * depth_ratio * 0.7 # Depth is typically ~70% of width for torso | |
| half_width = width_cm / 2 | |
| half_depth = estimated_depth_cm / 2 | |
| return round(2 * np.pi * np.sqrt((half_width**2 + half_depth**2) / 2), 2) | |
| measurements = {} | |
| # Shoulder Width | |
| left_shoulder = landmarks[mp_pose.PoseLandmark.LEFT_SHOULDER.value] | |
| right_shoulder = landmarks[mp_pose.PoseLandmark.RIGHT_SHOULDER.value] | |
| shoulder_width_px = abs(left_shoulder.x * image_width - right_shoulder.x * image_width) | |
| # Apply a slight correction factor for shoulders (they're usually detected well) | |
| shoulder_correction = 1.1 # 10% wider | |
| shoulder_width_px *= shoulder_correction | |
| measurements["shoulder_width"] = pixel_to_cm(shoulder_width_px) | |
| # Chest/Bust Measurement | |
| chest_y_ratio = 0.15 # Approximately 15% down from shoulder to hip | |
| chest_y = left_shoulder.y + (landmarks[mp_pose.PoseLandmark.LEFT_HIP.value].y - left_shoulder.y) * chest_y_ratio | |
| chest_correction = 1.15 # 15% wider than detected width | |
| chest_width_px = abs((right_shoulder.x - left_shoulder.x) * image_width) * chest_correction | |
| if frame is not None: | |
| chest_y_px = int(chest_y * image_height) | |
| center_x = (left_shoulder.x + right_shoulder.x) / 2 | |
| detected_width = get_body_width_at_height(frame, chest_y_px, center_x) | |
| if detected_width > 0: | |
| chest_width_px = max(chest_width_px, detected_width) | |
| chest_depth_ratio = 1.0 | |
| if depth_map is not None: | |
| chest_x = int(((left_shoulder.x + right_shoulder.x) / 2) * image_width) | |
| chest_y_px = int(chest_y * image_height) | |
| scale_y = 384 / image_height | |
| scale_x = 384 / image_width | |
| chest_y_scaled = int(chest_y_px * scale_y) | |
| chest_x_scaled = int(chest_x * scale_x) | |
| if 0 <= chest_y_scaled < 384 and 0 <= chest_x_scaled < 384: | |
| chest_depth = depth_map[chest_y_scaled, chest_x_scaled] | |
| max_depth = np.max(depth_map) | |
| chest_depth_ratio = 1.0 + 0.5 * (1.0 - chest_depth / max_depth) | |
| measurements["chest_width"] = pixel_to_cm(chest_width_px) | |
| measurements["chest_circumference"] = calculate_circumference(chest_width_px, chest_depth_ratio) | |
| # Waist Measurement | |
| left_hip = landmarks[mp_pose.PoseLandmark.LEFT_HIP.value] | |
| right_hip = landmarks[mp_pose.PoseLandmark.RIGHT_HIP.value] | |
| # Adjust waist_y_ratio to better reflect the natural waistline | |
| waist_y_ratio = 0.35 # 35% down from shoulder to hip (higher than before) | |
| waist_y = left_shoulder.y + (left_hip.y - left_shoulder.y) * waist_y_ratio | |
| # Use contour detection to dynamically estimate waist width | |
| if frame is not None: | |
| waist_y_px = int(waist_y * image_height) | |
| center_x = (left_hip.x + right_hip.x) / 2 | |
| detected_width = get_body_width_at_height(frame, waist_y_px, center_x) | |
| if detected_width > 0: | |
| waist_width_px = detected_width | |
| else: | |
| # Fallback to hip width if contour detection fails | |
| waist_width_px = abs(right_hip.x - left_hip.x) * image_width * 0.9 # 90% of hip width | |
| else: | |
| # Fallback to hip width if no frame is provided | |
| waist_width_px = abs(right_hip.x - left_hip.x) * image_width * 0.9 # 90% of hip width | |
| # Apply 30% correction factor to waist width | |
| waist_correction = 1.16 # 30% wider | |
| waist_width_px *= waist_correction | |
| # Get depth adjustment for waist if available | |
| waist_depth_ratio = 1.0 | |
| if depth_map is not None: | |
| waist_x = int(((left_hip.x + right_hip.x) / 2) * image_width) | |
| waist_y_px = int(waist_y * image_height) | |
| scale_y = 384 / image_height | |
| scale_x = 384 / image_width | |
| waist_y_scaled = int(waist_y_px * scale_y) | |
| waist_x_scaled = int(waist_x * scale_x) | |
| if 0 <= waist_y_scaled < 384 and 0 <= waist_x_scaled < 384: | |
| waist_depth = depth_map[waist_y_scaled, waist_x_scaled] | |
| max_depth = np.max(depth_map) | |
| waist_depth_ratio = 1.0 + 0.5 * (1.0 - waist_depth / max_depth) | |
| measurements["waist_width"] = pixel_to_cm(waist_width_px) | |
| measurements["waist"] = calculate_circumference(waist_width_px, waist_depth_ratio) | |
| # Hip Measurement | |
| hip_correction = 1.35 # Hips are typically 35% wider than detected landmarks | |
| hip_width_px = abs(left_hip.x * image_width - right_hip.x * image_width) * hip_correction | |
| if frame is not None: | |
| hip_y_offset = 0.1 # 10% down from hip landmarks | |
| hip_y = left_hip.y + (landmarks[mp_pose.PoseLandmark.LEFT_KNEE.value].y - left_hip.y) * hip_y_offset | |
| hip_y_px = int(hip_y * image_height) | |
| center_x = (left_hip.x + right_hip.x) / 2 | |
| detected_width = get_body_width_at_height(frame, hip_y_px, center_x) | |
| if detected_width > 0: | |
| hip_width_px = max(hip_width_px, detected_width) | |
| hip_depth_ratio = 1.0 | |
| if depth_map is not None: | |
| hip_x = int(((left_hip.x + right_hip.x) / 2) * image_width) | |
| hip_y_px = int(left_hip.y * image_height) | |
| hip_y_scaled = int(hip_y_px * scale_y) | |
| hip_x_scaled = int(hip_x * scale_x) | |
| if 0 <= hip_y_scaled < 384 and 0 <= hip_x_scaled < 384: | |
| hip_depth = depth_map[hip_y_scaled, hip_x_scaled] | |
| max_depth = np.max(depth_map) | |
| hip_depth_ratio = 1.0 + 0.5 * (1.0 - hip_depth / max_depth) | |
| measurements["hip_width"] = pixel_to_cm(hip_width_px) | |
| measurements["hip"] = calculate_circumference(hip_width_px, hip_depth_ratio) | |
| # Other measurements (unchanged) | |
| neck = landmarks[mp_pose.PoseLandmark.NOSE.value] | |
| left_ear = landmarks[mp_pose.PoseLandmark.LEFT_EAR.value] | |
| neck_width_px = abs(neck.x * image_width - left_ear.x * image_width) * 2.0 | |
| measurements["neck"] = calculate_circumference(neck_width_px, 1.0) | |
| measurements["neck_width"] = pixel_to_cm(neck_width_px) | |
| left_wrist = landmarks[mp_pose.PoseLandmark.LEFT_WRIST.value] | |
| sleeve_length_px = abs(left_shoulder.y * image_height - left_wrist.y * image_height) | |
| measurements["arm_length"] = pixel_to_cm(sleeve_length_px) | |
| shirt_length_px = abs(left_shoulder.y * image_height - left_hip.y * image_height) * 1.2 | |
| measurements["shirt_length"] = pixel_to_cm(shirt_length_px) | |
| # Thigh Circumference (improved with depth information) | |
| thigh_y_ratio = 0.2 # 20% down from hip to knee | |
| left_knee = landmarks[mp_pose.PoseLandmark.LEFT_KNEE.value] | |
| thigh_y = left_hip.y + (left_knee.y - left_hip.y) * thigh_y_ratio | |
| # Apply correction factor for thigh width | |
| thigh_correction = 1.2 # Thighs are typically wider than what can be estimated from front view | |
| thigh_width_px = hip_width_px * 0.5 * thigh_correction # Base thigh width on hip width | |
| # Use contour detection if frame is available | |
| if frame is not None: | |
| thigh_y_px = int(thigh_y * image_height) | |
| thigh_x = left_hip.x * 0.9 # Move slightly inward from hip | |
| detected_width = get_body_width_at_height(frame, thigh_y_px, thigh_x) | |
| if detected_width > 0 and detected_width < hip_width_px: # Sanity check | |
| thigh_width_px = detected_width # Use detected width | |
| # If depth map is available, use it for thigh measurement | |
| thigh_depth_ratio = 1.0 | |
| if depth_map is not None: | |
| thigh_x = int(left_hip.x * image_width) | |
| thigh_y_px = int(thigh_y * image_height) | |
| # Scale coordinates to match depth map size | |
| thigh_y_scaled = int(thigh_y_px * scale_y) | |
| thigh_x_scaled = int(thigh_x * scale_x) | |
| if 0 <= thigh_y_scaled < 384 and 0 <= thigh_x_scaled < 384: | |
| thigh_depth = depth_map[thigh_y_scaled, thigh_x_scaled] | |
| max_depth = np.max(depth_map) | |
| thigh_depth_ratio = 1.0 + 0.5 * (1.0 - thigh_depth / max_depth) | |
| measurements["thigh"] = pixel_to_cm(thigh_width_px) | |
| measurements["thigh_circumference"] = calculate_circumference(thigh_width_px, thigh_depth_ratio) | |
| left_ankle = landmarks[mp_pose.PoseLandmark.LEFT_ANKLE.value] | |
| trouser_length_px = abs(left_hip.y * image_height - left_ankle.y * image_height) | |
| measurements["trouser_length"] = pixel_to_cm(trouser_length_px) | |
| return measurements | |
| def upload_images(): | |
| if "front" not in request.files: | |
| return jsonify({"error": "Missing front image for reference."}), 400 | |
| # Get user height if provided, otherwise use default | |
| user_height_cm = request.form.get('height_cm') | |
| print(user_height_cm) | |
| if user_height_cm: | |
| try: | |
| user_height_cm = float(user_height_cm) | |
| except ValueError: | |
| user_height_cm = DEFAULT_HEIGHT_CM | |
| else: | |
| user_height_cm = DEFAULT_HEIGHT_CM | |
| received_images = {pose_name: request.files[pose_name] for pose_name in ["front", "left_side", "right_side", "back"] if pose_name in request.files} | |
| measurements, scale_factor, focal_length, results = {}, None, FOCAL_LENGTH, {} | |
| frames = {} | |
| for pose_name, image_file in received_images.items(): | |
| image_np = np.frombuffer(image_file.read(), np.uint8) | |
| frame = cv2.imdecode(image_np, cv2.IMREAD_COLOR) | |
| frames[pose_name] = frame # Store the frame for contour detection | |
| rgb_frame = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB) | |
| results[pose_name] = holistic.process(rgb_frame) | |
| image_height, image_width, _ = frame.shape | |
| if pose_name == "front": | |
| # Always use height for calibration (default or provided) | |
| if results[pose_name].pose_landmarks: | |
| _, scale_factor = calculate_distance_using_height( | |
| results[pose_name].pose_landmarks.landmark, | |
| image_height, | |
| user_height_cm | |
| ) | |
| else: | |
| # Fallback to object detection only if pose landmarks aren't detected | |
| scale_factor, focal_length = detect_reference_object(frame) | |
| depth_map = estimate_depth(frame) if pose_name in ["front", "left_side"] else None | |
| if results[pose_name].pose_landmarks: | |
| if pose_name == "front": | |
| measurements.update(calculate_measurements( | |
| results[pose_name], | |
| scale_factor, | |
| image_width, | |
| image_height, | |
| depth_map, | |
| frames[pose_name], # Pass the frame for contour detection | |
| user_height_cm | |
| )) | |
| # Debug information to help troubleshoot measurements | |
| debug_info = { | |
| "scale_factor": float(scale_factor) if scale_factor else None, | |
| "focal_length": float(focal_length), | |
| "user_height_cm": float(user_height_cm) | |
| } | |
| return jsonify({ | |
| "measurements": measurements, | |
| "debug_info": debug_info | |
| }) | |
| if __name__ == '__main__': | |
| app.run(host='0.0.0.0', port=8000) |