Développement d'une application GUI de reconnaissance de chiffres manuscrits avec PyTorch

  1. Aperçu du projet

  2. Installation des bibliothèques nécessaires

  3. Conception de l'interface graphique

  4. Chargement du modèle et prédiction

  5. Flux de prétraitement d'image

  6. Analyse du code complet

  7. Démonstration des résultats

  8. Aperçu du projet


Cet projet met en œuvre une application de reconnaissance de chiffres manuscrits basée sur PyTorch et Tkinter, permettant aux utilisateurs d'écrire des chiffres sur une toile qui seront identifiés en temps réel par le système. Les technologies principales utilisées sont :

  • Framework d'apprentissage profond PyTorch
  • Développement d'interface graphique Tkinter
  • Traitement d'images OpenCV
  • Bibliothèque de traitement d'images PIL
  1. Installation des bibliothèques nécessaires

Installation avec pip

# Installation des bibliothèques de base
pip install torch torchvision pillow numpy opencv-python

# Bibliothèques liées à l'interface graphique
pip install tk

Vérification de l'installation

import torch
print(torch.__version__)  # Devrait afficher 1.x.x
import tkinter as tk
print(tk.TkVersion)      # Devrait afficher 8.6

  1. Conception de l'interface graphique

class ApplicationManuscrite:
    def __init__(self, racine):
        self.racine = racine
        self.racine.title("Reconnaissance de chiffres manuscrits")
        
        # Création d'une toile de 280x280 pixels avec fond noir
        self.toile = tk.Canvas(racine, width=280, height=280, bg="black")
        self.toile.pack()
        
        # Zone des boutons
        cadre_boutons = tk.Frame(racine)
        cadre_boutons.pack(pady=10)
        
        # Boutons de reconnaissance et de nettoyage
        self.bouton_reconnaître = tk.Button(cadre_boutons, text="Reconnaître", command=self.reconnaître_chiffre)
        self.bouton_reconnaître.pack(side=tk.LEFT, padx=5)
        
        self.bouton_effacer = tk.Button(cadre_boutons, text="Effacer", command=self.nettoyer_toile)
        self.bouton_effacer.pack(side=tk.LEFT, padx=5)
        
        # Étiquette d'affichage des résultats
        self.etiquette_resultat = tk.Label(racine, text="Veuillez dessiner un chiffre dans la zone ci-dessus", font=("Arial", 16))
        self.etiquette_resultat.pack(pady=10)

Caractéristiques de l'interface :

  • Interface utilisateur simple et intuitive
  • Toile de 280x280 pixels simulant la taille du jeu de données MNIST
  • Boutons de reconnaissance et de nettoyage pour les interactions de base
  • Affichage des résultats en caractères de grande taille
  1. Chargement du modèle et prédiction

def __init__(self, racine):
    # ...Autre code d'initialisation...
    
    # Chargement du modèle pré-entraîné (sélection automatique GPU ou CPU)
    self.dispositif = torch.device("cuda:0" if torch.cuda.is_available() else "cpu")
    self.modele = ReseauCNN().to(self.dispositif)  # Initialisation et déplacement du modèle
    self.modele = torch.load("poids/model_9.pth", map_location=self.dispositif)  # Chargement des poids
    self.modele.to(self.dispositif).eval()  # Passage en mode évaluation

def reconnaître_chiffre(self):
    # ...Prétraitement d'image...
    
    # Conversion de l'image au format d'entrée du modèle
    transformation = transforms.Compose([
        transforms.ToTensor(),
        transforms.Normalize((0.1307,), (0.3081,))  # Paramètres de normalisation MNIST
    ])
    tenseur_img = transformation(img).unsqueeze(0).to(self.dispositif)
    
    # Prédiction
    with torch.no_grad():
        sortie = self.modele(tenseur_img)
        _, prediction = torch.max(sortie, 1)
    
    self.etiquette_resultat.config(text=f"Résultat : {prediction.item()}")

