Un petit article rapide sur un petit projet de vacances qui me trottait en tête depuis quelques temps.

J’avais dans l’idée de développer un petit programme qui viendrait modifier en temps réel le flux d’une webcam et qui écrirait dans une webcam virtuelle. De cette façon, on peut utiliser le flux modifié dans un programme tiers comme VLC ou Zoom.

Je ne pensais pas spécialement faire d’article sur ce sujet (juste pas le temps) mais j’ai rencontré un problème curieux avec apt-get que j’ai réussi à résoudre donc allez c’est parti.

Le programme

Le programme se trouve sur mon profil GitHub.

Je me suis inspiré du code présenté dans la chaîne YouTube de Giovanni Code. Il s’agit d’un script Python qui utilise les paquets suivants :

  • OpenCV2 pour la lecture et la manipulation des images d’une webcam
  • PyVirtualCam pour l’écriture d’un flux dans un périphérique de Webcam virtuelle
  • Numpy pour la création d’un filtre avancé de décalage temporel

A partir de maintenant je parle de “filtres” pour désigner les fonctions qui viennent modifier le flux de la caméra. Ce terme doit s’entendre par rapport aux filtres Unix qui lisent et écrivent des flux de données et non pas au sens du traitement du signal (ce qui n’aurait pas grand sens).

Les filtres qui modifient le flux de la webcam en temps réel sont au nombre impressionant de quatre.

On passe d’un filtre à l’autre en appuyant sur space et on quitte en appuyant sur q.

Le programme est perfectible, mais je voulais un petit prototype pour tester quelques idées rigolotes.

Présentation des filtres

Filtre des petits ronds

Celui là est directement inspiré du code de Giovanni Code : on crée des petits cercles colorés et ça donne une petite impression pixelisée très sympa.

Je ne détaille pas plus et vous renvoie vers sa chaîne pour plus d’information sur le sujet.

Filtre 1

Filtre des disques verts

Pour celui la, j’ai fait une version modifiée où l’image est remplacé par une grille de disques verts. Le rayon des différents disques est proportionnel à la luminosité du point considéré.

Je suis parti sur une expression assez simple de la luminosité d’un pixel en partant de ses niveaux de rouge $r$, vert $g$ et bleu $b$ (des entiers compris entre 0 et 255) :

\[I = 0.2126 \cdot r + 0.7152 \cdot g + 0.0722 \cdot b\]

La primitive de la fonction qui trace les disques est :

cv2.circle(window, (centerX, centerY), radius, (r, g, b), thickness)

Prendre un paramètre de thickness de $-1$ trace un disque plein plutôt qu’un cercle. Pour les paramètres de (r,g,b), je suis parti sur un vert un peu foncé de (0,175,0).

Enfin, le rayon doit être un entier, ce qui fait qu’on aura un effet de seuilage de la luminosité perçue. Mais le rendu n’est pas trop mal pour autant bien que l’image soit moins claire que pour le filtre précédent.

Filtre 2

Filtre des caractères ASCII

En gros ici, on représente chaque pixel par un caractère ASCCI. Plus le pixel est lumineux, et plus on choisira un caractère avec beaucoup de pixel.

Alors, bien entendu, ça dépendra de la police de caractères choisie, de la graisse etc. Par ailleurs, la variation du nombre de pixels entre deux caractères voisin n’est pas forcément toujours la même. Mais enfin, on arrive malgré tout à capturer l’effet.

Je me suis basé sur l’alphabet suivant, ordonné par luminosité croissante :

.-\':_,^=;><+!rc*/z?sLTv)J7(|Fi{C}fI31tlu[neoZ5Yxjya]2ESwqkP6h9d4VpOGbUAKXHm8RD#$Bg0MNWQ%&@

Le premier caractère de la liste est une espace, qui ne sera donc traduit par aucun pixel et qui codera la luminosité nulle. Le dernier caractère est l’arobase @ qui représentera la luminosité maximale.

On peut amplifier l’effet en donnant une intensité de couleur différente pour les différents caractères mais les combinaisons que j’ai testées ne m’ont pas parues convaincantes. J’ai gardé une couleur constante de (0,150,0)

