PROJET AUTOBLOG


Sam & Max: Python, Django, Git et du cul

Site original : Sam & Max: Python, Django, Git et du cul

⇐ retour index

Où il est présenté une méthode en Python pour afficher de la vidéo 3-bit dans son terminal.

jeudi 19 juin 2014 à 06:36

Ceci est un post invité de 01ivier posté sous licence creative common 3.0 unported.

Il est des choses inutiles plus passionnantes que d’autres.
Il semblerait que j’ai un petit faible pour les choses inutiles que je fais moi-même.

Car, afficher le contenu de sa webcam dans une console, ça ne sert clairement pas à grand chose mais j’ai pourtant crié très fort en serrant les poings quand j’y suis arrivé.

Capturer de la vidéo en Python.

Sous Nunux, il existe un joli petit paquet tout beau tout chaud qui permet de gérer du flux vidéo en Python : python-opencv.

Alors, zou :

sudo apt-get install python-opencv

Et pour afficher sa webcam, seules quelques lignes suffisent :

#-*- coding: utf-8 -*-
 
import cv2
 
# Choix du périphérique de capture. 0 pour /dev/video0.
cap = cv2.VideoCapture(0)
 
while True:
 
    # On capture l'image.
    ret,im = cap.read()
 
    # On l'affiche.
    cv2.imshow('Ma Webcam à moi',im)
 
    # Et on attend 40 millisecondes pour avoir du 25 images par seconde.
    key = cv2.waitKey(40)

GO !!

Je vais bien me garder de vous faire un tuto sur OpenCV tant cette librairie est puissante (comprendre : j’y panne que dalle). Si vous êtes curieux, vous pouvez aller faire un tour ici.

Et, parce que j’avais bien d’autres choses plus intéressantes à faire que d’aller visiter le lien ci-dessus, j’ai fait un petit print du im précédent et découvert une liste toute mignonne dont voici la structure:

# Avec des valeurs pour les niveaux comprises entre 0 et 255.
im[n° de ligne][n° de colonne][niveau de bleu, niveau de vert, niveau de rouge]

J’étais content car j’allais pouvoir récupérer les valeurs de chaque pixel de cette façon:

for ligne in range(hauteur):
 
    for colonne in range(largeur):
 
        niv_bleu = im[ligne][colonne][0]
        niv_vert = im[ligne][colonne][1]
        niv_rouge = im[ligne][colonne][2]

Si, pour un autre projet formidable, je n’avais pas eu à travailler avec une RasberryPi et ses petites cuisses de bébé, je pense que j’utiliserai encore cette méthode de bourrin.

Mais voilà, c’est juste ridicule quand on sait que la liste en question est en fait un array numpy et qu’il est 10, 100 fois plus rapide de faire:

for ligne in range(hauteur):
 
    for colonne in range(largeur):
 
        niv_bleu = im.item(ligne, colonne, 0)
        niv_vert = im.item(ligne, colonne, 1)
        niv_rouge = im.item(ligne, colonne, 2)

Je n’en suis pas à me dire que la prochaine fois que j’achèterai un grille-pain, je lirai la notice avant de l’utiliser, mais presque…

Petite remarque en passant : les valeurs de rouge, de vert et de bleu étant comprise entre 0 et 255, cela nous donne 256 valeurs pour chaque couleur.
Soit 256 x 256 x 256. Soit 2⁸ x 2⁸ x 2⁸. Soit 2²⁴ ==> les couleurs de notre flux sont codées par défaut sur 24 bits.

À noter qu’il est tout à fait possible d’analyser une image sans avoir à l’afficher, ce qui permet d’utiliser OpenCV dans un environnement sans gestionnaire de fenêtre.

Afficher de la couleur dans la console.

Bon. J’avais mes niveaux RVB pour chaque pixel. Il me fallait désormais afficher de la couleur dans la console.

Quelques requêtes Duck Duck Go plus tard, je découvre termcolor qui fait très bien le job mais dont on peut se passer en regardant les codes ANSI de plus près.

Bien entendu, la grande majorité des consoles étant limitées à 8 couleurs, soit 2³, soit 3-bit je me suis restreint à cette qualité.

Démonstration :

Il est possible d’obtenir beaucoup plus de couleurs en combinant les fonds, les caractères et les intensités comme le fait la libcaca, mais on ne joue pas vraiment dans la même cour.

