Cours:Classif : Différence entre versions

De troyesGEII
Aller à : navigation, rechercher
(Descripteur et Classifieur plus évolué)
(Descripteur et Classifieur plus évolué)
 
(6 révisions intermédiaires par le même utilisateur non affichées)
Ligne 101 : Ligne 101 :
  
  
* Descripteurs
+
* 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 :
 
** Méthodologie :
 
*** image en niveau de gris (éventuellement binarisation) : <code>cv2.cvtColor()</code>
 
*** image en niveau de gris (éventuellement binarisation) : <code>cv2.cvtColor()</code>
Ligne 110 : Ligne 111 :
 
*** 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#gadf1ad6a0b82947fa1fe3c3d497f260e0
 
*** https://docs.opencv.org/4.x/d3/dc0/group__imgproc__shape.html#ga819779b9857cc2f8601e6526a3a5bc71
 
*** 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 :
 
** Exemple :
 
<source lang=python>
 
<source lang=python>
 +
# Descripteurs de Fourier
 
from picamera2 import Picamera2
 
from picamera2 import Picamera2
 
import numpy as np
 
import numpy as np
Ligne 132 : Ligne 135 :
 
print(fd.shape, fd.dtype)
 
print(fd.shape, fd.dtype)
 
</source>
 
</source>
** Il sera nécessaire de normaliser ces descripteurs : https://dsp.stackexchange.com/questions/19982/fourier-descriptors-trying-to-classify-objects
+
<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
 
* KNN
 
** https://docs.opencv.org/4.5.1/d5/d26/tutorial_py_knn_understanding.html
 
** https://docs.opencv.org/4.5.1/d5/d26/tutorial_py_knn_understanding.html
Ligne 153 : Ligne 173 :
 
im_cv = cv2.cvtColor(img, cv2.COLOR_RGBA2BGR)
 
im_cv = cv2.cvtColor(img, cv2.COLOR_RGBA2BGR)
 
</source>
 
</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 :