Cours:Classif : Différence entre versions

De troyesGEII
Aller à : navigation, rechercher
(Technos matérielles et logicielles)
(Descripteur et Classifieur plus évolué)
 
(30 révisions intermédiaires par le même utilisateur non affichées)
Ligne 51 : Ligne 51 :
 
** initialise la camera
 
** initialise la camera
 
** affiche en continu son image
 
** affiche en continu son image
** sur l'appui d'une touche, réalise une capture (dans un objet <code>array>/code> ou <code>PIL</code>) et sauvegarde l'image dans un fichier
+
** sur l'appui d'une touche, réalise une capture (dans un objet <code>array</code> ou <code>PIL</code>) et sauvegarde l'image dans un fichier
  
 
En pratique :
 
En pratique :
Ligne 57 : Ligne 57 :
 
* Vous pouvez accéder aux dossiers de la Rpi depuis votre PC fixe, depuis le navigateur Dolphin avec comme url <code>sftp://root@10.98.33.83:22/</code>. Ce qui vous permettra par exemple d'éditer le fichier script depuis votre PC fixe.
 
* Vous pouvez accéder aux dossiers de la Rpi depuis votre PC fixe, depuis le navigateur Dolphin avec comme url <code>sftp://root@10.98.33.83:22/</code>. Ce qui vous permettra par exemple d'éditer le fichier script depuis votre PC fixe.
 
* Dans le terminal, <code>python monscript.py</code> pour executer votre script
 
* Dans le terminal, <code>python monscript.py</code> pour executer votre script
 +
* On peut facilement attendre l'appui d'une touche avec <code>cv2.waitkey()</code>
 
* Référence Python :
 
* Référence Python :
 
