{"metadata":{"kernelspec":{"language":"python","display_name":"Python 3","name":"python3"},"language_info":{"name":"python","version":"3.11.13","mimetype":"text/x-python","codemirror_mode":{"name":"ipython","version":3},"pygments_lexer":"ipython3","nbconvert_exporter":"python","file_extension":".py"},"kaggle":{"accelerator":"none","dataSources":[{"sourceId":108394,"databundleVersionId":14167939,"sourceType":"competition"}],"dockerImageVersionId":31153,"isInternetEnabled":true,"language":"python","sourceType":"notebook","isGpuEnabled":false}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"code","source":"import pandas as pd\nimport numpy as np\nimport os\nimport glob\n\n# Définition du chemin de base (ajusté selon votre structure fournie)\nBASE_PATH = \"/kaggle/input/h690/h690/h690\"\n# Assurez-vous que ce chemin est correct dans votre session Kaggle\nprint(f\"Chemin de base défini : {BASE_PATH}\")","metadata":{"_uuid":"8f2839f25d086af736a60e9eeb907d3b93b6e0e5","_cell_guid":"b1076dfc-b9ad-4769-8c92-a6c4dae69d19","trusted":true,"execution":{"iopub.status.busy":"2025-11-05T01:13:21.724326Z","iopub.execute_input":"2025-11-05T01:13:21.724803Z","iopub.status.idle":"2025-11-05T01:13:22.246167Z","shell.execute_reply.started":"2025-11-05T01:13:21.724775Z","shell.execute_reply":"2025-11-05T01:13:22.245009Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Chemin d'accès au fichier de métadonnées\nMETADATA_PATH = os.path.join(BASE_PATH, 'jd_sherds_info.csv')\n\n# Charger le DataFrame principal\ndf_meta = pd.read_csv(METADATA_PATH)\n\nprint(f\"Chargement de {METADATA_PATH} réussi.\")\nprint(\"Aperçu des données :\")\nprint(df_meta.head())\nprint(\"\\nInformations sur les colonnes et valeurs manquantes :\")\nprint(df_meta.info())","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-05T01:13:24.526679Z","iopub.execute_input":"2025-11-05T01:13:24.527545Z","iopub.status.idle":"2025-11-05T01:13:24.755180Z","shell.execute_reply.started":"2025-11-05T01:13:24.527515Z","shell.execute_reply":"2025-11-05T01:13:24.753825Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"IMAGE_DIR = os.path.join(BASE_PATH, 'sherd_images')\n\n# Créer une colonne pour l'ID du fragment\ndf_meta['sherd_id'] = df_meta['image_id'].apply(lambda x: x.split('_')[0])\n\n# Créer la colonne de chemin d'accès complet pour chaque image\ndef get_image_path(row):\n    # Les images sont nommées JDxxxxx_exterior.jpg ou JDxxxxx_interior.jpg\n    return os.path.join(IMAGE_DIR, f\"{row['image_id']}.jpg\")\n\ndf_meta['path'] = df_meta.apply(get_image_path, axis=1)\n\nprint(f\"\\nExemple de chemin d'accès créé: {df_meta['path'].iloc[0]}\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-05T01:13:28.052329Z","iopub.execute_input":"2025-11-05T01:13:28.052755Z","iopub.status.idle":"2025-11-05T01:13:28.335846Z","shell.execute_reply.started":"2025-11-05T01:13:28.052729Z","shell.execute_reply":"2025-11-05T01:13:28.334673Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# 1. Nettoyage de la colonne 'unit' (couche stratigraphique)\n# On simplifie pour l'entraînement : extraction du numéro de la couche\ndef clean_unit(unit_str):\n    if pd.isna(unit_str):\n        return 'UNKNOWN'\n    if unit_str.startswith('L'):\n        return int(unit_str[1:]) # Convertit L01 -> 1, L14 -> 14\n    else:\n        # Regrouper M, Z et autres cas spéciaux\n        return 'UNKNOWN' \n\ndf_meta['layer_num'] = df_meta['unit'].apply(clean_unit)\ndf_meta['is_unknown_layer'] = (df_meta['layer_num'] == 'UNKNOWN')\n\n# 2. Filtrage par 'part' (Partie du vase)\n# Remplir les valeurs manquantes pour la cohérence\ndf_meta['part'] = df_meta['part'].fillna('UNKNOWN_PART')\n\nprint(\"\\nRépartition des fragments par partie (part):\")\nprint(df_meta['part'].value_counts())","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-05T01:13:31.991310Z","iopub.execute_input":"2025-11-05T01:13:31.991658Z","iopub.status.idle":"2025-11-05T01:13:32.051812Z","shell.execute_reply.started":"2025-11-05T01:13:31.991636Z","shell.execute_reply":"2025-11-05T01:13:32.050536Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import pandas as pd\nimport os\nimport glob\nfrom typing import Dict, List, Tuple\nimport itertools\nimport random\nimport numpy as np\n\n# ======================================================================\n# 0. DÉFINITION DES CHEMINS (À VÉRIFIER) ⚠️\n# ======================================================================\n\n# REMPLACER CETTE VALEUR PAR LE VRAI CHEMIN RACINE DE VOS DONNÉES.\n# Ex: /kaggle/input/h690/h690/h690/\nBASE_PATH = '/kaggle/input/h690/h690/h690/' \nIMAGE_DIR = os.path.join(BASE_PATH, 'sherd_images')\nINFO_FILE_PATH = os.path.join(BASE_PATH, 'jd_sherds_info.csv') \n\n# Définition des noms de colonnes :\nFRAGMENT_COL = 'sherd_id' # Colonne de l'ID du fragment (JDxxxxx)\nUNIT_COL = 'unit'         # Colonne de l'unité d'excavation\nTYPE_COL = 'type'         # Colonne du type de poterie (vessel type)\n\nprint(f\"BASE_PATH : {BASE_PATH}\")\n\n# ======================================================================\n# A. DÉFINITION DE LA FONCTION 1 : Traitement de Tous les Fragments (2D -> 3D)\n# ======================================================================\n\ndef process_all_fragments_by_id(image_dir: str, target_points: int) -> Dict[str, np.ndarray]:\n    \"\"\"\n    Parcourt toutes les images pour extraire et normaliser les nuages de points de fracture.\n    \n    Retourne un dictionnaire {ID_FRAGMENT_COMPLET (ex: JD00001_exterior): Nuage_de_points_3D}\n    \n    ⚠️ ATTENTION: REMPLACER LA SIMULATION par votre VRAIE logique 2D->3D\n    \"\"\"\n    all_point_clouds = {}\n    \n    image_paths = glob.glob(os.path.join(image_dir, '*_exterior.jpg'))\n    image_paths.extend(glob.glob(os.path.join(image_dir, '*_interior.jpg')))\n    \n    print(f\"Tentative de traitement de {len(image_paths)} images...\")\n    target_points = 1024 # Assurer que la variable est définie si non passée\n\n    for i, path in enumerate(image_paths):\n        full_id = os.path.basename(path).replace('.jpg', '') \n        \n        # --- SIMULATION (À REMPLACER) ---\n        point_cloud = np.random.rand(target_points, 3).astype(np.float32) \n        # --- FIN DE LA SIMULATION ---\n        \n        all_point_clouds[full_id] = point_cloud\n        \n        if (i + 1) % 5000 == 0:\n             print(f\"  Fragments traités: {i+1}\")\n             \n    print(f\"Traitement terminé. {len(all_point_clouds)} nuages de points 3D générés/simulés.\")\n    return all_point_clouds\n\n# ----------------------------------------------------------------------\n\n# ======================================================================\n# B. DÉFINITION DE LA FONCTION 2 : Création des Paires DML (Positives et Négatives)\n# ======================================================================\n\ndef create_dml_pairs(all_point_clouds: Dict[str, np.ndarray], assembly_map_full: Dict[str, str]) -> List[Tuple[str, str, int]]:\n    \"\"\"\n    Crée les paires positives (label=1) et négatives (label=0) pour l'entraînement DML.\n    \"\"\"\n    all_fragments = list(all_point_clouds.keys())\n    \n    # --- Paires Positives (Label=1) ---\n    positive_pairs = []\n    groups = {}\n    for full_id, group in assembly_map_full.items():\n        if full_id in all_fragments: \n            if group not in groups:\n                groups[group] = []\n            groups[group].append(full_id)\n            \n    for group_name, sherd_list in groups.items():\n        if len(sherd_list) >= 2:\n            for sherd_A, sherd_B in itertools.combinations(sherd_list, 2):\n                positive_pairs.append(tuple(sorted([sherd_A, sherd_B])) + (1,))\n                \n    df_positive_pairs = pd.DataFrame(positive_pairs, columns=['sherd_A', 'sherd_B', 'label']).drop_duplicates()\n    n_positives = len(df_positive_pairs)\n    print(f\"  - {n_positives} Paires POSITIVES générées.\")\n\n\n    # --- Paires Négatives (Label=0) ---\n    negative_pairs = []\n    target_negatives = max(100, n_positives * 2) \n    \n    all_labeled_fragments = [id for id in all_fragments if id in assembly_map_full]\n    max_attempts = len(all_labeled_fragments) * 20\n    attempts = 0\n\n    while len(negative_pairs) < target_negatives and attempts < max_attempts:\n        if len(all_labeled_fragments) < 2: break\n            \n        sherd_A, sherd_B = random.sample(all_labeled_fragments, 2)\n        \n        sherd_A, sherd_B = sorted([sherd_A, sherd_B])\n        \n        group_A = assembly_map_full.get(sherd_A)\n        group_B = assembly_map_full.get(sherd_B)\n\n        if group_A and group_B and group_A != group_B:\n            pair = (sherd_A, sherd_B, 0)\n            if pair not in negative_pairs:\n                negative_pairs.append(pair)\n        \n        attempts += 1\n        \n    df_negative_pairs = pd.DataFrame(negative_pairs, columns=['sherd_A', 'sherd_B', 'label'])\n    n_negatives = len(df_negative_pairs)\n    print(f\"  - {n_negatives} Paires NÉGATIVES générées.\")\n\n    # --- Dataset Final ---\n    df_dml_dataset = pd.concat([df_positive_pairs, df_negative_pairs]).sample(frac=1).reset_index(drop=True)\n\n    return list(df_dml_dataset.itertuples(index=False, name=None))\n\n# ----------------------------------------------------------------------\n\n# ======================================================================\n# C. LOGIQUE D'EXÉCUTION DU PIPELINE DML (DÉMARRAGE)\n# ======================================================================\n\n# Charger le fichier d'information complet\ntry:\n    df_info = pd.read_csv(INFO_FILE_PATH)\nexcept FileNotFoundError:\n    print(f\"❌ ERREUR: Le fichier {INFO_FILE_PATH} est introuvable. Vérifiez BASE_PATH.\")\n    exit()\n\n# 1. CRÉATION DU SIGNAL DE VÉRITÉ TERRAIN (unit + type)\ndf_info['ASSEMBLY_GROUP_ID'] = df_info[UNIT_COL].astype(str) + '_' + df_info[TYPE_COL].astype(str)\n\n# Éliminer les lignes qui n'ont pas de groupe défini\ndf_info_labeled = df_info.dropna(subset=[UNIT_COL, TYPE_COL]).copy()\n\n# Créer la carte de base (Fragment ID sans suffixe -> Assembly Group)\nASSEMBLY_MAP_BASE = df_info_labeled.set_index(FRAGMENT_COL)['ASSEMBLY_GROUP_ID'].to_dict()\n\nprint(f\"\\n✅ Fragments de base labellisés (unit + type) : {len(ASSEMBLY_MAP_BASE)}\")\n\n# 2. EXTENSION DE LA CARTE AUX VUES 3D (EXTERIOR/INTERIOR)\nASSEMBLY_MAP_FULL = {}\nfor base_id, group_name in ASSEMBLY_MAP_BASE.items():\n    ASSEMBLY_MAP_FULL[f\"{base_id}_exterior\"] = group_name\n    ASSEMBLY_MAP_FULL[f\"{base_id}_interior\"] = group_name\n    \nprint(f\"✅ Vérité Terrain (ASSEMBLY_MAP_FULL) créée pour {len(ASSEMBLY_MAP_FULL)} vues de fragments.\")\n\n\n# 3. EXÉCUTION DU PIPELINE DML\n\nprint(\"\\n--- Démarrage de la génération des nuages de points 3D (Pipeline 2D -> 3D) ---\")\nall_point_clouds = process_all_fragments_by_id(IMAGE_DIR, target_points=1024)\n\n# Diagnostic de l'intersection\nprocessed_ids = set(all_point_clouds.keys())\nmapped_ids = set(ASSEMBLY_MAP_FULL.keys())\noverlap_ids = processed_ids.intersection(mapped_ids)\nprint(f\"\\n--- DIAGNOSTIC D'INTERSECTION ---\")\nprint(f\"Fragments 3D avec Vérité Terrain correspondante (overlap) : {len(overlap_ids)}\")\n\nif len(overlap_ids) < 2:\n    print(\"❌ ÉCHEC : Moins de 2 fragments en commun. Le set d'entraînement est insuffisant.\")\nelse:\n    print(\"\\n--- Création des paires d'entraînement DML ---\")\n    dml_training_pairs = create_dml_pairs(all_point_clouds, ASSEMBLY_MAP_FULL)\n\n    print(\"\\n--- VÉRIFICATION FINALE DES DONNÉES D'ENTRAÎNEMENT ---\")\n    print(f\"Total de nuages de points 3D générés : {len(all_point_clouds)}\")\n    print(f\"Total des paires DML (Positif/Négatif) pour l'entraînement : {len(dml_training_pairs)}\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-05T01:44:13.287933Z","iopub.execute_input":"2025-11-05T01:44:13.288336Z","iopub.status.idle":"2025-11-05T01:44:20.896896Z","shell.execute_reply.started":"2025-11-05T01:44:13.288309Z","shell.execute_reply":"2025-11-05T01:44:20.895776Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import cv2\nimport matplotlib.pyplot as plt\nimport os\n\n# Définition des variables de chemin (reprise des étapes précédentes)\n# Assumons que BASE_PATH et IMAGE_DIR sont définis:\n# BASE_PATH = \"/kaggle/input/h690/h690/h690\" \n# IMAGE_DIR = os.path.join(BASE_PATH, 'sherd_images') \n\n# Chemin d'accès à l'image du fragment JD00001 en utilisant la variable IMAGE_DIR\nEXAMPLE_IMG_PATH = os.path.join(IMAGE_DIR, 'JD00001_exterior.jpg')","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-05T01:13:46.737332Z","iopub.execute_input":"2025-11-05T01:13:46.737727Z","iopub.status.idle":"2025-11-05T01:13:47.030699Z","shell.execute_reply.started":"2025-11-05T01:13:46.737700Z","shell.execute_reply":"2025-11-05T01:13:47.029319Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import cv2\nimport matplotlib.pyplot as plt\nimport os\nimport numpy as np\n\ndef extract_final_fragment_contour(image_path, show_plot=False):\n    \"\"\"\n    Isole et nettoie le contour du fragment sur un fond clair.\n    Utilise le Recadrage, Canny, la Dilatation (pour la fermeture) et le Masquage (pour le nettoyage).\n    \"\"\"\n    img = cv2.imread(image_path)\n    if img is None: \n        print(f\"Erreur de chargement de l'image : {image_path}\")\n        return None, None\n\n    # --- 1. Recadrage pour éliminer le cadre externe ---\n    h, w, _ = img.shape\n    crop_percent = 0.05 \n    crop_h = int(h * crop_percent)\n    crop_w = int(w * crop_percent)\n    img_cropped = img[crop_h:h-crop_h, crop_w:w-crop_w]\n    \n    img_gray = cv2.cvtColor(img_cropped, cv2.COLOR_BGR2GRAY)\n    \n    # --- 2. Flou Gaussien ---\n    img_blurred = cv2.GaussianBlur(img_gray, (5, 5), 0)\n    \n    # --- 3. Détection de Bords Canny ---\n    edges = cv2.Canny(img_blurred, 50, 150) \n    \n    # --- 4. Dilatation (pour fermer les bords) ---\n    kernel = np.ones((7, 7), np.uint8) \n    edges_dilated = cv2.dilate(edges, kernel, iterations=3) \n    \n    # --- 5. Trouver les contours sur les bords dilatés ---\n    contours, _ = cv2.findContours(edges_dilated, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)\n\n    if not contours:\n        print(\"Aucun contour principal trouvé après Canny/Dilatation. Ajustez les hyperparamètres.\")\n        return None, None\n        \n    # Le plus grand contour est notre fragment (le seul à conserver)\n    largest_contour = max(contours, key=cv2.contourArea)\n    \n    # -------------------------------------------------------------------\n    # --- 6. Nettoyage du Contour (Création d'un Masque Propre) ---\n    # Pour éliminer les traits internes et les petits contours parasites,\n    # nous créons un masque rempli uniquement à partir du plus grand contour.\n    \n    clean_mask = np.zeros(img_cropped.shape[:2], dtype=np.uint8)\n    # Remplir le plus grand contour en blanc (255)\n    cv2.drawContours(clean_mask, [largest_contour], 0, 255, thickness=cv2.FILLED)\n    \n    # Maintenant, nous récupérons le contour du masque PROPRE\n    final_contours, _ = cv2.findContours(clean_mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)\n    final_largest_contour = final_contours[0] if final_contours else largest_contour\n    # -------------------------------------------------------------------\n\n    # --- 7. Affichage (pour vérification) ---\n    if show_plot:\n        contour_img = img_cropped.copy()\n        cv2.drawContours(contour_img, [final_largest_contour], -1, (0, 0, 255), 3) # Contour final en Bleu\n\n        plt.figure(figsize=(12, 5))\n        \n        plt.subplot(1, 3, 1)\n        plt.title(\"Bords Canny (Dilatés)\")\n        plt.imshow(edges_dilated, cmap='gray')\n        plt.axis('off')\n\n        plt.subplot(1, 3, 2)\n        plt.title(\"Masque Binaire Nettoyé\")\n        plt.imshow(clean_mask, cmap='gray')\n        plt.axis('off')\n\n        plt.subplot(1, 3, 3)\n        plt.title(\"Contour Final (Objet Isolé)\")\n        plt.imshow(cv2.cvtColor(contour_img, cv2.COLOR_BGR2RGB))\n        plt.axis('off')\n        plt.show()\n\n    # Le contour renvoyé est celui du masque propre\n    return final_largest_contour, clean_mask","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-05T01:13:52.406069Z","iopub.execute_input":"2025-11-05T01:13:52.407495Z","iopub.status.idle":"2025-11-05T01:13:52.424316Z","shell.execute_reply.started":"2025-11-05T01:13:52.407448Z","shell.execute_reply":"2025-11-05T01:13:52.422914Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import cv2\nimport matplotlib.pyplot as plt\nimport os\nimport numpy as np\n\ndef visualize_fracture_area(largest_contour, fragment_mask, img_cropped_shape, show_plot=True):\n    \"\"\"\n    Calcule et visualise l'Enveloppe Convexe pour identifier la zone de fracture.\n    \n    Args:\n        largest_contour (np.ndarray): Le contour principal nettoyé du fragment.\n        fragment_mask (np.ndarray): Le masque binaire propre du fragment (pour la taille).\n        img_cropped_shape (tuple): La forme de l'image recadrée originale (pour la visualisation).\n        show_plot (bool): Afficher la visualisation.\n    \"\"\"\n    if largest_contour is None or len(largest_contour) < 3:\n        print(\"Contour non valide pour l'analyse de convexité.\")\n        return largest_contour\n\n    # 1. Calculer l'Enveloppe Convexe (H)\n    convex_hull = cv2.convexHull(largest_contour, returnPoints=True)\n    \n    # NOTE: L'extraction des Défauts de Convexité (points 2 et 3) est laissée en commentaire.\n    # C'est la bonne approche théorique, mais pour simplifier le pipeline DML,\n    # nous laissons le DML apprendre la différence entre les segments lisses (Convex Hull)\n    # et les segments irréguliers (Fracture).\n    \n    # 2. Préparation de l'image de visualisation\n    # Créer une image 3 canaux (BGR) noire de la taille de l'image recadrée\n    img_viz = np.zeros(img_cropped_shape, dtype=np.uint8) \n    \n    # --- 3. Dessin des Contours ---\n    \n    # Dessiner le Masque/Fragment (Gris) comme arrière-plan visuel\n    cv2.drawContours(img_viz, [largest_contour], -1, (100, 100, 100), thickness=cv2.FILLED)\n\n    # Dessiner le Contour Original (Vert)\n    cv2.drawContours(img_viz, [largest_contour], -1, (0, 255, 0), 2)\n    \n    # Dessiner l'Enveloppe Convexe (Rouge)\n    # C'est la ligne rouge qui \"coupera les coins\" de la fracture\n    cv2.drawContours(img_viz, [convex_hull], -1, (255, 0, 0), 2) \n    \n    if show_plot:\n        plt.figure(figsize=(8, 8))\n        plt.title(\"Contour (Vert) vs. Enveloppe Convexe (Rouge)\")\n        plt.imshow(cv2.cvtColor(img_viz, cv2.COLOR_BGR2RGB))\n        plt.axis('off')\n        plt.show()\n\n    # Pour l'entrée du DML, nous retournons le contour entier.\n    return largest_contour","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-05T01:13:56.852482Z","iopub.execute_input":"2025-11-05T01:13:56.853344Z","iopub.status.idle":"2025-11-05T01:13:56.862910Z","shell.execute_reply.started":"2025-11-05T01:13:56.853313Z","shell.execute_reply":"2025-11-05T01:13:56.861784Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Assurez-vous d'avoir défini EXAMPLE_IMG_PATH et IMAGE_DIR dans une cellule précédente\n# import os\n# IMAGE_DIR = \"/kaggle/input/h690/h690/h690/sherd_images\"\n# EXAMPLE_IMG_PATH = os.path.join(IMAGE_DIR, 'JD00001_exterior.jpg') \n\n# ----------------------------------------------------------------------\n# Étape 1 : Extraction et Nettoyage du Contour du Fragment\n# ----------------------------------------------------------------------\nfinal_contour, clean_mask = extract_final_fragment_contour(EXAMPLE_IMG_PATH, show_plot=True) \n\nif final_contour is not None:\n    print(f\"\\nContour extrait avec succès : {len(final_contour)} points. Prêt pour l'analyse de convexité.\")\n    \n    # Récupérer la forme de l'image recadrée pour la visualisation (étape 7 de la fonction précédente)\n    # Si vous avez besoin de la forme exacte de l'image recadrée (pour aligner les couches)\n    # on peut la déduire de la taille du masque\n    img_cropped_shape = (*clean_mask.shape, 3) \n    \n    # ------------------------------------------------------------------\n    # Étape 2 : Analyse de Convexité (Isolation de la Zone de Fracture)\n    # ------------------------------------------------------------------\n    fracture_contour = visualize_fracture_area(\n        final_contour, \n        clean_mask, \n        img_cropped_shape, \n        show_plot=True\n    )\n\n    if fracture_contour is not None:\n        print(\"\\nAnalyse de la zone de fracture réussie. Prêt pour la normalisation 3D.\")\n\nelse:\n    print(\"Échec de l'extraction. Ajustez les hyperparamètres (Canny/Dilatation).\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-05T01:14:00.237886Z","iopub.execute_input":"2025-11-05T01:14:00.238206Z","iopub.status.idle":"2025-11-05T01:14:01.302326Z","shell.execute_reply.started":"2025-11-05T01:14:00.238185Z","shell.execute_reply":"2025-11-05T01:14:01.300939Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import numpy as np\nfrom scipy.spatial.distance import pdist\n\ndef normalize_and_create_point_cloud(contours, target_points=1024, thickness=1.0, show_stats=False):\n    \"\"\"\n    Convertit un ou plusieurs contours 2D en un nuage de points 3D normalisé (x, y, z).\n    \n    Args:\n        contours (list or np.ndarray): Une liste de contours (ou un seul contour).\n        target_points (int): Le nombre de points fixe requis par le DML (ex: 1024).\n        thickness (float): L'épaisseur de la poterie (utilisée pour la dimension Z).\n        show_stats (bool): Afficher les statistiques de normalisation.\n\n    Returns:\n        np.ndarray: Nuage de points normalisé de forme (target_points, 3).\n    \"\"\"\n    \n    # 1. Aplatir tous les contours en une seule liste de points (x, y)\n    if isinstance(contours, list):\n        # Concaténer si plusieurs contours sont passés (ex: externe et interne)\n        all_points = np.concatenate(contours, axis=0).squeeze()\n    else:\n        # Si un seul contour (le plus grand) est passé\n        all_points = contours.squeeze()\n        \n    if all_points.ndim == 1:\n        # Cas d'un seul point (rare, mais géré)\n        all_points = np.expand_dims(all_points, axis=0)\n        \n    num_original_points = len(all_points)\n    \n    # 2. Échantillonnage : Réduire ou augmenter le nombre de points à target_points\n    if num_original_points == 0:\n        # Cas d'un contour vide\n        return np.zeros((target_points, 3), dtype=np.float32)\n\n    if num_original_points > target_points:\n        # Réduction par échantillonnage uniforme\n        indices = np.linspace(0, num_original_points - 1, target_points, dtype=int)\n        sampled_points = all_points[indices]\n    elif num_original_points < target_points:\n        # Augmentation par répétition simple\n        ratio = target_points // num_original_points\n        remainder = target_points % num_original_points\n        \n        repeated_points = np.repeat(all_points, ratio, axis=0)\n        sampled_points = np.concatenate([repeated_points, all_points[:remainder]], axis=0)\n    else:\n        sampled_points = all_points\n\n    # S'assurer que sampled_points a exactement target_points lignes\n    if len(sampled_points) != target_points:\n         # Cas très rare, on rééchantillonne\n        indices = np.random.choice(len(sampled_points), size=target_points, replace=True)\n        sampled_points = sampled_points[indices]\n\n\n    # 3. Normalisation (Centrage et Mise à l'Échelle)\n    \n    # Centrage : Soustraire le centre de masse\n    centroid = np.mean(sampled_points, axis=0)\n    normalized_points_2d = sampled_points - centroid\n    \n    # Mise à l'Échelle : Diviser par la distance maximale (pour avoir une échelle unitaire)\n    if len(normalized_points_2d) > 1:\n        max_distance = np.max(pdist(normalized_points_2d))\n        if max_distance > 0:\n            normalized_points_2d /= max_distance\n    \n    # 4. Ajout de la dimension Z (3D)\n    # Z représente l'épaisseur du fragment.\n    \n    z_coords = np.full((target_points, 1), thickness)\n    normalized_point_cloud = np.hstack([normalized_points_2d, z_coords])\n\n    if show_stats:\n        print(f\"Points initiaux: {num_original_points}\")\n        print(f\"Nuage de points normalisé: {normalized_point_cloud.shape}\")\n        print(f\"Centre après normalisation (doit être proche de 0): {np.mean(normalized_point_cloud[:, :2], axis=0)}\")\n        \n    return normalized_point_cloud","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-05T01:14:07.237428Z","iopub.execute_input":"2025-11-05T01:14:07.237866Z","iopub.status.idle":"2025-11-05T01:14:07.438660Z","shell.execute_reply.started":"2025-11-05T01:14:07.237840Z","shell.execute_reply":"2025-11-05T01:14:07.437157Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Assurez-vous d'avoir défini EXAMPLE_IMG_PATH et IMAGE_DIR dans une cellule précédente\n# EXAMPLE_IMG_PATH = os.path.join(IMAGE_DIR, 'JD00001_exterior.jpg') \n# La fonction normalize_and_create_point_cloud doit avoir été définie avant cette cellule.\n\n# ----------------------------------------------------------------------\n# Étape 1 : Extraction et Nettoyage du Contour du Fragment\n# ----------------------------------------------------------------------\nfinal_contour, clean_mask = extract_final_fragment_contour(EXAMPLE_IMG_PATH, show_plot=True) \n\nif final_contour is not None:\n    print(f\"\\nContour extrait avec succès : {len(final_contour)} points. Prêt pour l'analyse de convexité.\")\n    \n    # Récupérer la forme de l'image recadrée (nécessaire pour la visualisation)\n    img_cropped_shape = (*clean_mask.shape, 3) \n    \n    # ------------------------------------------------------------------\n    # Étape 2 : Analyse de Convexité (Visualisation de la zone de Fracture)\n    # ------------------------------------------------------------------\n    # visualize_fracture_area retourne le même contour, mais effectue la vérification visuelle.\n    fracture_contour = visualize_fracture_area(\n        final_contour, \n        clean_mask, \n        img_cropped_shape, \n        show_plot=True\n    )\n\n    if fracture_contour is not None:\n        print(\"\\nAnalyse de la zone de fracture réussie. Maintenant, normalisation 3D.\")\n        \n        # ------------------------------------------------------------------\n        # Étape 3 : Normalisation 3D (Création du Nuage de Points pour le DML)\n        # ------------------------------------------------------------------\n        # Nous utilisons le contour obtenu (fracture_contour) comme entrée.\n        point_cloud = normalize_and_create_point_cloud(\n            contours=fracture_contour, \n            target_points=1024,  # Nombre de points typique pour PointNet\n            thickness=1.0,       # Épaisseur du fragment (peut être ajustée par vos données)\n            show_stats=True\n        )\n\n        print(\"\\n✅ Pipeline de prétraitement 2D -> 3D terminé.\")\n        print(f\"Le Nuage de Points 3D (entrée DML) a la forme : {point_cloud.shape}\")\n\nelse:\n    print(\"❌ Échec de l'extraction. Ajustez les hyperparamètres (Canny/Dilatation).\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-05T01:14:11.548109Z","iopub.execute_input":"2025-11-05T01:14:11.549533Z","iopub.status.idle":"2025-11-05T01:14:12.416930Z","shell.execute_reply.started":"2025-11-05T01:14:11.549428Z","shell.execute_reply":"2025-11-05T01:14:12.415864Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ======================================================================\n# A. DÉFINITION DE LA FONCTION 1 : Traitement de Tous les Fragments (2D -> 3D)\n# ======================================================================\n\ndef process_all_fragments_by_id(image_dir: str, target_points: int) -> Dict[str, np.ndarray]:\n    \"\"\"\n    Parcourt toutes les images pour extraire et normaliser les nuages de points de fracture (LOGIQUE RÉELLE).\n    \"\"\"\n    all_point_clouds = {}\n    \n    image_paths = glob.glob(os.path.join(image_dir, '*_exterior.jpg'))\n    image_paths.extend(glob.glob(os.path.join(image_dir, '*_interior.jpg')))\n    \n    print(f\"Tentative de traitement de {len(image_paths)} images...\")\n    \n    for i, path in enumerate(image_paths):\n        full_id = os.path.basename(path).replace('.jpg', '')\n        \n        try:\n            # --- DÉBUT DE LA LOGIQUE RÉELLE (REMPLACEMENT DE LA SIMULATION) ---\n            \n            # 1. Extraction et Nettoyage du Contour\n            final_contour, clean_mask = extract_final_fragment_contour(path, show_plot=False)\n            \n            if final_contour is None:\n                 # print(f\"ATTENTION: Échec de l'extraction pour {full_id}. Ignoré.\")\n                 continue # Passer au fragment suivant\n\n            # 2. Analyse de Convexité (Récupération du contour pour la normalisation)\n            # NOTE: On passe 'None' pour les arguments d'image dans visualize_fracture_area pour éviter les erreurs,\n            # car seules le contour et le masque sont essentiels pour la logique interne\n            fracture_contour, _ = visualize_fracture_area(final_contour, clean_mask, img_cropped_shape=None, show_plot=False)\n            \n            # 3. Création du Nuage de Points 3D (avec Normalisation)\n            point_cloud = normalize_and_create_point_cloud(\n                contours=fracture_contour,\n                target_points=target_points,\n                thickness=1.0, \n                show_stats=False\n            )\n            # --- FIN DE LA LOGIQUE RÉELLE ---\n            \n            all_point_clouds[full_id] = point_cloud\n            \n        except Exception as e:\n            # print(f\"ERREUR lors du traitement de {full_id}: {e}. Ignoré.\")\n            continue\n        \n        if (i + 1) % 5000 == 0:\n             print(f\"  Fragments traités: {i+1}\")\n             \n    print(f\"Traitement terminé. {len(all_point_clouds)} nuages de points 3D générés RÉEELLEMENT.\")\n    return all_point_clouds","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-05T01:52:33.265449Z","iopub.execute_input":"2025-11-05T01:52:33.266262Z","iopub.status.idle":"2025-11-05T01:52:33.276897Z","shell.execute_reply.started":"2025-11-05T01:52:33.266216Z","shell.execute_reply":"2025-11-05T01:52:33.275225Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import numpy as np\nimport tensorflow as tf\nfrom typing import Dict, List, Tuple\n\n# ======================================================================\n# CLASSE DE GÉNÉRATEUR DML (HÉRITANT DE tf.keras.utils.Sequence) - CORRIGÉE\n# ======================================================================\n\nclass DMLDataGenerator(tf.keras.utils.Sequence):\n    \"\"\"\n    Générateur de données pour l'entraînement DML (Deep Metric Learning)\n    en utilisant une liste de paires (sherd_A, sherd_B, label).\n    \"\"\"\n\n    def __init__(self, \n                 dml_pairs: List[Tuple[str, str, int]], \n                 all_point_clouds: Dict[str, np.ndarray], \n                 batch_size: int = 32,\n                 shuffle: bool = True):\n        \n        self.dml_pairs = dml_pairs\n        self.all_point_clouds = all_point_clouds\n        self.batch_size = batch_size\n        self.shuffle = shuffle\n        self.on_epoch_end()\n\n    def __len__(self):\n        # 'Nombre de lots (batches) par époque'\n        return int(np.floor(len(self.dml_pairs) / self.batch_size))\n\n    def on_epoch_end(self):\n        # Correction de l'erreur de syntaxe en utilisant des guillemets doubles\n        \"\"\"Mise à jour de l'index après chaque époque (pour le brassage)\"\"\" \n        self.indexes = np.arange(len(self.dml_pairs))\n        if self.shuffle == True:\n            np.random.shuffle(self.indexes)\n\n    def __getitem__(self, index):\n        # 'Générer un lot de données'\n        \n        # Sélectionner les indices pour le lot actuel\n        indexes = self.indexes[index*self.batch_size:(index+1)*self.batch_size]\n        batch_pairs = [self.dml_pairs[k] for k in indexes]\n\n        # Déterminer la forme d'un nuage de points\n        if not self.all_point_clouds:\n            raise ValueError(\"Le dictionnaire all_point_clouds est vide.\")\n            \n        # Détermination de la forme (N points, 3 coordonnées)\n        sample_id = batch_pairs[0][0] \n        if sample_id not in self.all_point_clouds:\n            for sherd_A, _, _ in batch_pairs:\n                 if sherd_A in self.all_point_clouds:\n                    sample_id = sherd_A\n                    break\n            \n        pc_shape = self.all_point_clouds[sample_id].shape if sample_id in self.all_point_clouds else (1024, 3) \n        \n        X_A = np.empty((self.batch_size, *pc_shape), dtype=np.float32)\n        X_B = np.empty((self.batch_size, *pc_shape), dtype=np.float32)\n        Y = np.empty((self.batch_size), dtype=int)\n\n        # Remplir les tableaux\n        for i, (sherd_A, sherd_B, label) in enumerate(batch_pairs):\n            pc_A = self.all_point_clouds.get(sherd_A)\n            pc_B = self.all_point_clouds.get(sherd_B)\n\n            if pc_A is None or pc_B is None:\n                pc_A = np.zeros(pc_shape, dtype=np.float32)\n                pc_B = np.zeros(pc_shape, dtype=np.float32)\n\n            X_A[i,] = pc_A\n            X_B[i,] = pc_B\n            Y[i] = label\n\n        return ([X_A, X_B], Y)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-05T01:54:16.995412Z","iopub.execute_input":"2025-11-05T01:54:16.995774Z","iopub.status.idle":"2025-11-05T01:54:36.989735Z","shell.execute_reply.started":"2025-11-05T01:54:16.995750Z","shell.execute_reply":"2025-11-05T01:54:36.988656Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import matplotlib.pyplot as plt\nfrom mpl_toolkits.mplot3d import Axes3D\nimport numpy as np\n\n# ⚠️ Vérifiez si 'all_point_clouds' contient des données réelles\nif 'JD00001_exterior' in all_point_clouds:\n    \n    # 1. Récupérer le nuage de points pour l'ID d'exemple\n    pc = all_point_clouds['JD00001_exterior']\n    \n    # pc a la forme (1024, 3) où les colonnes sont (X, Y, Z)\n    x = pc[:, 0]\n    y = pc[:, 1]\n    z = pc[:, 2] # C'est votre \"thickness\" (épaisseur) normalisée\n    \n    # 2. Créer la figure 3D\n    fig = plt.figure(figsize=(10, 10))\n    ax = fig.add_subplot(111, projection='3d')\n    \n    # 3. Afficher les points\n    ax.scatter(x, y, z, c=z, cmap='viridis', marker='.')\n    \n    # Configuration des axes\n    ax.set_xlabel('X Dimension')\n    ax.set_ylabel('Y Dimension')\n    ax.set_zlabel('Z (Épaisseur)')\n    ax.set_title(f\"Nuage de Points 3D pour JD00001_exterior (N={len(pc)})\")\n    \n    # Mettre l'échelle des axes en mode \"égal\"\n    max_range = np.array([x.max()-x.min(), y.max()-y.min(), z.max()-z.min()]).max() / 2.0\n    mid_x = (x.max()+x.min()) * 0.5\n    mid_y = (y.max()+y.min()) * 0.5\n    mid_z = (z.max()+z.min()) * 0.5\n    ax.set_xlim(mid_x - max_range, mid_x + max_range)\n    ax.set_ylim(mid_y - max_range, mid_y + max_range)\n    ax.set_zlim(mid_z - max_range, mid_z + max_range)\n    \n    plt.show()\n\nelse:\n    print(\"Le nuage de points JD00001_exterior n'est pas encore dans 'all_point_clouds'.\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-05T02:01:18.137039Z","iopub.execute_input":"2025-11-05T02:01:18.137375Z","iopub.status.idle":"2025-11-05T02:01:18.412739Z","shell.execute_reply.started":"2025-11-05T02:01:18.137353Z","shell.execute_reply":"2025-11-05T02:01:18.411482Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import tensorflow as tf\nfrom tensorflow.keras.models import Model\nfrom tensorflow.keras.layers import Layer, Input, Conv1D, Dense, GlobalMaxPooling1D, BatchNormalization\n\n# --- COUCHES ET FONCTIONS UTILITAIRES ---\n\n# 1. Couche de Normalisation L2 (CORRECTION DE POINTNET)\nclass L2NormalizationLayer(Layer):\n    \"\"\"Effectue la normalisation L2 du tenseur d'entrée (l'embedding).\"\"\"\n    def call(self, inputs):\n        return tf.nn.l2_normalize(inputs, axis=1)\n    def get_config(self):\n        return super(L2NormalizationLayer, self).get_config()\n\n# 2. Couche de Distance L2 (POUR LE MODÈLE SIAMIS)\nclass L2DistanceLayer(Layer):\n    \"\"\"Calcule la distance L2 (Euclidienne) entre les deux entrées (embeddings).\"\"\"\n    def call(self, inputs):\n        embedding_A, embedding_B = inputs\n        sum_squared = tf.reduce_sum(tf.square(embedding_A - embedding_B), axis=1, keepdims=True)\n        distance = tf.sqrt(sum_squared)\n        return distance\n    def get_config(self):\n        return super(L2DistanceLayer, self).get_config()\n\n# 3. Fonction de Perte Contrastive\ndef contrastive_loss(y_true, y_pred, margin=1.0):\n    y_true = tf.cast(y_true, tf.float32)\n    loss_similar = y_true * tf.square(y_pred)\n    loss_dissimilar = (1 - y_true) * tf.square(tf.maximum(margin - y_pred, 0))\n    return tf.reduce_mean(loss_similar + loss_dissimilar)\n\n# 4. Fonction MLP Block (Utile pour PointNet)\ndef mlp_block(inputs, filters):\n    x = Conv1D(filters, kernel_size=1, activation='relu')(inputs)\n    x = BatchNormalization()(x)\n    return x","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-05T02:18:13.957643Z","iopub.execute_input":"2025-11-05T02:18:13.958633Z","iopub.status.idle":"2025-11-05T02:18:13.969256Z","shell.execute_reply.started":"2025-11-05T02:18:13.958563Z","shell.execute_reply":"2025-11-05T02:18:13.967927Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# --- 5. Fonction pour créer le modèle PointNet (l'encodeur de caractéristiques) - CORRIGÉE ---\ndef create_pointnet_encoder(input_shape, embedding_dim=128):\n    \n    inputs = Input(shape=input_shape)\n    \n    # --- Extraction et Pooling ---\n    x = mlp_block(inputs, 64)\n    x = mlp_block(x, 64)\n    x = mlp_block(x, 64)\n    x = mlp_block(x, 128)\n    x = mlp_block(x, 1024)\n    global_feature = GlobalMaxPooling1D()(x)\n    \n    # --- Couches Fully Connected (MLP) pour l'embedding ---\n    x = Dense(512, activation='relu')(global_feature)\n    x = BatchNormalization()(x)\n    x = Dense(256, activation='relu')(x)\n    x = BatchNormalization()(x)\n    \n    embedding = Dense(embedding_dim)(x)\n    \n    # 🟢 CORRECTION APPLIQUÉE : Utilisation de la couche Keras L2NormalizationLayer\n    normalization_layer = L2NormalizationLayer()\n    normalized_embedding = normalization_layer(embedding)\n    \n    return Model(inputs, normalized_embedding, name=\"PointNet_Encoder\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-05T02:18:31.585527Z","iopub.execute_input":"2025-11-05T02:18:31.586331Z","iopub.status.idle":"2025-11-05T02:18:31.594210Z","shell.execute_reply.started":"2025-11-05T02:18:31.586291Z","shell.execute_reply":"2025-11-05T02:18:31.592859Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# --- 6. Assemblage et Compilation du Modèle Siamois ---\n\nPC_SHAPE = (1024, 3) \nEMBEDDING_DIM = 128\n\n# 1. Instancier l'encodeur PointNet\npointnet_encoder = create_pointnet_encoder(\n    input_shape=PC_SHAPE,\n    embedding_dim=EMBEDDING_DIM\n)\n\n# 2. Définir les entrées et traiter avec l'encodeur partagé\ninput_A = Input(shape=PC_SHAPE, name='input_A')\ninput_B = Input(shape=PC_SHAPE, name='input_B')\nembedding_A = pointnet_encoder(input_A)\nembedding_B = pointnet_encoder(input_B)\n\n# 3. Calcul de la distance L2\ndistance_layer = L2DistanceLayer(name='l2_distance_output')\ndistance = distance_layer([embedding_A, embedding_B]) \n\n# 4. Création et Compilation du Modèle Siamois\nsiamese_model = Model(inputs=[input_A, input_B], outputs=distance, name=\"Siamese_PointNet_DML\")\n\nsiamese_model.compile(\n    loss=contrastive_loss,\n    optimizer=tf.keras.optimizers.Adam(learning_rate=1e-4),\n    metrics=None\n)\n\nprint(\"✅ Modèle Siamois PointNet assemblé et compilé avec succès.\")\nprint(\"\\n--- Résumé du Modèle Siamois ---\")\nsiamese_model.summary()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-05T02:18:45.986190Z","iopub.execute_input":"2025-11-05T02:18:45.986554Z","iopub.status.idle":"2025-11-05T02:18:46.198808Z","shell.execute_reply.started":"2025-11-05T02:18:45.986533Z","shell.execute_reply":"2025-11-05T02:18:46.197491Z"}},"outputs":[],"execution_count":null}]}