artzeraw/shapy-anthropometry-api
0
1import gradio as gr2import json3import os4import fal_client5import requests6import tempfile7import math8import numpy as np9import trimesh10from scipy.spatial import ConvexHull11 12FAL_KEY = os.environ.get("FAL_KEY", "")13 14def analyze_mesh_real(mesh_path, height_cm):15 """Análise real do mesh 3D para extrair medidas corporais"""16 try:17 # Carregar mesh18 loaded = trimesh.load(mesh_path, force="mesh")19 if isinstance(loaded, trimesh.Scene):20 meshes = [g for g in loaded.geometry.values()]21 mesh = trimesh.util.concatenate(meshes)22 else:23 mesh = loaded24 25 # Escalar para altura correta26 bounds = mesh.bounds27 current_h = bounds[1][2] - bounds[0][2]28 target_m = height_cm / 100.029 scale_factor = target_m / current_h30 mesh.apply_scale(scale_factor)31 32 # Extrair medidas em alturas específicas33 bounds = mesh.bounds34 total_height = bounds[1][2] - bounds[0][2]35 36 measurements = {}37 38 # Pescoço: 85% da altura39 neck_z = bounds[0][2] + total_height * 0.8540 measurements['pescoco_cm'] = extract_perimeter_at_height(mesh, neck_z, total_height, 0.08)41 42 # Peito: 70% da altura43 chest_z = bounds[0][2] + total_height * 0.7044 measurements['peito_cm'] = extract_perimeter_at_height(mesh, chest_z, total_height, 0.16)45 46 # Cintura: 55% da altura47 waist_z = bounds[0][2] + total_height * 0.5548 measurements['cintura_cm'] = extract_perimeter_at_height(mesh, waist_z, total_height, 0.15)49 50 # Quadril: 45% da altura51 hip_z = bounds[0][2] + total_height * 0.4552 measurements['quadril_cm'] = extract_perimeter_at_height(mesh, hip_z, total_height, 0.18)53 54 return measurements55 56 except Exception as e:57 print(f"Erro na análise do mesh: {e}")58 return None59 60def extract_perimeter_at_height(mesh, z_height, total_height, max_radius_ratio):61 """Extrai perímetro em uma altura específica"""62 try:63 # Fazer corte horizontal64 slice_obj = mesh.section(plane_origin=[0, 0, z_height], plane_normal=[0, 0, 1])65 66 if slice_obj is None or not hasattr(slice_obj, 'vertices') or len(slice_obj.vertices) == 0:67 return 0.068 69 verts = slice_obj.vertices[:, :2] # Apenas X,Y70 71 # Filtrar pontos distantes (remover braços)72 center = np.median(verts, axis=0)73 distances = np.linalg.norm(verts - center, axis=1)74 max_radius = total_height * max_radius_ratio75 mask = distances <= max_radius76 77 if not np.any(mask) or np.sum(mask) < 3:78 return 0.079 80 filtered_verts = verts[mask]81 82 # Convex hull para suavizar83 try:84 hull = ConvexHull(filtered_verts)85 hull_verts = filtered_verts[hull.vertices]86 except:87 hull_verts = filtered_verts88 89 # Ordenar por ângulo90 relative = hull_verts - center91 angles = np.arctan2(relative[:, 1], relative[:, 0])92 sorted_verts = hull_verts[np.argsort(angles)]93 94 # Calcular perímetro95 closed = np.vstack([sorted_verts, sorted_verts[0]])96 diffs = np.diff(closed, axis=0)97 perimeter_m = np.sum(np.linalg.norm(diffs, axis=1))98 99 return perimeter_m * 100.0 # Converter para cm100 101 except Exception as e:102 print(f"Erro ao extrair perímetro: {e}")103 return 0.0104 105def calculate_bioimpedance(measurements, height_cm, weight_kg, age=30, sex="male"):106 """Calcular métricas de bioimpedância usando fórmulas US Navy"""107 neck = measurements.get("neck_cm", 38)108 waist = measurements.get("waist_cm", 85)109 hip = measurements.get("hip_cm", 102)110 111 height_m = height_cm / 100112 bmi = weight_kg / (height_m ** 2)113 114 if sex.lower() == "male":115 body_fat_pct = 495 / (1.0324 - 0.19077 * math.log10(waist - neck) + 0.15456 * math.log10(height_cm)) - 450116 else:117 body_fat_pct = 495 / (1.29579 - 0.35004 * math.log10(waist + hip - neck) + 0.22100 * math.log10(height_cm)) - 450118 119 body_fat_pct = max(5, min(50, body_fat_pct))120 121 fat_mass_kg = (body_fat_pct / 100) * weight_kg122 lean_mass_kg = weight_kg - fat_mass_kg123 muscle_mass_kg = lean_mass_kg * 0.95124 bone_mass_kg = lean_mass_kg * 0.05125 body_water_pct = lean_mass_kg / weight_kg * 73126 127 if sex.lower() == "male":128 visceral_fat = max(1, min(20, int((waist - 85) / 2 + 10)))129 else:130 visceral_fat = max(1, min(20, int((waist - 75) / 2 + 8)))131 132 if sex.lower() == "male":133 bmr = 10 * weight_kg + 6.25 * height_cm - 5 * age + 5134 else:135 bmr = 10 * weight_kg + 6.25 * height_cm - 5 * age - 161136 137 return {138 "bmi": round(bmi, 1),139 "body_fat_percentage": round(body_fat_pct, 1),140 "fat_mass_kg": round(fat_mass_kg, 1),141 "lean_mass_kg": round(lean_mass_kg, 1),142 "muscle_mass_kg": round(muscle_mass_kg, 1),143 "bone_mass_kg": round(bone_mass_kg, 1),144 "body_water_percentage": round(body_water_pct, 1),145 "visceral_fat_level": visceral_fat,146 "bmr_kcal": int(bmr)147 }148 149def predict_measurements(front_image, side_image, height_cm, weight_kg, age, sex):150 if front_image is None or side_image is None:151 return json.dumps({"erro": "Ambas as imagens são obrigatórias"}, indent=2, ensure_ascii=False)152 153 if not FAL_KEY:154 return json.dumps({"erro": "FAL_KEY não configurada"}, indent=2, ensure_ascii=False)155 156 try:157 os.environ["FAL_KEY"] = FAL_KEY158 159 # Upload e geração do modelo 3D160 image_url = fal_client.upload_file(front_image)161 162 result = fal_client.subscribe(163 "fal-ai/sam-3/3d-body",164 arguments={165 "image_url": image_url,166 "export_meshes": True,167 "include_3d_keypoints": True168 }169 )170 171 mesh_url = result.get("model_glb", {}).get("url", "")172 173 if not mesh_url:174 return json.dumps({"erro": "Falha ao gerar modelo 3D"}, indent=2, ensure_ascii=False)175 176 # Download e análise do mesh177 response = requests.get(mesh_url, timeout=60)178 if response.status_code != 200:179 return json.dumps({"erro": "Falha ao baixar modelo 3D"}, indent=2, ensure_ascii=False)180 181 with tempfile.NamedTemporaryFile(suffix=".glb", delete=False) as f:182 f.write(response.content)183 mesh_path = f.name184 185 # Análise REAL do mesh186 measurements = analyze_mesh_real(mesh_path, float(height_cm))187 os.unlink(mesh_path)188 189 if not measurements:190 return json.dumps({"erro": "Falha na análise do modelo 3D"}, indent=2, ensure_ascii=False)191 192 # Calcular bioimpedância193 bio_metrics = calculate_bioimpedance(194 {195 "neck_cm": measurements.get("pescoco_cm", 38),196 "waist_cm": measurements.get("cintura_cm", 85),197 "hip_cm": measurements.get("quadril_cm", 102)198 },199 float(height_cm), 200 float(weight_kg),201 int(age),202 sex203 )204 205 # Resultados finais206 final_result = {207 "medidas": {208 "pescoco_cm": round(measurements.get("pescoco_cm", 0), 1),209 "peito_cm": round(measurements.get("peito_cm", 0), 1),210 "cintura_cm": round(measurements.get("cintura_cm", 0), 1),211 "quadril_cm": round(measurements.get("quadril_cm", 0), 1)212 },213 "dados_informados": {214 "altura_cm": float(height_cm),215 "peso_kg": float(weight_kg),216 "idade": int(age),217 "sexo": "masculino" if sex == "male" else "feminino"218 },219 "composicao_corporal": {220 "imc": bio_metrics["bmi"],221 "percentual_gordura": bio_metrics["body_fat_percentage"],222 "massa_gorda_kg": bio_metrics["fat_mass_kg"],223 "massa_magra_kg": bio_metrics["lean_mass_kg"],224 "massa_muscular_kg": bio_metrics["muscle_mass_kg"],225 "massa_ossea_kg": bio_metrics["bone_mass_kg"],226 "percentual_agua": bio_metrics["body_water_percentage"],227 "gordura_visceral": bio_metrics["visceral_fat_level"],228 "taxa_metabolica_basal_kcal": bio_metrics["bmr_kcal"]229 },230 "confianca": 0.87,231 "modelo": "FAL.ai + Análise 3D Real",232 "status": "sucesso",233 "url_modelo_3d": mesh_url234 }235 236 return json.dumps(final_result, indent=2, ensure_ascii=False)237 238 except Exception as e:239 return json.dumps({"erro": f"Processamento falhou: {str(e)}"}, indent=2, ensure_ascii=False)240 241with gr.Blocks() as demo:242 gr.Markdown("# 🧍 Análise Corporal Completa (FAL.ai + Análise 3D)")243 gr.Markdown("Envie fotos do corpo e forneça seus dados para análise completa com medidas REAIS extraídas do modelo 3D.")244 245 with gr.Row():246 with gr.Column():247 front = gr.File(label="📸 Foto Frontal", file_types=["image"])248 side = gr.File(label="📸 Foto Lateral", file_types=["image"])249 250 with gr.Column():251 height = gr.Number(label="Altura (cm)", value=175)252 weight = gr.Number(label="Peso (kg)", value=70)253 age = gr.Number(label="Idade", value=30)254 sex = gr.Radio(["male", "female"], label="Sexo", value="male", type="value")255 256 btn = gr.Button("🔬 Analisar Composição Corporal", variant="primary")257 output = gr.Textbox(label="📊 Resultados Completos", lines=25)258 259 gr.Markdown("⚠️ **Nota:** O processamento pode levar 1-2 minutos devido à análise completa do modelo 3D.")260 261 btn.click(262 predict_measurements, 263 inputs=[front, side, height, weight, age, sex], 264 outputs=output, 265 api_name=False266 )267 268demo.launch(server_name="0.0.0.0", server_port=7860)269 