Coopérative numérique Corse
Raspberry Pi

[Mini-Tuto] Raspberry : Caméra réseau accessible via un navigateur Web

Avec HTML et Python (Flask)

Ce mini-tutoriel a pour but de présenter une façon de concevoir une caméra de surveillance accessible en réseau, en utilisant une Raspberry Pi et son Module Caméra 

Les codes et explications qui seront présentés par la suite ont été testés sur une Raspberry Pi 2 Model B, sur le système linux Raspbian. Quelques légers changements pourraient être nécessaires en fonction du système et du modèle.

Pour les explications suivantes, je pars du principe que vous connaissez les bases de Python, que vous savez installer les modules nécessaires (apt-get ou pip) et que vous savez ce qu'est une Raspberry Pi.


Installation du matériel

Nous n'avons besoin que du module caméra, branché sur le port CSI de la Raspberry Pi : d Et faire en sorte que la raspberry soit accessible en réseau (Ethernet ou Wifi)

Modules Python

Les modules python qui seront utilisés sont les suivants :

import time 
import io 
import threading
import picamera 
from flask import Flask, render_template, Response 

Capture d'images

Dans un premier temps, nous avons besoin de capturer les images avec notre caméra. Comme le but est de pouvoir transférer ces informations par le réseau et que le support final sera un navigateur, il est important que le format de sortie soit compatible et pas trop lourd. C'est pourquoi, au lieu d'envoyer un flux vidéo, nous enverrons des images. Le taux d'images par seconde ne sera pas très grand, mais comme le but est de fabriquer un système de surveillance, cela conviendra largement.

La logique de capture doit être développée dans un thread, afin d'éviter que l'application soit bloquée par les opérations longues :

class MaCamera(object):
    thread = None       # Thread qui capture les images via la caméra
    image = None        # Dernière image capturée par le thread
    dernier_acces = 0   # Date du dernier accès à la camera par un client

    def initialize(self):
        """
        Initialisation de notre logique de capture
        """
        if MaCamera.thread is None:
            # Démarrage du thread
            MaCamera.thread = threading.Thread(target=self._capture_thread)
            MaCamera.thread.start()
            # On attend tant qu'il n'y a pas d'image disponible
            while self.image is None:
                time.sleep(0)

    def get_image(self):
        """
        Fonction permettant de récupérer la dernière image capturée
        """
        MaCamera.dernier_acces = time.time()
        self.initialize()
        return self.image

    @classmethod
    def _capture_thread(cls):
        with picamera.PiCamera() as cam:
            # Réglages permettant de changer l'orientation des images capturées
            cam.hflip = False
            cam.vflip = False
            
            stream = io.BytesIO()
            for a in cam.capture_continuous(stream, 'jpeg', use_video_port=True):
                # Lecture d'une image
                stream.seek(0)
                cls.image = stream.read()

                # Reset du stream pour préparer la récupération de la prochaine image
                stream.seek(0)
                stream.truncate()

                # On coupe le thread (et la caméra) si personne n'a
                # accédé à la caméra depuis plus de 2 secondes
                if time.time() - cls.dernier_acces > 2:
                    break
        cls.thread = None

Dans la classe ci-dessus, la fonction initialize permet de lancer le thread de capture si celui-ci n'est pas déjà lancé. La fonction get_image permet de récupérer la dernière image capturée par la caméra. Et la fonction _capture_thread représente la routine qui permet la capture d'images.

Une logique se basant sur la date de dernier accès à la caméra a été implémentée afin d'éteindre la caméra si personne n'a visité la page web depuis plus de 2 secondes.


La page HTML

Pour cette partie, vous pouvez faire ce que vous voulez si vous maîtrisez le HTML et le CSS. L'important est d'avoir une balise img permettant d'accueillir l'image de sortie, voici la page HTML que j'utilise afin de prendre en compte le redimensionnement du navigateur :

<html>
  <head>
  	<style>
  		html, body {
  			margin: 0;
  			padding: 0;
  			background: black;
  			text-align: center;
  			overflow: hidden;
  		}
  		img {
  			position: absolute;
  			top: 0;
  			left: 0;
  			right: 0;
  			bottom: 0;
  			margin: auto;
  		}
  		@media (orientation:landscape) {
	  		img {
	  			height: 100vh;
	  			width: auto;
	  		}
  		}
		@media (orientation:portrait) {
	  		img {
	  			width: 100%;
	  			height: auto;
	  		}
  		}
  	</style>
  </head>
  <body>
    <img src="{{ url_for('image_url') }}">
  </body>
</html>

Lier tout ça avec Flask

À présent il ne nous manque qu'une seule chose, utiliser la classe MaCamera pour remplir la balise img à chaque image disponible. Pour cela, nous déclarons des routes accessibles via un serveur Web fourni par Flask :

# Création d'une application Flask
app = Flask(__name__)

@app.route('/')
def index():
    """
    Page de visualisation

    Note :
    Le fichier représentant la page HTML doit 
    être placé dans un dossier "templates"
    """
    return render_template('index.html')

def generateur(camera):
    """
    Cette fonction représente un générateur d'images
    Il utilise la fonction "get_image" de notre classe "MaCamera"
    """
    while True:
        img = camera.get_image()
        yield (b'--frame\r\n'
               b'Content-Type: image/jpeg\r\n\r\n' + img + b'\r\n')

# On définit une URL permettant la récupération d'images
@app.route('/image_url')
def image_url():
    """
    Route générant le flux d'images

    Doit être appelée depuis l'attribut "src" d'une balise "img"
    """
    return Response(generateur(MaCamera()), mimetype='multipart/x-mixed-replace; boundary=frame')

# Lancement du serveur Web
if __name__ == '__main__':
    # On utilise ici le port 3000
    app.run(host='0.0.0.0', debug=False, threaded=True, port=3000)

Dans le code ci-dessus, la route /image_url est utilisée comme attribut src dans notre page HTML. Le mimetype défini permet la récupération de chaque nouvelle image via la fonction generateur. Quand vous exécuterez ce code python, votre caméra sera accessible à l'adresse IP de la raspberry, sur le port 3000. Par exemple : http://192.168.1.12:3000