Cours:Classif : Différence entre versions
(→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> | ||
| − | * | + | <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.
Sommaire
- 1 Technos matérielles et logicielles
- 2 Connexion à la Rpi et test d'acquisition en ligne de commande
- 3 Capture d'image et affichage en temps réel
- 4 Prétraitement
- 5 Reconnaissance simple d'un seul objet, avec descripteurs géométriques élémentaires
- 6 Descripteur et Classifieur plus évolué
- 7 Plusieurs objets
- 8 Bouts de code Python
- 9 Références
Technos matérielles et logicielles
Vous utiliserez :
- Une Rpi 4 que vous programmerez depuis des postes utilisés en terminaux connectés par
ssh, avec redirection graphique (option- X). - Une camera PiCam Wide (grand angle)
- Le langage Python accompagné de
- la librairie libcamera pour les acquisitions :
- PIL et numpy pour les traitements bas niveaux
- les librairies opencv et dlib pour la classification et la reconnaissance :
É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- Explorer les options de cette application (
libcamera-still -h), en particulier-n,--immediate,--width,--heightet-o - Voir la page suivante pour le détails des options possibles : https://www.raspberrypi.com/documentation/computers/camera_software.html
- Explorer les options de cette application (
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
- PIL fait référence à Python Imaging Library : une bibliothèque Python de traitement d'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
arrayouPIL) 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.pypour 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
- 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) :
cv2.cvtColor() - détection des contours :
cv2.Canny()etv2.findContours() - description des contours :
cv2.ximgproc.fourierDescriptor()
- image en niveau de gris (éventuellement binarisation) :
- Références :
- Il sera nécessaire de normaliser ces descripteurs : https://dsp.stackexchange.com/questions/19982/fourier-descriptors-trying-to-classify-objects
- Exemple :
# 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
- KNN
- Redressement de la perspective
- Détecteur de coin de Harris : https://docs.opencv.org/4.5.1/dc/d0d/tutorial_py_features_harris.html
-
cv2.findHomography()etc2.warpPerspective() - ou https://docs.opencv.org/4.5.1/da/d6e/tutorial_py_geometric_transformations.html
- SVM
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 :
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 :
- OpenCV Tutorials
- Exemples OpenCV en c++
- K-Nearest Neighbour
- Image Segmentation with Watershed Algorithm
- connected Components et son exemple en cpp : https://docs.opencv.org/3.4/de/d01/samples_2cpp_2connected_components_8cpp-example.html#a3
- Introduction to Support Vector Machines
- Fourier Descriptors, voir également Contours example pour obtenir les contours d'un objet.
Archives de cette page :