Perso, quand ça s’est affiché en rouge pour la première fois, j’ai eu des frissons partout. Parce qu’il faut bien comprendre que je n’ai toujours aucune idée de ce que “\033[” et autres “m” veulent dire. J’ai copié/collé, c’est tout. Et dans ces cas là, quand ça marche, c’est toujours la fête.

Ce qui pourrait être un problème, on le voit à l’image, c’est qu’une fois que j’ai écrit TATA YOYO en rouge, le prompt devient lui aussi rouge, et ainsi de suite à chaque changement de couleur. Pour remédier à ça, il faut ajouter \033[0m à la fin du texte à afficher pour que le reste soit écrit avec la couleur par défaut du terminal.

Démonstration :

C’est d’ailleurs ce que fait termcolor, sauf que, dans notre cas, nous n’avons pas besoin de revenir à cette valeur par défaut à chaque affichage de pixel vu que le suivant sera lui aussi coloré.

Je vous en parle seulement parce que vous avez l’air sympa.

J’ajouterai qu’après avoir effectué un benchmark de folie exploitant brillamment les deux points qui clignotent à chaque seconde sur mon radio-réveil, il s’est avéré que la solution “maison” était plus performante que termcolor : mon choix était fait.

Du pixel au █

J’ai renoué contact avec le █ il n’y a pas si longtemps. Aussi étonnant que cela puisse paraître, alors que je baigne quasi quotidiennement dans l’informatique depuis 30 ans, il n’est pas impossible que notre dernière rencontre remonte à 1986 sur le Commodore 64 familial.

Pour vous donner une idée de l’émotion qui m’a traversé quand j’ai revu le █, vous pourriez très clairement user de l’expression “le █ d’Olivier” en lieu et place de “la madeleine de Proust” dans vos discussions. Mais, à l’oral, le █ passe mal, et c’est bien dommage.

Pour info, le pseudo unicode de █ c’est \u2588.

Et, pour afficher un █ en couleur, il suffit de faire comme vu au dessus.

Reste à trouver un moyen de passer des millions de couleurs potentielles de notre vidéo aux huit de notre console.

C’est là que vous allez comprendre pourquoi je me suis acharné avec mes captures d’écrans. C’était pour bien vous faire intégrer l’association entre les couleurs et la valeur qui les code. À savoir :

1 : rouge
2 : vert
3 : jaune
4 : bleu
5 : violet
6 : turquoise
7 : blanc

Et là qu’est qu’on remarque ?
Que cela respecte la synthèse additive si on attribue 1 au rouge, 2 au vert et 4 au bleu, bien entendu !

1 + 2 = 3 et en synthèse additive rouge + vert = jaune
1 + 4 = 5 et en synthèse additive rouge + bleu = violet
2 + 4 = 6 et en synthèse additive vert + bleu = turquoise
1 + 2 + 4 = 7 et en synthèse additive rouge + vert + bleu = blanc

Mettez-vous à ma place: je venais de découvrir l’Amérique !

Bon, rétrospectivement, cela ne constitue vraiment rien d’extraordinaire en soi dans la mesure où c’est ce qui découle logiquement d’un codage sur 3 bits mis en place par un être humain qui a juste envie de faire simple plutôt que de faire compliqué.

Mais tout de même, sur le moment…
… L’AMÉRIQUE BORDEL ! L’AMÉRIQUE !

Il devenait alors facile d’évaluer le degré de présence de chaque composante RVB d’un pixel puis de déterminer laquelle des 8 couleurs lui correspondait le plus.

Voilà comment je m’y suis pris :

# On initialise à 0 l'indice du pixel analysé 
indice_couleur = 0
 
# On analyse le niveau de Bleu du pixel.
# S'il est au dessus du seuil...
if img.item(ligne, colonne, 0) > seuil :
 
    #...on ajoute 4 à l'indice.
    indice_couleur += 4
 
# On analyse le niveau de Vert du pixel.
# S'il est au dessus du seuil...
if img.item(ligne, colonne, 1) > seuil :
 
    #...on ajoute 2 à l'indice.
    indice_couleur += 2
 
# On analyse le niveau de Rouge du pixel.
# S'il est au dessus du seuil...
if img.item(ligne, colonne, 2) > seuil :
 
    # ...on ajoute 1 à l'indice.
    indice_couleur += 1

L’indice obtenu correspond alors au code couleur ANSI à utiliser !!

Je veux dire.

Tout de même.

C’est super, non ?

Hum…

Bien, bien…
C’est bientôt fini, il me reste juste…

Quelques remarques supplémentaires.

1) Le noir ANSI est en fait du gris, et c’est bien moche, j’ai donc préféré partir du principe que la console aurait un fond noir et afficher un “espace” pour chaque pixel noir.

2) print(“\033[H\033[2J”) permet d’effacer la console comme le fait os.system(‘clear’).
Mais, j’imagine que ça devait faire trop de 033 dans le script pour moi, parce que, psychologiquement, ça ne passait pas.
J’ai un peu discuté avec moi-même et on a fini par décider d’utiliser le clear.

3) J’ai commencé par utiliser la concaténation pour ajouter mes █ colorés à mon texte_image final :

texte_image += u"\033[3{0}m█".format(indice_couleur))

Mais, Stack Overflow a tapé à la fenêtre et il m’a dit qu’il était beaucoup plus rapide de créer une liste puis d’en joindre les éléments.
J’ai benchmarké avec mon radio-réveil.
Stack Overflow avait raison.