Points clés :

  • Détection automatique et utilisation du GPU pour l'accélération
  • Chargement d'un modèle CNN pré-entraîné
  • Utilisation du mode eval() pour assurer la stabilité des prédictions
  • torch.no\_grad() pour réduire l'utilisation de la mémoire
  1. Flux de prétraitement d'image

def reconnaître_chiffre(self):
    # 1. Récupération de l'image de la toile (blanc sur fond noir)
    img = self.image
    
    # 2. Binarisation
    img_cv = np.array(img)
    _, img_bin = cv2.threshold(img_cv, 128, 255, cv2.THRESH_BINARY)
    
    # 3. Opération de dilatation pour épaissir les traits
    noyau = np.ones((3, 3), np.uint8)
    img_dilatée = cv2.dilate(img_bin, noyau, iterations=1)
    
    # 4. Recadrage de la région du chiffre
    img = Image.fromarray(img_dilatée)
    boite = img.getbbox()
    if boite:
        marge = 15  # Extension des bordures
        x0, y0, x1, y1 = boite
        x0, y0 = max(0, x0-marge), max(0, y0-marge)
        x1, y1 = min(img.width, x1+marge), min(img.height, y1+marge)
        img = img.crop((x0, y0, x1, y1))
        
        # 5. Redimensionnement centré à 28x28
        img.thumbnail((20, 20))
        img = ImageOps.pad(img, (28, 28), color='black')
    
    # 6. Redimensionnement final
    img = img.resize((28, 28))

Étapes de prétraitement :

  1. Récupération de l'image de la toile

  2. Traitement de binarisation

  3. Épaississement des traits par dilatation morphologique

  4. Recadrage intelligent de la région du chiffre

  5. Redimensionnement centré préservant les proportions

  6. Standardisation finale des dimensions

  7. Analyse du code complet


import tkinter as tk
from PIL import Image, ImageDraw, ImageOps
import torch
from torchvision import transforms
from ReseauCNN import ReseauCNN  # Modèle CNN personnalisé
import numpy as np
import cv2

