From 0d15b16e9f7f556f0cbfc3a5db890c3d2d60c44e Mon Sep 17 00:00:00 2001 From: "yanis.bouarfa" Date: Tue, 7 Jan 2025 15:14:21 +0100 Subject: [PATCH] =?UTF-8?q?Mise=20=C3=A0=20jour=20bayesian=20(fonctionnel)?= =?UTF-8?q?,=20histogramme=20+=20output?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- main.py | 68 ++++++++---- pip-dependencies.txt | 7 +- src/classifiers/bayesian.py | 207 ++++++++++++++++-------------------- src/pipeline.py | 162 ++++++++-------------------- 4 files changed, 189 insertions(+), 255 deletions(-) diff --git a/main.py b/main.py index 4408cbe..66953cd 100644 --- a/main.py +++ b/main.py @@ -1,31 +1,55 @@ -import time -from src.classifiers.bayesian import BayesianClassifier +import os +import cv2 from src.pipeline import ObjectDetectionPipeline +from src.classifiers.bayesian import BayesianClassifier 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() + # Chemin vers le modèle entraîné model_path = "models/bayesian_model.pth" - pipeline.load_model(model_path, bayesian_model) + + # Chargement du modèle bayésien + print(f"Chargement du modèle bayésien depuis {model_path}") + bayesian_model = BayesianClassifier() + try: + bayesian_model.load_model(model_path) + print(f"Modèle bayésien chargé depuis {model_path}") + except Exception as e: + print(f"Erreur lors du chargement du modèle : {e}") + exit(1) + + # Chemin de l'image de test + image_path = "data/page.png" + if not os.path.exists(image_path): + print(f"L'image de test {image_path} n'existe pas.") + exit(1) + + # Initialisation du dossier de sortie + output_dir = "output" + if not os.path.exists(output_dir): + os.makedirs(output_dir) + + # Initialisation de la pipeline + print("Initialisation de la pipeline...") + pipeline = ObjectDetectionPipeline(image_path=image_path, model=bayesian_model, output_dir=output_dir) # Chargement de l'image - pipeline.load_image() + print("Chargement de l'image...") + try: + pipeline.load_image() + except FileNotFoundError as e: + print(e) + exit(1) - # 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() + # Détection et classification des objets + print("Détection et classification des objets...") + try: + class_counts, detected_objects = pipeline.detect_and_classify_objects() + except Exception as e: + print(f"Erreur lors de la détection/classification : {e}") + exit(1) - # 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 + # Sauvegarde et affichage des résultats + print("Sauvegarde et affichage des résultats...") pipeline.display_results(class_counts, detected_objects) + + print(f"Les résultats ont été sauvegardés dans le dossier : {output_dir}") diff --git a/pip-dependencies.txt b/pip-dependencies.txt index 483931f..042dc10 100644 --- a/pip-dependencies.txt +++ b/pip-dependencies.txt @@ -1 +1,6 @@ -opencv-python pytorch tensorflow numpy pandas matplotlib \ No newline at end of file +opencv-python +torch +tensorflow +numpy +pandas +matplotlib \ No newline at end of file diff --git a/src/classifiers/bayesian.py b/src/classifiers/bayesian.py index 8a05832..3a2be5e 100644 --- a/src/classifiers/bayesian.py +++ b/src/classifiers/bayesian.py @@ -5,66 +5,69 @@ 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 + self.feature_means = {} + self.feature_variances = {} + self.class_priors = {} + self.classes = [] - 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 + # Initialize HOG descriptor with standard parameters + self.hog = cv2.HOGDescriptor( + _winSize=(28, 28), + _blockSize=(8, 8), + _blockStride=(4, 4), + _cellSize=(8, 8), + _nbins=9 ) - # Extraction des contours - contours, _ = cv2.findContours(binary_image, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) + def extract_features(self, image): + try: + # Convert image to grayscale + if len(image.shape) == 3 and image.shape[2] == 3: + gray_image = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY) + else: + gray_image = image - features = [] - for contour in contours: - if cv2.contourArea(contour) < 22: - continue + # Apply adaptive thresholding + binary_image = cv2.adaptiveThreshold( + gray_image, 255, cv2.ADAPTIVE_THRESH_MEAN_C, cv2.THRESH_BINARY_INV, 11, 2 + ) - 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)) + # Find contours + contours, _ = cv2.findContours(binary_image, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) + if not contours: + print("No contours found.") + return np.array([]) - # Calcul des caractéristiques HOG - hog = cv2.HOGDescriptor() - hog_features = hog.compute(letter_image) + features = [] + for contour in contours: + if cv2.contourArea(contour) < 22: + continue - features.append(hog_features.flatten()) + x, y, w, h = cv2.boundingRect(contour) + letter_image = gray_image[y:y + h, x:x + w] + letter_image = cv2.resize(letter_image, (28, 28)) - features = np.array(features) + # Compute HOG features + hog_features = self.hog.compute(letter_image) + features.append(hog_features.flatten()) - # Normalisation des caractéristiques - norms = np.linalg.norm(features, axis=1, keepdims=True) - features = features / np.where(norms > 1e-6, norms, 1) + features = np.array(features) + if features.size == 0: + print("No features extracted.") + return np.array([]) - return features + norms = np.linalg.norm(features, axis=1, keepdims=True) + features = features / np.where(norms > 1e-6, norms, 1) + + return features + except Exception as e: + print(f"Error in extract_features: {e}") + return np.array([]) 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 @@ -74,17 +77,24 @@ class BayesianClassifier: 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 + for img_name in os.listdir(class_folder_path): + img_path = os.path.join(class_folder_path, img_name) + if os.path.isfile(img_path): + try: + image = cv2.imread(img_path) + if image is not None: + features = self.extract_features(image) + if features.size > 0: + for feature in features: + class_features[class_name].append(feature) + total_images += 1 + else: + print(f"No features extracted for {img_path}") + else: + print(f"Failed to load image: {img_path}") + except Exception as e: + print(f"Error processing {img_path}: {e}") - # 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]) @@ -94,50 +104,7 @@ class BayesianClassifier: 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, @@ -147,37 +114,51 @@ class BayesianClassifier: 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)) + print(f"Model saved to {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) + model_data = torch.load(model_path, weights_only=False) 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)) + print(f"Model loaded from {model_path}") else: - print("Model does not exist: {}".format(model_path)) + print(f"No model found at {model_path}.") + + def predict(self, image): + try: + features = self.extract_features(image) + if features.size == 0: + print("Empty features, skipping prediction.") + return None + + posteriors = {} + 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) + posteriors[class_name] = posterior + + return max(posteriors, key=posteriors.get) + except Exception as e: + print(f"Error in prediction: {e}") + return None def visualize(self): - """ - Visualiser les moyennes des caractéristiques pour chaque classe. - """ if not self.classes: - print("No classes to visualize") + 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.title(f"Mean features for class: {class_name}") plt.plot(mean_features) plt.xlabel("Feature Index") plt.ylabel("Mean Value") diff --git a/src/pipeline.py b/src/pipeline.py index 5a84916..31f9d4e 100644 --- a/src/pipeline.py +++ b/src/pipeline.py @@ -3,182 +3,106 @@ import os from matplotlib import pyplot as plt from collections import defaultdict + class ObjectDetectionPipeline: - def __init__(self, image_path, model=None): + def __init__(self, image_path, model=None, output_dir="output", min_contour_area=50, binary_threshold=127): """ - Initialisation de la pipeline de détection d'objets avec le chemin de l'image et le modèle. + Initialisation de la pipeline de détection d'objets. :param image_path: Chemin de l'image à traiter - :param model: Modèle de classification à utiliser (par défaut, aucun modèle n'est chargé) + :param model: Modèle de classification à utiliser + :param output_dir: Dossier où les résultats seront sauvegardés + :param min_contour_area: Aire minimale des contours à prendre en compte + :param binary_threshold: Seuil de binarisation pour les canaux """ self.image_path = image_path self.image = None self.binary_image = None - self.model = model # Le modèle personnalisé à utiliser + self.model = model + self.output_dir = output_dir + self.min_contour_area = min_contour_area + self.binary_threshold = binary_threshold - 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éé.") + if not os.path.exists(self.output_dir): + os.makedirs(self.output_dir) def load_image(self): - """ - Charger l'image spécifiée par le chemin d'accès. - - :return: Image chargée - """ + """Charge l'image spécifié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 + """Prétraite l'image pour la préparer à l'inférence.""" channels = cv2.split(self.image) binary_images = [] for channel in channels: - _, binary_channel = cv2.threshold(channel, 127, 255, cv2.THRESH_BINARY_INV) + _, binary_channel = cv2.threshold(channel, self.binary_threshold, 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 - """ + """Détecte et classe les objets présents dans l'image.""" if self.model is None: - print("Aucun modèle de classification fourni.") - return {} + raise ValueError("Aucun modèle de classification fourni.") 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: + if cv2.contourArea(contour) < self.min_contour_area: 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) + if predicted_class is None: + print("Skipping object with invalid prediction.") + continue - # 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() + def save_results(self, class_counts, detected_objects): + """Sauvegarde les résultats de détection et classification.""" + # Sauvegarder l'image binaire + binary_output_path = os.path.join(self.output_dir, "binary_image.jpg") + cv2.imwrite(binary_output_path, self.binary_image) + # Sauvegarder l'image annotée + annotated_image = 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.rectangle(annotated_image, (x, y), (x + w, y + h), (0, 255, 0), 2) + cv2.putText(annotated_image, str(predicted_class), (x, y - 10), cv2.FONT_HERSHEY_SIMPLEX, 0.9, (0, 255, 0), 2) + annotated_output_path = os.path.join(self.output_dir, "annotated_image.jpg") + cv2.imwrite(annotated_output_path, annotated_image) - 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() + # Sauvegarder les classes et leurs occurrences + results_text_path = os.path.join(self.output_dir, "results.txt") + with open(results_text_path, "w") as f: + for class_name, count in class_counts.items(): + f.write(f"{class_name}: {count}\n") - def display_classes_count(self, class_counts): - """ - Afficher le nombre d'objets détectés par classe. + def display_results(self, class_counts, detected_objects): + """Affiche et sauvegarde les résultats.""" + self.save_results(class_counts, detected_objects) - :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.ylabel("Nombre d'objets") + plt.title("Distribution des classes détectées") plt.show()