1er ajout bayesian
This commit is contained in:
parent
8a12f4d73f
commit
ecd9ca1040
7 changed files with 534 additions and 0 deletions
BIN
data/page.png
Normal file
BIN
data/page.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 460 KiB |
BIN
data/plan.png
Normal file
BIN
data/plan.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 8.1 MiB |
78
generate_dataset.py
Normal file
78
generate_dataset.py
Normal file
|
@ -0,0 +1,78 @@
|
|||
from PIL import Image, ImageDraw, ImageFont
|
||||
import os
|
||||
|
||||
# Répertoire pour sauvegarder les images générées
|
||||
output_dir = "data/catalogue"
|
||||
|
||||
# Définir la taille de la police et de l'image
|
||||
font_size = 20 # Ajustez pour la taille souhaitée
|
||||
image_size = (28, 28) # Taille de l'image pour chaque caractère
|
||||
|
||||
# Listes des caractères à générer
|
||||
uppercase_letters = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
|
||||
lowercase_letters = "abcdefghijklmnopqrstuvwxyz"
|
||||
numbers = "0123456789"
|
||||
|
||||
# Chemin vers le fichier de police (à mettre à jour avec un chemin valide sur votre système)
|
||||
font_path = "arial.ttf" # Assurez-vous que cette police est disponible
|
||||
|
||||
# Créer le répertoire de sortie s'il n'existe pas
|
||||
os.makedirs(output_dir, exist_ok=True)
|
||||
|
||||
# Fonction pour créer des images de caractères
|
||||
def create_character_image(character, output_path):
|
||||
"""
|
||||
Crée une image contenant un caractère spécifique et la sauvegarde dans le chemin donné.
|
||||
|
||||
:param character: Caractère à dessiner
|
||||
:param output_path: Chemin où sauvegarder l'image
|
||||
"""
|
||||
# Créer une image vierge avec un fond blanc
|
||||
img = Image.new("RGB", image_size, "white")
|
||||
draw = ImageDraw.Draw(img)
|
||||
|
||||
# Charger la police
|
||||
try:
|
||||
font = ImageFont.truetype(font_path, font_size)
|
||||
except IOError:
|
||||
print(f"Fichier de police introuvable : {font_path}")
|
||||
return
|
||||
|
||||
# Calculer la position du texte pour centrer le caractère
|
||||
bbox = font.getbbox(character)
|
||||
text_width = bbox[2] - bbox[0]
|
||||
text_height = bbox[3] - bbox[1]
|
||||
text_x = (image_size[0] - text_width) // 2
|
||||
text_y = (image_size[1] - text_height) // 2
|
||||
|
||||
# Dessiner le caractère sur l'image
|
||||
draw.text((text_x, text_y), character, font=font, fill="black")
|
||||
|
||||
# Sauvegarder l'image
|
||||
img.save(output_path)
|
||||
|
||||
# Générer des images pour les lettres majuscules et minuscules
|
||||
for upper, lower in zip(uppercase_letters, lowercase_letters):
|
||||
upper_dir = os.path.join(output_dir, f"{upper}_") # Sous-dossier pour les majuscules
|
||||
lower_dir = os.path.join(output_dir, upper) # Sous-dossier pour les minuscules
|
||||
|
||||
os.makedirs(upper_dir, exist_ok=True) # Créer le sous-dossier pour les majuscules
|
||||
os.makedirs(lower_dir, exist_ok=True) # Créer le sous-dossier pour les minuscules
|
||||
|
||||
# Sauvegarder l'image de la lettre majuscule
|
||||
upper_image_path = os.path.join(upper_dir, f"{upper}.png")
|
||||
create_character_image(upper, upper_image_path)
|
||||
|
||||
# Sauvegarder l'image de la lettre minuscule
|
||||
lower_image_path = os.path.join(lower_dir, f"{lower}.png")
|
||||
create_character_image(lower, lower_image_path)
|
||||
|
||||
# Générer des images pour les chiffres
|
||||
for num in numbers:
|
||||
num_dir = os.path.join(output_dir, num) # Sous-dossier pour chaque chiffre
|
||||
os.makedirs(num_dir, exist_ok=True) # Créer le sous-dossier
|
||||
|
||||
num_image_path = os.path.join(num_dir, f"{num}.png")
|
||||
create_character_image(num, num_image_path)
|
||||
|
||||
print(f"Les images des lettres et des chiffres ont été générées dans le répertoire : {output_dir}")
|
31
main.py
31
main.py
|
@ -0,0 +1,31 @@
|
|||
import time
|
||||
from src.classifiers.bayesian import BayesianClassifier
|
||||
from src.pipeline import ObjectDetectionPipeline
|
||||
|
||||
if __name__ == "__main__":
|
||||
# Chemin de l'image à traiter
|
||||
image_path = "data/page.png"
|
||||
|
||||
# Initialisation du pipeline avec le chemin de l'image
|
||||
pipeline = ObjectDetectionPipeline(image_path)
|
||||
|
||||
# Initialisation et chargement du modèle Bayésien
|
||||
bayesian_model = BayesianClassifier()
|
||||
model_path = "models/bayesian_model.pth"
|
||||
pipeline.load_model(model_path, bayesian_model)
|
||||
|
||||
# Chargement de l'image
|
||||
pipeline.load_image()
|
||||
|
||||
# Mesure du temps d'exécution pour la détection et classification
|
||||
start_time = time.time()
|
||||
class_counts, detected_objects = pipeline.detect_and_classify_objects()
|
||||
end_time = time.time()
|
||||
|
||||
# Résultats
|
||||
print(f"Temps d'exécution: {end_time - start_time:.2f} secondes")
|
||||
print("Comptage des classes :", class_counts)
|
||||
print("Nombre d'objets détectés :", len(detected_objects))
|
||||
|
||||
# Affichage des résultats
|
||||
pipeline.display_results(class_counts, detected_objects)
|
185
src/classifiers/bayesian.py
Normal file
185
src/classifiers/bayesian.py
Normal file
|
@ -0,0 +1,185 @@
|
|||
import os
|
||||
import cv2
|
||||
import numpy as np
|
||||
import torch
|
||||
from collections import defaultdict
|
||||
import matplotlib.pyplot as plt
|
||||
|
||||
class BayesianClassifier:
|
||||
def __init__(self):
|
||||
"""
|
||||
Initialisation du classificateur Bayésien avec les paramètres nécessaires.
|
||||
"""
|
||||
self.feature_means = {} # Moyennes des caractéristiques pour chaque classe
|
||||
self.feature_variances = {} # Variances des caractéristiques pour chaque classe
|
||||
self.class_priors = {} # Probabilités a priori pour chaque classe
|
||||
self.classes = [] # Liste des classes disponibles
|
||||
|
||||
def extract_features(self, image):
|
||||
"""
|
||||
Extraire les caractéristiques d'une image donnée (Histogramme des Gradients Orientés - HOG).
|
||||
|
||||
:param image: Image en entrée
|
||||
:return: Tableau des caractéristiques normalisées
|
||||
"""
|
||||
# Conversion de l'image en niveaux de gris si nécessaire
|
||||
if len(image.shape) == 3 and image.shape[2] == 3:
|
||||
gray_image = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
|
||||
else:
|
||||
gray_image = image
|
||||
|
||||
# Binarisation de l'image
|
||||
binary_image = cv2.adaptiveThreshold(
|
||||
gray_image, 255, cv2.ADAPTIVE_THRESH_MEAN_C, cv2.THRESH_BINARY_INV, 11, 2
|
||||
)
|
||||
|
||||
# Extraction des contours
|
||||
contours, _ = cv2.findContours(binary_image, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
|
||||
|
||||
features = []
|
||||
for contour in contours:
|
||||
if cv2.contourArea(contour) < 22:
|
||||
continue
|
||||
|
||||
x, y, width, height = cv2.boundingRect(contour)
|
||||
letter_image = gray_image[y:y + height, x:x + width]
|
||||
letter_image = cv2.resize(letter_image, (28, 28))
|
||||
|
||||
# Calcul des caractéristiques HOG
|
||||
hog = cv2.HOGDescriptor()
|
||||
hog_features = hog.compute(letter_image)
|
||||
|
||||
features.append(hog_features.flatten())
|
||||
|
||||
features = np.array(features)
|
||||
|
||||
# Normalisation des caractéristiques
|
||||
norms = np.linalg.norm(features, axis=1, keepdims=True)
|
||||
features = features / np.where(norms > 1e-6, norms, 1)
|
||||
|
||||
return features
|
||||
|
||||
def train(self, dataset_path):
|
||||
"""
|
||||
Entraîner le classificateur avec un catalogue d'images organisées par classe.
|
||||
|
||||
:param dataset_path: Chemin vers le dossier contenant les images classées
|
||||
"""
|
||||
class_features = defaultdict(list)
|
||||
total_images = 0
|
||||
|
||||
for class_name in os.listdir(dataset_path):
|
||||
class_folder_path = os.path.join(dataset_path, class_name)
|
||||
if os.path.isdir(class_folder_path):
|
||||
if class_name not in self.classes:
|
||||
self.classes.append(class_name)
|
||||
|
||||
for image_name in os.listdir(class_folder_path):
|
||||
image_path = os.path.join(class_folder_path, image_name)
|
||||
if os.path.isfile(image_path):
|
||||
image = cv2.imread(image_path)
|
||||
if image is not None:
|
||||
features = self.extract_features(image)
|
||||
for feature in features:
|
||||
class_features[class_name].append(feature)
|
||||
total_images += 1
|
||||
|
||||
# Calcul des moyennes, variances et probabilités a priori
|
||||
for class_name in self.classes:
|
||||
if class_name in class_features:
|
||||
features = np.array(class_features[class_name])
|
||||
self.feature_means[class_name] = np.mean(features, axis=0)
|
||||
self.feature_variances[class_name] = np.var(features, axis=0) + 1e-6
|
||||
self.class_priors[class_name] = len(features) / total_images
|
||||
|
||||
print("Training completed for classes:", self.classes)
|
||||
|
||||
def predict(self, image):
|
||||
"""
|
||||
Prédire la classe d'une image donnée.
|
||||
|
||||
:param image: Image à classer
|
||||
:return: Classe prédite
|
||||
"""
|
||||
rotation_weights = {
|
||||
0: 1.0,
|
||||
90: 0.5,
|
||||
180: 0.5,
|
||||
270: 0.5
|
||||
}
|
||||
|
||||
posterior_probabilities = {}
|
||||
|
||||
for rotation, weight in rotation_weights.items():
|
||||
k = rotation // 90
|
||||
rotated_image = np.rot90(image, k)
|
||||
features = self.extract_features(rotated_image)
|
||||
|
||||
for class_name in self.classes:
|
||||
mean = self.feature_means[class_name]
|
||||
variance = self.feature_variances[class_name]
|
||||
prior = self.class_priors[class_name]
|
||||
|
||||
likelihood = -0.5 * np.sum((features - mean) ** 2 / variance) + np.log(2 * np.pi * variance)
|
||||
posterior = likelihood + np.log(prior)
|
||||
|
||||
weighted_posterior = posterior * (1 - weight * 0.5)
|
||||
|
||||
if class_name not in posterior_probabilities:
|
||||
posterior_probabilities[class_name] = weighted_posterior
|
||||
else:
|
||||
posterior_probabilities[class_name] = max(posterior_probabilities[class_name], weighted_posterior)
|
||||
|
||||
return max(posterior_probabilities, key=posterior_probabilities.get)
|
||||
|
||||
def save_model(self, model_path):
|
||||
"""
|
||||
Sauvegarder le modèle Bayésien dans un fichier.
|
||||
|
||||
:param model_path: Chemin du fichier de sauvegarde
|
||||
"""
|
||||
model_data = {
|
||||
"feature_means": self.feature_means,
|
||||
"feature_variances": self.feature_variances,
|
||||
"class_priors": self.class_priors,
|
||||
"classes": self.classes
|
||||
}
|
||||
if not os.path.exists(os.path.dirname(model_path)):
|
||||
os.makedirs(os.path.dirname(model_path))
|
||||
torch.save(model_data, model_path)
|
||||
print("Model saved in {}".format(model_path))
|
||||
|
||||
def load_model(self, model_path):
|
||||
"""
|
||||
Charger un modèle Bayésien sauvegardé.
|
||||
|
||||
:param model_path: Chemin du fichier de modèle
|
||||
"""
|
||||
if os.path.exists(model_path):
|
||||
model_data = torch.load(model_path)
|
||||
self.feature_means = model_data["feature_means"]
|
||||
self.feature_variances = model_data["feature_variances"]
|
||||
self.class_priors = model_data["class_priors"]
|
||||
self.classes = model_data["classes"]
|
||||
print("Model loaded from {}".format(model_path))
|
||||
else:
|
||||
print("Model does not exist: {}".format(model_path))
|
||||
|
||||
def visualize(self):
|
||||
"""
|
||||
Visualiser les moyennes des caractéristiques pour chaque classe.
|
||||
"""
|
||||
if not self.classes:
|
||||
print("No classes to visualize")
|
||||
return
|
||||
|
||||
for class_name in self.classes:
|
||||
mean_features = self.feature_means[class_name]
|
||||
|
||||
plt.figure(figsize=(10, 4))
|
||||
plt.title("Mean features for class: {}".format(class_name))
|
||||
plt.plot(mean_features)
|
||||
plt.xlabel("Feature Index")
|
||||
plt.ylabel("Mean Value")
|
||||
plt.grid(True)
|
||||
plt.show()
|
184
src/pipeline.py
Normal file
184
src/pipeline.py
Normal file
|
@ -0,0 +1,184 @@
|
|||
import cv2
|
||||
import os
|
||||
from matplotlib import pyplot as plt
|
||||
from collections import defaultdict
|
||||
|
||||
class ObjectDetectionPipeline:
|
||||
def __init__(self, image_path, model=None):
|
||||
"""
|
||||
Initialisation de la pipeline de détection d'objets avec le chemin de l'image et le modèle.
|
||||
|
||||
:param image_path: Chemin de l'image à traiter
|
||||
:param model: Modèle de classification à utiliser (par défaut, aucun modèle n'est chargé)
|
||||
"""
|
||||
self.image_path = image_path
|
||||
self.image = None
|
||||
self.binary_image = None
|
||||
self.model = model # Le modèle personnalisé à utiliser
|
||||
|
||||
def load_model(self, model_path: str, instance_classifier=None):
|
||||
"""
|
||||
Charger un modèle pré-entraîné ou un classifieur bayésien.
|
||||
|
||||
:param model_path: Chemin du fichier du modèle
|
||||
:param instance_classifier: Instance du classifieur à utiliser
|
||||
"""
|
||||
if os.path.exists(model_path):
|
||||
self.model = instance_classifier # Créer une instance du classifieur
|
||||
print(f"Chargement du modèle bayésien depuis {model_path}")
|
||||
self.model.load_model(model_path) # Charger les paramètres du modèle
|
||||
print(f"Modèle bayésien chargé depuis {model_path}")
|
||||
else:
|
||||
print(f"Aucun modèle trouvé à {model_path}. Un nouveau modèle sera créé.")
|
||||
|
||||
def load_image(self):
|
||||
"""
|
||||
Charger l'image spécifiée par le chemin d'accès.
|
||||
|
||||
:return: Image chargée
|
||||
"""
|
||||
self.image = cv2.imread(self.image_path)
|
||||
if self.image is None:
|
||||
raise FileNotFoundError(f"L'image {self.image_path} est introuvable.")
|
||||
return self.image
|
||||
|
||||
def preprocess_image(self):
|
||||
"""
|
||||
Prétraiter l'image pour la préparer à l'inférence.
|
||||
|
||||
:return: Image binarisée
|
||||
"""
|
||||
# Binarisation de l'image par canaux
|
||||
channels = cv2.split(self.image)
|
||||
binary_images = []
|
||||
|
||||
for channel in channels:
|
||||
_, binary_channel = cv2.threshold(channel, 127, 255, cv2.THRESH_BINARY_INV)
|
||||
binary_images.append(binary_channel)
|
||||
|
||||
# Fusionner les canaux binarisés
|
||||
binary_image = cv2.bitwise_or(binary_images[0], binary_images[1])
|
||||
binary_image = cv2.bitwise_or(binary_image, binary_images[2])
|
||||
self.binary_image = binary_image
|
||||
return binary_image
|
||||
|
||||
def detect_and_classify_objects(self):
|
||||
"""
|
||||
Détecter et classer les objets présents dans l'image en fonction des contours détectés.
|
||||
|
||||
:return: Dictionnaire des classes détectées et objets détectés
|
||||
"""
|
||||
if self.model is None:
|
||||
print("Aucun modèle de classification fourni.")
|
||||
return {}
|
||||
|
||||
self.binary_image = self.preprocess_image()
|
||||
|
||||
# Trouver les contours dans l'image binarisée
|
||||
contours, _ = cv2.findContours(self.binary_image, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
|
||||
|
||||
class_counts = defaultdict(int)
|
||||
detected_objects = []
|
||||
|
||||
for contour in contours:
|
||||
if cv2.contourArea(contour) < 50:
|
||||
continue
|
||||
|
||||
x, y, w, h = cv2.boundingRect(contour)
|
||||
letter_image = self.image[y:y + h, x:x + w]
|
||||
|
||||
# Prédiction avec le modèle
|
||||
predicted_class = self.model.predict(letter_image)
|
||||
|
||||
# Incrémenter le comptage de la classe prédite
|
||||
class_counts[predicted_class] += 1
|
||||
|
||||
# Ajouter les coordonnées et la classe prédite
|
||||
detected_objects.append((x, y, w, h, predicted_class))
|
||||
|
||||
return dict(sorted(class_counts.items())), detected_objects
|
||||
|
||||
def display_results(self, class_counts, detected_objects):
|
||||
"""
|
||||
Afficher les résultats de la détection et classification.
|
||||
|
||||
:param class_counts: Dictionnaire des classes détectées et leurs occurrences
|
||||
:param detected_objects: Liste des objets détectés avec leurs coordonnées et classes prédites
|
||||
"""
|
||||
self.display_binary_image()
|
||||
self.display_image_with_classes(detected_objects)
|
||||
self.display_image_with_annotations(detected_objects)
|
||||
self.display_classes_count(class_counts)
|
||||
|
||||
def display_binary_image(self):
|
||||
"""
|
||||
Afficher l'image binaire résultant du prétraitement.
|
||||
"""
|
||||
plt.figure(figsize=(self.binary_image.shape[1] / 100, self.binary_image.shape[0] / 100))
|
||||
plt.imshow(self.binary_image, cmap='gray')
|
||||
plt.axis('off')
|
||||
plt.show()
|
||||
|
||||
def display_image_with_classes(self, detected_objects):
|
||||
"""
|
||||
Afficher l'image avec les classes prédites annotées.
|
||||
|
||||
:param detected_objects: Liste des objets détectés avec leurs coordonnées et classes prédites
|
||||
"""
|
||||
image_with_classes_only = self.image.copy()
|
||||
|
||||
for (x, y, w, h, predicted_class) in detected_objects:
|
||||
if predicted_class[-1] == "_":
|
||||
text = predicted_class.split("_")[0].upper()
|
||||
else:
|
||||
text = predicted_class.lower()
|
||||
|
||||
cv2.rectangle(image_with_classes_only, (x, y), (x + w, y + h), (255, 255, 255), -1)
|
||||
font_scale = 0.7
|
||||
font_thickness = 2
|
||||
text_size = cv2.getTextSize(text, cv2.FONT_HERSHEY_SIMPLEX, font_scale, font_thickness)[0]
|
||||
text_x = x + (w - text_size[0]) // 2
|
||||
text_y = y + (h + text_size[1]) // 2
|
||||
|
||||
cv2.putText(image_with_classes_only, text, (text_x, text_y),
|
||||
cv2.FONT_HERSHEY_SIMPLEX, font_scale, (0, 0, 0), font_thickness)
|
||||
|
||||
fig = plt.figure(figsize=(image_with_classes_only.shape[1] / 100, image_with_classes_only.shape[0] / 100))
|
||||
plt.imshow(cv2.cvtColor(image_with_classes_only, cv2.COLOR_BGR2RGB))
|
||||
plt.axis('off')
|
||||
plt.show()
|
||||
|
||||
def display_image_with_annotations(self, detected_objects):
|
||||
"""
|
||||
Afficher l'image avec les annotations des objets détectés (rectangles et classes).
|
||||
|
||||
:param detected_objects: Liste des objets détectés avec leurs coordonnées et classes prédites
|
||||
"""
|
||||
image_with_annotations = self.image.copy()
|
||||
for (x, y, w, h, predicted_class) in detected_objects:
|
||||
if predicted_class[-1] == "_":
|
||||
text = predicted_class.split("_")[0].upper()
|
||||
else:
|
||||
text = predicted_class.lower()
|
||||
|
||||
cv2.rectangle(image_with_annotations, (x, y), (x + w, y + h), (0, 255, 0), 2)
|
||||
cv2.putText(image_with_annotations, text, (x, y - 10),
|
||||
cv2.FONT_HERSHEY_SIMPLEX, 0.9, (0, 255, 0), 2)
|
||||
|
||||
fig = plt.figure(figsize=(image_with_annotations.shape[1] / 100, image_with_annotations.shape[0] / 100))
|
||||
plt.imshow(cv2.cvtColor(image_with_annotations, cv2.COLOR_BGR2RGB))
|
||||
plt.axis('off')
|
||||
plt.show()
|
||||
|
||||
def display_classes_count(self, class_counts):
|
||||
"""
|
||||
Afficher le nombre d'objets détectés par classe.
|
||||
|
||||
:param class_counts: Dictionnaire des classes détectées et leurs occurrences
|
||||
"""
|
||||
plt.figure(figsize=(10, 5))
|
||||
plt.bar(class_counts.keys(), class_counts.values())
|
||||
plt.xlabel("Classes")
|
||||
plt.ylabel("Nombre de lettres")
|
||||
plt.title("Classes détectées et leur nombre")
|
||||
plt.show()
|
56
train.py
Normal file
56
train.py
Normal file
|
@ -0,0 +1,56 @@
|
|||
import os
|
||||
from collections import defaultdict
|
||||
import numpy as np
|
||||
import cv2
|
||||
|
||||
from src.classifiers.bayesian import BayesianClassifier
|
||||
|
||||
if __name__ == "__main__":
|
||||
# Chemin vers le dataset d'entraînement
|
||||
dataset_path = "data/catalogue"
|
||||
|
||||
# Initialisation du classifieur Bayésien
|
||||
bayesian_model = BayesianClassifier()
|
||||
|
||||
print("Début de l'entraînement...")
|
||||
|
||||
# Dictionnaire pour stocker les caractéristiques par classe
|
||||
class_features = defaultdict(list)
|
||||
total_images = 0
|
||||
|
||||
# Parcours des classes dans le dataset
|
||||
for class_name in os.listdir(dataset_path):
|
||||
class_folder_path = os.path.join(dataset_path, class_name)
|
||||
if not os.path.isdir(class_folder_path):
|
||||
continue # Ignorer les fichiers qui ne sont pas des dossiers
|
||||
|
||||
# Ajouter la classe au modèle si elle n'existe pas déjà
|
||||
if class_name not in bayesian_model.classes:
|
||||
bayesian_model.classes.append(class_name)
|
||||
|
||||
# Parcours des images dans le dossier de la classe
|
||||
for image_name in os.listdir(class_folder_path):
|
||||
image_path = os.path.join(class_folder_path, image_name)
|
||||
image = cv2.imread(image_path)
|
||||
|
||||
if image is not None:
|
||||
# Extraire les caractéristiques de l'image
|
||||
features = bayesian_model.extract_features(image)
|
||||
for feature in features:
|
||||
class_features[class_name].append(feature)
|
||||
total_images += 1
|
||||
|
||||
# Calcul des statistiques pour chaque classe
|
||||
for class_name in bayesian_model.classes:
|
||||
if class_name in class_features:
|
||||
features = np.array(class_features[class_name])
|
||||
bayesian_model.feature_means[class_name] = np.mean(features, axis=0)
|
||||
bayesian_model.feature_variances[class_name] = np.var(features, axis=0) + 1e-6 # Éviter la division par zéro
|
||||
bayesian_model.class_priors[class_name] = len(features) / total_images
|
||||
|
||||
print("Entraînement terminé.")
|
||||
|
||||
# Sauvegarde du modèle entraîné
|
||||
model_path = "models/bayesian_model.pth"
|
||||
bayesian_model.save_model(model_path)
|
||||
print(f"Modèle sauvegardé dans : {model_path}")
|
Loading…
Add table
Reference in a new issue