Spaces:
Sleeping
Sleeping
| 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() | |