Spaces:
Running
Running
| """ | |
| Utilitaires de traitement d'images pour l'application Rakuten. | |
| Ce module fournit des fonctions pour: | |
| - Charger et valider des images uploadées | |
| - Redimensionner les images pour le modèle | |
| - Prétraiter les images pour l'extraction de features ResNet50 | |
| """ | |
| from typing import Tuple, Optional | |
| from pathlib import Path | |
| import io | |
| import sys | |
| from PIL import Image | |
| import numpy as np | |
| # Ajouter le répertoire parent au path pour les imports | |
| sys.path.insert(0, str(Path(__file__).parent.parent)) | |
| from config import IMAGE_CONFIG | |
| def load_image_from_upload(uploaded_file) -> Image.Image: | |
| """ | |
| Charge une image depuis un fichier uploadé Streamlit. | |
| Args: | |
| uploaded_file: Objet UploadedFile de Streamlit | |
| Returns: | |
| Image PIL en mode RGB | |
| Raises: | |
| ValueError: Si le fichier n'est pas une image valide | |
| """ | |
| try: | |
| image = Image.open(uploaded_file) | |
| # Convertir en RGB si nécessaire (gère PNG avec transparence, etc.) | |
| if image.mode != "RGB": | |
| image = image.convert("RGB") | |
| return image | |
| except Exception as e: | |
| raise ValueError(f"Impossible de charger l'image: {e}") | |
| def validate_image(image: Image.Image) -> Tuple[bool, str]: | |
| """ | |
| Valide une image selon les critères de l'application. | |
| Args: | |
| image: Image PIL à valider | |
| Returns: | |
| Tuple (is_valid, message) | |
| """ | |
| # Vérifier les dimensions minimales | |
| min_size = 32 | |
| if image.width < min_size or image.height < min_size: | |
| return False, f"Image trop petite (minimum {min_size}x{min_size})" | |
| # Vérifier les dimensions maximales | |
| max_size = 10000 | |
| if image.width > max_size or image.height > max_size: | |
| return False, f"Image trop grande (maximum {max_size}x{max_size})" | |
| return True, "Image valide" | |
| def resize_image( | |
| image: Image.Image, | |
| target_size: Tuple[int, int] = None, | |
| maintain_aspect_ratio: bool = True | |
| ) -> Image.Image: | |
| """ | |
| Redimensionne une image vers la taille cible. | |
| Args: | |
| image: Image PIL à redimensionner | |
| target_size: Taille cible (width, height). Par défaut: config.IMAGE_CONFIG | |
| maintain_aspect_ratio: Si True, conserve le ratio et ajoute du padding | |
| Returns: | |
| Image PIL redimensionnée | |
| """ | |
| if target_size is None: | |
| target_size = IMAGE_CONFIG["target_size"] | |
| if maintain_aspect_ratio: | |
| return _resize_with_padding(image, target_size) | |
| else: | |
| return image.resize(target_size, Image.Resampling.LANCZOS) | |
| def _resize_with_padding( | |
| image: Image.Image, | |
| target_size: Tuple[int, int], | |
| fill_color: Tuple[int, int, int] = (255, 255, 255) | |
| ) -> Image.Image: | |
| """ | |
| Redimensionne une image en conservant le ratio et en ajoutant du padding. | |
| Args: | |
| image: Image PIL source | |
| target_size: Taille cible (width, height) | |
| fill_color: Couleur de remplissage RGB pour le padding | |
| Returns: | |
| Image PIL avec padding | |
| """ | |
| target_w, target_h = target_size | |
| original_w, original_h = image.size | |
| # Calculer le ratio de redimensionnement | |
| ratio = min(target_w / original_w, target_h / original_h) | |
| new_w = int(original_w * ratio) | |
| new_h = int(original_h * ratio) | |
| # Redimensionner l'image | |
| resized = image.resize((new_w, new_h), Image.Resampling.LANCZOS) | |
| # Créer l'image avec padding | |
| padded = Image.new("RGB", target_size, fill_color) | |
| # Centrer l'image redimensionnée | |
| paste_x = (target_w - new_w) // 2 | |
| paste_y = (target_h - new_h) // 2 | |
| padded.paste(resized, (paste_x, paste_y)) | |
| return padded | |
| def preprocess_for_resnet(image: Image.Image) -> np.ndarray: | |
| """ | |
| Prétraite une image pour l'extraction de features ResNet50. | |
| Applique les transformations standard ImageNet: | |
| - Redimensionnement à 224x224 | |
| - Normalisation des pixels | |
| - Expansion des dimensions pour le batch | |
| Args: | |
| image: Image PIL en RGB | |
| Returns: | |
| Array numpy de shape (1, 224, 224, 3) prêt pour ResNet50 | |
| """ | |
| # Redimensionner à 224x224 | |
| resized = resize_image(image, (224, 224), maintain_aspect_ratio=True) | |
| # Convertir en array numpy | |
| img_array = np.array(resized, dtype=np.float32) | |
| # Normalisation ImageNet (preprocessing Keras) | |
| # Mode 'caffe': BGR, centré sur ImageNet mean | |
| img_array = img_array[..., ::-1] # RGB -> BGR | |
| img_array[..., 0] -= 103.939 | |
| img_array[..., 1] -= 116.779 | |
| img_array[..., 2] -= 123.68 | |
| # Ajouter la dimension batch | |
| img_array = np.expand_dims(img_array, axis=0) | |
| return img_array | |
| def image_to_bytes(image: Image.Image, format: str = "PNG") -> bytes: | |
| """ | |
| Convertit une image PIL en bytes. | |
| Args: | |
| image: Image PIL | |
| format: Format de sortie (PNG, JPEG, etc.) | |
| Returns: | |
| Bytes de l'image | |
| """ | |
| buffer = io.BytesIO() | |
| image.save(buffer, format=format) | |
| return buffer.getvalue() | |
| def create_thumbnail( | |
| image: Image.Image, | |
| max_size: Tuple[int, int] = (150, 150) | |
| ) -> Image.Image: | |
| """ | |
| Crée une miniature d'une image. | |
| Args: | |
| image: Image PIL source | |
| max_size: Taille maximale de la miniature | |
| Returns: | |
| Image PIL miniature | |
| """ | |
| thumbnail = image.copy() | |
| thumbnail.thumbnail(max_size, Image.Resampling.LANCZOS) | |
| return thumbnail | |
| def get_image_info(image: Image.Image) -> dict: | |
| """ | |
| Retourne les informations d'une image. | |
| Args: | |
| image: Image PIL | |
| Returns: | |
| Dict avec les informations de l'image | |
| """ | |
| return { | |
| "width": image.width, | |
| "height": image.height, | |
| "mode": image.mode, | |
| "format": image.format, | |
| "size_str": f"{image.width}x{image.height}", | |
| } | |