OpenCV vision par ordinateur avec python
Howse Joseph
5 juillet 2018
2
i
PRÉFACE
ii
Chapitre 1
INTRODUCTION
1
2 CHAPITRE 1. INTRODUCTION
Chapitre 2
OpenCV gérer des fichiers, des caméras, et gui
(Graphical User Interfaces )
Ce chapitre présente la fonctionnalité d’E/S d’OpenCV. Nous discutons éga- lement d’un projet concept et les débuts d’une conception orientée objet pour ce projet, que nous se concrétisera dans les chapitres suivants.
En commençant par examiner les capacités d’E/S et les modèles de concep- tion, nous construisons notre projet de la même manière que nous ferions un sandwich : de l’extérieur po Tranches de pain et étaler ou terminer et coller, ve- nir avant plombages ou algorithmes. Nous choisissons cela approche parce que la vision par ordinateur est extraverti - il contemple le monde réel en dehors de notre ordinateur - et nous voulons appliquer tous nos travaux algorithmiques ultérieurs au monde réel grâce à une interface commune.
Tout le code pour ce chapitre peut être télécharger à partir de mon site web :http: // nummist .com / opencv / 3923_02.zip.
2.1 Scripts d’E/S de base.
Toutes les applications de CV doivent obtenir des images en entrée. La plu- part ont également besoin de produire des images en sortie. Une application CV interactive peut nécessiter une caméra en entrée source et une fenêtre en tant que destination de sortie. Cependant, d’autres sources possibles et les destinations incluent les fichiers image, les fichiers vidéo et les octets bruts. Par exemple, brut octets peuvent être reçus / envoyés via une connexion réseau ou peuvent être générés par un algorithme si nous incorporons des graphiques procéduraux dans notre application. Regardons à chacune de ces possibilités.
3
4CHAPITRE 2. OPENCV GÉRER DES FICHIERS, DES CAMÉRAS, ET GUI (GRAPHICAL USER INTERFACES )
2.2 Lecture et écriture d’un fichier image
OpenCV fournit les fonctions imread ()etimwrite () qui supportent di- vers fichiers formats pour les images fixes. Les formats pris en charge varient selon le système mais doivent toujours inclure le format BMP. En règle générale, PNG, JPEG et TIFF devraient être parmi les formats pris en charge aussi. Les images peuvent être chargées à partir d’un format de fichier et enregistrées un autre. Par exemple, convertissons une image de PNG en JPEG :
importcv2
image = cv2.imread (’MyPic.png’) cv2.imwrite (’MyPic.jpg’, image)
La plupart des fonctionnalités OpenCV que nous utilisons sont dans le module cv2. Vous pourriez rencontrer d’autres guides OpenCV qui reposent sur le Les modules cv ou cv2.cv, qui sont des versions héritées. Nous utilisons cv2.cv pour certaines constantes qui ne sont pas encore redéfinies en cv2.
Par défaut,imread ()renvoie une image au format couleur BGR, même si le fichier utilise un format en niveaux de gris. BGR (bleu-vert-rouge) représente le même espace colorimétrique que RVB (rouge-vert-bleu) mais l’ordre des octets est inversé.
En option, nous pouvons spécifier le mode deimread ()à êtreCV\_LOAD\_IMAGE\_COLOR (BGR),CV\_LOAD\_IMAGE\_GRAYSCALE(niveaux de gris) ouCV\_LOAD\_IMAGE\_\_UNCHANGED (soit BGR, soit en niveaux de gris, en fonction de l’espace colorimétrique du fi-
chier). Par exemple, chargeons un PNG en tant qu’image en niveaux de gris (perdant toute information de couleur dans le processus) et, ensuite, l’enregis- trer en tant qu’image PNG en niveaux de gris :
importer cv2
grayImage = cv2.imread (’MyPic.png !, cv2.CV\_LOAD\_IMAGE\_GRAYSCALE) ev2imwrite (’MyPicGray.png’, grayImage)
Quel que soit le mode, imread () rejette tout canal alpha (transparence).
le La fonction imwrite ()nécessite une image au format BGR ou en niveaux de gris avec nombre de bits par canal que le format de sortie peut prendre en charge. Par exemple, bmp nécessite 8 bits par canal alors que le format PNG autorise 8 ou 16 bits par canal.
2.3. CONVERSION ENTRE UNE IMAGE ET DES OCTETS BRUTS 5
2.3 Conversion entre une image et des octets bruts
Conceptuellement, un octet est un nombre entier allant de 0 à 255, tout au long de temps réel applications graphiques aujourd’hui, un pixel est générale- ment représenté par un octet par canal, bien que d’autres représentations sont également possibles.
Une image OpenCV est un tableau 2D ou 3D de type numpy. tableau. Un niveau de gris 8 bits image est un tableau 2D contenant des valeurs d’octets, une image BGR 24 bits est un tableau 3D, également contenant des valeurs de byte.
Nous pouvons accéder à ces valeurs en utilisant une expression comme image [0, 0] ouimage [o, 0, 0]. Le premier index est la coordonnée y du pixel, ou rangée, 0 étant le top. Le deuxième index est la coordonnée x du pixel, ou colonne, 0 étant le le plus à gauche. Le troisième indice (le cas échéant) représente un canal de couleur.
Par exemple, dans une image en niveaux de gris 8 bits avec un pixel blanc dans le coin supérieur gauche, L’image [0, 0] est 255. Pour une image BGR 24 bits avec un pixel bleu en haut à gauche coin, l’image [0, 0] est [255, 0, 0].
Comme alternative à l’utilisation d’une expression comme image [0, 0] ou . image [0, 0] = 128, nous pouvons utiliser une expres- sion comme image. item ((0, 0)) orimage.setitem ((0, 0), 128). Les dernieres expressions sont plus efficaces pour les opérations à un seul pixel. Cependant, comme nous le verrons dans les chapitres suivants, nous voulons généralement effectuer opérations sur de grandes tranches d’une image plutôt que sur des pixels uniques.
À condition qu’une image ait 8 bits par canal, nous pouvons la convertir en un Python standardbyte array, qui est unidimensionnel :
byteArray = bytearray (image)
Inversement, à condition que bytearray contienne des octets dans un ordre approprié, nous pouvons lancer et ensuite remodeler pour obtenir un numpy.
type de tableau qui est une image :
graylmage = numpy.array (grayByteArray) .reshape (hauteur, largeur) bgrImage = numpy.array (bgrByteArray) .reshape (hauteur, largeur, 3)
6CHAPITRE 2. OPENCV GÉRER DES FICHIERS, DES CAMÉRAS, ET GUI (GRAPHICAL USER INTERFACES ) Comme un exemple plus complet, convertissons bytearraycontenant des
octets aléatoires à un image prayscale et une image BGR :
importer cv2 importer numpy importer os
# Crée un tableau de 120 000 octets aléatoires.
randomByteArray = bytearray (os.urandom (120000)) flatNumpyArray = numpy.array (randomByteArray)
# Convertit le tableau pour faire une image en niveaux de gris 400x300.
grayImage = flatNumpyArray.reshape (300, 400) cv2.imwrite (’RandomGray.png’, grayImage)
# Convertir le tableau pour faire une image couleur 400x100.
bgrImage = flatNumpyArray.reshape (100, 400, 3) cv2.imwrite (’RandomColor.png’, bgrTmage)
Après avoir exécuté ce script, nous devrions avoir une paire d’images générées aléatoirement, RandomGray .png et RandomColor.png, dans le répertoire du script.
Ici, nous utilisons la fonction standard os.urandom () de Py- thon pour générer des octets bruts aléatoires, que nous convertis- sons ensuite en un tableau Numpy. Remarquez qu’il est également possible de générer un tableau Numpy aléatoire directement (et plus efficacement) en utilisant une instruction telle que numpy, au hasard : randint (0, 256, 120000) reshape (300, 400)la seule raison pour laquelle nous utilisons os. urandom () est d’aider à démontrer la conversion à partir d’octets bruts.
2.4 Lecture et écriture d’un fichier vidéo
OpenCV fournit les classes Video Capture et VideoWriter qui supportent di- vers formats de fichiers vidéo. Les formats pris en charge varient selon le système, mais doivent toujours inclure AVI. Via sa méthode read ():, une classe \verb VideoCapture ! peut être interrogée pour les nouvelles images atteindre la fin de son fichier vidéo. Chaque image est une image au format BGR. Inversement, une image peut être passée à la méthode write ()de la classe videowriter, qui ajoute l’image au fichier dans le videowriter. Regardons un exemple qui lit images d’un fichier AVI et les écrit dans un autre fichier AVI avec un encodage YUV :
2.5. CAPTURE D’IMAGES DE CAMÉRA 7 importer cv2
\vspace{4mm}
videoCapture = cv2.VideoCapture (’MyInputVid.avi’) fps = videoCapture.get (cv2.cv.CV_CAP_PROP_FPS)
size = (int (videoCapture.get (cv2.cv.CV_CAP_PROP_FRAME_WIDTH)), int (videoCapture.get (cv2.cv.CV_CAP_PROP_FRAME_ HEIGHT))) videoWriter = cv2.VideoWriter (
’MyOutputVid.avi’, cv2.cv.CV_FOURCC (’I’, ’4’, ’2’, ’0’), fps, taille) success, frame = videoCapture.read ()
en cas de succès: # Boucle jusqu’à ce qu’il n’y ait plus d’images.
videoWriter.write (image)
success, frame = videoCapture.read ()
Les arguments du constructeur de la classe videowriter méritent une at- tention particulière.
Le nom de fichier de la vidéo doit être spécifié. Tout fichier préexistant avec ce nom est écrasé. Un codec vidéo doit également être spécifié. Les codecs disponibles peuvent varier de système en système. Les options comprennent :
— ov2.ev.cv_FouRCC (’T’, 141, ’2’, 10 ") : Ceci est un YUV non compressé, 4 : 2 : 0 Chroma sous-échantillonnée. Cet encodage est lar- gement compatible mais produit de grandes des dossiers. L’extension de fichier doit être avi.
— cv2.ev.cv_FouRcC (’P’, ’I’, ’M’, ’1’) : Ceci est MPEG-1. L’ex- tension de fichier devrait être avi.
— cv2.ev.cv_FouRCC (’M’, ’J’, ’P,’ G ’): C’est un mouvement-JPEG.
Le fichier l’extension devrait être avi.
— cv2.ev.cv_FouRCC (’T’, ’# ,’ E ’,’ 0 ’) ! : Ceci est Ogg-Vorbis. Le fichier l’extension devrait être ogy.
— cv2.ev.cv_FOURCC (’F ,’ L ’,’ v ’,’ 1 ’) ! : Ceci est une vidéo Flash. Le fichier l’extension devrait être de 1 £.
Une fréquence d’images et une taille d’image doivent également être spéci- fiées. Puisque nous copions de une autre vidéo, ces propriétés peuvent être lues à partir de notre méthode get () de la classe VideoCapture
2.5 Capture d’images de caméra
Un flux de cadres de caméra est également représenté par la classe VideoCapture.
Cependant, pour une caméra, nous construisons une classe VideoCapture en passant l’index de l’appareil photo à la place du nom de fichier d’une vidéo.
8CHAPITRE 2. OPENCV GÉRER DES FICHIERS, DES CAMÉRAS, ET GUI (GRAPHICAL USER INTERFACES ) Considérons un exemple qui capture 10 secondes de vidéo à partir d’une caméra
et l’écrit à ah fichier AVI : importer cv2
cameraCapture = cv2.VideoCapture (0) fps = 30 # une hypothèse
size = (int (cameraCapture.get (cv2.cv.CV_CAP_PROP_FRAME_WIDTH)), int (cameraCapture.get (cv2.cv.CV_CAP_PROP_FRAME_HEIGHT)})
videoWriter = cv2.VideoWriter (:
’MyOutputVid.avi’, cv2.cv.CV_POURCC (’I’, ’4’, ’2’, ’0’), fps, taille) succès, frame = cameraCapture.read ()
numFramesRemaining = 10 * fps - 1
alors que le succès et numFramesRemaining> 0:
videoWriter.write (image)
succès, frame = cameraCapture.read () numFramesRemaining - = 1
Malheureusement, la méthode get () d’une classe videoCapture ne retourne pas un. valeur précise pour le taux de trame de la caméra ; il retourne toujours 0. Dans le but de créer une classe appropriée Vidéowriter pour la caméra, nous devons soit faire une hypothèse sur le taux de trame (comme nous l’avons fait dans le code précédemment) ou le mesurer en utilisant une minuterie. Cette dernière approche est meilleure et nous l’aborderons plus loin dans ce chapitre.
Le nombre de caméras et leur classement dépendent bien entendu du sys- tème. Malheureusement, OpenCV ne fournit aucun moyen d’interroger le nombre de caméras ou leurs propriétés. Si un index invalide est utilisé pour construire une VideoCapture class, la classe VideoCapture ne donnera pas de cadres ; sa méthode read () sera return (false, None).
La méthode read() est inappropriée lorsque nous devons synchroniser un ensemble de caméras ou une caméra à plusieurs têtes (comme une caméra stéréo ou un Kinect). Ensuite, nous utilisons plutôt les méthodes grab () et retrieve ().
Pour un ensemble de caméras :
success0 = cameraCaptured.grab () successi = cameraCapturel.grab () si succès0 et succès1:
frameO = cameraCapture0.retrieve () framel = cameraCapturel.retrieve ()
2.6. AFFICHAGE DES CADRES DE CAMÉRA DANS UNE FENÊTRE 9 Pour une caméra à plusieurs têtes, nous devons spécifier l’index d’une tête en tant qu’argument retrieve () :
success = multiHeadCameraCapture.grab() if success:
frameO = multiHeadCameraCapture.retrieve (channel = 0) framel = multiHeadCameraCapture.retrieve (channel = 1)
Nous étudierons plus en détail les caméras multi-têtes au chapitre 5, Détection de premier plan et Régions d’arrière-plan et profondeur.
2.6 Affichage des cadres de caméra dans une fe- nêtre
OpenCV permet aux fenêtres nommées d’être créées, redessinées et détruites en utilisant fonctions namedWindow (), imshow () et destroyWindow (). De plus, n’importe quelle fenêtre peut capturer l’entrée du clavier via la fonction waitKey () et l’entrée de la souris via le Fonction setMouseCallback (). Regardons un exemple où nous montrons des cadres d’entrée de la caméra en direct :
importer cv2 clicked = False
def onMouse (event, x, y, flags, param):
global clicked
if event == cv2.cv.CV_EVENT_LBUTTONUP:
clicked = True
cameraCapture = cv2.VideoCapture (0) cv2 .namedWindow (’MyWindow’ )
ev2.setMouseCallback (’MyWindow’, onMouse)
print ‘Showing camera feed. Click window or press any key to stop.’
success, frame = cameraCapture.read()
while success and cv2.waitKey(1) == -1 and not clicked:
cv2.imshow(’MyWindow’, frame)
success, frame = cameraCapture.read() v2. destroyWindow ( ’MyWindow’)
L’argument de waitKey () est un nombre de millisecondes à attendre pour l’entrée au clavier. La valeur de retour est -1 (aucune touche n’a été enfoncée) ou un code ASCII, comme 27 pour Esc. Pour obtenir la liste des codes-clés
10CHAPITRE 2. OPENCV GÉRER DES FICHIERS, DES CAMÉRAS, ET GUI (GRAPHICAL USER INTERFACES ) ASCII, voir http : //www.asciitable.com/. Notez également que Python fournit
une fonction standard, ord (), qui peut convertir caractère à son code clé ASCII.
Par exemple, ord ’a) renvoie 97.
Sur certains systèmes, attendez Key () peut renvoyer une valeur qui code plus que juste le code clé ASCII. (Un bug est connu pour se produire sur Linux quand al OpenCV utilise GTK comme bibliothèque d’interface graphique principale.) Sur tous les sys- tèmes, nous pouvons s’assurer que nous extrayons juste le code clé ASCII en lisant le dernier octet Qh de la valeur de retour, comme ceci :
keycode = cv2.waitKey (1) si keycode = -1 : !
code clé & = OxFF
Les fonctions de fenêtre d’OpenCV et waitKey () sont interdépendantes.
Fenêtres OpenCV sont mis à jour uniquement lorsque waitKey () est appelée, et waitKey () ne capture que l’entrée lorsqu’une fenêtre OpenCV a le focus.
Le rappel de souris transmis à setMouseCallback () doit prendre cinq argu- ments, comme vu dans notre exemple de code. L’argument param du callback est défini comme un troisième optionnel argument à setMouseCallback (). Par défaut, il est 0. L’argument d’événement de rappel est l’un des suivants :
cv2.cv.CV_EVENT_MOUSEMOVE: Mouse movement cv2.cv.CV_EVENT_LBUTTONDOWN: Left button down cv2.cv.CV_EVENT RBUTTONDOWN: Right button down cv2.cv.CV_EVENT_MBUTTONDOWN: Middle button down cv2.cv.CV_EVENT_LBUTTONUP: Left button up cv2.cv.CV_EVENT_RBUTTONUP: Right button up cv2.cv.cv_EVENT_mBuTToONUP: Middle button up
cv2.cv.CV_EVENT_LBUTTONDBLCLK: Left button double-click cv2.cv.CV_EVENT_RBUTTONDBLCLK: Right button double-click cv2.cv.CV_EVENT_MBUTTONDBLCLK: Middle button double-click
2.7. CONCEPT DE PROJET 11 L’argument de drapeaux de rappel de la souris peut être une combinaison de bits suivants :
cv2.cv.CV_EVENT_FLAG_LBUTTON: The left button pressed cv2.cv.CV_EVENT_FLAG_RBUTTON: The right button pressed cv2.cv.CV_EVENT_FLAG_MBUTTON: The middle button pressed cv2.cv.CV_EVENT_FLAG_CTRLKEY: The Ctrl key pressed cv2.cv.CV_EVENT_FLAG_SHIFTKEY: The Shift key pressed cv2.cv.CV_EVENT_FLAG_ALTKEY: The Alt key pressed
Malheureusement, OpenCV ne fournit aucun moyen de gérer les événements de fenêtre. Par exemple, nous ne pouvons pas arrêter notre application lorsque le bouton de fermeture de la fenêtre est cliqué. En raison de la gestion limitée des événements et des fonctionnalités de l’interface graphique d’OpenCV, les développeurs préfèrent l’intégrer avec un autre framework d’application. Plus tard dans cette chapitre, nous allons concevoir une couche d’abstraction pour aider à intégrer OpenCV dans tout cadre d’application.
2.7 Concept de projet
OpenCV est souvent étudié à travers une approche de livre de cuisine qui couvre beaucoup d’algorithmes mais rien sur le développement d’applications de haut niveau. Dans une certaine mesure, cette approche est compréhensible car les applications potentielles d’OpenCV sont si diverses. Par exemple, nous pour- rions l’utiliser dans un éditeur photo/vidéo, un jeu contrôlé par le mouvement, un un robot AI, ou une expérience de psychologie où nous enregistrons les mou- vements oculaires des participants. Dans des cas d’utilisation aussi différentes, pouvons-nous vraiment étudier un ensemble utile d’abstractions ?
Je crois que nous pouvons et le plus tôt nous commençons à créer des abs- tractions, le mieux, nous allons structurer notre étude d’OpenCV autour d’une seule application, mais, à chaque étape, nous allons concevoir un composant de cette application pour être extensible et réutilisable.
Nous allons développer une application interactive qui effectue le suivi du vi- sage et l’image _ manipulations sur l’entrée de la caméra en temps réel. Ce type d’application couvre une large gamme de fonctionnalités OpenCV et nous met
12CHAPITRE 2. OPENCV GÉRER DES FICHIERS, DES CAMÉRAS, ET GUI (GRAPHICAL USER INTERFACES ) au défi de créer un efficace, efficace ! la mise en oeuvre. Les utilisateurs remarque-
raient immédiatement des défauts, tels qu’une faible fréquence d’images. ou un suivi inexact. Pour obtenir les meilleurs résultats, nous allons essayer plusieurs approches en utilisant imagerie conventionnelle et imagerie en profondeur.
Plus précisément, notre application effectuera une fusion faciale en temps réel. Donné deux flux d’entrée caméra (ou, en option, entrée vidéo préenregis- trée), l’application va superposer les visages d’un flux sur les visages de l’autre.
Filtres et des distorsions seront appliquées pour donner à la scène fusionnée un aspect et une sensation unifiés. Utilisateurs devrait avoir l’expérience d’être en- gagé dans une performance en direct où ils entrent un autre environnement et un autre personnage. Ce type d’expérience utilisateur est populaire dans parcs d’attractions tels que Disneyland.
Nous appellerons notre application Cameo. Un camée est (en bijoux) un petit portrait d’un personne ou (au cinéma) un rôle très bref joué par une célébrité.
2.8 Un projet orienté objet
Les applications Python peuvent être écrites dans un style purement procé- dural. Ceci est souvent fait avec de petites applications comme nos scripts d’E/S de base, discutés précédemment. cependant, à partir de maintenant, nous allons utiliser un style orienté objet car il favorise la modularité et extensibilité.
De notre aperçu des fonctionnalités d’E / S d’OpenCV, nous savons que toutes les images sont similaires, quelle que soit leur source ou leur destination.
Peu importe comment nous obtenons un flux des images ou où nous l’envoyons en sortie, nous pouvons appliquer la même application spécifique logique à chaque image dans ce flux. Séparation du code d’E / S et du code d’application de- vient particulièrement pratique dans une application comme Cameo, qui utilise plusieurs flux E / S.
Nous allons créer des classes appelées CaptureManager et WindowManager comme haut niveau interfaces avec les flux d’E / S. Notre code d’application peut utiliser un CaptureManager pour lire de nouvelles trames et, facultativement, envoyer chaque trame à une ou plusieurs sorties, y compris un fichier image fixe, un fichier vidéo et une fenêtre (via une classe WindowManager). Une classe WindowManager permet à notre code d’application de gérer une fenêtre et des événements dans un style orienté objet.
CaptureManager et WindowManager sont tous deux extensibles. Nous pour- rions faire implémentations qui ne reposaient pas sur OpenCV en E/ S. En effet, l’annexe A, L’intégration avec Pygame utilise une sous-classe WindowManager.
2.9. RÉSUMÉ D’UNGESTIONNARE DE FLUX VIDÉO .CAPTUREMANAGER13
2.9 Résumé d’ungestionnare de flux vidéo .Cap- tureManager
Comme nous l’avons vu, OpenCV peut capturer, montrer et enregistrer un flux d’images de soit un fichier vidéo ou une caméra, mais il y a des considéra- tions spéciales dans chaque Cas. Notre classe CaptureManager résume certaines des différences et fournit un interface de niveau supérieur pour l’envoi d’images du flux de capture à un ou plusieurs sorties : un fichier image fixe, un fichier vidéo ou une fenêtre.
Une classe CaptureManager est initialisée avec une classe VideoCapture et a la Les méthodes enterFrame () et exitFrame () qui devraient normalement être appelées itération de la boucle principale d’une application. Entre un appel à enterFrame () et un appel à exitFrame (), l’application peut (un nombre illimité de fois) définir une propriété channe1 et obtenir une propriété de cadre.
La propriété de canal est initialement 0 et seulement multi-tête les caméras utilisent d’autres valeurs. La propriété frame est une image correspondant à la l’état du canal actuel quand enterFrame () a été appelé.
Une classe CaptureManager a également writeImage (), startWritingVvideo () et la méthodes stopWritingVideo () qui peuvent être appelées à tout moment.
Écriture de fichier réelle est reportée jusqu’à la sortie Frame (). Aussi pendant la méthode de sortie Frame (), le cadre la propriété peut être affichée dans une fenêtre, selon que l’application code fournit une classe WindowManager soit comme un argument au constructeur de CaptureManager ou en définissant une propriété, previewWindowManager.
Si le code de l’application manipule frame, les manipulations sont reflétées dans tous les fichiers enregistrés et dans la fenêtre. Une classe CaptureManager a un constructeur argument et une propriété appelée shouldMirrorPreview, qui devrait être vraie si nous voulons que l’image soit reflétée (horizontalement) dans la fenêtre mais pas dans fichiers enregistrés. En règle générale, face à une caméra, les utilisateurs préfèrent se refléterdans le flux de la caméra en direct . Rappelez-vous qu’une classe VideowWriter a besoin d’une fréquence d’images, mais OpenCV ne fournit pas n’importe quel moyen d’obtenir une fréquence d’image précise pour une caméra. La classe CaptureManager fonctionne autour de cette limitation en utilisant un compteur de trames et la norme de Python.
temps () fonction pour estimer la fréquence d’images si nécessaire. Cette ap- proche n’est pas infaillible. En fonction des fluctuations du débit d’images et de l’implémentation dépendante du système de temps. temps (), l’exactitude de l’estimation pourrait encore être mauvaise dans certains cas. Cependant, si nous déployons sur un matériel inconnu, il vaut mieux que de supposer que la caméra de l’utilisateur a une fréquence d’image particulière.
14CHAPITRE 2. OPENCV GÉRER DES FICHIERS, DES CAMÉRAS, ET GUI (GRAPHICAL USER INTERFACES ) Créons un fichier appelé gestionnaires. py, qui contiendra notre implémenta-
tion de CaptureManager. La mise en œuvre s’avère être assez longue. Donc, nous allons regarder en plusieurs morceaux. Tout d’abord, ajoutons les importations, un constructeur et les propriétés, comme suit :
import cv2 import numpy import time
class CaptureManager (object) :
def __init__(self, capture, previewWindowManager = None, shouldMirrorPreview = False):
self .previewWindowManager = previewWindowManager self.shouldMirrorPreview = shouldMirrorPreview self. capture = capture
self. channel = 6
self. _enteredFrame = False self. frame = None
self. _imageFilename = None self. videoFilename = None self. videoEncoding = None self. videoWriter = None
self. startTime = None
self. framesElapsed = long(0) self. _fpsEstimate = None
@property
def channel (sel£) : return self. channel
@channel. setter
def channel(self, value):
if self, channel !+ value:
2.9. RÉSUMÉ D’UNGESTIONNARE DE FLUX VIDÉO .CAPTUREMANAGER15
self. channel = value self. frame = None
@property
def frame (self):
if self._enteredFrame and self. frame is None:
_, self. frame = self._capture. retrieve ( channel = self.channel)
return self._frame
@property
def isWritingImage (self):
return self. imageFilename is not None
@property
def isWritingvideo(self) :
return self. videoFilename is not None
Notez que la plupart des variables membres sont non publiques, comme indi- qué par le trait de soulignement préfixe dans les noms de variables, tels que self ._enteredFrame, Ces variables non publiques se rapportent à l’état de l’image en cours et à toute opération d’écriture de fichier. Comme précédemment discuté, le code de l’application doit seulement configurer quelques choses, qui sont implé- menté comme arguments constructeurs et propriétés publiques paramétrables : la caméra canal, le gestionnaire de fenêtre et l’option pour refléter l’aperçu de la caméra.
Par convention, en Python, les variables précédées d’un seul Le trait de soulignement doit être traité comme protégé (accessible uniquement dans le Classe et ses sous-classes), tandis que les va- riables précédées d’un double Le soulignement doit être traité comme privé (accessible uniquement dans la classe).
Poursuivant notre implémentation, ajoutons enterFrame () et exitFrame () méthodes aux gestionnaires .py :
def enterFrame (self):
“Capture the next frame, if any."""
# But first, check that any previous frame was exited.
assert not self._enteredFrame, \
"previous enterFrame() had no matching exitFrame()’
16CHAPITRE 2. OPENCV GÉRER DES FICHIERS, DES CAMÉRAS, ET GUI (GRAPHICAL USER INTERFACES )
if self. capture is not None:
self. enteredFrame = self._capture.grab() def exitFrame (self):
“""Draw to the window. Write to files. Release the frame.""*
# Check whether any grabbed frame is retrievable.
# The getter may retrieve and cache the frame.
if self.frame is None:
self. enteredPrame = False return
# Update the FPS estimate and related variables.
if self. framesElapsed == 0:
self. startTime = time.time() else:
timeElapsed = time.time() - self. startTime
self. fpsEstimate = self. framesElapsed / timeElapsed self. _framesElapsed += 1
# Draw to the window, if any.
if self.previewWindowManager is not None:
if self.shouldMirrorPreview:
mirroredFrame = numpy.fliplr(self._frame) .copy() self. previewWindowManager . show (mirroredFrame) else:
self. previewWindowManager.show(self, frame)
# Write to the image file, if any.
if self.isWritingImage:
cv2.imwrite(self._imageFilename, self._frame) self. _imagePilename = None
# Write to the video file, if any.
self. writeVideoFrame ()
# Release the frame.
self. frame = None
self. enteredFrame = False
Notez que l’implémentation de enterFrame () n’obtient (synchronise) qu’une
2.9. RÉSUMÉ D’UNGESTIONNARE DE FLUX VIDÉO .CAPTUREMANAGER17 image, considérant que la récupération effective d’un canal est reportée à une lecture ultérieure de la variable frame. L’implémentation de exit Frame () prend l’image du canal actuel, estime une fréquence d’images, affiche l’image via le gestionnaire de fenêtres (le cas échéant) et répond à toutes les demandes en attente d’écriture de l’image dans des fichiers.
Plusieurs autres méthodes concernent également l’écriture de fichiers. Pour terminer notre implémentation de classe, ajoutons les méthodes d’écriture de fichiers restantes aux gestionnaires. py :
def writeImage (self, filename) :
“Write the next exited frame to an image file."""
self. _imageFilename = filename def startWritingVideo(
self, filename,
encoding = cv2.cv.CV_FOURCC(’I’,’4’,’2’,’0’)):
“start writing exited frames to a video file."""
self, _videoFilename = filename self, videoBncoding = encoding def stopWritingVideo (self):
“w"gtop writing exited frames to a video file."""
self. videoFilename = None self. videoEncoding = None self. videoWriter = None def _writeVideoFrame (self):
if not self.isWritingVideo:
return
if self. videoWriter is None:
fps = self._capture.get (cv2.cv.CV_CAP_PROP_FPS) if fps == 0.0:
# The capture’s FPS is unknown so use an estimate.
if self. _framesElapsed < 20:
# Wait until more frames elapse so that the
# estimate is more stable.
return else:
fps = self. _fpsEstimate
size = (int (self. _capture.get ( cv2.cv.CV_CAP_PROP_FRAME_WIDTH)) , int (self. _capture.get (
18CHAPITRE 2. OPENCV GÉRER DES FICHIERS, DES CAMÉRAS, ET GUI (GRAPHICAL USER INTERFACES ) cv2.cv.CV_CAP_PROP_FRAME_HEIGHT) )}
self. videoWriter = cv2.VideoWriter ( self. _videoPilename, self. videoEncoding, self. videoWriter.write (self. frame)
Les méthodes publiques, writeImage (), startWritingVideo () et stopWrit ingVideo (), enregistre simplement les paramètres pour les opérations d’écriture de fichiers, alors que les opérations d’écriture proprement dites sont reportées à l’appel de sortie suivant Frame (). La méthode non publique writeVideoFrame () crée ou ajoute un fichier vidéo dans une manière qui devrait être familière de nos scripts précédents. (Voir le ReadingMWriting un section de fichier vidéo.) Cependant, dans les situations où le taux de trame est inconnu, nous sautons quelques images au début de la session de capture de sorte que nous avons le temps de construire un estimation du taux de trame.
Bien que notre implémentation actuelle de CaptureManager repose sur Vi- deoCapture, nous pourrait faire d’autres implémentations qui n’utilisent pas OpenCV pour l’entrée. Par exemple, nous pourrions faire une sous-classe qui a été instanciée avec une connexion socket, dont l’octet flux pourrait être analysé comme un flux d’images. En outre, nous pourrions faire une sous-classe utilisé une bibliothèque de caméra tiers avec un support matériel différent de ce que OpenCV fournit. Cependant, pour Cameo, notre implémentation actuelle est suffisante.
2.10 Résumé : gestionaire d’une fenêtre et d’un clavier .WindowManager
Comme nous l’avons vu, OpenCV fournit des fonctions qui provoquent la création d’une fenêtre, être détruit, montrer une image et traiter les événements.
Plutôt que d’être des méthodes de une classe de fenêtre, ces fonctions nécessitent le passage d’un nom de fenêtre comme argument. Puisque cette interface n’est pas orientée objet, elle n’est pas cohérente avec les généralités d’OpenCV. style.
De plus, il est peu probable qu’il soit compatible avec d’autres fenêtres ou évé- nements. interfaces que nous pourrions éventuellement vouloir utiliser à la place d’OpenCV.
Pour l’orientation de l’objet et l’adaptabilité, nous résumons cette fonction- nalité dans une classe WindowManager avec createWindow (), destroyWindow (), Méthodes show () et processEvents (). En tant que propriété, une classe WindowManager a un objet fonction appelé keypressCallback, qui (sinon None) est appelé depuis processEvents () en réponse à une pression sur une touche.
L’objet keypressCallback doit prendre un seul argument, un code clé ASCII.
Ajoutons cette implémentation de WindowManager aux gestionnaires .py :
2.10. RÉSUMÉ : GESTIONAIRE D’UNE FENÊTRE ET D’UN CLAVIER .WINDOWMANAGER19
class WindowManager (object) :
def __init__(self, windowName, keypressCallback = None):
self .keypressCallback = keypressCallback self. windowName = windowName
self._isWindowCreated = False
@property
def isWindowCreated(self) : return self._isWindowCreated def createWindow (self):
cv2.namedWindow(self._windowName) self. isWindowCreated = True def show(self, frame):
cv2.imshow(self._windowName, frame) def destroyWindow (self):
cv2.destroyWindow(self._windowName) self. isWindowCreated = False def processEvents (self):
keycode = cv2.waitKey (1)
if self.keypressCallback is not None and keycode !=
# Discard any non-ASCII info encoded by GTK.
keycode &= 0xFF
self .keypressCal1back (keycode)
Notre implémentation actuelle ne prend en charge que les événements de cla- vier, ce qui sera suffisant pour Cameo. Cependant, nous pourrions aussi modifier windowManager pour supporter les événements souris. Par exemple, l’interface de la classe pourrait être étendue pour inclure un mouseCallback propriété (et l’argument constructeur facultatif) mais pourrait autrement rester le même.
Avec un cadre d’événement autre que OpenCV, nous pourrions soutenir des les types d’événement de la même manière, en ajoutant des propriétés de rappel.
L’annexe A, Intégration avec Pygame, montre une sous-classe WindowMa- nager qui est mis en œuvre avec le cadre de gestion des fenêtres et des événe- ments de Pygame au lieu de OpenCV. Cette implémentation améliore la classe WindowManager de base en gérer correctement les événements de fermeture, par exemple, lorsque l’utilisateur clique sur la fenêtre bouton de fermeture Po- tentiellement, de nombreux autres types d’événements peuvent également être
20CHAPITRE 2. OPENCV GÉRER DES FICHIERS, DES CAMÉRAS, ET GUI (GRAPHICAL USER INTERFACES ) gérés via Pygame.
2.11 Appliquer pour tout — cameo.Cameo
Notre application est représentée par une classe, Cameo, avec deux mé- thodes : run () etonkeypress (). A l’initialisation, une classe Cameo crée une classe WindowManager avec onKeypress ()en tant que rappel,ainsi qu’une classe CaptureManager utilisant une caméra et la classe WindowManager.
Lorsque run () est appelée, l’application exécute une commande principale boucle dans laquelle les trames et les événements sont traités. À la suite du traitement de l’événement, onkeypress ()peut être appelé. La barre d’espace provoque une capture d’écran, onglet provoque un screencast (un enregistrement vidéo) pour démarrer / arrêter, et Esc provoque l’application quitter.
Dans le même répertoire que les gestionnaires .py, créons un fichier appelé caméo. py contenant l’implémentation suivante de Cameo :
import cv2
from managers import WindowManager, CaptureManager class Cameo(object) :
def init__(self):
self._windowManager = WindowManager(’Cameo’, self .onKeypress)
self._captureManager = CaptureManager (
cv2.VideoCapture(0), self, windowManager, True) def run(self):
“""Run the main loop.""TM"
self. windowManager.createWindow()
while self. windowManager. isWindowCreated:
self. captureManager.enterFrame () frame = self._captureManager.frame
# TODO: Filter the frame (Chapter 3).
self. captureManager .exitFrame () self ._windowManager . processEvents () def onKeypress (self, keycode);
"""Handle a keypress.
2.12. RÉSUMÉ 21 space -> Take a screenshot.
tab -> Start/stop recording a screencast.
escape -> Quit.
"""
if keycode == 32: # space
self._captureManager .writeImage (‘screenshot .png’) elif keycode == 9: # tab
if not self._captureManager.isWritingVideo:
self. _captureManager.startWritingVideo (
‘screencast .avi’) else:
self. captureManager. stopWritingVideo() elif keycode == 27: # escape
self. _windowManager.destroyWindow O if __name__=="__main__":
Cameo () .run{)
Lors de l’exécution de l’application, notez que le flux de la caméra en direct est mis en miroir, tandis que captures d’écran et screencasts ne le sont pas. C’est le comportement prévu, car nous passons Vrai pour shouldMirrorPreview lors de l’initialisation de la classe CaptureManager.
Jusqu’à présent, nous ne manipulons les cadres d’aucune façon, sauf pour les refléter en tant qu’aperçu. Nous allons commencer à ajouter des effets plus intéressants dans le Chapitre 3, Filtrage des images.
2.12 Résumé
Maintenant, nous devrions avoir une application qui affiche un flux de ca- méra, écoute pour entrée au clavier, et (sur commande) enregistre une capture d’écran ou un screencast. Nous sommes prêt à étendre l’application en insérant un code de filtrage d’image (Chapitre 3, Filtrage des images)entre le début et la fin de chaque image. En option, nous sommes aussi prêt à intégrer d’autres pilotes de caméra ou d’autres cadres applicatifs (Annexe A, Intégration avec Pygame), en plus de celles supportées par OpenCV.
22CHAPITRE 2. OPENCV GÉRER DES FICHIERS, DES CAMÉRAS, ET GUI (GRAPHICAL USER INTERFACES )
Chapitre 3
Filtrer les images
Ce chapitre présente quelques techniques pour modifier les images. Notre objectif est d’atteindre effets artistiques, similaires aux filtres que l’on peut trouver dans les applications d’édition d’images, comme Photoshop ou Gimp.
Comme nous procédons à la mise en œuvre des filtres, vous pouvez essayer de les appliquer à n’importe quel BGR image, puis enregistrez ou affichez le résultat. Pour apprécier pleinement chaque effet, essayez avec diverses conditions d’éclairage et sujets. À la fin de ce chapitre, nous allons intégrer des filtres dans l’application Cameo.
Tout le code terminé pour ce chapitre peut être téléchargé à partir de mon site Web :http://nummist.com/opencv/3923_03.zip.
3.1 Créer des modules
Comme nos classes CaptureManager et WindowManager, nos filtres de- vraient être réutilisables à l’extérieur du camée. Ainsi, nous devrions séparer les filtres dans leur propre module Python ou fichier.
Créons un fichier appelé filters.py dans le même répertoire que cameo.py.
Nous avons besoin de les instructions d’importation suivantes dans filters.py :
import cv2 import numpy import utils
Créons aussi un fichier appelé utils.py dans le même répertoire. Il devrait conte- nir le déclarations d’importation suivantes :
23
24 CHAPITRE 3. FILTRER LES IMAGES import cv2
import numpy
import scipy.interpolate
Nous allons ajouter des fonctions de filtrage et des classes à filters.py, tandis que fonctions mathématiques à usage général ira dans utils.py. Mixage de chaînes - voir en Technicolor
3.2 Le mixage des canaux– voir en technicolor.
Le pixel de destination est une fonction de la couleur du pixel source cor- respondant (seulement). Plus précisément, la valeur de chaque canal au pixel de destination est fonction de toutes les valeurs des canaux au pixel source. En pseudocode, pour une image BGR :
dst.b = funcB (src.b, src.g, src.r) dst.g = funcG (src.b, src.g, src.r) dst.r = funcR (src.b, src.g, src.r)
Nous pouvons définir ces fonctions comme bon nous semble. Potentiellement, nous pouvons cartographier une scène les couleurs sont très différentes de celles d’une caméra ou de nos yeux.
Une utilisation du mélange de canaux consiste à simuler un autre espace de couleur plus petit à l’intérieur du RGB ou BGR. En affectant des valeurs égales à deux canaux, nous pouvons réduire une partie du espace de couleur et créer l’impression que notre palette est basée sur seulement deux couleurs de lumière (mélangée additivement) ou deux encres (mélangée soustractive).
Ce type d’effet peut offrir une valeur nostalgique parce que les premiers films couleur et les premiers graphiques numériques avaient plus palettes limitées que les graphiques numériques aujourd’hui.
Par exemple, inventons quelques espaces de couleur notionnels qui rappellent Les films Technicolor des années 1920 et les graphismes CGA des années 1980.
Tous ces concepts les espaces colorimétriques peuvent représenter des gris, mais aucun ne peut représenter la gamme de couleurs complète de RVB :
•RC (rouge, cyan) : Notez que le rouge et le cyan peuvent se mélanger pour faire des gris. Cette couleur l’espace ressemble à Technicolor Process 2 et CGA Palette 3.
• RGV (rouge, vert, valeur) : Notez que le rouge et le vert ne peuvent pas se mélanger pour faire des gris. Nous devons donc également spécifier la valeur ou la blancheur. Cet espace de couleur ressemble à Processus Technicolor 1.
3.2. LE MIXAGE DES CANAUX– VOIR EN TECHNICOLOR. 25
•CMV (cyan, magenta, valeur) : Notez que le cyan et le magenta ne peuvent pas se mélanger faire des gris. Nous devons donc également spécifier la valeur ou la blancheur. Cette couleur l’espace ressemble à CGA Palette 1.
Ce qui suit est une capture d’écran de The Toll of the Sea (1922), un film tourné en Technicolor Process 2 :
L’image suivante provient de Commander Keen : Goodbye Galaxy (1991), un jeu qui supporte CGA Palette 1.
26 CHAPITRE 3. FILTRER LES IMAGES
3.3 Simulation de l’espace de couleurs RC
L’espace colorimétrique RC est facile à simuler dans BGR. Le bleu et le vert peuvent se mélanger pour faire du cyan. Par en faisant la moyenne des canaux B et G et en stockant le résultat dans B et G, nous avons effectivement effondre ces deux canaux en un, C. Pour soutenir cet effet, ajoutons ce qui suit fonction à filters.py :
def recolorRC(src, dst):
"""Simulate conversion from BGR to RC (red, cyan).
The source and destination images must both be in BGR format.
Blues and greens are replaced with cyans.
Pseudocode:
dst.b = dst.g = 0.5 * (src.b + src.g) dst.r = src.r
"""
b, g, r = cv2.split(src)
cv2.addWeighted(b, 0.5, g, 0.5, 0, b) cv2.merge((b, b, r), dst)
Trois choses se passent dans cette fonction :
1. En utilisant split (), nous extrayons les canaux de notre image source en unidimensionnel tableaux. Après avoir mis les données dans ce format, nous pouvons écrire un canal clair et simple code de mélange.
2. En utilisant addWeighted (), nous remplaçons les valeurs du canal B par une moyenne de B et G. Les arguments à addWeighted () sont (dans l’ordre) la première source tableau, un poids appliqué au premier tableau source, le deuxième tableau source, un poids appliqué au second tableau source, une constante ajoutée au résultat, et un tableau de destination.
3. En utilisant fusionner (), nous remplaçons les valeurs de notre image de destination par canaux modifiés. Notez que nous utilisons deux fois b comme argument parce que nous voulez que les canaux B et G de la destination soient égaux.
Des étapes similaires (fractionnement, modification et fusion de canaux) peuvent être appliquées à d’autres simulations d’espace de couleur.
3.4 Simulation de l’espace colorimétrique RGV
L’espace colorimétrique RGV est juste un peu plus difficile à simuler dans BGR. Notre intuition pourrait dire que nous devrions mettre toutes les valeurs
3.5. SIMULATION DE L’ESPACE COULEUR CMV 27 de canal B à 0 parce que RGV ne peut pas représenter bleu. Cependant, ce changement serait erroné, car il rejetterait le bleu composante de légèreté et, ainsi, transformer les gris et les bleus pâles en jaunes. Au lieu, nous voulons que les gris restent gris tandis que les bleus pâles deviennent gris. Pour atteindre ce résultat, nous devrions réduire les valeurs B au minimum par pixel de B, G et R. cet effet dans filters.py comme la fonction suivante :
def recolorRGV(src, dst):
"""Simulate conversion from BGR to RGV (red, green, value).
The source and destination images must both be in BGR format.
Blues are desaturated.
Pseudocode:
dst.b = min(src.b, src.g, src.r) dst.g = src.g
dst.r = src.r
"""
b, g, r = cv2.split(src) cv2.min(b, g, b)
cv2.min(b, r, b)
cv2.merge((b, g, r), dst)
La fonction min () calcule les minimums par élément des deux premiers argu- ments et les écrit au troisième argument.
3.5 Simulation de l’espace couleur CMV
La simulation de l’espace couleur CMV est assez similaire à la simulation de RGV, sauf que la partie désaturée du spectre est jaune au lieu de bleu.
Pour désaturer les jaunes, nous devrions augmenter les valeurs B au maximum par pixel de B, G et R. Voici un implémentation que nous pouvons ajouter à filters.py :
def recolorCMV(src, dst):
"""Simulate conversion from BGR to CMV (cyan, magenta, value).
The source and destination images must both be in BGR format.
Yellows are desaturated.
Pseudocode:
dst.b = max(src.b, src.g, src.r) dst.g = src.g
dst.r = src.r
"""
b, g, r = cv2.split(src) cv2.max(b, g, b)
cv2.max(b, r, b)
cv2.merge((b, g, r), dst)
28 CHAPITRE 3. FILTRER LES IMAGES La fonction max () calcule les maximums par élément des deux premiers arguments et les écrit au troisième argument.
De par leur conception, les trois effets précédents ont tendance à produire des distorsions majeures de couleur, surtout quand l’image source est colorée en premier lieu. Si nous voulons fabriquer subtil effets, le mélange de canaux avec des fonctions arbitraires n’est probablement pas la meilleure approche.
Courbes - espace de couleur de flexion
Les courbes sont une autre technique de remappage des couleurs. Mixage de canaux et courbes sont similaires dans la mesure où la couleur à un pixel de destination est une fonction de la couleur à la pixel source correspondant (seulement). Cependant, dans les détails, le mélange de canaux et les courbes sont des approches dissemblables. Avec les courbes, la valeur d’une chaîne sur un pixel de destination est une fonction de (seulement) la valeur du même canal au pixel source. De plus, nous faisons ne pas définir les fonctions directement ; à la place, pour chaque fonction, nous définissons un ensemble de contrôle les points à partir desquels la fonction est interpolée. En pseudocode, pour une image BGR :
dst.b = funcB(src.b) where funcB interpolates pointsB dst.g = funcG(src.g) where funcG interpolates pointsG dst.r = funcR(src.r) where funcR interpolates pointsR
Le type d’interpolation peut varier entre les implémentations, bien qu’il faille éviter les pentes discontinues aux points de contrôle et, à la place, produire des courbes. Nous allons utiliser l’interpolation spline cubique chaque fois que le nombre de points de contrôle est suffisant.
3.6 Formuler une courbe
Notre première étape vers les filtres basés sur des courbes consiste à convertir les points de contrôle en une fonction. La plupart de ce travail est fait pour nous par une fonction SciPy appelée interp1d (), qui prend deux tableaux (coordon- nées x et y) et renvoie une fonction qui interpole les points. En tant qu’argument optionnel de interp1d (), nous pouvons spécifier une sorte d’interpolation, qui, en principe, peut être linéaire, proche, nulle, slinéaire (linéaire sphérique), qua- dratique ou cubique, bien que toutes les options ne soient pas implémentées dans la version actuelle de SciPy. Un autre argument facultatif, bounds_error, peut être défini sur False pour autoriser extrapolation ainsi que l’interpolation.
Modifions utils.py et ajoutons une fonction qui enveloppe interp1d () avec un léger interface plus simple :
3.7. MISE EN CACHE ET APPLICATION D’UNE COURBE 29 def createCurveFunc(points):
"""Return a function derived from control points."""
if points is None:
return None
numPoints = len(points) if numPoints < 2:
return None
xs, ys = zip(*points) if numPoints < 4:
kind = ’linear’
# ’quadratic’ is not implemented.
else:
kind = ’cubic’
return scipy.interpolate.interp1d(xs, ys, kind, bounds_error = False)
Plutôt que deux tableaux séparés de coordonnées, notre fonction prend un tableau de (x, y) paires, ce qui est probablement un moyen plus lisible de spécifier le contrôle points. Le tableau doit être ordonné de sorte que x augmente d’un index à prochain. En règle générale, pour les effets d’apparence naturelle, les valeurs y devraient également augmenter, et les premier et dernier points de contrôle doivent être (0, 0) et (255, 255) pour préserver le noir et blanc. Notez que nous traiterons x comme la valeur d’entrée d’un canal et y en tant que valeur de sortie correspondante. Par exemple, (128, 160) égayer les tons moyens d’une chaîne.
Notez que l’interpolation cubique nécessite au moins quatre points de contrôle.
S’il y a seulement deux ou trois points de contrôle, nous retombons à l’interpo- lation linéaire mais, pour effets naturels, ce cas devrait être évité.
3.7 Mise en cache et application d’une courbe
Nous pouvons maintenant obtenir la fonction d’une courbe qui interpole les points de contrôle arbitraires. Cependant, cette fonction peut être coûteuse.
Nous ne voulons pas l’exécuter une fois par canal, par pixel (par exemple, 921 600 fois par trame si elle est appliquée à trois canaux de 640 x 480 vidéo).
Heureusement, nous traitons généralement avec seulement 256 entrées possibles valeurs (en 8 bits par canal) et nous pouvons précalculer à peu de frais et stocker autant valeurs de sortie. Ensuite, notre coût par canal, par pixel est juste une recherche de la mise en cache valeur de sortie.
Modifions utils.py et ajoutons des fonctions pour créer un tableau de re- cherche pour une fonction donnée et pour appliquer le tableau de recherche à un autre tableau (par exemple, une image) :
30 CHAPITRE 3. FILTRER LES IMAGES def createLookupArray(func, length = 256):
"""Return a lookup for whole-number inputs to a function.
The lookup values are clamped to [0, length - 1].
"""
if func is None:
return None
lookupArray = numpy.empty(length) i = 0
while i < length:
func_i = func(i)
lookupArray[i] = min(max(0, func_i), length - 1) i += 1
return lookupArray
def applyLookupArray(lookupArray, src, dst):
"""Map a source to a destination using a lookup."""
if lookupArray is None:
return
dst[:] = lookupArray[src]
Notez que l’approche dans createLookupArray () est limitée à l’entrée de nombres entiers valeurs, car la valeur d’entrée est utilisée comme un index dans un tableau. Le applyLookupArray () fonction fonctionne en utilisant les valeurs d’un tableau source en tant qu’index dans le tableau de recherche. La notation de tranche de Python ([ :]) est utilisée pour copier les valeurs recherchées dans une destination tableau.
Considérons une autre optimisation. Et si nous voulons toujours appliquer deux ou plusieurs courbes en succession ? Effectuer plusieurs recherches est inef- ficace et peut provoquer perte de précision. Nous pouvons éviter ce problème en combinant deux fonctions de courbe dans une fonction avant de créer un tableau de recherche. Modifions utils.py à nouveau et ajoutons le fonction suivante qui renvoie un composite de deux fonctions données :
def createCompositeFunc(func0, func1):
"""Return a composite of two functions."""
if func0 is None:
return func1 if func1 is None:
return func0
return lambda x: func0(func1(x))
L’approche dans createCompositeFunc () est limitée aux fonctions d’entrée qui prendre un seul argument. Les arguments doivent être de types compatibles.
3.7. MISE EN CACHE ET APPLICATION D’UNE COURBE 31 Notez l’utilisation de Le mot-clé lambda de Python pour créer une fonction anonyme.
Voici un dernier problème d’optimisation. Et si nous voulons appliquer la même courbe à tous les canaux d’une image ? Fractionnement et réorganisation des canaux est un gaspillage, dans ce cas, car nous n’avons pas besoin de faire la distinction entre les canaux. Nous avons juste besoin d’un ... l’indexation di- mensionnelle, telle qu’utilisée par applyLookupArray (). Modifions utils.py pour ajouter une fonction qui renvoie une interface unidimensionnelle à un tableau donné préexistant peut être multidimensionnel :
def createFlatView(array):
"""Return a 1D view of an array of any dimensionality."""
flatView = array.view() flatView.shape = array.size return flatView
Le type de retour est numpy.view, qui a à peu près la même interface que numpy.array, mais numpy.view ne possède qu’une référence aux données, pas une copie.
L’approche dans createFlatView () fonctionne pour les images avec n’im- porte quel nombre de canaux. Ainsi, il nous permet de faire abstraction de la différence entre les images en niveaux de gris et en couleur dans cas où nous souhaitons traiter tous les canaux de la même manière.
Concevoir des filtres de courbe orientés objet
Puisque nous mettons en cache un tableau de recherche pour chaque courbe, nos filtres basés sur des courbes ont des données associés avec eux. Ainsi, ils doivent être des classes, pas seulement des fonctions. Faisons une paire de classes de filtre de courbe, avec des classes de niveau supérieur correspondantes qui peuvent appliquer n’importe quelle fonction, pas seulement une fonction de courbe :
— VFuncFilter : Ceci est une classe qui est instanciée avec une fonction, qu’elle peut plus tard s’appliquer à une image en utilisant apply (). La fonction est appliquée au V canal (valeur) d’une image en niveaux de gris ou à tous les canaux d’une image en couleur.
— VcurveFilter : Ceci est une sous-classe de VFuncFilter. Au lieu d’être instancié avec une fonction, il est instancié avec un ensemble de points de contrôle, qu’il utilise en interne pour créer une fonction de courbe.
— BGRFuncFilter : Il s’agit d’une classe instanciée avec jusqu’à quatre fonc- tions, qu’il peut ensuite appliquer à une image BGR en utilisant apply ().
instancié avec une fonction, il est instancié avec un ensemble de points
32 CHAPITRE 3. FILTRER LES IMAGES de contrôle, qu’il utilise en interne pour créer une fonction de courbe.
L’une des fonctions est appliquée à tous les canaux et les trois autres les fonctions sont chacune appliquée à un seul canal. La fonction globale est appliqué d’abord, puis les fonctions par canal.
— BGRCurveFilter : il s’agit d’une sous-classe de BGRFuncFilter. Au lieu d’être instancié avec quatre fonctions, il est instancié avec quatre en- sembles de contrôle points, qu’il utilise en interne pour créer des fonctions de courbe.
De plus, toutes ces classes acceptent un argument constructeur de type nu- mérique, comme numpy.uint8 pour 8 bits par canal. Ce type est utilisé pour déterminer comment De nombreuses entrées doivent être dans le tableau de recherche.
Regardons d’abord les implémentations de VFuncFilter et VcurveFilter, qui Les deux peuvent être ajoutés à filters.py :
class VFuncFilter(object):
"""A filter that applies a function to V (or all of BGR)."""
def __init__(self, vFunc = None, dtype = numpy.uint8):
length = numpy.iinfo(dtype).max + 1
self._vLookupArray = utils.createLookupArray(vFunc, length) def apply(self, src, dst):
"""Apply the filter with a BGR or gray source/destination."""
srcFlatView = utils.flatView(src) dstFlatView = utils.flatView(dst)
utils.applyLookupArray(self._vLookupArray, srcFlatView, dstFlatView)
class VCurveFilter(VFuncFilter):
"""A filter that applies a curve to V (or all of BGR)."""
def __init__(self, vPoints, dtype = numpy.uint8):
VFuncFilter.__init__(self, utils.createCurveFunc(vPoints), dtype)
Ici, nous internalisons l’utilisation de plusieurs de nos fonctions précédentes : createCurveFunc (), createLookupArray (), flatView () et applyLookupArray (). Nous utilisons aussi numpy.iinfo () pour déterminer la plage pertinente de valeurs de recherche, basée sur le type numérique donné. Maintenant, regardons les implémentations de BGRFuncFilter et BGRCurveFilter, qui peut être ajouté à filters.py :
class BGRFuncFilter(object):
"""A filter that applies different functions to each of BGR."""
3.8. ÉMULATION DE FILMS PHOTO 33 def __init__(self, vFunc = None, bFunc = None, gFunc = None, rFunc = None, dtype = numpy.uint8):
length = numpy.iinfo(dtype).max + 1
self._bLookupArray = utils.createLookupArray(
utils.createCompositeFunc(bFunc, vFunc), length) self._gLookupArray = utils.createLookupArray(
utils.createCompositeFunc(gFunc, vFunc), length) self._rLookupArray = utils.createLookupArray(
utils.createCompositeFunc(rFunc, vFunc), length) def apply(self, src, dst):
"""Apply the filter with a BGR source/destination."""
b, g, r = cv2.split(src)
utils.applyLookupArray(self._bLookupArray, b, b) utils.applyLookupArray(self._gLookupArray, g, g) utils.applyLookupArray(self._rLookupArray, r, r) cv2.merge([b, g, r], dst)
class BGRCurveFilter(BGRFuncFilter):
"""A filter that applies different curves to each of BGR."""
def __init__(self, vPoints = None, bPoints = None, gPoints = None, rPoints = None, dtype = numpy.uint8):
BGRFuncFilter.__init__(self, utils.createCurveFunc(vPoints), utils.createCurveFunc(bPoints), utils.createCurveFunc(gPoints),
utils.createCurveFunc(rPoints), dtype)
Encore une fois, nous intériorisons l’utilisation de plusieurs de nos fonctions précédentes : createCurveFunc (), createCompositeFunc (), createLookupArray () et applyLookupArray (). Nous utilisons également iinfo (), split () et merge ().
Ces quatre classes peuvent être utilisées telles quelles, avec des fonctions personnalisées ou des points de contrôle passé comme arguments à l’instancia- tion. Alternativement, nous pouvons faire d’autres sous-classes ce coder en dur certaines fonctions ou points de contrôle. De telles sous-classes pourraient être instancié sans aucun argument.
3.8 Émulation de films photo
Une utilisation courante des courbes est d’émuler les palettes qui étaient courantes dans le pré-numérique la photographie. Chaque type de film photo a sa propre interprétation de la couleur (ou gris), mais nous pouvons généraliser à propos de certaines différences avec les capteurs numériques. Le film a tendance à subir une perte de détails et de saturation dans l’ombre, alors que le numérique tend à souffrir ces défauts dans les faits saillants. En outre, le film a tendance à
34 CHAPITRE 3. FILTRER LES IMAGES avoir une saturation inégale différentes parties du spectre. Donc, chaque film a certaines couleurs qui apparaissent ou sautent.
Ainsi, quand nous pensons à de belles photos de films, nous pouvons penser à des scènes (ou rendus) qui sont brillants et qui ont certaines couleurs do- minantes. Au d’autres extrêmes, nous pouvons nous rappeler l’aspect trouble de film sous-exposé ne pouvait pas être amélioré beaucoup par les efforts du technicien de laboratoire.
Nous allons créer quatre filtres semblables à des films en utilisant des courbes.
Ils sont inspirés par trois types de film et une technique de traitement :
— Kodak Portra, une famille de films optimisés pour les portraits et les mariages
— Fuji Provia, une famille de films à usage général
— Fuji Velvia, une famille de films optimisés pour les paysages
— Le cross-processing, une technique de traitement de film non-standard, parfois utilisée pour produire un regard grungy dans la mode et la pho- tographie de bande
Chaque effet d’émulation de film est une sous-classe très simple de BGRCur- veFilter. Nous venons Remplacer le constructeur pour spécifier un ensemble de points de contrôle pour chaque canal. Le choix des points de contrôle est basé sur les recommandations du photographe Petteri Sulonen. Voir son article sur les courbes de film à :
http://www.prime-junta.net/pont/How_to/100_Curves_and_Films/_Curves_and_films.html .
Les effets Portra, Provia et Velvia devraient produire des images d’apparence normale. le l’effet ne devrait pas être évident sauf dans les comparaisons avant- après.
3.9 Émulation de Kodak Portra
Portra a une large gamme de surbrillance qui tend vers des couleurs chaudes (ambre), tandis que les ombres sont plus froides (plus bleues). Comme un film de portrait, il a tendance à faire des gens le teint est plus juste. En outre, il exagère certaines couleurs courantes de vêtements, tels que blanc laiteux (par exemple, une robe de mariée) et bleu foncé (par exemple, un costume ou jeans).
Ajoutons cette implémentation d’un filtre Portra à filters.py :
class BGRPortraCurveFilter(BGRCurveFilter):
"""A filter that applies Portra-like curves to BGR."""
3.10. ÉMULATION DE FUJI PROVIA 35 def __init__(self, dtype = numpy.uint8):
BGRCurveFilter.__init__(
self,
vPoints = [(0,0),(23,20),(157,173),(255,255)], bPoints = [(0,0),(41,46),(231,228),(255,255)], gPoints = [(0,0),(52,47),(189,196),(255,255)], rPoints = [(0,0),(69,69),(213,218),(255,255)], dtype = dtype)
3.10 Émulation de Fuji Provia
Provia a un fort contraste et est légèrement cool (bleu) dans la plupart des tons. Ciel, l’eau, et l’ombre sont améliorées plus que le soleil. Ajoutons cette implémentation d’un Provia filtre à filters.py :
class BGRProviaCurveFilter(BGRCurveFilter):
"""A filter that applies Provia-like curves to BGR."""
def __init__(self, dtype = numpy.uint8):
BGRCurveFilter.__init__(
self,
bPoints = [(0,0),(35,25),(205,227),(255,255)], gPoints = [(0,0),(27,21),(196,207),(255,255)], rPoints = [(0,0),(59,54),(202,210),(255,255)], dtype = dtype)
3.11 Emulation de Fuji Velvia
Velvia a des ombres profondes et des couleurs vives. Il peut souvent produire des ciels azurés nuages de jour et cramoisi au coucher du soleil. L’effet est difficile à émuler mais ici est une tentative que nous pouvons ajouter à filters.py : class BGRVelviaCurveFilter(BGRCurveFilter):
"""A filter that applies Velvia-like curves to BGR."""
def __init__(self, dtype = numpy.uint8):
BGRCurveFilter.__init__(
self,
vPoints = [(0,0),(128,118),(221,215),(255,255)],
bPoints = [(0,0),(25,21),(122,153),(165,206),(255,255)], gPoints = [(0,0),(25,21),(95,102),(181,208),(255,255)], rPoints = [(0,0),(41,28),(183,209),(255,255)],
dtype = dtype)
36 CHAPITRE 3. FILTRER LES IMAGES
3.12 Émulation du traitement croisé
Le traitement croisé produit une teinte forte, bleue ou bleu-vert dans les ombres et un fort, jaune ou jaune verdâtre dans les reflets. Noir et blanc ne sont pas nécessairement conservé. En outre, le contraste est très élevé. Photos croisées traitées prennent un malade apparence. Les gens semblent jaunis, tandis que les objets inanimés ont l’air tachés. Éditons filters.py pour ajouter l’implémentation suivante d’un filtre de traitement croisé :
class BGRCrossProcessCurveFilter(BGRCurveFilter):
"""A filter that applies cross-process-like curves to BGR."""
def __init__(self, dtype = numpy.uint8):
BGRCurveFilter.__init__(
self,
bPoints = [(0,20),(255,235)],
gPoints = [(0,0),(56,39),(208,226),(255,255)], rPoints = [(0,0),(56,22),(211,255),(255,255)], dtype = dtype)
3.13 Mettre en évidence les bords
Les bords jouent un rôle majeur dans la vision humaine et informatique.
Nous, en tant qu’êtres humains, pouvons reconnaître facilement de nombreux types d’objets et leur pose juste en voyant une silhouette rétro-éclairée ou un croquis approximatif. En effet, quand l’art met l’accent sur les bords et pose, il semble souvent transmettre l’idée d’un archétype, comme The Thinker de Rodin ou Superman de Joe Shuster. Le logiciel peut aussi raisonner sur les arêtes, les poses et les archétypes. Nous allons discuter de ces types de raisonnement dans les chapitres suivants.
Pour l’instant, nous nous intéressons à une utilisation simple des bords pour l’effet artistique. nous vont tracer les bords d’une image avec des lignes noires et audacieuses. L’effet devrait être rappelant une bande dessinée ou une autre illustration, dessinée avec un feutre.
OpenCV fournit de nombreux filtres de recherche de bord, y compris La- placian (), Sobel () et Scharr (). Ces filtres sont censés transformer les régions non-edge en noir tout en tournant bord des régions aux couleurs blanches ou saturées. Cependant, ils sont enclins à mal identifier bruit comme bords. Cette faille peut être atténuée en rendant floue une image avant d’essayer de trouver ses bords. OpenCV fournit également de nombreux filtres flous, y compris blur () (simple moyenne), medianBlur () et GaussianBlur (). Les arguments à la re- cherche de bord et les filtres de flou varient, mais comprennent toujours ksize,
3.13. METTRE EN ÉVIDENCE LES BORDS 37 un nombre entier impair qui représente la largeur et la hauteur (en pixels) du noyau du filtre.
A kernel is a set of weights that are applied to a region in the source image to generate a single pixel in the destination image.
For example, a ksize of 7 implies that 49 (7 x 7) source pixels are considered in generating each destination pixel. We can think of a kernel as a piece of frosted glass moving over the source image and letting through a diffused blend of the source’s light.
Pour flouter, utilisons medianBlur (), qui est efficace pour supprimer numé- rique bruit vidéo, en particulier dans les images couleur. Pour la recherche de bord, utilisons Laplacian (), qui produit des lignes de bord en gras, en parti- culier dans les images en niveaux de gris. Après l’application medianBlur (), mais avant d’appliquer Laplacian (), nous devrions convertir à partir de BGR en niveaux de gris.
Une fois que nous avons le résultat de Laplacian (), nous pouvons l’inver- ser pour obtenir des bords noirs sur un fond blanc. Ensuite, nous pouvons le normaliser (de sorte que ses valeurs vont de 0 à 1) et multipliez-le avec l’image source pour assombrir les bords. Implémentons ceci approche dans filters.py :
def strokeEdges(src, dst, blurKsize = 7, edgeKsize = 5):
if blurKsize >= 3:
blurredSrc = cv2.medianBlur(src, blurKsize)
graySrc = cv2.cvtColor(blurredSrc, cv2.COLOR_BGR2GRAY) else:
graySrc = cv2.cvtColor(src, cv2.COLOR_BGR2GRAY)
cv2.Laplacian(graySrc, cv2.cv.CV_8U, graySrc, ksize = edgeKsize) normalizedInverseAlpha = (1.0 / 255) * (255 - graySrc)
channels = cv2.split(src) for channel in channels:
channel[:] = channel * normalizedInverseAlpha cv2.merge(channels, dst)
Notez que nous autorisons la spécification des tailles de noyau en arguments pour strokeEdges (). L’argument blurKsize est utilisé comme ksize pour me- dianBlur (), alors que edgeKsize est utilisé comme ksize pour Laplacian (). Avec mes webcams, je trouve qu’une valeur blurKsize de 7 et la valeur edgeKsize de 5 sont les meilleures. Malheureusement, medianBlur () est cher avec un grand ksize comme 7. Si vous rencontrez des problèmes de performances lors de l’exé- cution strokeEdges (), essayez de réduire la valeur de blurKsize. Pour désactiver le flou, définissez-le sur valeur inférieure à 3.