class ApplicationManuscrite:
    def __init__(self, racine):
        self.racine = racine
        self.racine.title("Reconnaissance de chiffres manuscrits")

        # Création de la toile de dessin
        self.toile = tk.Canvas(racine, width=280, height=280, bg="black")
        self.toile.pack()

        # Cadre pour les boutons
        cadre_boutons = tk.Frame(racine)
        cadre_boutons.pack(pady=10)

        # Bouton de reconnaissance
        self.bouton_reconnaître = tk.Button(
            cadre_boutons, text="Reconnaître", command=self.reconnaître_chiffre)
        self.bouton_reconnaître.pack(side=tk.LEFT, padx=5)

        # Bouton d'effacement
        self.bouton_effacer = tk.Button(
            cadre_boutons, text="Effacer", command=self.nettoyer_toile)
        self.bouton_effacer.pack(side=tk.LEFT, padx=5)

        # Étiquette d'affichage des résultats
        self.etiquette_resultat = tk.Label(
            racine, text="Veuillez dessiner un chiffre dans la zone ci-dessus", font=("Arial", 16))
        self.etiquette_resultat.pack(pady=10)

        # Initialisation des variables de dessin
        self.image = Image.new("L", (280, 280), 0)  # Image en niveaux de gris, initialement noire
        self.dessin = ImageDraw.Draw(self.image)
        self.derniere_x = None
        self.derniere_y = None

        # Liaison des événements de la souris
        self.toile.bind("<b1-motion>", self.dessiner)
        self.toile.bind("<buttonrelease-1>", self.reinitialiser)

        # Chargement du modèle pré-entraîné (sélection automatique GPU ou CPU)
        self.dispositif = torch.device("cuda:0" if torch.cuda.is_available() else "cpu")
        self.modele = ReseauCNN().to(self.dispositif)  # Initialisation et déplacement du modèle
        self.modele = torch.load("poids/model_9.pth", map_location=self.dispositif)  # Chargement des poids
        self.modele.to(self.dispositif).eval()  # Placement sur le bon dispositif et passage en mode évaluation

    def dessiner(self, evenement):
        x, y = evenement.x, evenement.y
        if self.derniere_x and self.derniere_y:
            # Dessin sur la toile (blanc pur, sans anti-aliasing)
            self.toile.create_line(
                self.derniere_x, self.derniere_y, x, y,
                width=15,  # Lignes épaissies
                fill="white",  # Blanc pur
                capstyle=tk.ROUND,
                smooth=False  # Désactivation de l'anti-aliasing
            )
            # Dessin sur l'image (blanc pur)
            self.dessin.line(
                [self.derniere_x, self.derniere_y, x, y],
                fill=255,  # Blanc pur (255)
                width=15  # Épaississement
            )
        self.derniere_x = x
        self.derniere_y = y

    def reinitialiser(self, evenement):
        self.derniere_x = None
        self.derniere_y = None

    def nettoyer_toile(self):
        self.toile.delete("all")
        self.image = Image.new("L", (280, 280), 0)
        self.dessin = ImageDraw.Draw(self.image)
        self.etiquette_resultat.config(text="Veuillez dessiner un chiffre dans la zone ci-dessus")

    def reconnaître_chiffre(self):
        # 1. Récupération directe de l'image (déjà blanc sur fond noir)
        img = self.image

        # 2. Conversion en tableau numpy pour binarisation
        img_cv = np.array(img)
        _, img_bin = cv2.threshold(img_cv, 128, 255, cv2.THRESH_BINARY)  # Binarisation

        # 3. Épaississement des traits (opération de dilatation)
        noyau = np.ones((3, 3), np.uint8)  # Noyau 3x3
        img_dilatée = cv2.dilate(img_bin, noyau, iterations=1)  # Une dilatation

        # 4. Conversion vers PIL pour traitement ultérieur
        img = Image.fromarray(img_dilatée)

        # 5. Trouver la boîte englobante du chiffre et le centrer (recadrage plus agressif)
        boite = img.getbbox()
        if boite:
            # Extension de la zone de recadrage pour éviter de couper les traits
            marge = 15
            x0, y0, x1, y1 = boite
            x0 = max(0, x0 - marge)
            y0 = max(0, y0 - marge)
            x1 = min(img.width, x1 + marge)
            y1 = min(img.height, y1 + marge)
            img = img.crop((x0, y0, x1, y1))

            # Remplissage centré à 28x28, préservation du ratio
            img.thumbnail((20, 20))  # D'abord redimensionner à 20x20 maximum
            img = ImageOps.pad(img, (28, 28), color='black')  # Remplissage à 28x28

        # 6. Redimensionnement à 28x28 (assurer la cohérence des dimensions)
        img = img.resize((28, 28))

        # 7. Normalisation (cohérent avec l'entraînement)
        transformation = transforms.Compose([
            transforms.ToTensor(),
            transforms.Normalize((0.1307,), (0.3081,))  # Moyenne et écart-type MNIST
        ])
        # Déplacement du tenseur d'entrée vers le dispositif actuel (GPU/CPU)
        tenseur_img = transformation(img).unsqueeze(0).to(self.dispositif)

        # Prédiction
        with torch.no_grad():
            sortie = self.modele(tenseur_img)
            _, prediction = torch.max(sortie, 1)

        self.etiquette_resultat.config(text=f"Résultat : {prediction.item()}")

        # Sauvegarde des images d'entrée et prétraitées (pour débogage)
        img.save("debug_entree.png")  # Sauvegarde de l'entrée originale
        img.save("debug_traitee.png")  # Sauvegarde après prétraitement


if __name__ == "__main__":
    racine = tk.Tk()
    app = ApplicationManuscrite(racine)
    racine.mainloop()
</buttonrelease-1></b1-motion>
  1. Démonstration des résultats

À travers la mise en œuvre de ce projet, nous avons développé une application complète de reconnaissance de chiffres manusrcits, couvrant l'ensemble du processus allant de la conception de l'interface graphique au déploiement du modèle d'apprentissage profond. Les lecteurs peuvent étendre et optimiser ce cadre de base en fonction de leurs besoins spécifiques.

Étiquettes: PyTorch Tkinter reconnaissance de chiffres traitement d'images apprentissage profond

Publié le 17 juin à 06h17