diy:projets:chromakey
Différences
Ci-dessous, les différences entre deux révisions de la page.
| Les deux révisions précédentesRévision précédenteProchaine révision | Révision précédente | ||
| diy:projets:chromakey [2018/05/24 09:15] – [Sources du projet] sdurand | diy: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' | Il est important de noter que vous aurez besoin d'une installation d' | ||
| 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 | + | les images traitées aussi simplement que possible, |
| ou, pour plus d' | ou, pour plus d' | ||
| Ce programme nécessite une installation d' | Ce programme nécessite une installation d' | ||
| 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' | ||
| - | votre disposition [[https:// | ||
| - | les **options de compilation** nécessaire pour compiler un programme utilisant OpenCV. Ce makefile requiert que votre fichier soit nommé chroma_key.cpp, | ||
| - | 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' | ||
| ==== 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' | l' | ||
| - | (une version complète du code est disponible sur [[https://github.com/SDurand7/ChromaKey/tree/master/ChromaKey|mon github]]) (fichier | + | <code C++> |
| + | // frame: notre 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' | ||
| + | // 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' |
| - | // 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' | + | Vec3b *Fi = frame.ptr< |
| - | // Fi: un pointeur vers la ligne i de notre image | + | const Vec3b *Bi = background.ptr< |
| - | // 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' |
| - | // 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 " | |
| - | // On parcourt les lignes de l' | + | if(inBounds(Fi[j][0], |
| - | | + | // 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' | Vous pouvez également voir que j' | ||
| - | la valeur de notre couleur "à remplacer" | + | la valeur de notre couleur "à remplacer" |
| - | [[https:// | + | |
| Ligne 140: | Ligne 133: | ||
| comme suit. | comme suit. | ||
| - | // On déclare simplement une matrice, | + | <code C++> |
| - | | + | // On déclare simplement une matrice, |
| - | | + | // et on utilise la fonction imread d' |
| - | | + | // pour charger l' |
| - | | + | Mat background; |
| + | background = imread(argv[5]); | ||
| + | </ | ||
| 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++> |
| - | | + | Mat frame; |
| - | | + | VideoCapture cam(0); |
| - | | + | 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 | ||
| + | </ | ||
| Sur un raspberry 3, avec GCC (et donc g++) version 6.3.0, la norme C++14 et l' | Sur un raspberry 3, avec GCC (et donc g++) version 6.3.0, la norme C++14 et l' | ||
| Ligne 168: | Ligne 165: | ||
| D' | D' | ||
| - | do { | + | <code C++> |
| - | cam >> frame; | + | do { |
| - | | + | |
| + | } while(frame.empty()); | ||
| + | </ | ||
| Avec OpenCV, l' | Avec OpenCV, l' | ||
| 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, | + | <code C++> |
| + | chromakey(frame, | ||
| + | </ | ||
| J'ai également choisi de faire passer les valeurs de B, G, R (la couleur à remplacer) et de l' | J'ai également choisi de faire passer les valeurs de B, G, R (la couleur à remplacer) et de l' | ||
| 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' | + | < |
| - | | + | Tant que(votre condition d' |
| - | | + | | |
| - | | + | | |
| - | | + | | |
| - | | + | | |
| - | | + | | |
| - | | + | |
| + | cv:: | ||
| + | </ | ||
| **Il est important de noter que votre raspberry doit être capable d' | **Il est important de noter que votre raspberry doit être capable d' | ||
| 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(" | + | <code C++> |
| - | | + | imencode(" |
| - | | + | const unsigned int size = buffer.size(); |
| - | | + | fwrite(& |
| + | fwrite(buffer.data(), | ||
| + | </ | ||
| Ainsi que celles-ci **avant la boucle**: | Ainsi que celles-ci **avant la boucle**: | ||
| - | std:: | + | <code C++> |
| - | | + | std:: |
| - | | + | jpg_parameters.push_back(CV_IMWRITE_JPEG_QUALITY); |
| - | | + | jpg_parameters.push_back(75); |
| + | std:: | ||
| + | </ | ||
| Sans rentrer dans les détails parce que **ce n'est pas cette partie du programme qui nous intéresse**, | Sans rentrer dans les détails parce que **ce n'est pas cette partie du programme qui nous intéresse**, | ||
| 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' | sur lequel vous travaillez. Cette affichage nécessitera comme annoncé au début de ce tutoriel une installation d' | ||
| - | compilateur C++ sur votre machine (et n' | + | compilateur C++ sur votre machine (et n' |
| - | 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' | Si vous êtes curieux de savoir ce qui se passe vraiment ici, je vous invite à consulter la documentation d' | ||
| 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 | ||
| + | </ | ||
| 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 | ||
| + | </ | ||
| + | |||
| + | **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 " | ||
| **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 | + | ===== Sources |
| - | ==== Vidéo de démonstration | + | ==== Sources du projet |
| - | Comme vous pourrez le voir dans cette vidéo, on n' | + | === " |
| - | en temps réel. Vous observerez également également que dans des conditions moyennes, une partie de l' | + | |
| - | ne souhaiterai pas voir disparaître disparaît (ici, mon bras). | + | <code C++> |
| + | #include < | ||
| + | #include < | ||
| + | #include < | ||
| + | #include <omp.h> | ||
| - | {{youtube> | ||
| - | ==== 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& | ||
| + | // 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< | ||
| + | const Vec3b *Bi = background.ptr< | ||
| + | |||
| + | // 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], | ||
| + | for(int k = 0; k < 3; k++) { | ||
| + | Fi[j][k] | ||
| + | } | ||
| + | } | ||
| + | } | ||
| + | } | ||
| + | } | ||
| + | |||
| + | |||
| + | int main(int argc, char** argv) { | ||
| + | // Just a simple parameters check | ||
| + | if(argc != 6) { | ||
| + | std::cerr << " | ||
| + | |||
| + | 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 << " | ||
| + | |||
| + | return INVALID_IMAGE; | ||
| + | } | ||
| + | |||
| + | int B, G, R, offset; | ||
| + | |||
| + | // Getting the parameters from the command line | ||
| + | std:: | ||
| + | std:: | ||
| + | std:: | ||
| + | std:: | ||
| + | |||
| + | 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:: | ||
| + | jpg_parameters.push_back(CV_IMWRITE_JPEG_QUALITY); | ||
| + | jpg_parameters.push_back(75); | ||
| + | |||
| + | std:: | ||
| + | |||
| + | for(int frame_count = 0; frame_count < NUMBER_OF_FRAMES; | ||
| + | const auto start = std:: | ||
| + | |||
| + | do { | ||
| + | cam >> frame; | ||
| + | } while(frame.empty()); | ||
| + | |||
| + | chromakey(frame, | ||
| + | |||
| + | // Encoding the frame | ||
| + | imencode(" | ||
| + | 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(& | ||
| + | fwrite(buffer.data(), | ||
| + | |||
| + | // We use stderr to print an FPS counter and not because we are already writing images on stdout | ||
| + | const std:: | ||
| + | |||
| + | std::cerr << " | ||
| + | } | ||
| + | std::cerr << std:: | ||
| + | |||
| + | return ALL_GOOD; | ||
| + | } | ||
| + | </ | ||
| + | |||
| + | === " | ||
| + | |||
| + | <code C++> | ||
| + | #include < | ||
| + | #include < | ||
| + | #include < | ||
| + | |||
| + | // 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' | ||
| + | // 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; | ||
| + | // Reading the encoded image from stdin | ||
| + | // unsigned int and uchar size are the same on ARM and 64 bits regular computer | ||
| + | fread(& | ||
| + | fread(buffer, | ||
| + | |||
| + | std:: | ||
| - | 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/ | + | frame = cv:: |
| + | |||
| + | // Displaying the received frame | ||
| + | cv:: | ||
| - | Cela inclus à la fois la partie capture/traitement de la vidéo et la partie affichage | + | // This wait is mandatory for the image to actually display |
| - | méthode d' | + | cv:: |
| - | [[https:// | + | } |
| + | return ALL_GOOD; | ||
| + | } | ||
| + | </ | ||
| ---- | ---- | ||
| - | // | + | // |
diy/projets/chromakey.1527153339.txt.gz · Dernière modification : de sdurand
