diff --git a/data/page.png b/data/page.png new file mode 100644 index 0000000..321d9b7 Binary files /dev/null and b/data/page.png differ diff --git a/data/plan.png b/data/plan.png new file mode 100644 index 0000000..e33cda1 Binary files /dev/null and b/data/plan.png differ diff --git a/generate_dataset.py b/generate_dataset.py new file mode 100644 index 0000000..79eea81 --- /dev/null +++ b/generate_dataset.py @@ -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}") diff --git a/main.py b/main.py index e69de29..4408cbe 100644 --- a/main.py +++ b/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) diff --git a/src/classifiers/bayesian.py b/src/classifiers/bayesian.py new file mode 100644 index 0000000..8a05832 --- /dev/null +++ b/src/classifiers/bayesian.py @@ -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() diff --git a/src/pipeline.py b/src/pipeline.py new file mode 100644 index 0000000..5a84916 --- /dev/null +++ b/src/pipeline.py @@ -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() diff --git a/train.py b/train.py new file mode 100644 index 0000000..7badc14 --- /dev/null +++ b/train.py @@ -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}")