Sommaire
Pour mettre en place une webcam, on connecte une caméra à un ordinateur sur lequel on fait tourner un serveur retransmettant les images. Celles-ci sont alors accessibles via des requêtes au serveur.
Cet article présente comment implémenter un serveur de webcam en Haskell. Le serveur proposé transmet l'image courante en réponse aux requêtes HTTP. En parallèle (via un thread léger), il met également à jour l'image courante à partir du flux vidéo.
Capture vidéo
Tout d'abord, on a besoin d'ouvrir et de capturer le flux vidéo de la webcam. Ceci est très facile à faire avec OpenCV, une bibliothèque classique de traitement d'images et de vision artificielle. OpenCV est implémentée en C++ mais possède des interfaces pour de nombreux autres langages, notamment pour Haskell avec haskell-opencv.
Dans le code suivant, la fonction openCam
ouvre le premier périphérique vidéo (id 0) et configure sa fréquence de rafraichissement à 5 images par seconde. Puis la fonction captureCam
lit une image OpenCV (de type Mat ('S ['D, 'D]) 'D 'D
) depuis le périphérique vidéo (de type VideoCapture
). Enfin, la fonction imgToPng
convertit une image OpenCV en image PNG affichage par un navigateur web.
{-# language DataKinds #-}
{-# LANGUAGE OverloadedStrings #-}
import Control.Concurrent (forkIO)
import Control.Monad (forever, unless, liftM)
import Data.ByteString (ByteString)
import Data.ByteString.Lazy (fromStrict)
import Data.IORef (atomicWriteIORef, IORef, newIORef, readIORef)
import qualified Web.Scotty as SC
import OpenCV
import OpenCV.VideoIO.Types
openCam :: IO (Maybe VideoCapture)
openCam = do
cap <- newVideoCapture
exceptErrorIO $ videoCaptureOpen cap $ VideoDeviceSource 0 Nothing
isOpened <- videoCaptureIsOpened cap
case isOpened of
False -> return Nothing
True -> videoCaptureSetD cap VideoCapPropFps 5 >> (return $ Just cap)
captureCam :: VideoCapture -> IO (Maybe (Mat ('S ['D, 'D]) 'D 'D))
captureCam cap = videoCaptureGrab cap >> videoCaptureRetrieve cap
imgToPng :: Mat ('S ['D, 'D]) 'D 'D -> ByteString
imgToPng = exceptError . imencode (OutputPng defaultPngParams)
On peut tester ces fonctions localement, avec le code suivant. La fonction loopCam
lit une image (en utilisant captureCam
), affiche cette image dans une fenêtre et boucle récursivement tant qu'on n'a pas appuyé sur la touche Echap. La fonction principale main
se résume à ouvrir un périphérique vidéo (avec openCam
), à créer une fenêtre et à lancer la boucle loopCam
.
main :: IO ()
main = do
capMaybe <- openCam
case capMaybe of
Nothing -> putStrLn "couldn't open device"
Just cap -> withWindow "webcamer" (loopCam cap)
loopCam :: VideoCapture -> Window -> IO ()
loopCam cap window = do
imgMaybe <- captureCam cap
case imgMaybe of
Nothing -> return ()
Just img -> do
imshow window img
key <- waitKey 20
unless (key == 27) $ loopCam cap window
Si on exécute ce code, on devrait avoir une fenêtre affichant le flux vidéo de la webcam à 5 FPS.
Serveur web
En utilisant la bibliothèque scotty, créons maintenant un serveur web qui va fournir le flux vidéo. À la place des fonctions main
et loopCam
précédentes, la fonction main
suivante ouvre le périphérique vidéo et lance runServer
. Cette fonction runServer
lance un serveur scotty qui fournit deux routes. Pour la route principale "/", le serveur fournit la page principale (le fichier index.html
). Pour la route "/out.png", il lit une image depuis la webcam, la convertit en PNG puis l'envoie au client HTTP.
main :: IO ()
main = do
capMaybe <- openCam
case capMaybe of
Nothing -> putStrLn "couldn't open device"
Just cap -> runServer 3042 cap
runServer :: Int -> VideoCapture -> IO ()
runServer port cap = SC.scotty port $ do
SC.get "/" $ SC.file "index.html"
SC.get "/out.png" $ do
SC.setHeader "Content-Type" "image/png"
imgMaybe <- SC.liftAndCatchIO $ liftM imgToPng <$> captureCam cap
case imgMaybe of
Nothing -> return ()
Just img -> SC.raw $ fromStrict img
Ce serveur web envoie une image à la demande du client. Pour afficher le flux vidéo, le client doit donc régulièrement demander une nouvelle image. Ceci est fait dans la page index.html
: la fonction updateImg
demande la route "/out.png" au server puis met à jour la page HTML quand l'image a été reçue. Cette fonction est appelée toutes les 200 ms (c'est-à-dire à 5 FPS), grâce à la fonction JavaScript setInterval
.
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8"/>
</head>
<body>
<h1>webcamer</h1>
<img id="my_img"> </img>
<script>
function updateImg() {
fetch("out.png")
.then(response => response.blob())
.then(function(myBlob){
URL.revokeObjectURL(my_img.src);
my_img.src = URL.createObjectURL(myBlob);
});
}
const my_interval = setInterval(updateImg, 200);
</script>
</body>
</html>
Gérer plusieurs clients
Le serveur web précédent lit une image, depuis le flux vidéo, quand un client demande la route "/out.png". Cependant, ceci ne fonctionne plus s'il y a plusieurs clients car le flux vidéo ne fournit plus assez d'images. Pour résoudre ce problème, il suffit de lire le flux et de gérer les requêtes HTTP de façon indépendante.
Le code suivant utilise une référence mutable IORef pour stocker l'image courante. Cette image est lue dans la fonction runServer
quand un client envoie une requête HTTP, et elle est modifiée dans la fonction runCam
quand une nouvelle image est disponible depuis le flux vidéo. Finallement, la fonction main
se résume à initialiser la référence mutable et à lancer runServer
et runCam
en parallèle, via forkIO
(threads légers).
main :: IO ()
main = do
capMaybe <- openCam
case capMaybe of
Nothing -> putStrLn "couldn't open device"
Just cap -> do
Just png0 <- liftM imgToPng <$> captureCam cap
pngRef <- newIORef png0
_ <- forkIO $ runCam cap pngRef
runServer 3042 pngRef
runServer :: Int -> IORef ByteString -> IO ()
runServer port pngRef = SC.scotty port $ do
SC.get "/" $ SC.file "index.html"
SC.get "/out.png" $ do
SC.setHeader "Content-Type" "image/png"
img <- SC.liftAndCatchIO (readIORef pngRef)
SC.raw $ fromStrict img
runCam :: VideoCapture -> IORef ByteString -> IO ()
runCam cap pngRef = forever $ do
imgMaybe <- liftM imgToPng <$> captureCam cap
maybe (return ()) (atomicWriteIORef pngRef) imgMaybe
Ainsi, si plusieurs clients HTTP demandent une image alors qu'une seule image est disponible dans le flux vidéo durant ce laps de temps, le serveur envoie la même image et le flux s'affiche correctement chez tous les clients.
Récapitulatif
Le code final est résumé ci-dessous. Il gère la capture vidéo de la webcam, le service web et les clients multiples. Le tout en 35 lignes de Haskell (sans les commentaires ni les signatures de fonctions, mais fonctionnel quand même).
{-# LANGUAGE OverloadedStrings #-}
import Control.Concurrent (forkIO)
import Control.Monad (forever, liftM)
import Data.ByteString.Lazy (fromStrict)
import Data.IORef (atomicWriteIORef, newIORef, readIORef)
import Web.Scotty (get, file, raw, scotty, liftAndCatchIO, setHeader)
import OpenCV
import OpenCV.VideoIO.Types
main = do
capMaybe <- openCam
case capMaybe of
Nothing -> putStrLn "couldn't open device"
Just cap -> do
Just png0 <- liftM imgToPng <$> captureCam cap
pngRef <- newIORef png0
_ <- forkIO $ runCam cap pngRef
runServer 3042 pngRef
runServer port pngRef = scotty port $ do
get "/" $ file "index.html"
get "/out.png" $ do
setHeader "Content-Type" "image/png"
img <- liftAndCatchIO (readIORef pngRef)
raw $ fromStrict img
runCam cap pngRef = forever $ do
imgMaybe <- liftM imgToPng <$> captureCam cap
maybe (return ()) (atomicWriteIORef pngRef) imgMaybe
openCam = do
cap <- newVideoCapture
exceptErrorIO $ videoCaptureOpen cap $ VideoDeviceSource 0 Nothing
isOpened <- videoCaptureIsOpened cap
case isOpened of
False -> return Nothing
True -> videoCaptureSetD cap VideoCapPropFps 5 >> (return $ Just cap)
captureCam cap = videoCaptureGrab cap >> videoCaptureRetrieve cap
imgToPng = exceptError . imencode (OutputPng defaultPngParams)
# reprise d'un lien précédent
Posté par nokomprendo (site web personnel) . Évalué à 2. Dernière modification le 14 décembre 2018 à 10:56.
Pour info, je l'avais déjà envoyé dans les liens mais là c'est une version rédigée plus complète.
# balise video ?
Posté par Laurent J (site web personnel, Mastodon) . Évalué à 3.
Ça fait quelques années déjà que les navigateurs implémentent la balise
<video>
et des fonctions de streaming.La lib OpenCV a-t-elle des fonctions de flux, genre génère du MPEG-TS à partir de la caméra ? Avec Nginx et ce module nginx-rtmp, ce serait certainement plus efficace en terme de bande passante, et de fluidité, non ?
[^] # Re: balise video ?
Posté par nokomprendo (site web personnel) . Évalué à 4.
OpenCV a quelques fonctionnalités de capture et peut aussi utiliser gstreamer mais je n'en sais pas beaucoup plus. Pour le streaming, tu as parfaitement raison : l'exemple proposé n'est pas du tout le plus efficace, c'est juste pour s'amuser un peu avec quelques lignes de Haskell.
[^] # Re: balise video ?
Posté par ted (site web personnel) . Évalué à 2.
Pour ceux qui passent par ici pour trouver de quoi streamer une webcam facilement, il existe mjpg_streamer. Je ne crois pas qu'il soit dans les dépôts, mais il est très simple à compiler, et demande très peu de ressources. C'est ce que j'utilise pour contrôler mon imprimante 3D à distance avec Octoprint.
Un LUG en Lorraine : https://enunclic-cappel.fr
# mapM_
Posté par hsyl20 (site web personnel) . Évalué à 1.
C'est un détail mais tu peux remplacer
maybe (return ())
parmapM_
.[^] # Re: mapM_
Posté par nokomprendo (site web personnel) . Évalué à 1.
Exact, merci pour l'info.
Suivre le flux des commentaires
Note : les commentaires appartiennent à celles et ceux qui les ont postés. Nous n’en sommes pas responsables.