Outils pour utilisateurs

Outils du site


diy:projets:chromakey

Différences

Ci-dessous, les différences entre deux révisions de la page.

Lien vers cette vue comparative

Les deux révisions précédentesRévision précédente
Prochaine révision
Révision précédente
diy:projets:chromakey [2018/05/24 09:14] – [Les performances] sduranddiy:projets:chromakey [2018/07/02 09:35] (Version actuelle) – [Sources du projet] sdurand
Ligne 13: Ligne 13:
 Il est important de noter que vous aurez besoin d'une installation d'OpenCV sur votre raspberry (j'ai utilisé tout au long du développement la version 3.3.0) et d'un compilateur C++ (de préférence à jour). Il est important de noter que vous aurez besoin d'une installation d'OpenCV sur votre raspberry (j'ai utilisé tout au long du développement la version 3.3.0) et d'un compilateur C++ (de préférence à jour).
 De plus, si vous ne travaillez pas directement sur le raspberry (mais par exemple en ssh), je met à votre disposition un programme permettant de visualiser  De plus, si vous ne travaillez pas directement sur le raspberry (mais par exemple en ssh), je met à votre disposition un programme permettant de visualiser 
-les images traitées aussi simplement que possible (voir [[https://github.com/SDurand7/ChromaKey/tree/master/RemoteDisplay|mon github]]+les images traitées aussi simplement que possible, 
 ou, pour plus d'explications, la rubrique "Vous travaillez à distance" de la partie [[https://wiki.ensfea.fr/doku.php?id=diy:projets:chromakey#le_reste_du_code|"Le reste du code"]]). ou, pour plus d'explications, la rubrique "Vous travaillez à distance" de la partie [[https://wiki.ensfea.fr/doku.php?id=diy:projets:chromakey#le_reste_du_code|"Le reste du code"]]).
 Ce programme nécessite une installation d'**OpenCV** sur votre pc, et **netcat** sur votre pc et le raspberry. Ce programme nécessite une installation d'**OpenCV** sur votre pc, et **netcat** sur votre pc et le raspberry.
Ligne 77: Ligne 77:
 ===== Développement du programme ===== ===== Développement du programme =====
  
-==== La compilation ==== 
- 
-Pour ne pas vous embêter avec la compilation de votre code, je vous recommande fortement d'utiliser le makefile mis à  
-votre disposition [[https://github.com/SDurand7/ChromaKey/tree/master/ChromaKey|ici]], notamment parce qu'il inclut toutes  
-les **options de compilation** nécessaire pour compiler un programme utilisant OpenCV. Ce makefile requiert que votre fichier soit nommé chroma_key.cpp, mais vous êtes évidemment libre de  
-modifier ça à votre convenance. Vous devrez également veiller à ce que ce soit bien votre compilateur C++ qui soit renseigné dans le makefile (variable CC). L'exécutable généré sera nommé **chrk**. 
  
 ==== La fonction de traitement ==== ==== La fonction de traitement ====
Ligne 88: Ligne 82:
 La fonction de traitement (qui est en quelque sorte le coeur de notre code) se contente de **traduire scrupuleusement  La fonction de traitement (qui est en quelque sorte le coeur de notre code) se contente de **traduire scrupuleusement 
 l'algorithme général** (avec quelques subtilitées utiles pour les performances). Pour définir les paramètres votre fonction, sachez que dans OpenCV **une image est de type Mat**. l'algorithme général** (avec quelques subtilitées utiles pour les performances). Pour définir les paramètres votre fonction, sachez que dans OpenCV **une image est de type Mat**.
-(une version complète du code est disponible sur [[https://github.com/SDurand7/ChromaKey/tree/master/ChromaKey|mon github]]) (fichier //chroma_key.cpp//).+<code C++> 
 +// framenotre image à traiter 
 +// background: notre image de fond 
 +// B, G et R: les valeurs des trois canaux qui correspondent a la couleur  
 +// qu'on veut supprimer de l'image à traiter 
 +// Fi: un pointeur vers la ligne i de notre image 
 +// Bi: un pointeur vers la ligne i de notre fond 
 +// offset: une tolérance utilisée pour améliorer le rendu car, en pratique,  
 +// un fond qui peut nous sembler unis ne l'est pas totalement une fois numérisé.
  
-   // frame: notre image à traiter +// On parcourt les lignes de l'image 
-   // background: notre image de fond +for(int i = 0; i < frame.rows; i++) { 
-   // B, G et R: les valeurs des trois canaux qui correspondent a la couleur  +    // On récupère les pointeurs sur les lignes de nos images 
-   // qu'on veut supprimer de l'image à traiter +    Vec3b *Fi = frame.ptr<Vec3b>(i); // Vec3b, car chaque pixel est représenté par 3 composantes 
-   // Fi: un pointeur vers la ligne i de notre image +    const Vec3b *Bi = background.ptr<Vec3b>(i); 
-   // Bi: un pointeur vers la ligne i de notre fond +  
-   // offset: une tolérance utilisée pour améliorer le rendu car, en pratique,  +    // On parcourt les colonnes de l'image 
-   // un fond qui peut nous sembler unis ne l'est pas totalement une fois numérisé. +    for(int j = 0; j < frame.cols; j++) { 
-    +        // Si notre couleur (codée en BGR dans OpenCV) correspond à la couleur "cible" 
-   // On parcourt les lignes de l'image +        if(inBounds(Fi[j][0], B, offset) && inBounds(Fi[j][1], G, offset) && inBounds(Fi[j][2], R, offset)) { 
-   for(int i = 0; i < frame.rows; i++) { +            // On remplace les trois composantes du pixel de notre image par celles du pixel de notre fond 
-       // On récupère les pointeurs sur les lignes de nos images +            for(int k = 0; k < 3; k++) {  
-       Vec3b *Fi = frame.ptr<Vec3b>(i); // Vec3b, car chaque pixel est représenté par 3 composantes +                Fi[j][k] = Bi[j][k]; 
-       const Vec3b *Bi = background.ptr<Vec3b>(i); +            
-    +        
-       // On parcourt les colonnes de l'image +    
-       for(int j = 0; j < frame.cols; j++) { +} 
-           // Si notre couleur (codée en BGR dans OpenCV) correspond à la couleur "cible" +</code>
-           if(inBounds(Fi[j][0], B, offset) && inBounds(Fi[j][1], G, offset) && inBounds(Fi[j][2], R, offset)) { +
-               // On remplace les trois composantes du pixel de notre image par celles du pixel de notre fond +
-               for(int k = 0; k < 3; k++) {  +
-                   Fi[j][k] = Bi[j][k]; +
-               +
-           +
-       +
-   }+
  
 Je dois admettre qu'à première vue ça pique un peu, surtout si vous débutez en C/C++ et que le principe des **pointeurs** ne vous est pas familier. Je dois admettre qu'à première vue ça pique un peu, surtout si vous débutez en C/C++ et que le principe des **pointeurs** ne vous est pas familier.
Ligne 125: Ligne 119:
  
 Vous pouvez également voir que j'utilise dans ma condition une fonction nommée //**inBounds**//. Cette fonction prend en paramètre la valeur d'une composante d'un pixel,  Vous pouvez également voir que j'utilise dans ma condition une fonction nommée //**inBounds**//. Cette fonction prend en paramètre la valeur d'une composante d'un pixel, 
-la valeur de notre couleur "à remplacer" et une valeur de tolérance (//l'offset//). Cette fonction est également disponible dans le fichier chroma_key.cpp sur  +la valeur de notre couleur "à remplacer" et une valeur de tolérance (//l'offset//). 
-[[https://github.com/SDurand7/ChromaKey/tree/master/ChromaKey|mon github]] au cas où vous ne souhaiteriez pas l'écrire vous même.+
  
  
Ligne 140: Ligne 133:
 comme suit. comme suit.
  
-   // On déclare simplement une matrice,  +<code C++> 
-   // et on utilise la fonction imread d'OpenCV (cv::imread si vous n'utilisez pas le namespace cv)  +// On déclare simplement une matrice,  
-   // pour charger l'image dans background +// et on utilise la fonction imread d'OpenCV (cv::imread si vous n'utilisez pas le namespace cv)  
-   Mat background; +// pour charger l'image dans background 
-   background = imread(argv[5]);+Mat background; 
 +background = imread(argv[5]); 
 +</code>
 Comme vous pouvez le constater, j'ai fait le choix de faire passer le chemin vers mon image de fond en paramètre  Comme vous pouvez le constater, j'ai fait le choix de faire passer le chemin vers mon image de fond en paramètre 
 de mon programme, libre à vous de faire comme il vous plaît. de mon programme, libre à vous de faire comme il vous plaît.
Ligne 157: Ligne 152:
 par **background.cols**). par **background.cols**).
  
-   Mat frame; +<code C++> 
-   VideoCapture cam(0); +Mat frame; 
-   cam.set(3, WIDTH); // Cette ligne permet de définir la largeur des images capturées +VideoCapture cam(0); 
-   cam.set(4, HEIGHT); // Même chose que ci-dessus, mais cette fois pour la hauteur des images+cam.set(3, WIDTH); // Cette ligne permet de définir la largeur des images capturées 
 +cam.set(4, HEIGHT); // Même chose que ci-dessus, mais cette fois pour la hauteur des images 
 +</code>
  
 Sur un raspberry 3, avec GCC (et donc g++) version 6.3.0, la norme C++14 et l'option de compilation -O3 j'obtiens des  Sur un raspberry 3, avec GCC (et donc g++) version 6.3.0, la norme C++14 et l'option de compilation -O3 j'obtiens des 
Ligne 168: Ligne 165:
 D'ailleurs, pour récupérer les images, rien de plus simple. D'ailleurs, pour récupérer les images, rien de plus simple.
  
-   do { +<code C++> 
-      cam >> frame; +do { 
-   } while(frame.empty());+   cam >> frame; 
 +} while(frame.empty()); 
 +</code>
  
 Avec OpenCV, l'opérateur **>>** permet de lire une image depuis une VideoCapture. Cet opérateur est **strictement  Avec OpenCV, l'opérateur **>>** permet de lire une image depuis une VideoCapture. Cet opérateur est **strictement 
Ligne 181: Ligne 180:
 On doit maitenant traiter la frame qu'on à récupérer avec notre fonction de traitement (nommée chromakey dans mon code). On doit maitenant traiter la frame qu'on à récupérer avec notre fonction de traitement (nommée chromakey dans mon code).
  
-   chromakey(frame, B, G, R, offset, background);+<code C++> 
 +chromakey(frame, B, G, R, offset, background); 
 +</code>
  
 J'ai également choisi de faire passer les valeurs de B, G, R (la couleur à remplacer) et de l'offset en **paramètres du programme**. Cela permet de ne pas avoir à recompiler  J'ai également choisi de faire passer les valeurs de B, G, R (la couleur à remplacer) et de l'offset en **paramètres du programme**. Cela permet de ne pas avoir à recompiler 
Ligne 207: Ligne 208:
  
 Vous devriez obtenir un **main** structuré de cette manière: Vous devriez obtenir un **main** structuré de cette manière:
-   Tant que(votre condition d'arrêt): +<code> 
-   |   Récupérer l'image; +Tant que(votre condition d'arrêt): 
-   |   Traiter l'image; +|   Récupérer l'image; 
-   |    +|   Traiter l'image; 
-   |   cv::imshow(l'image); +|    
-   |   cv::waitKey(un petit temps d'attente); +|   cv::imshow(l'image); 
-        +|   cv::waitKey(un petit temps d'attente); 
-   cv::destroyAllWindows();+     
 +cv::destroyAllWindows(); 
 +</code>
        
 **Il est important de noter que votre raspberry doit être capable d'ouvrir des fenêtres graphiques.** **Il est important de noter que votre raspberry doit être capable d'ouvrir des fenêtres graphiques.**
Ligne 223: Ligne 226:
 sur le réseau. Vous aurez besoin qu'il soit installé sur **votre poste** et sur **votre raspberry**.  sur le réseau. Vous aurez besoin qu'il soit installé sur **votre poste** et sur **votre raspberry**. 
 Vous allez également devoir rajouter les lignes suivantes **à la fin de votre boucle**: Vous allez également devoir rajouter les lignes suivantes **à la fin de votre boucle**:
-   imencode(".jpg", frame, buffer, jpg_parameters); +<code C++> 
-   const unsigned int size = buffer.size(); +imencode(".jpg", frame, buffer, jpg_parameters); 
-   fwrite(&size, sizeof(unsigned int), 1, stdout); +const unsigned int size = buffer.size(); 
-   fwrite(buffer.data(), sizeof(uchar), size, stdout);+fwrite(&size, sizeof(unsigned int), 1, stdout); 
 +fwrite(buffer.data(), sizeof(uchar), size, stdout); 
 +</code>
 Ainsi que celles-ci **avant la boucle**: Ainsi que celles-ci **avant la boucle**:
-   std::vector<int> jpg_parameters; +<code C++> 
-   jpg_parameters.push_back(CV_IMWRITE_JPEG_QUALITY); +std::vector<int> jpg_parameters; 
-   jpg_parameters.push_back(75); +jpg_parameters.push_back(CV_IMWRITE_JPEG_QUALITY); 
-   std::vector<uchar> buffer; +jpg_parameters.push_back(75); 
 +std::vector<uchar> buffer;  
 +</code>
  
 Sans rentrer dans les détails parce que **ce n'est pas cette partie du programme qui nous intéresse**, on compresse les  Sans rentrer dans les détails parce que **ce n'est pas cette partie du programme qui nous intéresse**, on compresse les 
 images traitées en jpeg pour les transmettre sur le réseau et les afficher en exécutant un autre programme sur le poste  images traitées en jpeg pour les transmettre sur le réseau et les afficher en exécutant un autre programme sur le poste 
 sur lequel vous travaillez. Cette affichage nécessitera comme annoncé au début de ce tutoriel une installation d'OpenCV et un  sur lequel vous travaillez. Cette affichage nécessitera comme annoncé au début de ce tutoriel une installation d'OpenCV et un 
-compilateur C++ sur votre machine (et n'oubliez pas netcat). Le code du programme à exécuter est disponible [[https://github.com/SDurand7/ChromaKey/tree/master/RemoteDisplay|ici]],  +compilateur C++ sur votre machine (et n'oubliez pas netcat). 
-il est accompagné du makefile permettant de le compiler.+
  
 Si vous êtes curieux de savoir ce qui se passe vraiment ici, je vous invite à consulter la documentation d'OpenCV. Si vous êtes curieux de savoir ce qui se passe vraiment ici, je vous invite à consulter la documentation d'OpenCV.
Ligne 243: Ligne 249:
 Une fois vos deux programmes prêts à être exécuté, il ne vous reste plus qu'à ouvrir un terminal sur votre pc et un terminal sur  Une fois vos deux programmes prêts à être exécuté, il ne vous reste plus qu'à ouvrir un terminal sur votre pc et un terminal sur 
 votre raspberry et à exécuter **sur votre poste**: votre raspberry et à exécuter **sur votre poste**:
-   netcat -l -p un_numéro_de_port | bin/dbgrm+<code shell> 
 +netcat -l -p un_numéro_de_port | dbgrm 
 +</code>
 Et **sur votre raspberry**: Et **sur votre raspberry**:
-   bin/chrk paramètres_de_la commande | netcat ip_de_votre_poste le_même_numéro_de_port+<code shell> 
 +chrk paramètres_de_la commande | netcat ip_de_votre_poste le_même_numéro_de_port 
 +</code> 
 + 
 +**Le programme exécuter sur votre machine (ci-dessus) se contentera simplement de lire les données de chaque image sur stdin, de les décoder et de les afficher** (cf rubrique "Sources").
  
 **Vous avez terminé !** Cependant, vous êtes probablement déçu de la fluidité de la vidéo si vous utilisez la 2ème méthode. **Vous avez terminé !** Cependant, vous êtes probablement déçu de la fluidité de la vidéo si vous utilisez la 2ème méthode.
Ligne 296: Ligne 308:
  
  
-===== Vidéo de démonstration et sources du projet =====+===== Sources du projet =====
  
-==== Vidéo de démonstration ====+==== Sources du projet ====
  
-Comme vous pourrez le voir dans cette vidéo, on n'obtient pas un framerate suffisant pour un usage  +=== "Émetteur" === 
-en temps réelVous observerez également également que dans des conditions moyennes, une partie de l'image qu'on  +  
-ne souhaiterai pas voir disparaître disparaît (ici, mon bras).+<code C++> 
 +#include <iostream> 
 +#include <chrono> 
 +#include <opencv2/opencv.hpp> 
 +#include <omp.h>
  
-{{youtube>jZObECC9kJc?large}} 
  
-==== Sources du projet ====+#define WIDTH 1280 
 +#define HEIGHT 720 
 +#define FRAME_SIZE WIDTH*HEIGHT*3 
 +#define NUMBER_OF_FRAMES 150 
 + 
 +#define ALL_GOOD 0 
 +#define INVALID_ARGUMENTS -1 
 +#define INVALID_IMAGE -2 
 + 
 + 
 +// For a bit of comfort 
 +using namespace cv; 
 + 
 +// Simple function to test if a channel of a pixel (B, G or R) is within the interval [value - offset; value + offset] 
 +inline bool inBounds(int value, int bound, int offset) { 
 + return abs(value - bound) <offset; 
 +
 + 
 + 
 +// Most important part of the code: 
 +// Replaces the pixels of the frame close enough (depending of the offset) to the color B, G, R 
 +inline void chromakey(Mat& frame, int B, int G, int R, int offset, const Mat& background) { 
 + // Using openMP was definitely worth on a Raspberry Pi 3 model B 
 + #pragma omp parallel for 
 + // Iterating on the rows of our frame 
 + for(int i 0; i < frame.rows; i++) { 
 + // Using a pointer to the rows is the fastest way to access a matrix 
 + Vec3b *Fi frame.ptr<Vec3b>(i); 
 + const Vec3b *Bi background.ptr<Vec3b>(i); 
 +  
 + // Iterating on the elements of each row 
 + for(int j 0; j < frame.cols; j++) { 
 + // If the pixel (i, j) of our frame is close enough to B, G and R, we replace it with the color of the background (i, j) pixel 
 + if(inBounds(Fi[j][0], B, offset) && inBounds(Fi[j][1], G, offset) && inBounds(Fi[j][2], R, offset)) { 
 + for(int k 0; k < 3; k++) {  
 + Fi[j][k] Bi[j][k]; 
 +
 +
 +
 +
 +
 + 
 + 
 +int main(int argc, char** argv) { 
 + // Just a simple parameters check 
 + if(argc ! 6) { 
 + std::cerr << "Usage: ./chrk R G B offset /path/to/background_image" << std::endl; 
 + 
 + return INVALID_ARGUMENTS; 
 +
 + 
 + // This Mat contains our background 
 + Mat background; 
 + background = imread(argv[5]); 
 + 
 + // Testing that the images size is the same as defined 
 + // We could also just reshape the image, it would be a bit less restrictive 
 + if(!background.data || background.cols != WIDTH || background.rows != HEIGHT) { 
 + std::cerr << "ERROR: Invalid background image" << std::endl; 
 + 
 + return INVALID_IMAGE; 
 +
 + 
 + int B, G, R, offset; 
 + 
 + // Getting the parameters from the command line 
 + std::istringstream rr(argv[1]); 
 + std::istringstream rg(argv[2]); 
 + std::istringstream rb(argv[3]); 
 + std::istringstream rt(argv[4]); 
 + 
 + rb >> B; 
 + rg >> G; 
 + rr >> R; 
 + rt >> offset; 
 + 
 + // This Mat will contain the images 
 + Mat frame; 
 + 
 + // Initializing the capture, should be equivalent to:  
 + // VideoCapture cam(0); 
 + VideoCapture cam(0); 
 + 
 + // Setting the width and height of the capture as defined 
 + // If not set, default resolution is 640*480 
 + cam.set(3, WIDTH); 
 + cam.set(4, HEIGHT);  
 + 
 + // Adding the OpenCV JPEG parameters to a vector, this is used to encode a Mat with non-default settings 
 + std::vector<int> jpg_parameters; 
 + jpg_parameters.push_back(CV_IMWRITE_JPEG_QUALITY); 
 + jpg_parameters.push_back(75); 
 + 
 + std::vector<uchar> buffer;  
 + 
 + for(int frame_count = 0; frame_count < NUMBER_OF_FRAMES; frame_count++) { 
 + const auto start = std::chrono::high_resolution_clock::now(); 
 + 
 + do { 
 + cam >> frame; 
 + } while(frame.empty()); 
 + 
 + chromakey(frame, B, G, R, offset, background); 
 + 
 + // Encoding the frame 
 + imencode(".jpg", frame, buffer, jpg_parameters); 
 + const unsigned int size = buffer.size(); 
 + 
 + // Writing the encoded image to stdout 
 + // unsigned int and uchar size are the same on ARM and 64 bits regular computer 
 + fwrite(&size, sizeof(unsigned int), 1, stdout); 
 + fwrite(buffer.data(), sizeof(uchar), size, stdout); 
 + 
 + // We use stderr to print an FPS counter and not because we are already writing images on stdout 
 + const std::chrono::duration<float> elapsed = std::chrono::high_resolution_clock::now() - start; 
 + 
 + std::cerr << "\rFPS: " << 1/elapsed.count(); 
 +
 + std::cerr << std::endl; 
 + 
 + return ALL_GOOD; 
 +}  
 +</code> 
 + 
 +=== "Récepteur" === 
 + 
 +<code C++> 
 +#include <iostream> 
 +#include <chrono> 
 +#include <opencv2/opencv.hpp> 
 + 
 +// WIDTH, HEIGHT and NUMBER_OF_FRAMES should always be set to the same value as in the transmitter 
 +#define WIDTH 1280 
 +#define HEIGHT 720 
 +// Maximum size of the received frame 
 +// In theory, if jpeg doesn't compress the image at all, it could be larger than that because of the format overhead 
 +// I'm not sure it can ever happen, so, as this program should not be used for critical stuff (it really should not), I'm just gonna assume it won't happen 
 +#define FRAME_SIZE WIDTH*HEIGHT*3  
 +#define NUMBER_OF_FRAMES 150 
 + 
 +#define ALL_GOOD 0 
 + 
 + 
 + 
 +int main(int argc, char **argv) { 
 + // Actual size of the received frame 
 + unsigned int size;  
 + uchar buffer[FRAME_SIZE]; 
 + cv::Mat frame; 
 + 
 + for(int frame_count = 0; frame_count < NUMBER_OF_FRAMES; frame_count++) { 
 + // Reading the encoded image from stdin 
 + // unsigned int and uchar size are the same on ARM and 64 bits regular computer 
 + fread(&size, sizeof(unsigned int), 1, stdin); 
 + fread(buffer, sizeof(uchar), size, stdin); 
 + 
 + std::vector<uchar> data(buffer, buffer+size);
  
-Comme je l'ai répété à plusieurs reprises tout au long de ce tutoriel, les sources de tout le projet sont  + // Decoding the received frame 
-disponibles à [[https://github.com/SDurand7/ChromaKey|cette adresse]]. + frame = cv::imdecode(cv::Mat(data), cv::IMREAD_UNCHANGED); 
 +  
 + // Displaying the received frame 
 + cv::imshow("frame", frame);
  
-Cela inclus à la fois la partie capture de la vidéo et la partie affichage (étant donné que j'ai utilisé la seconde + // This wait is mandatory for the image to actually display 
-méthode d'affichage), respectivement dans les répertoires  + cv::waitKey(1); 
-[[https://github.com/SDurand7/ChromaKey/tree/master/ChromaKey|ChromaKey]] et [[https://github.com/SDurand7/ChromaKey/tree/master/RemoteDisplay|RemoteDisplay]]. + }
  
 + return ALL_GOOD;
 +}
 +</code>
  
 ---- ----
-//Auteur: Sylvain Durand//+//Auteur: S. Durand//
diy/projets/chromakey.1527153283.txt.gz · Dernière modification : 2018/05/24 09:14 de sdurand