{"id":6851,"date":"2020-05-30T17:25:06","date_gmt":"2020-05-30T17:25:06","guid":{"rendered":"https:\/\/www.erroussafi.com\/?p=6851"},"modified":"2020-05-30T17:25:06","modified_gmt":"2020-05-30T17:25:06","slug":"projet-opencv-et-tensorflow-un-fond-virtuel-pour-votre-webcam","status":"publish","type":"post","link":"https:\/\/www.erroussafi.com\/index.php\/2020\/05\/30\/projet-opencv-et-tensorflow-un-fond-virtuel-pour-votre-webcam\/","title":{"rendered":"[PROJET OpenCV et Tensorflow] &#8211; Un fond virtuel pour votre Webcam."},"content":{"rendered":"\n<p>Durant cette p\u00e9riode du COVID, la visioconf\u00e9rence est devenue tr\u00e8s courante. Certains logiciels comme Teams proposent de remplacer le fond de votre webcam par un flou ou bien par une image, permettant une plus grande intimit\u00e9 lors de vos r\u00e9unions de travail. Ce projet a pour but de cr\u00e9er un logiciel ouvert, qui propose cette fonctionnalit\u00e9 avec n&#8217;importe quel logiciel et vous permettre un choix d&#8217;image \u00e0 placer.<\/p>\n\n\n\n<!--more-->\n\n\n\n<p>Pour ce projet nous allons utilis\u00e9 un language simple &#8220;python&#8221;, au vu de la grande disponibilit\u00e9 des modules. Nous utiliserons aussi une biblioth\u00e8que de traitement d&#8217;image &#8220;OpenCV&#8221; et la biblioth\u00e8que de google TensorFlow pour la partie machine learning du traitement.<\/p>\n\n\n\n<p>Loin de r\u00e9pliquer le moteur de Microsoft Teams, tr\u00e8s \u00e9volu\u00e9 permettant une image nette et limpide, Il s&#8217;av\u00e8re que nous pouvons r\u00e9ellement obtenir des r\u00e9sultats assez d\u00e9cents avec des composants open source, open source et juste un peu de notre propre code.<\/p>\n\n\n\n<p>Pour les besoins du code, j&#8217;utilise une machine virtuelle ubuntu sur laquelle je fais mes d\u00e9veloppement, l&#8217;acc\u00e8s \u00e0 la webcam se fera donc \u00e0 travers les acc\u00e8s hardware linux. Un port vers windows est tr\u00e8s simple. Le code python reste assez portable plus ou moins quelques adaptations.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Lire le flux de la webcam<\/h2>\n\n\n\n<p>Premi\u00e8re chose: comment allons-nous obtenir le flux vid\u00e9o de notre webcam pour le traitement?<br>La lecture d&#8217;un cadre depuis la webcam avec python-opencv est tr\u00e8s simple:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>import cv2\nmaCapture = cv2.VideoCapture('\/dev\/video0')\nsuccess, frame = MaCapture.read()<\/code><\/pre>\n\n\n\n<p>ci dessous un exemple des adaptations qu&#8217;on peut faire sur le code pour recevoir un flux en bonne d\u00e9finition \u00e0 720p et avec une vitesse de d\u00e9filement (framerate) \u00e0 60 FPS :<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>height, width = 720, 1280\nmaCapture.set(cv2.CAP_PROP_FRAME_WIDTH ,width)\nmaCapture.set(cv2.CAP_PROP_FRAME_HEIGHT,height)\nmaCapture.set(cv2.CAP_PROP_FPS, 60)<\/code><\/pre>\n\n\n\n<p>Ce param\u00e8trages va d\u00e9finir une limite sup\u00e9rieur de FPS, mais nous n&#8217;allons pas r\u00e9cup\u00e9rer toutes les &#8220;frames&#8221; pour rendre notre code rapide et fluide.<\/p>\n\n\n\n<p>La boucle suivante va lancer la capture infinie de la webcam.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>while True:\n    success, frame = maCapture.read()<\/code><\/pre>\n\n\n\n<p>Pour tester nous allons faire une capture et l&#8217;enregistrer sur un fichier image :<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>cv2.imwrite(\"testcapture.jpg\", frame)<\/code><\/pre>\n\n\n\n<p>Ca fonctionne :<\/p>\n\n\n\n<figure class=\"wp-block-image size-large is-resized\"><img loading=\"lazy\" decoding=\"async\" src=\"https:\/\/www.erroussafi.com\/wp-content\/uploads\/2020\/05\/capture.jpg-1024x576.jpg\" alt=\"\" class=\"wp-image-6854\" width=\"563\" height=\"316\" srcset=\"https:\/\/www.erroussafi.com\/wp-content\/uploads\/2020\/05\/capture.jpg-1024x576.jpg 1024w, https:\/\/www.erroussafi.com\/wp-content\/uploads\/2020\/05\/capture.jpg-300x169.jpg 300w, https:\/\/www.erroussafi.com\/wp-content\/uploads\/2020\/05\/capture.jpg-768x432.jpg 768w, https:\/\/www.erroussafi.com\/wp-content\/uploads\/2020\/05\/capture.jpg-870x489.jpg 870w, https:\/\/www.erroussafi.com\/wp-content\/uploads\/2020\/05\/capture.jpg-895x503.jpg 895w, https:\/\/www.erroussafi.com\/wp-content\/uploads\/2020\/05\/capture.jpg.jpg 1280w\" sizes=\"auto, (max-width: 563px) 100vw, 563px\" \/><figcaption>capture de test<\/figcaption><\/figure>\n\n\n\n<h2 class=\"wp-block-heading\">Trouver le fond dans l&#8217;image<\/h2>\n\n\n\n<p>C&#8217;est la tache la plus difficile. Il s&#8217;agit de detecter sur chaque image les pixels appartenant au fond afin de les supprimer ou au moins les marquer grace \u00e0 un masque pour pouvoir extraire la personne et remplacer le fond.<\/p>\n\n\n\n<p>Pour faire ce type d&#8217;op\u00e9rations, les outils comme Zoom et Microsoft Teams utilisent le machine learning, et plus pr\u00e9cisemment des r\u00e9seaux neuronaux de convolution. Ce qui permet, un r\u00e9sultat tr\u00e8s correct. Cependant, si on veut faire la m\u00eame chose, nous devons trouver un environnement machine learning, et l&#8217;entrainer avec une sommes d&#8217;images du m\u00eame type (une grande biblioth\u00e8que d&#8217;images de personnes devant leurs webcam dans des environnement et des fond d&#8217;ecran d&#8217;int\u00e9rieur de maison). <\/p>\n\n\n\n<p>Heureusement, Google a d\u00e9j\u00e0 fait tout ce travail et a ouvert un r\u00e9seau neuronal pr\u00e9-form\u00e9 pour la \u00absegmentation des personnes\u00bb appel\u00e9 BodyPix qui va nous \u00e9conomiser beaucoup de temps.<\/p>\n\n\n\n<p>BodyPix est disponible en code .js (javascript) sur environnement node uniquement, et pour cel\u00e0, nous allons commencer par configurer un petit environnement \/ projet tensorflow-gpu + node conteneuris\u00e9. Il est beaucoup plus facile de l&#8217;utiliser avec nvidia-docker que de configurer toutes les bonnes d\u00e9pendances sur votre h\u00f4te, cela ne n\u00e9cessite qu&#8217;un docker et un pilote GPU \u00e0 jour sur l&#8217;h\u00f4te. J&#8217;ai une carte nvidia sur mon laptop ce qui facilite l&#8217;usage de ce backend.<\/p>\n\n\n\n<p>Notre petit conteneur aura donc un fichier package.json avec les d\u00e9pendances :<\/p>\n\n\n\n<pre class=\"wp-block-preformatted\">{\n    \"name\": \"bodypix\",\n    \"version\": \"0.0.1\",\n    \"dependencies\": {\n        \"@tensorflow-models\/body-pix\": \"^2.0.5\",\n        \"@tensorflow\/tfjs-node-gpu\": \"^1.7.1\"\n    }\n}<\/pre>\n\n\n\n<p>Un fichier Dockerfile<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code># Image de base avec les pr\u00e9requis Tensorflow\nFROM nvcr.io\/nvidia\/cuda:10.0-cudnn7-runtime-ubuntu18.04\n# Installation de nodejs\nRUN apt update &amp;&amp; apt install -y curl make build-essential \\\n    &amp;&amp; curl -sL https:\/\/deb.nodesource.com\/setup_12.x | bash - \\\n    &amp;&amp; apt-get -y install nodejs \\\n    &amp;&amp; mkdir \/.npm \\\n    &amp;&amp; chmod 777 \/.npm\n# S'assurer d'avoir assez de m\u00e9moire GPU sur notre conteneur\nENV TF_FORCE_GPU_ALLOW_GROWTH=true\n# Installer les d\u00e9pendances\nWORKDIR \/src\nCOPY package.json \/src\/\nRUN npm install\n# Configurer notre app.js comme point d'entr\u00e9e du backend\nCOPY app.js \/src\/\nENTRYPOINT node \/src\/app.js<\/code><\/pre>\n\n\n\n<p>Pour expliquer le principe de l&#8217;application, app.js, ce script devra r\u00e9pondre \u00e0 une image qui aura \u00e9t\u00e9 transmise via un HTTP POST. Sur la r\u00e9ponse, il doit mettre un binaire (un tableau 2D de pixels binaires, o\u00f9 la valeur zero pixel corespond \u00e0 l&#8217;arri\u00e8re-plan). dans ce script nous allons appeler les m\u00e9thodes de TensorFlow et bodypix (noter que ce code est tr\u00e8s basique et inspir\u00e9 de plusieurs exemples sur internet, il ne contient pas de gestion d&#8217;erreur, toute requ\u00eate diff\u00e9rente d&#8217;un post avec une image causera une erreur) :<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>const tfjs = require('@tensorflow\/tfjs-node-gpu');\nconst bodyPix = require('@tensorflow-models\/body-pix');\nconst http = require('http');\n(async () => {\n    const net = await bodyPix.load({\n        architecture: 'MobileNetV1',\n        outputStride: 16,\n        multiplier: 0.75,\n        quantBytes: 2,\n    });\n    const monServeur = http.createServer();\n    monServeur.on('request', async (req, res) => {\n        var chunks = &#91;];\n        req.on('data', (chunk) => {\n            chunks.push(chunk);\n        });\n        req.on('end', async () => {\n            const image = tfjs.node.decodeImage(Buffer.concat(chunks));\n            segmentation = await net.segmentPerson(image, {\n                flipHorizontal: false,\n                internalResolution: 'medium',\n                segmentationThreshold: 0.7,\n            });\n            res.writeHead(200, { 'Content-Type': 'application\/octet-stream' });\n            res.write(Buffer.from(segmentation.data));\n            res.end();\n            tfjs.dispose(image);\n        });\n    });\n    monServeur.listen(9000);\n})();<\/code><\/pre>\n\n\n\n<p>De retour sur notre code python, nous allons cr\u00e9er une fonction qui prend comme argument notre &#8220;frame&#8221; directement captur\u00e9e par maCapture sur la webcam (voir d\u00e9but de l&#8217;article), l&#8217;envoyer au serveur qu&#8217;on vient de cr\u00e9er et r\u00e9cup\u00e9rer le masque pour le retourner :<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>def get_mask(frame, bodypix_url='http:\/\/localhost:9000'):\n    _, data = cv2.imencode(\".jpg\", frame)\n    r = requests.post(\n        url=bodypix_url,\n        data=data.tobytes(),\n        headers={'Content-Type': 'application\/octet-stream'})\n    # transformer les bytes raw en un tableau numpy\n    mask = np.frombuffer(r.content, dtype=np.uint8)\n    mask = mask.reshape((frame.shape&#91;0], frame.shape&#91;1]))\n    return mask<\/code><\/pre>\n\n\n\n<p>Le r\u00e9sultat ressemblera \u00e0 ce masque :<\/p>\n\n\n\n<figure class=\"wp-block-image size-large is-resized\"><img loading=\"lazy\" decoding=\"async\" src=\"https:\/\/www.erroussafi.com\/wp-content\/uploads\/2020\/05\/mask-1024x683.png\" alt=\"\" class=\"wp-image-6855\" width=\"505\" height=\"336\" srcset=\"https:\/\/www.erroussafi.com\/wp-content\/uploads\/2020\/05\/mask-1024x683.png 1024w, https:\/\/www.erroussafi.com\/wp-content\/uploads\/2020\/05\/mask-300x200.png 300w, https:\/\/www.erroussafi.com\/wp-content\/uploads\/2020\/05\/mask-768x512.png 768w, https:\/\/www.erroussafi.com\/wp-content\/uploads\/2020\/05\/mask-370x247.png 370w, https:\/\/www.erroussafi.com\/wp-content\/uploads\/2020\/05\/mask.png 1140w\" sizes=\"auto, (max-width: 505px) 100vw, 505px\" \/><figcaption>masque sur base de la capture de ma webcam<\/figcaption><\/figure>\n\n\n\n<p>Maintenant pour notre fond, nous allons utiliser cette superbe image de la place &#8220;Jamaa lafna&#8221;, l&#8217;image doit \u00eatre modifi\u00e9e sur un logiciel de traitement d&#8217;image pour respecter le 16:9 ratio. Et sur notre code python nous allons faire le reste des adaptations :<\/p>\n\n\n\n<figure class=\"wp-block-image size-large is-resized\"><img loading=\"lazy\" decoding=\"async\" src=\"https:\/\/www.erroussafi.com\/wp-content\/uploads\/2020\/05\/background_marrakech-1024x683.png\" alt=\"\" class=\"wp-image-6856\" width=\"577\" height=\"384\" srcset=\"https:\/\/www.erroussafi.com\/wp-content\/uploads\/2020\/05\/background_marrakech-1024x683.png 1024w, https:\/\/www.erroussafi.com\/wp-content\/uploads\/2020\/05\/background_marrakech-300x200.png 300w, https:\/\/www.erroussafi.com\/wp-content\/uploads\/2020\/05\/background_marrakech-768x512.png 768w, https:\/\/www.erroussafi.com\/wp-content\/uploads\/2020\/05\/background_marrakech-370x247.png 370w, https:\/\/www.erroussafi.com\/wp-content\/uploads\/2020\/05\/background_marrakech.png 1140w\" sizes=\"auto, (max-width: 577px) 100vw, 577px\" \/><figcaption>fond d&#8217;\u00e9cran<\/figcaption><\/figure>\n\n\n\n<p>Voici notre code :<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code># Importer notre fond virtuel\nreplacement_bg_raw = cv2.imread(\"fondjamaalafna.jpg\")\n# redimensionner pour etre de la m\u00eame taille que la capture webcam\nwidth, height = 720, 1280\nreplacement_bg = cv2.resize(replacement_bg_raw, (width, height))\n# la magie opere ici : combiner le fond et l image webcam en utilisant le masque et son inverse pour extraire le fond et ma tete\ninv_mask = 1-mask\nfor c in range(frame.shape&#91;2]):\n    frame&#91;:,:,c] = frame&#91;:,:,c]*mask + replacement_bg&#91;:,:,c]*inv_mask<\/code><\/pre>\n\n\n\n<p>ce qui nous donne :<\/p>\n\n\n\n<figure class=\"wp-block-image size-large\"><img loading=\"lazy\" decoding=\"async\" width=\"1024\" height=\"683\" src=\"https:\/\/www.erroussafi.com\/wp-content\/uploads\/2020\/05\/TEST_BACKGROUND-1024x683.png\" alt=\"\" class=\"wp-image-6857\" srcset=\"https:\/\/www.erroussafi.com\/wp-content\/uploads\/2020\/05\/TEST_BACKGROUND-1024x683.png 1024w, https:\/\/www.erroussafi.com\/wp-content\/uploads\/2020\/05\/TEST_BACKGROUND-300x200.png 300w, https:\/\/www.erroussafi.com\/wp-content\/uploads\/2020\/05\/TEST_BACKGROUND-768x512.png 768w, https:\/\/www.erroussafi.com\/wp-content\/uploads\/2020\/05\/TEST_BACKGROUND-370x247.png 370w, https:\/\/www.erroussafi.com\/wp-content\/uploads\/2020\/05\/TEST_BACKGROUND.png 1140w\" sizes=\"auto, (max-width: 1024px) 100vw, 1024px\" \/><figcaption>Combinaison du fond et de la capture webcam avec le xor du masque cr\u00e9e par notre serveur<\/figcaption><\/figure>\n\n\n\n<p>Voil\u00e0 \u00e0 pr\u00e9sent nous arrivons \u00e0 faire cette op\u00e9ration image par image, le prochain chapitre va nous montrer comment exporter ce flux d&#8217;image sous forme d&#8217;une webcam virtuelle qu&#8217;on pourra par la suite utiliser dans les diff\u00e9rents autres logiciels \u00e0 la place de notre webcam classique pour disposer de cette fonctionnalit\u00e9. Pour des besoins de d\u00e9ploiement nous allons cr\u00e9er un conteneur docker pour notre fakewebcam avec notre code.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Sortie de flux vid\u00e9o virtuel<\/h2>\n\n\n\n<p>Nous allons utiliser pyfakewebcam et v4l2loopback pour cr\u00e9er un faux appareil webcam. le tout dans un conteneur :<\/p>\n\n\n\n<p>requirement.txt <\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>numpy==1.18.2\nopencv-python==4.2.0.32\nrequests==2.23.0\npyfakewebcam==0.1.0<\/code><\/pre>\n\n\n\n<p>Dockerfile<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>ROM python:3-buster\n# avoir pip \u00e0 jour\nRUN pip install --upgrade pip\n# install opencv dependencies\nRUN apt-get update &amp;&amp; \\\n    apt-get install -y \\\n      `# opencv requirements` \\\n      libsm6 libxext6 libxrender-dev \\\n      `# opencv video opening requirements` \\\n      libv4l-dev\n# installer les requirements\nWORKDIR \/src\nCOPY requirements.txt \/src\/\nRUN pip install --no-cache-dir -r \/src\/requirements.txt\n# copier le fondvirtuel\nCOPY fondjamaalafna.jpg \/data\/\n# demarrer notre script de fakewebcam\nCOPY fake.py \/src\/\nENTRYPOINT python -u fake.py<\/code><\/pre>\n\n\n\n<p>Pour cr\u00e9er une fausse webcam, nous aurons besoin d&#8217;acc\u00e9der directement au shell linux afin d&#8217;activer le module grace \u00e0 <code>v4l2loopback<\/code> et connecter cette fausse webcam au flux de sortie de notre script :<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>sudo apt install v4l2loopback-dkms<\/code><\/pre>\n\n\n\n<p>et configurer le module linux :<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>sudo modprobe -r v4l2loopback\nsudo modprobe v4l2loopback devices=1 video_nr=20 card_label=\"v4l2loopback\" exclusive_caps=1<\/code><\/pre>\n\n\n\n<p>la valeur card_label sera le nom de la camera dans certaines applications (chrome, firefox, zoom ..) et le video_nr=20 c&#8217;est le suffixe du device sur linux pour faire que \/dev\/video20 soit l&#8217;adresse de notre camera.<\/p>\n\n\n\n<p>Ensuite nous allons inclure fakewebcam \u00e0 notre script python avec les m\u00eames hauteurs et largeurs utilis\u00e9es pr\u00e9cedemment pour redimensionner l&#8217;image :<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>fake = pyfakewebcam.FakeWebcam('\/dev\/video20', width, height)<\/code><\/pre>\n\n\n\n<p>Tr\u00e8s important \u00e0 noter que pyfakewebcam s&#8217;attend \u00e0 recevoir des images en RVB (rouge, vert, bleu) tandis que nos op\u00e9rations OpenCV sont dans l&#8217;ordre des canaux BGR (bleu, vert, rouge).<br><br>Pour regler cel\u00e0, nous allons rajouter ce bout de code :<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>frame = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)\nfake.schedule_frame(frame)<\/code><\/pre>\n\n\n\n<h2 class=\"wp-block-heading\">Conclusion<\/h2>\n\n\n\n<p>Et voil\u00e0, notre code permettra donc la mise \u00e0 disposition de cette webcam virtuelle qui remplace continuellement le fond de la capture webcam par une image de notre choix. Je me suis beaucoup inspir\u00e9 bien sur d&#8217;articles sur internet, j&#8217;esp\u00e8re avoir aid\u00e9 \u00e0 simplifier la compr\u00e9hension du code qui reste ouvert. <\/p>\n\n\n\n<p>Des pistes de d\u00e9veloppement seraient, au lieu de remplacer le background par une image statique, le remplacer par une vid\u00e9o, ou bien par un effet de distortion (blur, flou) comme le fait ci bien Microsoft Teams.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Sources :<\/h2>\n\n\n\n<p><a href=\"https:\/\/towardsdatascience.com\/virtual-background-for-video-conferencing-in-python-and-opencv-a-silly-approach-5f5ad1a5abef\">https:\/\/towardsdatascience.com\/virtual-background-for-video-conferencing-in-python-and-opencv-a-silly-approach-5f5ad1a5abef<\/a><\/p>\n\n\n\n<p><a href=\"https:\/\/elder.dev\/posts\/open-source-virtual-background\/\">https:\/\/elder.dev\/posts\/open-source-virtual-background\/<\/a><\/p>\n\n\n\n<p><a href=\"https:\/\/nodejs.org\/en\/docs\/guides\/getting-started-guide\/\">https:\/\/nodejs.org\/en\/docs\/guides\/getting-started-guide\/<\/a><\/p>\n\n\n\n<p><a href=\"https:\/\/docker-curriculum.com\/\">https:\/\/docker-curriculum.com\/<\/a><\/p>\n","protected":false},"excerpt":{"rendered":"<p>Durant cette p\u00e9riode du COVID, la visioconf\u00e9rence est devenue tr\u00e8s courante. Certains logiciels comme Teams proposent de remplacer le fond de votre webcam par un flou ou bien par une image, permettant une plus grande intimit\u00e9 lors de vos r\u00e9unions de travail. Ce projet a pour but de cr\u00e9er un logiciel ouvert, qui propose cette [&hellip;]<\/p>\n","protected":false},"author":1,"featured_media":6859,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"_jetpack_memberships_contains_paid_content":false,"footnotes":"","jetpack_publicize_message":"Nouvel article : Une webcam virtuelle avec suppression de fond, projet Python, openCV, Tensorflow docket et node.js","jetpack_publicize_feature_enabled":true,"jetpack_social_post_already_shared":true,"jetpack_social_options":{"image_generator_settings":{"template":"highway","enabled":false},"version":2}},"categories":[94,9],"tags":[],"class_list":["post-6851","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-open-source","category-technique"],"jetpack_publicize_connections":[],"jetpack_featured_media_url":"https:\/\/www.erroussafi.com\/wp-content\/uploads\/2020\/05\/featured.png","jetpack_sharing_enabled":true,"_links":{"self":[{"href":"https:\/\/www.erroussafi.com\/index.php\/wp-json\/wp\/v2\/posts\/6851","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/www.erroussafi.com\/index.php\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/www.erroussafi.com\/index.php\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/www.erroussafi.com\/index.php\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/www.erroussafi.com\/index.php\/wp-json\/wp\/v2\/comments?post=6851"}],"version-history":[{"count":0,"href":"https:\/\/www.erroussafi.com\/index.php\/wp-json\/wp\/v2\/posts\/6851\/revisions"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/www.erroussafi.com\/index.php\/wp-json\/wp\/v2\/media\/6859"}],"wp:attachment":[{"href":"https:\/\/www.erroussafi.com\/index.php\/wp-json\/wp\/v2\/media?parent=6851"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/www.erroussafi.com\/index.php\/wp-json\/wp\/v2\/categories?post=6851"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/www.erroussafi.com\/index.php\/wp-json\/wp\/v2\/tags?post=6851"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}