VIDEO SURVEILLANCE AVEC RASPBERRY PI ET FLASK

 Je suis sûr que vous savez maintenant que j'ai publié un livre et quelques vidéos sur Flask en coopération avec O'Reilly Media. Bien que la couverture du framework Flask dans ceux-ci soit assez complète, il y a un petit nombre de fonctionnalités qui, pour une raison ou une autre, n'ont pas été beaucoup mentionnées, alors j'ai pensé que ce serait une bonne idée d'écrire des articles à leur sujet ici.

Cet article est dédié au streaming , une fonctionnalité intéressante qui donne aux applications Flask la possibilité de fournir des réponses volumineuses efficacement partitionnées en petits morceaux, potentiellement sur une longue période de temps. Pour illustrer le sujet, je vais vous montrer comment créer un serveur de streaming vidéo en direct!

REMARQUE : il y a maintenant un suivi de cet article, Flask Video Streaming Revisited , dans lequel je décris quelques améliorations du serveur de streaming présentées ici.

Qu'est-ce que le streaming?

Le streaming est une technique dans laquelle le serveur fournit la réponse à une demande par blocs. Je peux penser à quelques raisons pour lesquelles cela pourrait être utile:

  • Très grandes réponses . Le fait de devoir assembler une réponse en mémoire uniquement pour la renvoyer au client peut être inefficace pour des réponses très volumineuses. Une alternative serait d'écrire la réponse sur le disque puis de renvoyer le fichier avec flask.send_file(), mais cela ajoute des E / S au mixage. Fournir la réponse en petites portions est une bien meilleure solution, en supposant que les données peuvent être générées par blocs.
  • Données en temps réel . Pour certaines applications, une demande peut avoir besoin de renvoyer des données provenant d'une source en temps réel. Un bon exemple de ceci est un flux vidéo ou audio en temps réel. De nombreuses caméras de sécurité utilisent cette technique pour diffuser des vidéos sur des navigateurs Web.

Implémentation du streaming avec Flask

Flask fournit une prise en charge native de la diffusion en continu des réponses grâce à l'utilisation de fonctions de générateur . Un générateur est une fonction spéciale qui peut être interrompue et reprise. Considérez la fonction suivante:

def gen():
    yield 1
    yield 2
    yield 3

Il s'agit d'une fonction qui s'exécute en trois étapes, chacune renvoyant une valeur. Décrire comment les fonctions de générateur sont implémentées sort du cadre de cet article, mais si vous êtes un peu curieux, la session shell suivante vous donnera une idée de la façon dont les générateurs sont utilisés:

>>> x = gen()
>>> x
<generator object gen at 0x7f06f3059c30>
>>> next(x)
1
>>> next(x)
2
>>> next(x)
3
>>> next(x)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
StopIteration

Vous pouvez voir dans cet exemple simple qu'une fonction de générateur peut renvoyer plusieurs résultats en séquence. Flask utilise cette caractéristique des fonctions du générateur pour implémenter le streaming.

L'exemple ci-dessous montre comment l'utilisation du streaming permet de générer une table de données volumineuse, sans avoir à assembler la table entière en mémoire:

from flask import Response, render_template
from app.models import Stock

def generate_stock_table():
    yield render_template('stock_header.html')
    for stock in Stock.query.all():
        yield render_template('stock_row.html', stock=stock)
    yield render_template('stock_footer.html')

@app.route('/stock-table')
def stock_table():
    return Response(generate_stock_table())

Dans cet exemple, vous pouvez voir comment Flask fonctionne avec les fonctions du générateur. Une route qui renvoie une réponse diffusée doit renvoyer un Responseobjet initialisé avec la fonction de générateur. Flask se charge alors d'appeler le générateur et d'envoyer tous les résultats partiels sous forme de blocs au client.

Pour cet exemple particulier, si vous supposez que Stock.query.all()renvoie le résultat d'une requête de base de données en tant qu'itérable, vous pouvez générer une table potentiellement volumineuse une ligne à la fois, donc quel que soit le nombre d'éléments dans la requête, la consommation de mémoire dans le processus Python sera ne pas devenir de plus en plus grand en raison de la nécessité d'assembler une grande chaîne de réponse.

