OctusTech/cartalogo-api
0
1import os2import torch3import open_clip4from PIL import Image, ImageEnhance, ImageFilter5import numpy as np6import cv27from typing import Dict, List, Tuple, Optional8import json9import colorsys10from sklearn.cluster import KMeans11import openai12from pathlib import Path13import base6414 15# ===== CONFIGURAÇÃO PARA HUGGING FACE SPACES =====16def setup_directories():17 """Configura diretórios com permissão de escrita para Hugging Face Spaces"""18 # Diretórios permitidos no Hugging Face Spaces19 temp_dir = Path(os.getenv('TEMP_PATH', '/tmp/temp'))20 thumbnails_dir = Path(os.getenv('THUMBNAILS_PATH', '/tmp/thumbnails'))21 data_dir = Path(os.getenv('DATA_PATH', '/tmp/data'))22 models_dir = Path(os.getenv('MODELS_PATH', '/tmp/models'))23 24 # Cria os diretórios se não existirem25 temp_dir.mkdir(parents=True, exist_ok=True)26 thumbnails_dir.mkdir(parents=True, exist_ok=True)27 data_dir.mkdir(parents=True, exist_ok=True)28 models_dir.mkdir(parents=True, exist_ok=True)29 30 print(f"✅ Diretórios configurados:")31 print(f" 📁 Temp: {temp_dir}")32 print(f" 📁 Thumbnails: {thumbnails_dir}")33 print(f" 📁 Data: {data_dir}")34 print(f" 📁 Models: {models_dir}")35 36 return {37 'temp': temp_dir,38 'thumbnails': thumbnails_dir,39 'data': data_dir,40 'models': models_dir41 }42 43# Configura diretórios globalmente44DIRS = setup_directories()45 46class JewelryImageProcessor:47 def __init__(self):48 """Inicializa o processador especializado em joias"""49 self.device = "cuda" if torch.cuda.is_available() else "cpu"50 51 # Carrega modelo OpenCLIP (melhor que CLIP padrão)52 model_name = os.getenv('EMBEDDING_MODEL', 'ViT-L-14')53 pretrained = os.getenv('EMBEDDING_PRETRAINED', 'laion2b_s32b_b82k')54 55 try:56 self.model, _, self.preprocess = open_clip.create_model_and_transforms(57 model_name, pretrained=pretrained, device=self.device58 )59 self.tokenizer = open_clip.get_tokenizer(model_name)60 print(f"🤖 Modelo {model_name} carregado no dispositivo: {self.device}")61 except Exception as e:62 print(f"❌ Erro ao carregar modelo OpenCLIP: {e}")63 # Fallback para modelo básico64 model_name = 'ViT-B-32'65 pretrained = 'openai'66 self.model, _, self.preprocess = open_clip.create_model_and_transforms(67 model_name, pretrained=pretrained, device=self.device68 )69 self.tokenizer = open_clip.get_tokenizer(model_name)70 print(f"🔄 Usando modelo fallback: {model_name}")71 72 # Configuração OpenAI para descrições73 openai.api_key = os.getenv('OPENAI_API_KEY')74 75 # Configuração de caminhos usando diretórios seguros76 self.temp_dir = DIRS['temp']77 self.thumbnails_dir = DIRS['thumbnails']78 self.data_dir = DIRS['data']79 80 def generate_embedding(self, image_path: str) -> Optional[np.ndarray]:81 """Gera embedding da imagem usando OpenCLIP"""82 try:83 image = Image.open(image_path).convert('RGB')84 image_input = self.preprocess(image).unsqueeze(0).to(self.device)85 86 with torch.no_grad():87 image_features = self.model.encode_image(image_input)88 # Normaliza o vetor89 image_features = image_features / image_features.norm(dim=-1, keepdim=True)90 91 return image_features.cpu().numpy().flatten()92 except Exception as e:93 print(f"❌ Erro ao gerar embedding para {image_path}: {e}")94 return None95 96 def generate_text_embedding(self, text: str) -> Optional[np.ndarray]:97 """Gera embedding de texto usando OpenCLIP"""98 try:99 text_tokens = self.tokenizer([text]).to(self.device)100 101 with torch.no_grad():102 text_features = self.model.encode_text(text_tokens)103 # Normaliza o vetor104 text_features = text_features / text_features.norm(dim=-1, keepdim=True)105 106 return text_features.cpu().numpy().flatten()107 except Exception as e:108 print(f"❌ Erro ao gerar embedding de texto para '{text}': {e}")109 return None110 111 def analyze_jewelry_technical(self, image_path: str) -> Dict:112 """Análise técnica especializada para joias"""113 try:114 # Carrega imagem115 image = cv2.imread(image_path)116 if image is None:117 print(f"❌ Não foi possível carregar a imagem: {image_path}")118 return {}119 120 image_rgb = cv2.cvtColor(image, cv2.COLOR_BGR2RGB)121 122 analysis = {123 "basic_metrics": self._analyze_basic_metrics(image_rgb),124 "color_analysis": self._analyze_jewelry_colors(image_rgb),125 "metal_analysis": self._analyze_metal_properties(image_rgb),126 "stone_analysis": self._analyze_stones(image_rgb),127 "shape_analysis": self._analyze_jewelry_shapes(image_rgb),128 "texture_analysis": self._analyze_textures(image_rgb),129 "reflection_analysis": self._analyze_reflections(image_rgb),130 "quality_assessment": self._assess_image_quality(image_rgb)131 }132 133 return analysis134 except Exception as e:135 print(f"❌ Erro na análise técnica de {image_path}: {e}")136 return {}137 138 def _analyze_basic_metrics(self, image: np.ndarray) -> Dict:139 """Métricas básicas da imagem"""140 try:141 height, width = image.shape[:2]142 143 # Brilho médio144 brightness = np.mean(image)145 146 # Contraste147 gray = cv2.cvtColor(image, cv2.COLOR_RGB2GRAY)148 contrast = np.std(gray)149 150 # Saturação151 hsv = cv2.cvtColor(image, cv2.COLOR_RGB2HSV)152 saturation = np.mean(hsv[:, :, 1])153 154 return {155 "dimensions": {"width": int(width), "height": int(height)},156 "brightness": float(brightness),157 "contrast": float(contrast),158 "saturation": float(saturation),159 "aspect_ratio": float(width / height)160 }161 except Exception as e:162 print(f"❌ Erro na análise básica: {e}")163 return {}164 165 def _analyze_jewelry_colors(self, image: np.ndarray) -> Dict:166 """Análise de cores específica para joias"""167 try:168 # Extrai cores dominantes169 dominant_colors = self._extract_dominant_colors(image, k=8)170 171 # Classifica cores por categoria de joia172 color_categories = self._classify_jewelry_colors(dominant_colors)173 174 # Analisa temperatura de cor175 color_temperature = self._analyze_color_temperature(image)176 177 return {178 "dominant_colors": dominant_colors,179 "color_categories": color_categories,180 "color_temperature": color_temperature,181 "color_harmony": self._analyze_color_harmony(dominant_colors)182 }183 except Exception as e:184 print(f"❌ Erro na análise de cores: {e}")185 return {}186 187 def _analyze_metal_properties(self, image: np.ndarray) -> Dict:188 """Análise específica para propriedades metálicas"""189 try:190 # Converte para HSV para melhor análise de metais191 hsv = cv2.cvtColor(image, cv2.COLOR_RGB2HSV)192 193 # Detecta áreas metálicas por brilho e saturação194 brightness_mask = hsv[:, :, 2] > 150 # Áreas brilhantes195 low_saturation_mask = hsv[:, :, 1] < 100 # Baixa saturação (típico de metais)196 metal_mask = brightness_mask & low_saturation_mask197 198 metal_percentage = np.sum(metal_mask) / (image.shape[0] * image.shape[1])199 200 # Analisa tons metálicos201 metal_regions = image[metal_mask]202 if len(metal_regions) > 0:203 avg_metal_color = np.mean(metal_regions, axis=0)204 metal_type = self._classify_metal_type(avg_metal_color)205 else:206 metal_type = "unknown"207 avg_metal_color = [0, 0, 0]208 209 return {210 "metal_percentage": float(metal_percentage),211 "metal_type": metal_type,212 "average_metal_color": avg_metal_color.tolist(),213 "metallic_shine_intensity": self._calculate_shine_intensity(image, metal_mask)214 }215 except Exception as e:216 print(f"❌ Erro na análise de metais: {e}")217 return {}218 219 def _analyze_stones(self, image: np.ndarray) -> Dict:220 """Análise específica para pedras preciosas"""221 try:222 # Detecta áreas com alta saturação (típico de pedras coloridas)223 hsv = cv2.cvtColor(image, cv2.COLOR_RGB2HSV)224 high_saturation_mask = hsv[:, :, 1] > 100225 226 stone_percentage = np.sum(high_saturation_mask) / (image.shape[0] * image.shape[1])227 228 # Analisa cores das pedras229 stone_regions = image[high_saturation_mask]230 stone_colors = []231 232 if len(stone_regions) > 0:233 # Agrupa cores das pedras234 try:235 kmeans = KMeans(n_clusters=min(5, max(1, len(stone_regions)//100)), random_state=42, n_init=10)236 if len(stone_regions) > 100:237 kmeans.fit(stone_regions)238 stone_colors = [color.tolist() for color in kmeans.cluster_centers_]239 except Exception as kmeans_error:240 print(f"❌ Erro no K-means para pedras: {kmeans_error}")241 stone_colors = []242 243 return {244 "stone_percentage": float(stone_percentage),245 "stone_colors": stone_colors,246 "stone_types": [self._classify_stone_type(color) for color in stone_colors],247 "transparency_level": self._analyze_transparency(image, high_saturation_mask)248 }249 except Exception as e:250 print(f"❌ Erro na análise de pedras: {e}")251 return {}252 253 def _analyze_jewelry_shapes(self, image: np.ndarray) -> Dict:254 """Análise de formas geométricas típicas de joias"""255 try:256 gray = cv2.cvtColor(image, cv2.COLOR_RGB2GRAY)257 258 # Detecta bordas259 edges = cv2.Canny(gray, 50, 150)260 261 # Encontra contornos262 contours, _ = cv2.findContours(edges, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)263 264 shapes = []265 for contour in contours:266 area = cv2.contourArea(contour)267 if area > 100: # Filtra contornos muito pequenos268 shape_type = self._classify_jewelry_shape(contour)269 shapes.append({270 "type": shape_type,271 "area": float(area),272 "perimeter": float(cv2.arcLength(contour, True))273 })274 275 return {276 "detected_shapes": shapes,277 "primary_shape": shapes[0]["type"] if shapes else "irregular",278 "shape_complexity": len(shapes),279 "edge_density": float(np.sum(edges > 0) / (edges.shape[0] * edges.shape[1]))280 }281 except Exception as e:282 print(f"❌ Erro na análise de formas: {e}")283 return {}284 285 def _analyze_textures(self, image: np.ndarray) -> Dict:286 """Análise de texturas (polido, fosco, texturizado)"""287 try:288 gray = cv2.cvtColor(image, cv2.COLOR_RGB2GRAY)289 290 # Calcula variância local (indica textura)291 kernel = np.ones((9, 9), np.float32) / 81292 mean = cv2.filter2D(gray.astype(np.float32), -1, kernel)293 sqr_mean = cv2.filter2D((gray.astype(np.float32))**2, -1, kernel)294 variance = sqr_mean - mean**2295 296 texture_intensity = np.mean(variance)297 298 # Detecta padrões repetitivos299 pattern_score = self._detect_patterns(gray)300 301 return {302 "texture_intensity": float(texture_intensity),303 "texture_type": self._classify_texture_type(texture_intensity),304 "pattern_score": float(pattern_score),305 "surface_finish": self._classify_surface_finish(texture_intensity, np.std(gray))306 }307 except Exception as e:308 print(f"❌ Erro na análise de texturas: {e}")309 return {}310 311 def _analyze_reflections(self, image: np.ndarray) -> Dict:312 """Análise de reflexos e brilho (importante para joias)"""313 try:314 gray = cv2.cvtColor(image, cv2.COLOR_RGB2GRAY)315 316 # Detecta áreas muito brilhantes (reflexos)317 bright_threshold = np.percentile(gray, 95)318 reflection_mask = gray > bright_threshold319 320 reflection_percentage = np.sum(reflection_mask) / (gray.shape[0] * gray.shape[1])321 322 # Analisa distribuição dos reflexos323 reflection_distribution = self._analyze_reflection_distribution(reflection_mask)324 325 return {326 "reflection_percentage": float(reflection_percentage),327 "reflection_intensity": float(np.mean(gray[reflection_mask]) if np.any(reflection_mask) else 0),328 "reflection_distribution": reflection_distribution,329 "shine_quality": self._assess_shine_quality(reflection_percentage, reflection_distribution)330 }331 except Exception as e:332 print(f"❌ Erro na análise de reflexos: {e}")333 return {}334 335 def _assess_image_quality(self, image: np.ndarray) -> Dict:336 """Avalia qualidade geral da imagem para joias"""337 try:338 gray = cv2.cvtColor(image, cv2.COLOR_RGB2GRAY)339 340 # Nitidez (usando variância do Laplaciano)341 sharpness = cv2.Laplacian(gray, cv2.CV_64F).var()342 343 # Exposição (distribuição de histograma)344 hist = cv2.calcHist([gray], [0], None, [256], [0, 256])345 exposure_score = self._calculate_exposure_score(hist)346 347 # Score geral de qualidade348 quality_score = min(1.0, (sharpness / 1000 + exposure_score) / 2)349 350 return {351 "sharpness": float(sharpness),352 "exposure_score": float(exposure_score),353 "overall_quality": float(quality_score),354 "quality_grade": self._grade_quality(quality_score)355 }356 except Exception as e:357 print(f"❌ Erro na avaliação de qualidade: {e}")358 return {}359 360 def generate_ai_description(self, image_path: str, piece_metadata: Dict) -> str:361 """Gera descrição detalhada usando GPT-4 Vision"""362 try:363 # Verifica se a chave da OpenAI está disponível364 if not openai.api_key:365 return f"Joia {piece_metadata.get('name', 'sem nome')} da coleção {piece_metadata.get('collection', 'N/A')} - Descrição IA não disponível"366 367 # Converte imagem para base64368 with open(image_path, "rb") as image_file:369 base64_image = base64.b64encode(image_file.read()).decode('utf-8')370 371 prompt = f"""372 Analise esta imagem de joia e forneça uma descrição detalhada e técnica em português.373 374 Metadados da peça:375 - SKU: {piece_metadata.get('sku', 'N/A')}376 - Nome: {piece_metadata.get('name', 'N/A')}377 - Material: {piece_metadata.get('material', 'N/A')}378 - Pedra: {piece_metadata.get('stone', 'N/A')}379 - Coleção: {piece_metadata.get('collection', 'N/A')}380 381 Descreva:382 1. Tipo de joia e formato geral383 2. Materiais visíveis (metais, pedras, acabamentos)384 3. Cores predominantes e secundárias385 4. Estilo e design (moderno, clássico, vintage, etc.)386 5. Detalhes técnicos visíveis (lapidação, engastes, texturas)387 6. Qualidade aparente e acabamento388 7. Características que facilitariam a busca por similaridade389 390 Seja específico e use termos técnicos de joalheria quando apropriado.391 """392 393 response = openai.ChatCompletion.create(394 model="gpt-4-vision-preview",395 messages=[396 {397 "role": "user",398 "content": [399 {"type": "text", "text": prompt},400 {401 "type": "image_url",402 "image_url": {403 "url": f"data:image/png;base64,{base64_image}"404 }405 }406 ]407 }408 ],409 max_tokens=500410 )411 412 return response.choices[0].message.content413 414 except Exception as e:415 print(f"❌ Erro ao gerar descrição IA: {e}")416 return f"Joia {piece_metadata.get('name', 'sem nome')} da coleção {piece_metadata.get('collection', 'N/A')}"417 418 def create_thumbnail(self, image_path: str, output_path: str, size: Tuple[int, int] = (200, 200)) -> bool:419 """Cria thumbnail da imagem usando o diretório seguro"""420 try:421 # Garante que o diretório de output está no local correto422 output_path = str(self.thumbnails_dir / Path(output_path).name)423 424 with Image.open(image_path) as img:425 # Mantém proporção426 img.thumbnail(size, Image.Resampling.LANCZOS)427 428 # Cria imagem quadrada com fundo branco429 thumbnail = Image.new('RGB', size, (255, 255, 255))430 431 # Centraliza a imagem432 x = (size[0] - img.width) // 2433 y = (size[1] - img.height) // 2434 thumbnail.paste(img, (x, y))435 436 # Salva thumbnail437 thumbnail.save(output_path, 'PNG', quality=95)438 return True439 except Exception as e:440 print(f"❌ Erro ao criar thumbnail de {image_path}: {e}")441 return False442 443 # ===== MÉTODOS AUXILIARES DE CLASSIFICAÇÃO =====444 445 def _extract_dominant_colors(self, image: np.ndarray, k: int = 8) -> List[Dict]:446 """Extrai cores dominantes com informações detalhadas"""447 try:448 pixels = image.reshape(-1, 3)449 kmeans = KMeans(n_clusters=k, random_state=42, n_init=10)450 kmeans.fit(pixels)451 452 colors = []453 labels = kmeans.labels_454 unique, counts = np.unique(labels, return_counts=True)455 456 for i, color in enumerate(kmeans.cluster_centers_):457 percentage = counts[i] / len(labels) * 100458 rgb = [int(c) for c in color]459 460 colors.append({461 "rgb": rgb,462 "hex": "#{:02x}{:02x}{:02x}".format(*rgb),463 "percentage": float(percentage),464 "hsl": self._rgb_to_hsl(rgb),465 "color_name": self._get_color_name(rgb)466 })467 468 return sorted(colors, key=lambda x: x['percentage'], reverse=True)469 except Exception as e:470 print(f"❌ Erro ao extrair cores dominantes: {e}")471 return []472 473 def _classify_metal_type(self, color: np.ndarray) -> str:474 """Classifica tipo de metal baseado na cor"""475 try:476 r, g, b = color477 478 # Ouro amarelo479 if r > 200 and g > 180 and b < 150:480 return "gold_yellow"481 # Ouro branco/prata482 elif abs(r - g) < 20 and abs(g - b) < 20 and r > 180:483 return "silver_white_gold"484 # Ouro rosé485 elif r > 200 and g > 150 and g < 180 and b < 150:486 return "rose_gold"487 # Cobre488 elif r > 180 and g > 100 and g < 150 and b < 100:489 return "copper"490 else:491 return "unknown_metal"492 except:493 return "unknown_metal"494 495 def _classify_stone_type(self, color: List[float]) -> str:496 """Classifica tipo de pedra baseado na cor"""497 try:498 r, g, b = color499 500 if r < 100 and g < 100 and b > 150:501 return "blue_stone" # Safira, topázio azul502 elif r > 150 and g < 100 and b < 100:503 return "red_stone" # Rubi, granada504 elif r < 100 and g > 150 and b < 100:505 return "green_stone" # Esmeralda, jade506 elif r > 200 and g > 200 and b > 200:507 return "clear_stone" # Diamante, cristal508 elif r > 150 and g > 100 and b > 150:509 return "purple_stone" # Ametista510 else:511 return "colored_stone"512 except:513 return "unknown_stone"514 515 def _rgb_to_hsl(self, rgb: List[int]) -> List[float]:516 """Converte RGB para HSL"""517 try:518 r, g, b = [x/255.0 for x in rgb]519 h, l, s = colorsys.rgb_to_hls(r, g, b)520 return [h*360, s*100, l*100]521 except:522 return [0, 0, 0]523 524 def _get_color_name(self, rgb: List[int]) -> str:525 """Retorna nome aproximado da cor"""526 try:527 r, g, b = rgb528 529 # Cores básicas para joias530 if r > 200 and g > 200 and b > 200:531 return "branco"532 elif r < 50 and g < 50 and b < 50:533 return "preto"534 elif r > 200 and g > 180 and b < 100:535 return "dourado"536 elif r > 180 and g > 180 and b > 180:537 return "prateado"538 elif r > 150 and g < 100 and b < 100:539 return "vermelho"540 elif r < 100 and g < 100 and b > 150:541 return "azul"542 elif r < 100 and g > 150 and b < 100:543 return "verde"544 elif r > 150 and g > 100 and b > 150:545 return "roxo"546 elif r > 200 and g > 150 and b < 100:547 return "laranja"548 else:549 return "multicolorido"550 except:551 return "indefinido"552 553 def _classify_jewelry_colors(self, colors: List[Dict]) -> Dict:554 """Classifica cores por categorias de joias"""555 try:556 categories = {557 "metal_tones": [],558 "stone_colors": [],559 "accent_colors": []560 }561 562 for color in colors:563 color_name = color.get("color_name", "")564 if color_name in ["dourado", "prateado", "branco"]:565 categories["metal_tones"].append(color)566 elif color.get("percentage", 0) > 5: # Cores significativas567 categories["stone_colors"].append(color)568 else:569 categories["accent_colors"].append(color)570 571 return categories572 except:573 return {"metal_tones": [], "stone_colors": [], "accent_colors": []}574 575 def _analyze_color_temperature(self, image: np.ndarray) -> str:576 """Analisa temperatura de cor da imagem"""577 try:578 avg_color = np.mean(image, axis=(0, 1))579 r, g, b = avg_color580 581 if r > g and r > b:582 return "warm" # Tons quentes583 elif b > r and b > g:584 return "cool" # Tons frios585 else:586 return "neutral"587 except:588 return "neutral"589 590 def _analyze_color_harmony(self, colors: List[Dict]) -> str:591 """Analisa harmonia das cores"""592 try:593 if len(colors) < 2:594 return "monochromatic"595 596 # Analisa diferenças de matiz597 hues = [color.get("hsl", [0, 0, 0])[0] for color in colors[:3]] # Top 3 cores598 hue_differences = [abs(hues[i] - hues[i+1]) for i in range(len(hues)-1)]599 600 if not hue_differences:601 return "monochromatic"602 603 avg_diff = np.mean(hue_differences)604 605 if avg_diff < 30:606 return "analogous"607 elif avg_diff > 150:608 return "complementary"609 else:610 return "triadic"611 except:612 return "neutral"613 614 # ===== MÉTODOS AUXILIARES ESPECÍFICOS =====615 616 def _calculate_shine_intensity(self, image: np.ndarray, metal_mask: np.ndarray) -> float:617 """Calcula intensidade do brilho metálico"""618 try:619 if not np.any(metal_mask):620 return 0.0621 gray = cv2.cvtColor(image, cv2.COLOR_RGB2GRAY)622 metal_brightness = np.mean(gray[metal_mask])623 return float(metal_brightness / 255.0)624 except:625 return 0.0626 627 def _analyze_transparency(self, image: np.ndarray, stone_mask: np.ndarray) -> str:628 """Analisa nível de transparência das pedras"""629 try:630 if not np.any(stone_mask):631 return "opaque"632 633 # Calcula variância de brilho nas áreas das pedras634 gray = cv2.cvtColor(image, cv2.COLOR_RGB2GRAY)635 stone_variance = np.var(gray[stone_mask])636 637 if stone_variance > 1000:638 return "transparent"639 elif stone_variance > 500:640 return "translucent"641 else:642 return "opaque"643 except:644 return "opaque"645 646 def _classify_jewelry_shape(self, contour: np.ndarray) -> str:647 """Classifica forma geométrica de joias"""648 try:649 # Aproxima contorno650 epsilon = 0.02 * cv2.arcLength(contour, True)651 approx = cv2.approxPolyDP(contour, epsilon, True)652 653 # Classifica baseado no número de vértices654 vertices = len(approx)655 656 if vertices == 3:657 return "triangular"658 elif vertices == 4:659 return "quadrilateral"660 elif vertices > 8:661 return "circular"662 else:663 return "polygonal"664 except:665 return "irregular"666 667 def _detect_patterns(self, gray: np.ndarray) -> float:668 """Detecta padrões repetitivos na textura"""669 try:670 # Usa transformada de Fourier para detectar padrões671 f_transform = np.fft.fft2(gray)672 f_shift = np.fft.fftshift(f_transform)673 magnitude_spectrum = 20 * np.log(np.abs(f_shift) + 1)674 675 # Calcula energia dos picos (indica padrões)676 threshold = np.percentile(magnitude_spectrum, 95)677 pattern_energy = np.sum(magnitude_spectrum > threshold)678 679 return float(pattern_energy / (gray.shape[0] * gray.shape[1]))680 except:681 return 0.0682 683 def _classify_texture_type(self, texture_intensity: float) -> str:684 """Classifica tipo de textura"""685 try:686 if texture_intensity < 100:687 return "polished"688 elif texture_intensity < 500:689 return "satin"690 else:691 return "textured"692 except:693 return "unknown"694 695 def _classify_surface_finish(self, texture_intensity: float, std_dev: float) -> str:696 """Classifica acabamento da superfície"""697 try:698 if texture_intensity < 50 and std_dev < 30:699 return "mirror_polish"700 elif texture_intensity < 200:701 return "high_polish"702 elif texture_intensity < 500:703 return "satin_finish"704 else:705 return "matte_finish"706 except:707 return "unknown_finish"708 709 def _analyze_reflection_distribution(self, reflection_mask: np.ndarray) -> str:710 """Analisa distribuição dos reflexos"""711 try:712 if not np.any(reflection_mask):713 return "no_reflections"714 715 # Calcula centros dos reflexos716 contours, _ = cv2.findContours(reflection_mask.astype(np.uint8), cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)717 718 if len(contours) <= 1:719 return "single_reflection"720 elif len(contours) <= 3:721 return "few_reflections"722 else:723 return "multiple_reflections"724 except:725 return "unknown_distribution"726 727 def _assess_shine_quality(self, reflection_percentage: float, distribution: str) -> str:728 """Avalia qualidade do brilho"""729 try:730 if reflection_percentage > 0.1 and distribution in ["few_reflections", "multiple_reflections"]:731 return "excellent_shine"732 elif reflection_percentage > 0.05:733 return "good_shine"734 elif reflection_percentage > 0.02:735 return "moderate_shine"736 else:737 return "low_shine"738 except:739 return "unknown_shine"740 741 def _calculate_exposure_score(self, hist: np.ndarray) -> float:742 """Calcula score de exposição baseado no histograma"""743 try:744 # Verifica distribuição do histograma745 total_pixels = np.sum(hist)746 747 # Pixels muito escuros ou muito claros indicam má exposição748 dark_pixels = np.sum(hist[:50]) / total_pixels749 bright_pixels = np.sum(hist[200:]) / total_pixels750 751 # Score ideal é ter poucos pixels extremos752 exposure_score = 1.0 - (dark_pixels + bright_pixels)753 return max(0.0, min(1.0, exposure_score))754 except:755 return 0.5756 757 def _grade_quality(self, quality_score: float) -> str:758 """Classifica qualidade em grades"""759 try:760 if quality_score >= 0.8:761 return "excellent"762 elif quality_score >= 0.6:763 return "good"764 elif quality_score >= 0.4:765 return "fair"766 else:767 return "poor"768 except:769 return "unknown"