** [https://docs.python.org/fr/3/tutorial/ Le tutoriel Python]
 
** [https://docs.python.org/fr/3/tutorial/ Le tutoriel Python]
Ligne 62 : Ligne 63 :
 
=== Prétraitement ===
 
=== Prétraitement ===
  
* Modification éventuelle de la zone de capture de la camera.
+
* Modification éventuelle de la zone de capture de la camera (''crop'').
* Conversion en image niv. de gris.
+
* Conversion en image niveaux de gris, sur 8 bits.
* Binarisation.
+
* Binarisation (en mettant l'objet à 255, le fond à 0).
  
=== Reconnaissance simple d'un seul objet, avec descripteurs géométriques ===
+
=== Reconnaissance simple d'un seul objet, avec descripteurs géométriques élémentaires ===
  
* En ne plaçant qu'un seul objet dans le champ de la camera, calculer et afficher ses descripteurs de forme : longueur/largeur de l'objet, cf https://raphael.candelier.fr/?blog=Image%20Moments
+
* En ne plaçant qu'un seul objet dans le champ de la camera, calculer et afficher ses descripteurs de forme : longueur et largeur de l'objet, cf https://raphael.candelier.fr/?blog=Image%20Moments (calcul de ''l'' et ''w'')
 
* Construire une décision idoine à l'aide de la largeur et de la longueur de l'ellipse englobante, afin de discrimer trois classes d'objets (par exemple : jeton court, jeton long, jeton rond)
 
* Construire une décision idoine à l'aide de la largeur et de la longueur de l'ellipse englobante, afin de discrimer trois classes d'objets (par exemple : jeton court, jeton long, jeton rond)
  
=== Classifieur plus évolué ===
 
  
 +
Exemple de calcul de <code>l</code> et <code>w</code> à partir d'une image binaire représentée dans un tableau <code>numpy</code> (<code>arr</code> de type <code>ndarray</code>) :
 +
<source lang=python>
 +
img = arr.astype(np.float64, copy=False)
 +
H, W = img.shape
 +
yy, xx = np.indices((H, W))
 +
 +
M00 = float(np.sum(img))
 +
M10 = float(np.sum(xx * img))
 +
M01 = float(np.sum(yy * img))
 +
M11 = float(np.sum(xx * yy * img))
 +
M20 = float(np.sum((xx * xx) * img))
 +
M02 = float(np.sum((yy * yy) * img))
 +
 +
xm = int(M10 / M00)
 +
ym = int(M01 / M00)
 +
 +
mu20 = M20 / M00 - xm * xm
 +
mu02 = M02 / M00 - ym * ym
 +
mu11 = M11 / M00 - xm * ym
 +
 +
t = math.sqrt(4.0 * mu11 * mu11 + (mu20 - mu02) * (mu20 - mu02))
 +
l = int(math.sqrt(8.0 * (mu20 + mu02 + t)))
 +
w = int(math.sqrt(8.0 * (mu20 + mu02 - t)))
 +
</source>
 +
 +
=== Descripteur et Classifieur plus évolué ===
 +
 +
 +
* Descripteurs de Fourier
 +
** On transforme le contour d’un objet (suite de points) en un signal périodique, puis on le décompose en harmoniques avec la transformée de Fourier. Les premiers coefficients décrivent la forme globale et, une fois normalisés, ils permettent de comparer/reconnaître des formes indépendamment de la position, de la taille et de la rotation.
 +
** Méthodologie :
 +
*** image en niveau de gris (éventuellement binarisation) : <code>cv2.cvtColor()</code>
 +
*** détection des contours : <code>cv2.Canny()</code> et <code>v2.findContours()</code>
 +
*** description des contours : <code>cv2.ximgproc.fourierDescriptor()</code>
 +
** Références :
 +
*** https://docs.opencv.org/4.x/d4/d73/tutorial_py_contours_begin.html
 +
*** https://docs.opencv.org/4.x/d3/dc0/group__imgproc__shape.html#gadf1ad6a0b82947fa1fe3c3d497f260e0
 +
*** https://docs.opencv.org/4.x/d3/dc0/group__imgproc__shape.html#ga819779b9857cc2f8601e6526a3a5bc71
 +
** Il sera nécessaire de normaliser ces descripteurs : https://dsp.stackexchange.com/questions/19982/fourier-descriptors-trying-to-classify-objects
 +
** Exemple :
 +
<source lang=python>
 +
# Descripteurs de Fourier
 +
from picamera2 import Picamera2
 +
import numpy as np
 +
import cv2
 +
 +
picam = Picamera2()
 +
picam.start()
 +
a = picam.capture_array("main")
 +
gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
 +
edges = cv2.Canny(gray, 100, 200)
 +
contours, hierarchy = cv2.findContours(edges, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
 +
cnt = max(contours, key=cv2.contourArea)
 +
cnt = cnt.astype(np.float32)
 +
 +
# Paramètres utiles :
 +
# nbElt = nombre de points ré-échantillonnés sur le contour
 +
# nbFD  = nombre de descripteurs gardés
 +
fd = cv2.ximgproc.fourierDescriptor(cnt, nbElt=128, nbFD=16)
 +
 +
print(fd.shape, fd.dtype)
 +
</source>
 +
<source lang=python>
 +
# Normalisation
 +
fd = np.squeeze(fd)
 +
 +
# Assure forme (nbFD, 2)
 +
if fd.ndim == 1 and fd.shape[0] == 2*nbFD:
 +
    fd = fd.reshape(nbFD, 2)
 +
 +
re = fd[:, 0].astype(np.float32)
 +
im = fd[:, 1].astype(np.float32)
 +
 +
mag = np.sqrt(re*re + im*im)  # rotation-invariant
 +
mag[0] = 0.0  # enlève la composante continue (liée à la translation)
 +
 +
# normalisation d'échelle (et normalisation globale)
 +
norm = np.linalg.norm(mag) + 1e-12
 +
feat = mag / norm
 +
</source>
 +
* KNN
 +
** https://docs.opencv.org/4.5.1/d5/d26/tutorial_py_knn_understanding.html
 
* Redressement de la perspective
 
* Redressement de la perspective
* Descripteurs
+
** Détecteur de coin de Harris : https://docs.opencv.org/4.5.1/dc/d0d/tutorial_py_features_harris.html
* KNN
+
** <code>cv2.findHomography()</code> et <code>c2.warpPerspective()</code>
 +
** ou https://docs.opencv.org/4.5.1/da/d6e/tutorial_py_geometric_transformations.html
 
* SVM
 
* SVM
  
 
=== Plusieurs objets ===
 
=== Plusieurs objets ===
  
* Segmentation
+
* Segmentation nécessaire pour séparer les objets.
 +
* Puis une labellisation pour les numéroter.
 +
 
 +
=== Bouts de code Python ===
 +
 
 +
* Convertir une image couleur img (4 canaux) en image OpenCV :
 +
<source lang=python>
 +
import cv2
 +
im_cv = cv2.cvtColor(img, cv2.COLOR_RGBA2BGR)
 +
</source>
 +
* Acquisition d'image en recommançant en cas d'échec :
 +
 
 +
{{boîte déroulante/début|titre=[[Media:capture_image.py|capture_image.py]]}}
 +
<source lang=python>
 +
import time
 +
from picamera2 import Picamera2
 +
 
 +
def capture_image_with_retries(
 +
    max_attempts: int = 5,
 +
    warmup_s: float = 0.3,
 +
    retry_delay_s: float = 0.5,
 +
):
 +
    """
 +
    Initialise Picamera2 + capture une image.
 +
    Si échec, réessaie en recréant complètement la caméra.
 +
    Retourne un numpy array (image) ou lève RuntimeError après max_attempts.
 +
    """
 +
    last_err = None
 +
 
 +
    for attempt in range(1, max_attempts + 1):
 +
        picam = None
 +
        try:
 +
            picam = Picamera2()
 +
 
 +
            # Optionnel mais souvent utile: choisir explicitement une config "preview"
 +
            config = picam.create_preview_configuration(main={"size": (1280, 720)})
 +
            picam.configure(config)
 +
 
 +
            picam.start()
 +
 
 +
            # Laisse le temps à l'auto-exposition/auto-gain de se stabiliser
 +
            time.sleep(warmup_s)
 +
 
 +
            # Capture (guillemet manquant corrigé)
 +
            img = picam.capture_array("main")
 +
 
 +
            # Vérifs basiques pour détecter une "non acquisition"
 +
            if img is None:
 +
                raise RuntimeError("capture_array a renvoyé None")
 +
            if getattr(img, "size", 0) == 0:
 +
                raise RuntimeError("image vide (size=0)")
 +
            if len(getattr(img, "shape", ())) < 2:
 +
                raise RuntimeError(f"shape invalide: {getattr(img, 'shape', None)}")
 +
 
 +
            # Succès
 +
            return img
 +
 
 +
        except Exception as e:
 +
            last_err = e
 +
            print(f"[Tentative {attempt}/{max_attempts}] Échec acquisition: {e}")
 +
 
 +
        finally:
 +
            # Nettoyage propre pour pouvoir repartir clean
 +
            try:
 +
                if picam is not None:
 +
                    picam.stop()
 +
            except Exception:
 +
                pass
 +
            try:
 +
                if picam is not None:
 +
                    picam.close()
 +
            except Exception:
 +
                pass
 +
 
 +
        time.sleep(retry_delay_s)
 +
 
 +
    raise RuntimeError(f"Impossible de capturer une image après {max_attempts} tentatives. Dernière erreur: {last_err}")
 +
 
 +
 
 +
if __name__ == "__main__":
 +
    img = capture_image_with_retries(max_attempts=10)
 +
    print("Capture OK:", img.shape, img.dtype)
 +
</source>
 +
{{boîte déroulante/fin}}
  
 
=== Références  ===
 
=== Références  ===

Version actuelle datée du 2 février 2026 à 15:19

TP Classification : détection d'objet en temps réel par vision

Le travail de cette étape va consister à

  • analyser des images acquises en "temps réel" afin de détecter et identifier des objets
  • les objets seront
    • dans un premier temps des jetons de nain jaune
    • dans un second temps des briques lego.

Technos matérielles et logicielles

Vous utiliserez :

Étapes :

  • Connexion à la Rpi et test d'acquisition en ligne de commande
  • Capture d'image et affichage en temps réel, avec Python
  • Prétraitement
  • Reconnaissance simple d'un seul objet, avec descripteurs géométriques
  • Classifieur plus évolué (knn, svm)
  • Plusieurs objets

Connexion à la Rpi et test d'acquisition en ligne de commande

  • Connecter (si cela n'est pas fait) la PiCam à la Rpi4
  • Dans un terminal, se connecter à la Rpi en ssh : ssh -X root@10.98.33.XX
  • Tester la PiCam avec libcamera-hello (la capture en video doit s'afficher sur l'écran de la Rpi). Avec les informations affichées, identifier :
    • le modèle du capteur,
    • ses caractéristiques (résolution, format, cadence, etc ...).
  • Tester l'acquisition d'image avec l'éxecutable libcamera-still

Capture d'image et affichage en temps réel

En exploitant la documentation Picamera2 (principalement section 6 - Capturing images and requests)

  • Tester les deux exemples Capturing arrays et Capturing PIL images
  • Écrire un script Python qui :
    • initialise la camera
    • affiche en continu son image
    • sur l'appui d'une touche, réalise une capture (dans un objet array ou PIL) et sauvegarde l'image dans un fichier

En pratique :

  • Vous pouvez lancer un interpréteur Python dans le terminal pour tester des choses
  • Vous pouvez accéder aux dossiers de la Rpi depuis votre PC fixe, depuis le navigateur Dolphin avec comme url sftp://root@10.98.33.83:22/. Ce qui vous permettra par exemple d'éditer le fichier script depuis votre PC fixe.
  • Dans le terminal, python monscript.py pour executer votre script
  • On peut facilement attendre l'appui d'une touche avec cv2.waitkey()
  • Référence Python :

Prétraitement

  • Modification éventuelle de la zone de capture de la camera (crop).
  • Conversion en image niveaux de gris, sur 8 bits.
  • Binarisation (en mettant l'objet à 255, le fond à 0).

Reconnaissance simple d'un seul objet, avec descripteurs géométriques élémentaires

  • En ne plaçant qu'un seul objet dans le champ de la camera, calculer et afficher ses descripteurs de forme : longueur et largeur de l'objet, cf https://raphael.candelier.fr/?blog=Image%20Moments (calcul de l et w)
  • Construire une décision idoine à l'aide de la largeur et de la longueur de l'ellipse englobante, afin de discrimer trois classes d'objets (par exemple : jeton court, jeton long, jeton rond)


Exemple de calcul de l et w à partir d'une image binaire représentée dans un tableau numpy (arr de type ndarray) :

img = arr.astype(np.float64, copy=False)
H, W = img.shape
yy, xx = np.indices((H, W))

M00 = float(np.sum(img))
M10 = float(np.sum(xx * img))
M01 = float(np.sum(yy * img))
M11 = float(np.sum(xx * yy * img))
M20 = float(np.sum((xx * xx) * img))
M02 = float(np.sum((yy * yy) * img))

xm = int(M10 / M00)
ym = int(M01 / M00)

mu20 = M20 / M00 - xm * xm
mu02 = M02 / M00 - ym * ym
mu11 = M11 / M00 - xm * ym

t = math.sqrt(4.0 * mu11 * mu11 + (mu20 - mu02) * (mu20 - mu02))
l = int(math.sqrt(8.0 * (mu20 + mu02 + t)))
w = int(math.sqrt(8.0 * (mu20 + mu02 - t)))

Descripteur et Classifieur plus évolué

# Descripteurs de Fourier
from picamera2 import Picamera2
import numpy as np
import cv2

picam = Picamera2()
picam.start()
a = picam.capture_array("main")
gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
edges = cv2.Canny(gray, 100, 200)
contours, hierarchy = cv2.findContours(edges, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
cnt = max(contours, key=cv2.contourArea)
cnt = cnt.astype(np.float32)

# Paramètres utiles :
# nbElt = nombre de points ré-échantillonnés sur le contour
# nbFD  = nombre de descripteurs gardés
fd = cv2.ximgproc.fourierDescriptor(cnt, nbElt=128, nbFD=16)

print(fd.shape, fd.dtype)
# Normalisation
fd = np.squeeze(fd)

# Assure forme (nbFD, 2)
if fd.ndim == 1 and fd.shape[0] == 2*nbFD:
    fd = fd.reshape(nbFD, 2)

re = fd[:, 0].astype(np.float32)
im = fd[:, 1].astype(np.float32)

mag = np.sqrt(re*re + im*im)  # rotation-invariant
mag[0] = 0.0  # enlève la composante continue (liée à la translation)

# normalisation d'échelle (et normalisation globale)
norm = np.linalg.norm(mag) + 1e-12
feat = mag / norm

Plusieurs objets

  • Segmentation nécessaire pour séparer les objets.
  • Puis une labellisation pour les numéroter.

Bouts de code Python

  • Convertir une image couleur img (4 canaux) en image OpenCV :
import cv2
im_cv = cv2.cvtColor(img, cv2.COLOR_RGBA2BGR)
  • Acquisition d'image en recommançant en cas d'échec :

capture_image.py

import time
from picamera2 import Picamera2

def capture_image_with_retries(
    max_attempts: int = 5,
    warmup_s: float = 0.3,
    retry_delay_s: float = 0.5,
):
    """
    Initialise Picamera2 + capture une image.
    Si échec, réessaie en recréant complètement la caméra.
    Retourne un numpy array (image) ou lève RuntimeError après max_attempts.
    """
    last_err = None

    for attempt in range(1, max_attempts + 1):
        picam = None
        try:
            picam = Picamera2()

            # Optionnel mais souvent utile: choisir explicitement une config "preview"
            config = picam.create_preview_configuration(main={"size": (1280, 720)})
            picam.configure(config)

            picam.start()

            # Laisse le temps à l'auto-exposition/auto-gain de se stabiliser
            time.sleep(warmup_s)

            # Capture (guillemet manquant corrigé)
            img = picam.capture_array("main")

            # Vérifs basiques pour détecter une "non acquisition"
            if img is None:
                raise RuntimeError("capture_array a renvoyé None")
            if getattr(img, "size", 0) == 0:
                raise RuntimeError("image vide (size=0)")
            if len(getattr(img, "shape", ())) < 2:
                raise RuntimeError(f"shape invalide: {getattr(img, 'shape', None)}")

            # Succès
            return img

        except Exception as e:
            last_err = e
            print(f"[Tentative {attempt}/{max_attempts}] Échec acquisition: {e}")

        finally:
            # Nettoyage propre pour pouvoir repartir clean
            try:
                if picam is not None:
                    picam.stop()
            except Exception:
                pass
            try:
                if picam is not None:
                    picam.close()
            except Exception:
                pass

        time.sleep(retry_delay_s)

    raise RuntimeError(f"Impossible de capturer une image après {max_attempts} tentatives. Dernière erreur: {last_err}")


if __name__ == "__main__":
    img = capture_image_with_retries(max_attempts=10)
    print("Capture OK:", img.shape, img.dtype)

Références

OpenCV :

Archives de cette page :