CoolFace
Apppublic

OctusTech/cartalogo-api

sourceHugging Faceupdated 1y agoView on Hugging Face
0likes
image_processor.py680 linesDownload Raw Back to root
1import os2import sys3from pathlib import Path4from dotenv import load_dotenv5import torch6import open_clip7from PIL import Image, ImageEnhance, ImageFilter8import numpy as np9import cv210from typing import Dict, List, Tuple, Optional11import json12import colorsys13from sklearn.cluster import KMeans14import time15 16# FORÇA carregamento das variáveis de ambiente17env_file = Path(__file__).parent / ".env"18print(f"🔍 Carregando .env de: {env_file}")19print(f"🔍 Arquivo existe: {env_file.exists()}")20 21if env_file.exists():22    # Carrega com override para forçar23    load_result = load_dotenv(env_file, override=True, verbose=True)24    print(f"🔍 Load result: {load_result}")25else:26    print("❌ Arquivo .env não encontrado!")27    # Tenta carregar do diretório atual28    load_dotenv(override=True, verbose=True)29 30class JewelryImageProcessor:31    def __init__(self):32        """Inicializa o processador especializado em joias"""33        print("🤖 Inicializando processador de imagens para joias...")34        35        # Configuração de caminhos36        self.root_dir = Path(__file__).parent37        self.images_dir = self.root_dir / "imagens"38        self.thumbnails_dir = self.root_dir / "thumbnails"39        40        # Cria diretório de thumbnails41        self.thumbnails_dir.mkdir(exist_ok=True)42        43        # Configuração do dispositivo44        self.device = "cuda" if torch.cuda.is_available() else "cpu"45        print(f"🔧 Dispositivo: {self.device}")46        47        # Carrega modelo OpenCLIP48        try:49            model_name = os.getenv('EMBEDDING_MODEL', 'ViT-L-14')50            pretrained = os.getenv('EMBEDDING_PRETRAINED', 'laion2b_s32b_b82k')51            52            print(f"📥 Carregando modelo {model_name} ({pretrained})...")53            self.model, _, self.preprocess = open_clip.create_model_and_transforms(54                model_name, pretrained=pretrained, device=self.device55            )56            self.tokenizer = open_clip.get_tokenizer(model_name)57            print("✅ Modelo OpenCLIP carregado com sucesso!")58            59        except Exception as e:60            print(f"❌ Erro ao carregar modelo OpenCLIP: {e}")61            raise62        63        # DEBUG: Configuração OpenAI v1.0+64        print("\n🔍 DEBUG OpenAI:")65        66        # Tenta diferentes formas de obter a chave67        openai_api_key = None68        69        # Método 1: os.getenv70        openai_api_key = os.getenv('OPENAI_API_KEY')71        print(f"🔍 Método 1 (os.getenv): {openai_api_key[:15] if openai_api_key else 'None'}...")72        73        # Método 2: os.environ.get74        if not openai_api_key or openai_api_key.startswith('your-'):75            openai_api_key = os.environ.get('OPENAI_API_KEY')76            print(f"🔍 Método 2 (os.environ): {openai_api_key[:15] if openai_api_key else 'None'}...")77        78        # Método 3: Lê diretamente do arquivo .env79        if not openai_api_key or openai_api_key.startswith('your-'):80            try:81                env_file = self.root_dir / ".env"82                if env_file.exists():83                    with open(env_file, 'r', encoding='utf-8') as f:84                        for line in f:85                            if line.startswith('OPENAI_API_KEY='):86                                openai_api_key = line.split('=', 1)[1].strip()87                                print(f"🔍 Método 3 (arquivo): {openai_api_key[:15]}...")88                                break89            except Exception as e:90                print(f"🔍 Erro método 3: {e}")91        92        self.openai_client = None93        94        print(f"🔑 OpenAI API Key final: {'Sim' if openai_api_key else 'Não'}")95        if openai_api_key:96            print(f"🔑 Primeiros caracteres: {openai_api_key[:15]}...")97            print(f"🔑 Começa com sk-: {openai_api_key.startswith('sk-')}")98 99        if openai_api_key and openai_api_key.startswith('sk-'):100            try:101                from openai import OpenAI102                self.openai_client = OpenAI(api_key=openai_api_key)103                print("✅ OpenAI v1.0+ configurado para descrições IA")104                105                # Testa a conexão106                try:107                    # Faz um teste simples para verificar se a API key funciona108                    test_response = self.openai_client.models.list()109                    print("✅ Conexão com OpenAI testada com sucesso")110                except Exception as test_error:111                    print(f"⚠️  Erro ao testar conexão OpenAI: {test_error}")112                    self.openai_client = None113                114            except ImportError:115                print("⚠️  OpenAI não instalado - descrições IA desabilitadas")116                self.openai_client = None117            except Exception as e:118                print(f"⚠️  Erro ao configurar OpenAI: {e}")119                self.openai_client = None120        else:121            print("⚠️  OpenAI API key inválida - descrições IA desabilitadas")122            self.openai_client = None123    124    def generate_embedding(self, image_path: Path) -> Optional[np.ndarray]:125        """Gera embedding da imagem usando OpenCLIP"""126        try:127            if not image_path.exists():128                print(f"❌ Arquivo não encontrado: {image_path}")129                return None130            131            # Carrega e processa imagem132            image = Image.open(image_path).convert('RGB')133            image_input = self.preprocess(image).unsqueeze(0).to(self.device)134            135            with torch.no_grad():136                image_features = self.model.encode_image(image_input)137                # Normaliza o vetor138                image_features = image_features / image_features.norm(dim=-1, keepdim=True)139                140            return image_features.cpu().numpy().flatten()141            142        except Exception as e:143            print(f"❌ Erro ao gerar embedding para {image_path.name}: {e}")144            return None145    146    def analyze_jewelry_technical(self, image_path: Path) -> Dict:147        """Análise técnica especializada para joias"""148        try:149            if not image_path.exists():150                return {}151            152            # Carrega imagem153            image = cv2.imread(str(image_path))154            if image is None:155                print(f"❌ Não foi possível carregar a imagem: {image_path}")156                return {}157            158            image_rgb = cv2.cvtColor(image, cv2.COLOR_BGR2RGB)159            160            print(f"🔍 Analisando {image_path.name}...")161            162            analysis = {163                "basic_metrics": self._analyze_basic_metrics(image_rgb),164                "color_analysis": self._analyze_jewelry_colors(image_rgb),165                "metal_analysis": self._analyze_metal_properties(image_rgb),166                "stone_analysis": self._analyze_stones(image_rgb),167                "shape_analysis": self._analyze_jewelry_shapes(image_rgb),168                "texture_analysis": self._analyze_textures(image_rgb),169                "reflection_analysis": self._analyze_reflections(image_rgb),170                "quality_assessment": self._assess_image_quality(image_rgb)171            }172            173            return analysis174            175        except Exception as e:176            print(f"❌ Erro na análise técnica de {image_path.name}: {e}")177            return {}178    179    def extract_visual_features(self, image_path: Path, technical_analysis: Dict) -> List[Dict]:180        """Extrai características visuais estruturadas para a tabela visual_features"""181        features = []182        183        try:184            # Características de cor185            color_analysis = technical_analysis.get('color_analysis', {})186            dominant_colors = color_analysis.get('dominant_colors', [])187            188            for i, color in enumerate(dominant_colors[:3]):  # Top 3 cores189                features.append({190                    'feature_type': 'dominant_color',191                    'feature_value': color.get('hex', '#000000'),192                    'confidence_score': min(0.99, color.get('percentage', 0) / 100)193                })194            195            # Características de metal196            metal_analysis = technical_analysis.get('metal_analysis', {})197            if metal_analysis.get('has_metallic_elements', False):198                features.append({199                    'feature_type': 'material',200                    'feature_value': 'metallic',201                    'confidence_score': min(0.99, metal_analysis.get('metal_percentage', 0))202                })203            204            # Características de pedras205            stone_analysis = technical_analysis.get('stone_analysis', {})206            if stone_analysis.get('has_colored_stones', False):207                features.append({208                    'feature_type': 'material',209                    'feature_value': 'gemstone',210                    'confidence_score': min(0.99, stone_analysis.get('stone_percentage', 0))211                })212            213            # Características de forma214            shape_analysis = technical_analysis.get('shape_analysis', {})215            complexity = shape_analysis.get('shape_complexity', 'unknown')216            if complexity != 'unknown':217                features.append({218                    'feature_type': 'shape',219                    'feature_value': complexity,220                    'confidence_score': 0.8221                })222            223            # Características de textura224            texture_analysis = technical_analysis.get('texture_analysis', {})225            texture_type = texture_analysis.get('texture_type', 'unknown')226            if texture_type != 'unknown':227                features.append({228                    'feature_type': 'texture',229                    'feature_value': texture_type,230                    'confidence_score': 0.75231                })232            233            # Características de qualidade234            quality_assessment = technical_analysis.get('quality_assessment', {})235            quality_grade = quality_assessment.get('quality_grade', 'unknown')236            if quality_grade != 'unknown':237                features.append({238                    'feature_type': 'quality',239                    'feature_value': quality_grade,240                    'confidence_score': quality_assessment.get('quality_score', 0.5)241                })242            243        except Exception as e:244            print(f"⚠️  Erro ao extrair características visuais: {e}")245        246        return features247    248    def generate_image_tags(self, image_path: Path, piece_metadata: Dict, technical_analysis: Dict) -> List[Dict]:249        """Gera tags automáticas para a imagem"""250        tags = []251        252        try:253            # Tags baseadas nos metadados254            category = piece_metadata.get('category', '')255            if category:256                tags.append({257                    'tag': category,258                    'confidence': 0.95,259                    'tag_type': 'category'260                })261            262            collection = piece_metadata.get('collection', '')263            if collection:264                tags.append({265                    'tag': collection.lower(),266                    'confidence': 0.9,267                    'tag_type': 'collection'268                })269            270            stone = piece_metadata.get('stone', '')271            if stone and stone.lower() not in ['', 'nan', 'none']:272                tags.append({273                    'tag': stone.lower(),274                    'confidence': 0.85,275                    'tag_type': 'material'276                })277            278            metal = piece_metadata.get('predominant_metal', '')279            if metal and metal.lower() not in ['', 'nan', 'none']:280                tags.append({281                    'tag': metal.lower().replace(' ', '_'),282                    'confidence': 0.85,283                    'tag_type': 'material'284                })285            286            # Tags baseadas na análise técnica287            color_analysis = technical_analysis.get('color_analysis', {})288            dominant_colors = color_analysis.get('dominant_colors', [])289            290            for color in dominant_colors[:2]:  # Top 2 cores291                if color.get('percentage', 0) > 15:  # Apenas cores significativas292                    color_name = self._get_color_name_from_rgb(color.get('rgb', [0, 0, 0]))293                    tags.append({294                        'tag': color_name,295                        'confidence': min(0.8, color.get('percentage', 0) / 100),296                        'tag_type': 'color'297                    })298            299            # Tags de estilo baseadas no nome do arquivo300            filename = image_path.name.lower()301            if 'modelo' in filename:302                tags.append({303                    'tag': 'lifestyle',304                    'confidence': 0.9,305                    'tag_type': 'style'306                })307            else:308                tags.append({309                    'tag': 'product_shot',310                    'confidence': 0.9,311                    'tag_type': 'style'312                })313            314            # Tags de qualidade315            quality_assessment = technical_analysis.get('quality_assessment', {})316            quality_score = quality_assessment.get('quality_score', 0)317            if quality_score > 0.7:318                tags.append({319                    'tag': 'high_quality',320                    'confidence': quality_score,321                    'tag_type': 'quality'322                })323            elif quality_score > 0.3:324                tags.append({325                    'tag': 'medium_quality',326                    'confidence': quality_score,327                    'tag_type': 'quality'328                })329            330        except Exception as e:331            print(f"⚠️  Erro ao gerar tags: {e}")332        333        return tags334    335    def create_thumbnail(self, image_path: Path, size: Tuple[int, int] = None) -> Optional[Path]:336        """Cria thumbnail da imagem"""337        try:338            if size is None:339                size = (int(os.getenv('THUMBNAIL_SIZE', 200)), int(os.getenv('THUMBNAIL_SIZE', 200)))340            341            if not image_path.exists():342                return None343            344            # Define caminho do thumbnail345            thumbnail_name = f"thumb_{image_path.stem}.png"346            thumbnail_path = self.thumbnails_dir / thumbnail_name347            348            with Image.open(image_path) as img:349                # Mantém proporção350                img.thumbnail(size, Image.Resampling.LANCZOS)351                352                # Cria imagem quadrada com fundo branco353                thumbnail = Image.new('RGB', size, (255, 255, 255))354                355                # Centraliza a imagem356                x = (size[0] - img.width) // 2357                y = (size[1] - img.height) // 2358                thumbnail.paste(img, (x, y))359                360                # Salva thumbnail361                thumbnail.save(thumbnail_path, 'PNG', quality=95)362                363            print(f"✅ Thumbnail criado: {thumbnail_name}")364            return thumbnail_path365            366        except Exception as e:367            print(f"❌ Erro ao criar thumbnail de {image_path.name}: {e}")368            return None369    370    def generate_ai_description(self, image_path: Path, piece_metadata: Dict) -> str:371        """Gera descrição detalhada usando GPT-4 Vision (OpenAI v1.0+)"""372        try:373            if not self.openai_client:374                print(f"    ⚠️  OpenAI não configurado, usando descrição básica")375                return self._generate_basic_description(piece_metadata)376        377            # Converte imagem para base64378            import base64379            with open(image_path, "rb") as image_file:380                base64_image = base64.b64encode(image_file.read()).decode('utf-8')381        382            prompt = f"""383        Analise esta imagem de joia e forneça uma descrição detalhada e técnica em português brasileiro.384        385        Metadados da peça:386        - SKU: {piece_metadata.get('sku', 'N/A')}387        - Nome: {piece_metadata.get('name', 'N/A')}388        - Material: {piece_metadata.get('material', 'N/A')}389        - Pedra: {piece_metadata.get('stone', 'N/A')}390        - Coleção: {piece_metadata.get('collection', 'N/A')}391        392        Descreva detalhadamente:393        1. Tipo de joia e formato geral394        2. Materiais visíveis (metais, pedras, acabamentos)395        3. Cores predominantes e tonalidades396        4. Estilo e design (moderno, clássico, vintage, etc.)397        5. Detalhes técnicos (lapidação, engastes, texturas)398        6. Qualidade aparente e acabamento399        7. Características distintivas para busca400        401        Use terminologia técnica de joalheria. Seja específico e descritivo.402        """403        404            print(f"    🤖 Gerando descrição IA para {image_path.name}...")405        406            response = self.openai_client.chat.completions.create(407                model="gpt-4o",  # Modelo mais recente e estável408                messages=[409                    {410                        "role": "user",411                        "content": [412                            {"type": "text", "text": prompt},413                            {414                                "type": "image_url",415                                "image_url": {416                                    "url": f"data:image/png;base64,{base64_image}",417                                    "detail": "low"  # Reduz custo418                                }419                            }420                        ]421                    }422                ],423                max_tokens=400,424                temperature=0.3  # Mais consistente425            )426        427            description = response.choices[0].message.content428            print(f"    ✅ Descrição IA gerada ({len(description)} caracteres)")429            return description430        431        except Exception as e:432            print(f"    ⚠️  Erro ao gerar descrição IA para {image_path.name}: {e}")433            print(f"    🔄 Usando descrição básica como fallback")434            return self._generate_basic_description(piece_metadata)435    436    def _generate_basic_description(self, piece_metadata: Dict) -> str:437        """Gera descrição básica sem IA"""438        name = piece_metadata.get('name', 'Joia')439        collection = piece_metadata.get('collection', '')440        material = piece_metadata.get('material', '')441        stone = piece_metadata.get('stone', '')442        443        description = f"{name}"444        if collection:445            description += f" da coleção {collection}"446        if material:447            description += f", confeccionada em {material}"448        if stone:449            description += f" com {stone}"450        451        return description452    453    def find_images_from_metadata(self, piece_metadata: Dict) -> List[Dict]:454        """Encontra imagens baseado nos metadados da planilha"""455        images = []456        sku = piece_metadata.get('sku', '')457        458        # Baseado na estrutura real do CSV: apenas arquivo_peca_01459        filename = piece_metadata.get('arquivo_peca_01', '').strip()460        461        if filename and filename != 'nan' and filename != '':462            image_path = self.images_dir / filename463            464            if image_path.exists():465                # Determina tipo baseado no nome do arquivo466                filename_lower = filename.lower()467                468                if any(keyword in filename_lower for keyword in ["modelo", "person", "wearing", "lifestyle", "usando"]):469                    image_type = "lifestyle"470                elif any(keyword in filename_lower for keyword in ["detalhe", "detail", "close"]):471                    image_type = "detail"472                elif any(keyword in filename_lower for keyword in ["embalagem", "packaging", "box"]):473                    image_type = "packaging"474                else:475                    image_type = "product"476                477                images.append({478                    'path': image_path,479                    'filename': filename,480                    'type': image_type,481                    'size': image_path.stat().st_size,482                    'column': 'arquivo_peca_01'483                })484                print(f"  ✅ Encontrada: {filename} ({image_type})")485            else:486                print(f"  ❌ Não encontrada: {filename}")487        else:488            print(f"  ⚠️  Nenhum arquivo especificado para SKU: {sku}")489        490        return images491    492    def _get_color_name_from_rgb(self, rgb: List[int]) -> str:493        """Retorna nome da cor baseado em RGB"""494        if not rgb or len(rgb) != 3:495            return "indefinido"496        497        r, g, b = rgb498        499        # Cores básicas para joias500        if r > 200 and g > 200 and b > 200:501            return "branco"502        elif r < 50 and g < 50 and b < 50:503            return "preto"504        elif r > 200 and g > 180 and b < 100:505            return "dourado"506        elif r > 180 and g > 180 and b > 180:507            return "prateado"508        elif r > 150 and g < 100 and b < 100:509            return "vermelho"510        elif r < 100 and g < 100 and b > 150:511            return "azul"512        elif r < 100 and g > 150 and b < 100:513            return "verde"514        elif r > 150 and g > 100 and b > 150:515            return "roxo"516        elif r > 200 and g > 150 and b < 100:517            return "laranja"518        else:519            return "multicolorido"520    521    # Métodos de análise técnica (mantidos iguais)522    def _analyze_basic_metrics(self, image: np.ndarray) -> Dict:523        """Métricas básicas da imagem"""524        height, width = image.shape[:2]525        brightness = float(np.mean(image))526        527        gray = cv2.cvtColor(image, cv2.COLOR_RGB2GRAY)528        contrast = float(np.std(gray))529        530        hsv = cv2.cvtColor(image, cv2.COLOR_RGB2HSV)531        saturation = float(np.mean(hsv[:, :, 1]))532        533        return {534            "dimensions": {"width": width, "height": height},535            "brightness": brightness,536            "contrast": contrast,537            "saturation": saturation,538            "aspect_ratio": float(width / height)539        }540    541    def _analyze_jewelry_colors(self, image: np.ndarray) -> Dict:542        """Análise de cores para joias"""543        try:544            dominant_colors = self._extract_dominant_colors(image, k=6)545            546            return {547                "dominant_colors": dominant_colors,548                "color_count": len(dominant_colors),549                "primary_color": dominant_colors[0] if dominant_colors else None550            }551        except:552            return {"dominant_colors": [], "color_count": 0, "primary_color": None}553    554    def _extract_dominant_colors(self, image: np.ndarray, k: int = 6) -> List[Dict]:555        """Extrai cores dominantes"""556        try:557            pixels = image.reshape(-1, 3)558            559            # Reduz amostra para performance560            if len(pixels) > 10000:561                indices = np.random.choice(len(pixels), 10000, replace=False)562                pixels = pixels[indices]563            564            kmeans = KMeans(n_clusters=k, random_state=42, n_init=10)565            kmeans.fit(pixels)566            567            colors = []568            labels = kmeans.labels_569            unique, counts = np.unique(labels, return_counts=True)570            571            for i, color in enumerate(kmeans.cluster_centers_):572                percentage = counts[i] / len(labels) * 100573                rgb = [int(c) for c in color]574                575                colors.append({576                    "rgb": rgb,577                    "hex": "#{:02x}{:02x}{:02x}".format(*rgb),578                    "percentage": float(percentage)579                })580            581            return sorted(colors, key=lambda x: x['percentage'], reverse=True)582            583        except Exception as e:584            print(f"⚠️  Erro na extração de cores: {e}")585            return []586    587    def _analyze_metal_properties(self, image: np.ndarray) -> Dict:588        """Análise básica de propriedades metálicas"""589        try:590            hsv = cv2.cvtColor(image, cv2.COLOR_RGB2HSV)591            592            # Detecta áreas metálicas (brilhantes + baixa saturação)593            brightness_mask = hsv[:, :, 2] > 150594            low_saturation_mask = hsv[:, :, 1] < 80595            metal_mask = brightness_mask & low_saturation_mask596            597            metal_percentage = float(np.sum(metal_mask) / (image.shape[0] * image.shape[1]))598            599            return {600                "metal_percentage": metal_percentage,601                "has_metallic_elements": metal_percentage > 0.1602            }603        except:604            return {"metal_percentage": 0.0, "has_metallic_elements": False}605    606    def _analyze_stones(self, image: np.ndarray) -> Dict:607        """Análise básica de pedras"""608        try:609            hsv = cv2.cvtColor(image, cv2.COLOR_RGB2HSV)610            high_saturation_mask = hsv[:, :, 1] > 100611            stone_percentage = float(np.sum(high_saturation_mask) / (image.shape[0] * image.shape[1]))612            613            return {614                "stone_percentage": stone_percentage,615                "has_colored_stones": stone_percentage > 0.05616            }617        except:618            return {"stone_percentage": 0.0, "has_colored_stones": False}619    620    def _analyze_jewelry_shapes(self, image: np.ndarray) -> Dict:621        """Análise básica de formas"""622        try:623            gray = cv2.cvtColor(image, cv2.COLOR_RGB2GRAY)624            edges = cv2.Canny(gray, 50, 150)625            edge_density = float(np.sum(edges > 0) / (edges.shape[0] * edges.shape[1]))626            627            return {628                "edge_density": edge_density,629                "shape_complexity": "high" if edge_density > 0.1 else "low"630            }631        except:632            return {"edge_density": 0.0, "shape_complexity": "unknown"}633    634    def _analyze_textures(self, image: np.ndarray) -> Dict:635        """Análise básica de texturas"""636        try:637            gray = cv2.cvtColor(image, cv2.COLOR_RGB2GRAY)638            texture_variance = float(np.var(gray))639            640            return {641                "texture_variance": texture_variance,642                "texture_type": "smooth" if texture_variance < 1000 else "textured"643            }644        except:645            return {"texture_variance": 0.0, "texture_type": "unknown"}646    647    def _analyze_reflections(self, image: np.ndarray) -> Dict:648        """Análise básica de reflexos"""649        try:650            gray = cv2.cvtColor(image, cv2.COLOR_RGB2GRAY)651            bright_threshold = np.percentile(gray, 95)652            reflection_mask = gray > bright_threshold653            reflection_percentage = float(np.sum(reflection_mask) / (gray.shape[0] * gray.shape[1]))654            655            return {656                "reflection_percentage": reflection_percentage,657                "has_reflections": reflection_percentage > 0.05658            }659        except:660            return {"reflection_percentage": 0.0, "has_reflections": False}661    662    def _assess_image_quality(self, image: np.ndarray) -> Dict:663        """Avaliação básica de qualidade"""664        try:665            gray = cv2.cvtColor(image, cv2.COLOR_RGB2GRAY)666            667            # Nitidez usando variância do Laplaciano668            sharpness = float(cv2.Laplacian(gray, cv2.CV_64F).var())669            670            # Score de qualidade simples671            quality_score = min(1.0, sharpness / 1000)672            673            return {674                "sharpness": sharpness,675                "quality_score": quality_score,676                "quality_grade": "high" if quality_score > 0.7 else "medium" if quality_score > 0.3 else "low"677            }678        except:679            return {"sharpness": 0.0, "quality_score": 0.0, "quality_grade": "unknown"}680