import gradio as gr import numpy as np import matplotlib.pyplot as plt from PIL import Image, ImageDraw import io # Créer des images de test colorées et asymétriques def create_test_image(image_type, size=256): """Crée une image de test colorée avec des formes asymétriques""" img = Image.new('RGB', (size, size), 'white') draw = ImageDraw.Draw(img) if image_type == "licorne": # Corps de la licorne (ellipse rose) draw.ellipse([80, 120, 180, 200], fill='#FF69B4', outline='#FF1493', width=3) # Corne (triangle doré) - asymétrique ! draw.polygon([125, 80, 135, 80, 130, 50], fill='#FFD700', outline='#FFA500') # Œil draw.ellipse([110, 140, 120, 150], fill='black') # Crinière (cercles colorés) draw.ellipse([90, 90, 110, 110], fill='#FF6347') draw.ellipse([140, 95, 160, 115], fill='#9370DB') elif image_type == "nounours": # Corps (cercle marron) draw.ellipse([90, 110, 170, 190], fill='#8B4513', outline='#654321', width=3) # Oreilles (asymétriques) draw.ellipse([100, 80, 130, 110], fill='#8B4513') draw.ellipse([130, 85, 160, 115], fill='#8B4513') # Légèrement décalée # Museau draw.ellipse([115, 140, 145, 160], fill='#DEB887') # Yeux draw.ellipse([110, 125, 120, 135], fill='black') draw.ellipse([140, 125, 150, 135], fill='black') # Nez asymétrique draw.ellipse([125, 145, 135, 155], fill='black') elif image_type == "chat": # Corps (ellipse grise avec contour noir) draw.ellipse([90, 130, 170, 190], fill='#808080', outline='black', width=3) # Tête draw.ellipse([105, 90, 155, 140], fill='#808080', outline='black', width=3) # Oreilles triangulaires (asymétriques) draw.polygon([115, 90, 125, 70, 135, 90], fill='#808080', outline='black', width=2) draw.polygon([125, 95, 135, 75, 145, 95], fill='#808080', outline='black', width=2) # Décalée # Intérieur des oreilles (rose) draw.polygon([119, 86, 124, 76, 131, 86], fill='#FFB6C1') draw.polygon([129, 91, 134, 81, 141, 91], fill='#FFB6C1') # Yeux (grands ronds blancs) draw.ellipse([112, 102, 128, 118], fill='white', outline='black', width=2) draw.ellipse([132, 102, 148, 118], fill='white', outline='black', width=2) # Pupilles noires draw.ellipse([118, 108, 122, 112], fill='black') draw.ellipse([138, 108, 142, 112], fill='black') # Museau triangulaire draw.polygon([125, 125, 135, 125, 130, 132], fill='#FFB6C1', outline='black', width=1) # Langue rose qui dépasse (asymétrique !) draw.ellipse([128, 132, 140, 142], fill='#FF69B4', outline='black', width=1) # Moustaches (asymétriques) draw.line([90, 120, 110, 122], fill='black', width=2) draw.line([90, 125, 110, 125], fill='black', width=2) draw.line([150, 120, 170, 118], fill='black', width=2) # Légèrement différente draw.line([150, 125, 170, 127], fill='black', width=2) # Queue (courbe asymétrique noire) draw.arc([160, 120, 200, 180], 0, 180, fill='black', width=8) return img # Définir les transformations transformations = { "identite": { "name": "Identité (aucune transformation)", "matrix": np.array([[1, 0], [0, 1]]), "explanation": "La matrice identité ne change rien à l'image" }, "rotation_90": { "name": "Rotation 90° (sens horaire)", "matrix": np.array([[0, 1], [-1, 0]]), "explanation": "Rotation d'un quart de tour vers la droite" }, "symetrie_h": { "name": "Symétrie horizontale", "matrix": np.array([[-1, 0], [0, 1]]), "explanation": "Miroir vertical - l'image se retourne de gauche à droite" }, "symetrie_v": { "name": "Symétrie verticale", "matrix": np.array([[1, 0], [0, -1]]), "explanation": "Miroir horizontal - l'image se retourne de haut en bas" }, "homothetie_2": { "name": "Homothétie x2", "matrix": np.array([[2, 0], [0, 2]]), "explanation": "Agrandissement d'un facteur 2 dans toutes les directions" }, "homothetie_05": { "name": "Homothétie x0.5", "matrix": np.array([[0.5, 0], [0, 0.5]]), "explanation": "Réduction d'un facteur 2 dans toutes les directions" }, "cisaillement": { "name": "Cisaillement", "matrix": np.array([[1, 0.5], [0, 1]]), "explanation": "Déformation oblique - l'image 'penche' vers la droite" } } def draw_grid_overlay(ax, matrix, color='gray', alpha=0.3, grid_range=150, spacing=20): """Dessine un quadrillage transformé par la matrice""" # Créer les points du quadrillage x_lines = np.arange(-grid_range, grid_range + spacing, spacing) y_lines = np.arange(-grid_range, grid_range + spacing, spacing) # Lignes verticales for x in x_lines: y_points = np.array([[-grid_range], [grid_range]]) x_points = np.array([[x], [x]]) # Appliquer la transformation points = np.vstack([x_points.flatten(), y_points.flatten()]) transformed = matrix @ points ax.plot(transformed[0], transformed[1], color=color, alpha=alpha, linewidth=0.8) # Lignes horizontales for y in y_lines: x_points = np.array([[-grid_range], [grid_range]]) y_points = np.array([[y], [y]]) # Appliquer la transformation points = np.vstack([x_points.flatten(), y_points.flatten()]) transformed = matrix @ points ax.plot(transformed[0], transformed[1], color=color, alpha=alpha, linewidth=0.8) def transform_image_pixels(img_array, matrix): """Transforme une image pixel par pixel avec la matrice donnée""" height, width = img_array.shape[:2] # Créer une image de sortie (fond blanc) if len(img_array.shape) == 3: transformed = np.ones((height, width, 3), dtype=np.uint8) * 255 else: transformed = np.ones((height, width), dtype=np.uint8) * 255 # Centrer les coordonnées center_x, center_y = width // 2, height // 2 # Pour chaque pixel de l'image de sortie for y in range(height): for x in range(width): # Coordonnées centrées coord_x = x - center_x coord_y = center_y - y # Inverser Y pour avoir origine en bas à gauche # Appliquer la transformation inverse pour trouver le pixel source try: inv_matrix = np.linalg.inv(matrix) original_coord = inv_matrix @ np.array([coord_x, coord_y]) # Reconvertir en coordonnées image orig_x = int(original_coord[0] + center_x) orig_y = int(center_y - original_coord[1]) # Vérifier si le pixel source est dans l'image if 0 <= orig_x < width and 0 <= orig_y < height: transformed[y, x] = img_array[orig_y, orig_x] except np.linalg.LinAlgError: # Matrice non inversible, laisser blanc pass return transformed def apply_transformation(image_choice, transform_choice): """Applique la transformation et crée la visualisation comparative""" # Créer l'image de test original_img = create_test_image(image_choice) original_array = np.array(original_img) # Récupérer la transformation transform_info = transformations[transform_choice] matrix = transform_info["matrix"] # Transformer l'image transformed_array = transform_image_pixels(original_array, matrix) transformed_img = Image.fromarray(transformed_array) # Créer la figure avec deux sous-graphiques fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(12, 6)) #fig.suptitle('Transformations Matricielles', fontsize=16, fontweight='bold') # Image originale ax1.imshow(original_img, extent=[-128, 128, -128, 128]) ax1.set_title('Image originale', fontsize=14, color='#FF1493', fontweight='bold', bbox=dict(boxstyle="round,pad=0.3", facecolor='#FFB6C1', alpha=0.7)) # Quadrillage original draw_grid_overlay(ax1, np.eye(2), alpha=0.4) # Cadre rose for spine in ax1.spines.values(): spine.set_color('#FF1493') spine.set_linewidth(3) ax1.set_xlim(-150, 150) ax1.set_ylim(-150, 150) ax1.set_aspect('equal') ax1.grid(False) ax1.set_xticks([]) ax1.set_yticks([]) # Image transformée ax2.imshow(transformed_img, extent=[-128, 128, -128, 128]) ax2.set_title('Après la transformation !', fontsize=14, color='#8A2BE2', fontweight='bold', bbox=dict(boxstyle="round,pad=0.3", facecolor='#DDA0DD', alpha=0.7)) # Quadrillage fixe (même que l'original pour comparaison) draw_grid_overlay(ax2, np.eye(2), alpha=0.4) # Cadre violet for spine in ax2.spines.values(): spine.set_color('#8A2BE2') spine.set_linewidth(3) ax2.set_xlim(-150, 150) ax2.set_ylim(-150, 150) ax2.set_aspect('equal') ax2.grid(False) ax2.set_xticks([]) ax2.set_yticks([]) plt.tight_layout() # Convertir en image pour Gradio buf = io.BytesIO() plt.savefig(buf, format='png', dpi=100, bbox_inches='tight') buf.seek(0) result_img = Image.open(buf) plt.close() # Créer le texte d'explication matrix_str = f"[{matrix[0,0]:4.1f}, {matrix[0,1]:4.1f}]\n[{matrix[1,0]:4.1f}, {matrix[1,1]:4.1f}]" explanation = f"📐 **Matrice de transformation :**\n\n```\n{matrix_str}\n```\n\n💡 **Explication :** {transform_info['explanation']}" return result_img, explanation # Interface Gradio def create_interface(): with gr.Blocks(title="🦄 Transformations Matricielles", theme=gr.themes.Soft()) as demo: gr.Markdown(""" # 🦄 Transformations Matricielles 🦄 ### Visualisez les effets de transformations matricielles sur des images ! """) with gr.Row(): with gr.Column(): # Choix de l'image image_choice = gr.Dropdown( choices=[("Licorne 🦄", "licorne"), ("Nounours 🧸", "nounours"), ("Chat 🐱", "chat")], value="licorne", label="1️⃣ Choisissez votre image :" ) # Choix de la transformation transform_choice = gr.Dropdown( choices=[(info["name"], key) for key, info in transformations.items()], value="identite", label="2️⃣ Choisissez votre transformation :" ) # Explication de la matrice explanation_text = gr.Markdown(value="", label="Matrice et explication") # Résultat result_image = gr.Image(label="Visualisation", type="pil") # Auto-update quand on change les paramètres def update_all(img_choice, trans_choice): result_img, explanation = apply_transformation(img_choice, trans_choice) return result_img, explanation image_choice.change(update_all, [image_choice, transform_choice], [result_image, explanation_text]) transform_choice.change(update_all, [image_choice, transform_choice], [result_image, explanation_text]) # Initialisation demo.load(update_all, [image_choice, transform_choice], [result_image, explanation_text]) gr.Markdown(""" --- 💡 **Astuce :** Vous pouvez observer les effets des matrices sur l'image initiale : - **homothétie** : les objets changent de taille - **rotation** : tout tourne ensemble - **cisaillement** : les images s'aplatissent """) return demo # Lancer l'application if __name__ == "__main__": demo = create_interface() demo.launch()