4) Par contre, ce que Stack Overflow s’était bien gardé de me dire, c’est que l’affichage en console avait ses propres limites internes.
Bilan, après avoir optimisé mon code du mieux que je le pouvais, j’ai constaté que le script calculait de toutes façon les texte_image plus vite que la console ne pouvait les afficher.

Ce qui relève un peu du FAIL quand on y pense.

Donc, si vous avez une idée pour que ça ne scintille plus au delà de 25 lignes de hauteur, je suis preneur, sachant que je suis tout à fait à même d’entendre que j’ai fait n’importe quoi dès le début.

ÉDIT: Dans les commentaires, Tmonjalo a proposé une solution qui résout le problème du scintillement en faisant revenir le curseur en haut à gauche plutôt que d’effacer la console. J’ai donc édité le code en conséquence. Merci à lui.

La totale.

Voici le script final. Il est diffusé sous les termes de la très sérieuse WTFPL.

Les variables à modifier pour faire des tests sont le seuil, la largeurOut et la hauteurOut.

À noter aussi que si vous faite un petit…

cap = cv2.VideoCapture("VotreFilm.avi")

… au lieu d’ouvrir la webcam en /dev/video0, et bien vous allez voir VotreFilm.avi dans la console. Super génial !

#-*- coding: utf-8 -*-
 
import cv2
import os
 
# Définition du flux capturé.
# Comme elle sera, de toutes façons, retaillée à la baisse,
# elle est fixée à la valeur la plus petite supportée par la webcam.
# À noter que cette valeur minimale peut varier en fonction de votre cam.
largeurIn = 160
hauteurIn = 120
 
# Définition du flux qui s'affichera en console.
# À savoir le nombre de caractères en largeur et en hauteur.
largeurOut = 60
hauteurOut = 20
 
# Seuil de présence des couleurs rouge, vert, bleu dans un pixel. 
# Entre 0 et 255.
seuil = 120
 
# Choix du périphérique de capture.
# Ici /dev/video0
cap = cv2.VideoCapture(0)
 
# Configuration de la définition du flux.
cap.set(3, largeurIn)
cap.set(4, hauteurIn)
 
# On efface la console.
os.system('clear')
 
# On définit une position de référence pour le curseur.
# En haut à gauche, donc, puisqu'on vient juste d'effacer la console.
print ('\033[s')
 
# Pendant... tout le temps...
while True:
 
    # On capture une image.
    ret, img = cap.read()
 
    # On retaille l'image capturée.
    img = cv2.resize(img,(largeurOut, hauteurOut))
 
    # On initialise une liste qui contiendra tous les éléments de l'image
    liste_image = []
 
    # Pour chaque ligne de l'image.
    for ligne in range(hauteurOut):
 
        # Pour chaque colonne de chaque ligne.
        for colonne in range(largeurOut):
 
            # On initialise à 0 l'indice du pixel analysé 
            indice_couleur = 0
 
            # On analyse le niveau de bleu du pixel.
            # S'il est au dessus du seuil...
            if img.item(ligne, colonne, 0) > seuil :
 
                #...on ajoute 4 à l'indice.
                indice_couleur += 4
 
            # On analyse le niveau de bleu du pixel.
            # S'il est au dessus du seuil...
            if img.item(ligne, colonne, 1) > seuil :
 
                #...on ajoute 2 à l'indice.
                indice_couleur += 2
 
            # On analyse le niveau de bleu du pixel.
            # S'il est au dessus du seuil...
            if img.item(ligne, colonne, 2) > seuil :
 
                # ...on ajoute 1 à l'indice.
                indice_couleur += 1
 
            # Si l'indice obtenu est différent de 0...
            if indice_couleur:
 
                # ...on ajoute un █ coloré à la liste.
                liste_image.append(u"\033[3{0}m█".format(indice_couleur))
 
            # Si l'indice est égal à 0...
            if not indice_couleur:
 
                # ...on ajoute un espace (noir ?) à la liste.
                liste_image.append(" ")
 
        # On fait en sorte que le terminal retrouve sa couleur initiale
        liste_image.append("\n\033[00m")
 
    # On produit un string en mettant bout à bout tous les éléments de la liste
    texte_image = ''.join(liste_image)
 
    # On affiche l'image.
    print(texte_image)
 
    # On replace le curseur à la position de référence.
    print ('\033[u')
 
    # On attend 40 millisecondes pour obtenir du 25 images par seconde.
    key = cv2.waitKey(40)

Des vidéos ! Des vidéos !

Voici une petite vidéo réalisée pour promouvoir un événement à nous. À partir de la 44ème seconde, on peut m’y voir coiffé d’un masque de soudeur en train de faire tenir au plafond un donut géant au moyen d’une batte de base-ball en aluminium :

Pour plus d’information sur cet événement vous pouvez allez voir ici et admirer, par la même occasion, notre magnifique affiche réalisée en pur Python.

Enfin, compte tenu des tauliers du site, je ne pouvais passer à côté de la figure imposée.
Je vous propose donc un extrait de Deep 3-bit, hommage appuyé à Vuk Cosic et son légendaire Deep ASCII.

flattr this!