CoolFace
Apppublic

OctusTech/cartalogo-api

sourceHugging Faceupdated 1y agoView on Hugging Face
0likes
enhanced_image_processor.py769 linesDownload Raw Back to scripts
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"