L’effet est désormais beaucoup moins visuel : arrivez vous à distinguer les formes ? L’image est toujours peu ou prou la même que dans les deux cas précédent : je lève le bras droit pour faire coucou. On peut essayer d’affiner la grille et d’afficher plus de caractères mais je n’ai pas poussé plus avant ces investigations.

Filtre 3

A noter qu’en vidéo, ça reste quand même un peu plus lisible. Si j’ai la foi, je rajouterai ça dans le corps de l’article

Filtre temporel

Alors ça c’est mon petit chouchou. Il y a longtemps j’avais vu je ne sais plus trop où (fête de la Science ?) une série de webcam qui introduisaient des décalages temporels dans le flux vidéo. Grosso modo, la Webcam écrit en permanence dans un buffer et le flux de sortie vient piocher dans le buffer pour afficher des pixels d’un temps passé. Si on note $E(x,y,t)$ la couleur du pixel de coordonnées $(x,y)$ à un instant $t$, tel que lu par la webcam d’entrée, alors la webcam de sortie verra un pixel $S$ défini par :

\[S(x,y,t) = E(x,y, t')\]

Avec bien entendu $t’<=t$ (on ne lit pas dans le futur)

On peut imaginer plein de façons différentes de mélanger espace et temps et d’une manière générale, on écrira :

\[t'(x,y) = f(x,y)\]

De mon côté, j’ai introduit un décalage ligne à ligne : ce qui se situe en haut de l’image est mis à jour plus rapidement que ce qui est en bas. Avec le repère qui va bien ($x$ selon la largeur, de gauche à droite; $y$ selon la hauteur, du haut vers le bas; origine du repère au coin supérieur gauche de l’image), on aurait donc quelque chose du genre :

\[t'(x,y) = t(x,y) + y\]

A ce niveau de la rédaction de l’article, il faudrait que je fasse un petit dessin de la façon dont le buffer se remplit. Il y a une analogie assez directe avec ce qu’on obtiendrait, dans le cadre de la Relativité Restreinte, si on se regardait dans un grand miroir crénelé incliné de 45° (la partie haute étant la plus proche de nous). Bon j’y reviendrait probablement plus tard…

Voici quoiqu’il en soit ce qu’on obtient. Une fois encore, on comprend mieux ce qu’il se passe avec une vidéo mais comment dire… on le sent que j’ai hâte de finir cette première version de l’article et de passer à autre chose ?

Filtre 4

Ecriture dans une caméra virtuelle

Assez vite, je me suis dit que ça serait cool si je pouvais envoyer tout ça dans une caméra virtuelle.

Une petite recherche m’a orienté vers PyVirtualCam qui semble totalement répondre à mon besoin.

Ce paquet s’appuie, sur Linux, sur le paquet v4l2loopback-dkms qui est un module du noyau qui permet de créer des périphériques virtuels type webcam à la volée.

Un problème de paquet

Hélas, lors de l’installation de ce paquet via apt, mon système d’ordinaire courtois s’est mis à m’insulter :

$ sudo apt-get install v4l2loopback-dkms

[...]

Préparation du dépaquetage de 
.../v4l2loopback-dkms_0.12.7-2ubuntu2~22.04.1_all.deb ...
Dépaquetage de v4l2loopback-dkms (0.12.7-2ubuntu2~22.04.1) ...
Paramétrage de v4l2loopback-dkms (0.12.7-2ubuntu2~22.04.1) ...
Loading new v4l2loopback-0.12.7 DKMS files...
Building for 6.8.0-57-generic
Building initial module for 6.8.0-57-generic
ERROR: Cannot create report: [Errno 17] File exists: 
'/var/crash/v4l2loopback-dkms.0.crash'
Error! Bad return status for module build on kernel: 6.8.0-57-generic 
(x86_64)
Consult /var/lib/dkms/v4l2loopback/0.12.7/build/make.log for more 
information.
dpkg: erreur de traitement du paquet v4l2loopback-dkms (--configure) :
  le sous-processus paquet v4l2loopback-dkms script post-installation 
installé a renvoyé un état de sortie d'erreur 10
Des erreurs ont été rencontrées pendant l'exécution :
  v4l2loopback-dkms
E: Sub-process /usr/bin/dpkg returned an error code (1)

Le fichier de log de compilation indiqué dans l’erreur (que je n’ai pas eu la présence d’esprit de sauvegarder du coup je ne peux pas le reproduire ici) indiquait en gros que la fonction strlcpy appelée dans v4l2loopback.c n’était pas définie. Une petite recherche m’a appris qu’il s’agissait d’une fonction venant de l’univers BSD.

Ce problème précis est mentionné à divers endroit du net et pour différents systèmes (ArchLinux, LinuxMint, Ubuntu, et directement sur le github du projet v4l2loopback).

Le problème semble être une incompatibilité du paquet v4l2loopback-dkms avec les noyaux trop récents (ou les dépôts trop anciens, selon comment vous regardez le problème).

On peut résoudre ça en installant v4l2loopback depuis les sources ou alors, comme j’ai fait, en mettant une énorme rustine de fortune dans le code source du dépôt. A la réflexion ce n’est pas super propre car apt pense que le paquet provient du dépôt alors qu’on utilise une version modifiée… Mais enfin ça fera le job le temps du test…

La fonction strlcpy a son équivalent dans la librairie standard du C : strscpy :

Ok les prototypes sont les mêmes, on peut tenter un truc un peu bourrin :

$ sudo sed -i 's/strlcpy/strscpy/' ./src/v4l2loopback-0.12.7/v4l2loopback.c # PAS TESTE !

Attention, je n’ai pas testé ça. J’ai fait la modif sous vim :

$ sudo vim ./src/v4l2loopback-0.12.7/v4l2loopback.c
:%s/strlcpy/strscpy/

On tente ensuite la réinstallation (sans faire de remove/install avec apt sinon on écrase nos modifications) :

$ sudo dpkg --configure v4l2loopback-dkms
Paramétrage de v4l2loopback-dkms (0.12.7-2ubuntu2~22.04.1) ...
Removing old v4l2loopback-0.12.7 DKMS files...
Deleting module v4l2loopback-0.12.7 completely from the DKMS tree.
Loading new v4l2loopback-0.12.7 DKMS files...
Building for 6.8.0-57-generic
Building initial module for 6.8.0-57-generic
[...]
$ # Done, ça a marché !

Création d’une caméra virtuelle

On crée une caméra virtuelle en chargeant le module v4l2loopback avec modprobe

$ ll /dev/vid*

crw-rw----+ 1 root video 81, 0 avril 18 13:59 /dev/video0
crw-rw----+ 1 root video 81, 1 avril 18 13:59 /dev/video1
crw-rw----+ 1 root video 81, 2 avril 18 13:59 /dev/video2
crw-rw----+ 1 root video 81, 3 avril 18 13:59 /dev/video3

$ sudo modprobe v4l2loopback

$ ll /dev/vid*
crw-rw----+ 1 root video 81, 0 avril 18 13:59 /dev/video0
crw-rw----+ 1 root video 81, 1 avril 18 13:59 /dev/video1
crw-rw----+ 1 root video 81, 2 avril 18 13:59 /dev/video2
crw-rw----+ 1 root video 81, 3 avril 18 13:59 /dev/video3
crw-rw----+ 1 root video 81, 4 avril 21 07:47 /dev/video4

Ici on constate qu’une nouvelle caméra fait son apparition dans /dev/, la caméra video4

Mon code python prend la première caméra virtuelle qu’il trouve, il n’y a qu’à s’assurer que dans le fichier webcam.py la variable VIRTUAL_CAM est bien positionnée à True.

Utilisation dans des logiciels tiers

On peut ensuite connecter VLC sur le flux de cette caméra virtuelle. Il faut aller dans Média > Ouvrir un périphérique de capture... et ensuite sélectionner notre caméra (ici donc video4)

VLC 1

Et là, ça fonctionne !

VLC 2

Pareil avec Zoom, il suffit de sélectionner notre caméra au début du meeting. En appuyant sur space on pourra en outre changer les effets. On peut même imaginer ajouter un petit texte affichant le nom du participant et divers informations. Je crois d’ailleurs que c’est ce que propose ObsProject qui est une alternative à v4l2loopback un peu overkill pour d’autres plateformes mais enfin rien ne vaut le DIY ^^

Zoom 1

Zoom 2