Réponses en plusieurs parties

L'exemple de tableau ci-dessus génère une page traditionnelle en petites portions, avec toutes les parties concaténées dans le document final. C'est un bon exemple de la façon de générer des réponses volumineuses, mais quelque chose d'un peu plus excitant est de travailler avec des données en temps réel.

Une utilisation intéressante du streaming est de faire en sorte que chaque morceau remplace le précédent dans la page, car cela permet aux flux de "jouer" ou de s'animer dans la fenêtre du navigateur. Avec cette technique, chaque morceau du flux peut être une image, ce qui vous donne un flux vidéo sympa qui s'exécute dans le navigateur!

Le secret pour implémenter les mises à jour sur place est d'utiliser une réponse en plusieurs parties . Les réponses en plusieurs parties consistent en un en-tête qui comprend l'un des types de contenu en plusieurs parties, suivi des parties, séparées par un marqueur de limite et chacune ayant son propre type de contenu spécifique à la partie.

Il existe plusieurs types de contenu en plusieurs parties pour différents besoins. Afin d'avoir un flux où chaque partie remplace la partie précédente, le multipart/x-mixed-replacetype de contenu doit être utilisé. Pour vous aider à vous faire une idée de ce à quoi cela ressemble, voici la structure d'un flux vidéo en plusieurs parties:

HTTP/1.1 200 OK
Content-Type: multipart/x-mixed-replace; boundary=frame

--frame
Content-Type: image/jpeg

<jpeg data here>
--frame
Content-Type: image/jpeg

<jpeg data here>
...

Comme vous le voyez ci-dessus, la structure est assez simple. L'en- Content-Typetête principal est défini sur multipart/x-mixed-replaceet une chaîne de délimitation est définie. Ensuite, chaque partie est incluse, précédée de deux tirets et de la chaîne de délimitation de la partie dans leur propre ligne. Les parties ont leur propre en- Content-Typetête, et chaque partie peut éventuellement inclure un en- Content-Lengthtête avec la longueur en octets de la charge utile de la partie, mais au moins pour les images, les navigateurs sont capables de traiter le flux sans la longueur.

Création d'un serveur de streaming vidéo en direct

Il y a eu suffisamment de théorie dans cet article, il est maintenant temps de créer une application complète qui diffuse des vidéos en direct sur les navigateurs Web.

Il existe de nombreuses façons de diffuser des vidéos sur les navigateurs, et chaque méthode a ses avantages et ses inconvénients. La méthode qui fonctionne bien avec la fonction de diffusion en continu de Flask consiste à diffuser une séquence d'images JPEG indépendantes. Cela s'appelle Motion JPEG et est utilisé par de nombreuses caméras de sécurité IP. Cette méthode a une faible latence, mais la qualité n'est pas la meilleure, car la compression JPEG n'est pas très efficace pour la vidéo animée.

Ci-dessous, vous pouvez voir une application Web étonnamment simple mais complète qui peut servir un flux Motion JPEG:

#!/usr/bin/env python
from flask import Flask, render_template, Response
from camera import Camera

app = Flask(__name__)

@app.route('/')
def index():
    return render_template('index.html')

def gen(camera):
    while True:
        frame = camera.get_frame()
        yield (b'--frame\r\n'
               b'Content-Type: image/jpeg\r\n\r\n' + frame + b'\r\n')

@app.route('/video_feed')
def video_feed():
    return Response(gen(Camera()),
                    mimetype='multipart/x-mixed-replace; boundary=frame')

if __name__ == '__main__':
    app.run(host='0.0.0.0', debug=True)

Cette application importe une Cameraclasse chargée de fournir la séquence de trames. Mettre la partie de contrôle de la caméra dans un module séparé est une bonne idée dans ce cas, de cette façon l'application Web reste propre, simple et générique.

L'application dispose de deux itinéraires. L' /itinéraire dessert la page principale, qui est définie dans le index.htmlmodèle. Ci-dessous, vous pouvez voir le contenu de ce fichier modèle:

<html>
  <head>
    <title>Video Streaming Demonstration</title>
  </head>
  <body>
    <h1>Video Streaming Demonstration</h1>
    <img src="{{ url_for('video_feed') }}">
  </body>
</html>

Il s'agit d'une simple page HTML avec juste un en-tête et une balise d'image. Notez que l' srcattribut de la balise image pointe vers la deuxième route de cette application, et c'est là que la magie opère.

La /video_feedroute renvoie la réponse en continu. Étant donné que ce flux renvoie les images à afficher dans la page Web, l'URL de cet itinéraire se trouve dans l' srcattribut de la balise d'image. Le navigateur gardera automatiquement l'élément d'image mis à jour en affichant le flux d'images JPEG, car les réponses en plusieurs parties sont prises en charge dans la plupart / tous les navigateurs (faites-moi savoir si vous trouvez un navigateur qui n'aime pas cela).

La fonction de générateur utilisée dans la /video_feedroute est appelée gen()et prend comme argument une instance de la Cameraclasse. L' mimetypeargument est défini comme indiqué ci-dessus, avec le multipart/x-mixed-replacetype de contenu et une limite définie sur la chaîne "frame".

La gen()fonction entre dans une boucle où elle renvoie en continu les images de la caméra sous forme de blocs de réponse. La fonction demande à la caméra de fournir une image en appelant la camera.get_frame()méthode, puis elle donne cette image formatée comme un bloc de réponse avec un type de contenu de image/jpeg, comme indiqué ci-dessus.

Obtention d'images à partir d'une caméra vidéo

Il ne reste plus qu'à implémenter la Cameraclasse, qui devra se connecter au matériel de la caméra et télécharger des images vidéo en direct à partir de celle-ci. La bonne chose à propos de l'encapsulation de la partie dépendante du matériel de cette application dans une classe est que cette classe peut avoir différentes implémentations pour différentes personnes, mais le reste de l'application reste le même. Vous pouvez considérer cette classe comme un pilote de périphérique, qui fournit une implémentation uniforme quel que soit le périphérique matériel utilisé.

L'autre avantage d'avoir la Cameraclasse séparée du reste de l'application est qu'il est facile de tromper l'application en lui faisant croire qu'il y a une caméra alors qu'en réalité il n'y en a pas, car la classe de caméra peut être implémentée pour émuler une caméra sans matériel réel. . En fait, pendant que je travaillais sur cette application, le moyen le plus simple pour moi de tester le streaming était de le faire et de ne pas avoir à m'inquiéter du matériel jusqu'à ce que tout le reste fonctionne. Ci-dessous, vous pouvez voir l'implémentation simple de la caméra émulée que j'ai utilisée:

from time import time

class Camera(object):
    def __init__(self):
        self.frames = [open(f + '.jpg', 'rb').read() for f in ['1', '2', '3']]

    def get_frame(self):
        return self.frames[int(time()) % 3]

Cette mise en œuvre se lit trois images à partir du disque appelé 1.jpg2.jpget 3.jpgpuis les renvoie un après l' autre à plusieurs reprises, à raison d'une image par seconde. La get_frame()méthode utilise l'heure actuelle en secondes pour déterminer laquelle des trois images renvoyer à un moment donné. Assez simple, non?

Pour exécuter cette caméra émulée, j'avais besoin de créer les trois cadres. En utilisant gimp, j'ai créé les images suivantes:

Cadre 1 Cadre 2 Cadre 3

Parce que la caméra est émulée, cette application fonctionne sur n'importe quel environnement, vous pouvez donc l'exécuter dès maintenant! J'ai cette application prête à être utilisée sur GitHub . Si vous êtes familier avec, gitvous pouvez le cloner avec la commande suivante:

$ git clone https://github.com/miguelgrinberg/flask-video-streaming.git

Si vous préférez le télécharger, vous pouvez obtenir un fichier zip ici .

Une fois l'application installée, créez un environnement virtuel et installez Flask dedans. Ensuite, vous pouvez exécuter l'application comme suit:

$ python app.py

Après avoir démarré l'application, entrez http://localhost:5000dans votre navigateur Web et vous verrez le flux vidéo émulé lire les images 1, 2 et 3 encore et encore. Assez cool, non?

Une fois que tout fonctionnait, j'ai allumé mon Raspberry Pi avec son module de caméra et implémenté une nouvelle Cameraclasse qui convertit le Pi en un serveur de streaming vidéo, en utilisant le picamerapackage pour contrôler le matériel. Je ne parlerai pas de cette implémentation de caméra ici, mais vous pouvez la trouver dans le code source dans le fichier camera_pi.py.

Si vous avez un Raspberry Pi et un module de caméra, vous pouvez modifier app.pypour importer la Cameraclasse de ce module, puis vous pourrez diffuser en direct la caméra Pi, comme je le fais dans la capture d'écran suivante:

Cadre 1

Si vous souhaitez faire fonctionner cette application de streaming avec une caméra différente, il vous suffit d'écrire une autre implémentation de la Cameraclasse. Si vous finissez par en écrire un, je vous serais reconnaissant de le contribuer à mon projet GitHub.

Limitations de la diffusion en continu

Lorsque l'application Flask traite des requêtes régulières, le cycle de requête est court. Le web worker reçoit la requête, appelle la fonction de gestionnaire et renvoie finalement la réponse. Une fois la réponse renvoyée au client, le travailleur est libre et prêt à répondre à une autre demande.

Lorsqu'une demande utilisant le streaming est reçue, le worker reste attaché au client pendant toute la durée du flux. Lorsqu'il travaille avec des flux longs et sans fin tels qu'un flux vidéo d'une caméra, un collaborateur reste verrouillé sur le client jusqu'à ce que le client se déconnecte. Cela signifie en fait qu'à moins que des mesures spécifiques ne soient prises, l'application ne peut servir que autant de clients qu'il y a de web workers. Lorsque vous travaillez avec l'application Flask en mode débogage, cela signifie qu'un seul, vous ne pourrez donc pas connecter une deuxième fenêtre de navigateur pour regarder le flux à partir de deux endroits en même temps.

Il existe des moyens de surmonter cette limitation importante. La meilleure solution à mon avis est d'utiliser un serveur Web basé sur coroutine tel que gevent , que Flask prend entièrement en charge. Avec l'utilisation de coroutines, gevent est capable de gérer plusieurs clients sur un seul thread de travail, car gevent modifie les fonctions d'E / S Python pour émettre des commutateurs de contexte si nécessaire.

Conclusion

Au cas où vous l'auriez manqué ci-dessus, le code qui prend en charge cet article est ce référentiel GitHub: https://github.com/miguelgrinberg/flask-video-streaming/tree/v1 . Vous trouverez ici une implémentation générique du streaming vidéo qui ne nécessite pas de caméra, ainsi qu'une implémentation pour le module de caméra Raspberry Pi. Cet article de suivi décrit certaines améliorations que j'ai apportées après la publication initiale de cet article.

J'espère que cet article vous éclairera sur le thème du streaming. Je me suis concentré sur le streaming vidéo car c'est un domaine dans lequel j'ai une certaine expérience, mais le streaming a beaucoup plus d'utilisations que la vidéo. Par exemple, cette technique peut être utilisée pour maintenir une connexion entre le client et le serveur en vie pendant une longue période, permettant au serveur de pousser de nouvelles informations dès qu'elles deviennent disponibles. De nos jours, le protocole Web Socket est un moyen plus efficace d'y parvenir, mais Web Socket est assez récent et ne fonctionne que dans les navigateurs modernes, tandis que le streaming fonctionnera sur à peu près tous les navigateurs auxquels vous pouvez penser.

Si vous avez des questions, n'hésitez pas à les écrire ci-dessous. Je prévois de continuer à documenter davantage les sujets Flask peu connus, donc j'espère que vous vous connecterez avec moi d'une manière ou d'une autre pour savoir quand d'autres articles seront publiés. J'espère vous voir dans le prochain!


Commentaires

Posts les plus consultés de ce blog

Comment fonctionne l'optimise d'Adam

RESEAU DE NEURONE CONVOLUTIF

Comment utiliser les diagrammes PlantUML dans Visual Studio Code pour Windows 10