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


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.

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

  • bob

    Bonjour,

    «je n’ai toujours aucune idée de ce que “33[” et autres “m” veulent dire»

    http://en.wikipedia.org/wiki/ANSI_escape_code

    “33” c’est le caractère ESC en octal, tu rajoute “[” pour démarrer la séquence et dire au terminal : «là mon gars va falloir formater le texte»
    ensuite tu fermes avec “33[0m” qui a pour effet de remettre à zéro les réglages.

    m c’est le paramètre pour régler la couleur, le soulignage, le surlignage …
    Jette un œil au tableau des CSI codes : http://en.wikipedia.org/wiki/ANSI_escape_code#CSI_codes pour faire d’autres trucs exemple :

    print "33[7;32;4mTATA YOYO33[0m"

    Va afficher TATA YOYO en noir sur fond vert le tout souligné.

  • bob

    Bonjour,

    «Je n’ai toujours aucune idée de ce que “33[” et autres “m” veulent dire.»

    33 c’est le caractère ESC en octal, pour une séquence «ANSI escape code» tu ouvres par «\O33[» ensuite il y a le paramètre m qui gère tout ce que tourne autour des couleurs du surlignage/soulignage, tu sépares les valeurs du paramètre par un point virgule, les valeurs du paramètres sont placées avant le paramètre.

    print "33[7;32;4mTATA YOYO33[0m

    Va afficher TATA YOYO souligné en vers mais en négatif (fond vert et lettres noires)

  • Eliot

    J’ai une sensation similaire à celle que j’ai eu lors de l’article sur le script qui utilisait les sujets des mails pour composer des images pornos.

    C’est juste magnifique.

  • Al

    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.

    Utiliser une autre console.

    Nan, sérieusement.

    En fait, au niveau performances, les consoles ne sont pas toutes égales, et malheureusement elles partagent souvent pas mal de code, ce qui fait que toutes les consoles Gnome que je connaissent ont des perf pas terribles (vu qu’elles sont toutes basées sur le même code). Xterm, c’est pareil. Ne parlons même pas des TTY, je pense que c’est lié au fait qu’ils n’utilisent pas de pile graphique très avancée, mais leur lenteur est indécente. Les deux meilleures consoles que je connaisse sont Konsole et dérivés (genre Yakuake) qui sont peut-être un peu gourmand en RAM (pas vérifié personnellement) et pour KDE, et URxvt qui ressemble vachement à Xterm niveau tronche, mais il paraît qu’il est pas mal configurable.

    Pour plus d’information, lire cet article et tester avec le script qu’il propose.

    Article très intéressant sinon, la démarche est super sympa =).

  • nderambure

    @Eliot, je peux t’affirmer que cette sensation se lie directement dans les yeux d’Olivier quand il entre joyeusement dans le bureau la matin, vers 15h30.

  • 01ivier Post author

    @bob : Merci… :-)

    @Eliot : Merci… :-D

    @Al : Merci… :-P
    …mais, urxvt (que je ne connaissais pas) n’a pas résolu le pb… :-/

    @tmonjalo: MERCIII !! \o/
    Putain oui… c’est ça le truc !
    Pas sûr qu’on gagne beaucoup en performance, mais le scintillement disparaît puisque que l’image n’est jamais effacée entièrement.

    Il suffit de juste de faire un ‘clear’ avant de définir la position de référence et zou… c’est propre à souhait !
    Bien entendu, passé une certaine définition, ça lag… mais au moins le pb est inhérent au code.

    Je vais faire une mise à jour du script et de l’article dans la soirée.

    Merci encore !

    @nderambure : Rhooo tu exagères… on va dire 15h00 du matin, au plus tard… :-p

  • k3c

    Chouette article, bravo.
    Mais je me fais l’effet d’un dinosaure, moi qui ai commencé à programmer à coups d’escapes séquences, avec plein de “j’efface du curseur à la fin de la ligne”, et autres “je me positionne ligne 32 position 14”.

  • Al

    @01ivier : Bah dommage alors =/. Je pensais franchement que ça atténuerait au moins un peu le scintillement, vu que chez moi sous Konsole ça scintille très peu, en général surtout quand y’a un lag, un changement de bureau ou autre réduire dans la barre des tâches/pouf réapparition vaudou. Franchement, le coup de remplacer le clear par autre chose est sans doute bien plus efficace que ma pseudolution (les néologismes baveux sont la vie).

    Après, franchement, j’adore ton script, je m’éclate avec comme un petit fou =3. Tu m’as inspiré, l’ami ^^ !

  • kontre

    Pour récupérer un pixel dans un tableau à plusieurs dimensions, il faut indexer sur plusieurs dimensions. im[ligne][colonne][0] est lent parce que python fait en réalité ((im[ligne])[colonne])[0]. La méthode item marche, mais im[ligne, colonne, 0] est plus court et plus parlant.

    Sinon, ça me choque de parcourir un tableau numpy élément par élément, mais comme y’a une liste en sortie c’est inévitable.

    # À faire avant les boucles
    import numpy as np
    couleurs = np.array([4, 2, 1])
     
    # ...
     
    # À faire dans les boucles
    # On vérifie si chaque composante de couleur est supérieure au seuil
    # is_color est booléen
    is_color = img[ligne, colonne, :] > seuil
     
    # On récupère les indices de couleurs si la couleur y est
    colors = couleurs[is_color]
     
    # On somme les couleurs sélectionnées
    indice_couleur = np.sum(colors)

    Bien sûr ça peut se faire en une ligne, mais c’est moins compréhensible:

    indice_couleur = np.sum(couleurs[img[ligne, colonne, :] > seuil])

    Voilà, ça ne rend pas le truc plus utile, mais c’est un truc inutile optimisé !

    (et puis largeurIn c’est pas pep8 :p)

  • batisteo

    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 !

    Oh, on pourrait faire un chmod en couleur.
    La sortie de ls serait très jolie ! :·)

  • Aeyos

    Merci pour cet article magnifique, tu à solutionner mon problème de traitement d’image depuis mon serveur unix. Pour le coup je n’ai pas encore eu le temps de tester (mais j’ai quand même pris celui de vérifier qu’opencv soit soit porté sur freebsd :p ).
    Si je parvient à réalisé mon rêve je ferais une prière pour toi :)

  • danyd

    Bonjour, le résultat est magnifique !)

    est-il possible d’enregistrer le résultat dans un fichier .avi ou .gif ??

Comments are closed.

Des questions Python sans rapport avec l'article ? Posez-les sur IndexError.