あっきぃ日誌

鉄道ブログのような技術系ブログのようななにか

Picamera2ライブラリのMJPEGストリーミングサーバーをためす

11日目です。

adventar.org

Picamera2ライブラリのMJPEGストリーミングサーバーをためす

昨日のメダカメラを触っていて思い出しましたが、メダカメラで使っているPicameraライブラリのMJPEGストリーミングサーバーをPicamera2に移行できるかの調査をしたいのでした。

おさらいをすると、Raspberry Pi OS Bullseyeではカメラ関係の技術スタックがlibcameraに移行した影響で、Picameraライブラリはlibcameraとは組み合わせ不可になりました(技術スタックをLegacyに戻すことは可能)。

akkiesoft.hatenablog.jp

libcameraに対応したPicamera2がRaspberry Pi公式で開発されることになり、2月にはプレビューリリースが、直近では0.3.7のベータリリース6まで開発が進んでいます。

akkiesoft.hatenablog.jp

Picamera2は大量のサンプルが用意されており、Picameraからの移行を想定してか、同じ用途のサンプルも整備されています。

github.com

サンプルを見比べる

Picamera2のMJPEGストリーミングサーバーのスクリプトはこちら。

github.com

Picameraのスクリプトはこちら。

github.com

Picamera2のスクリプトはPicameraのものをベースに開発されているらしく、パット見の差分は結構少ないです。実際のdiffも量は多いものの、いくつかの処理がなくなったりインデントが上がったりしている程度に見えます。

--- /Users/akkie/Desktop/web_streaming.py	2022-12-11 12:54:14
+++ /Users/akkie/Desktop/mjpeg_server.py	2022-12-11 12:54:12
@@ -1,39 +1,43 @@
+#!/usr/bin/python3
+
+# Mostly copied from https://picamera.readthedocs.io/en/release-1.13/recipes2.html
+# Run this script, then point a web browser at http:<this-ip-address>:8000
+# Note: needs simplejpeg to be installed (pip3 install simplejpeg).
+
 import io
-import picamera
 import logging
 import socketserver
-from threading import Condition
 from http import server
+from threading import Condition, Thread
 
-PAGE="""\
+from picamera2 import Picamera2
+from picamera2.encoders import JpegEncoder
+from picamera2.outputs import FileOutput
+
+PAGE = """\
 <html>
 <head>
-<title>picamera MJPEG streaming demo</title>
+<title>picamera2 MJPEG streaming demo</title>
 </head>
 <body>
-<h1>PiCamera MJPEG Streaming Demo</h1>
+<h1>Picamera2 MJPEG Streaming Demo</h1>
 <img src="stream.mjpg" width="640" height="480" />
 </body>
 </html>
 """
 
# ベースになるクラスが変わった模様
-class StreamingOutput(object):
+
+class StreamingOutput(io.BufferedIOBase):
     def __init__(self):
         self.frame = None
-        self.buffer = io.BytesIO()
         self.condition = Condition()
 
     def write(self, buf):
# bufの開始のデータを見なくて良くなったのか、インデントが上がっている
-        if buf.startswith(b'\xff\xd8'):
-            # New frame, copy the existing buffer's content and notify all
-            # clients it's available
-            self.buffer.truncate()
-            with self.condition:
-                self.frame = self.buffer.getvalue()
-                self.condition.notify_all()
-            self.buffer.seek(0)
-        return self.buffer.write(buf)
+        with self.condition:
+            self.frame = buf
+            self.condition.notify_all()
 
+
 class StreamingHandler(server.BaseHTTPRequestHandler):
     def do_GET(self):
         if self.path == '/':
@@ -73,16 +77,20 @@
             self.send_error(404)
             self.end_headers()
 
+
 class StreamingServer(socketserver.ThreadingMixIn, server.HTTPServer):
     allow_reuse_address = True
     daemon_threads = True
 
# withぶんを使わないでインデントを上げたみたいなノリ
-with picamera.PiCamera(resolution='640x480', framerate=24) as camera:
-    output = StreamingOutput()
-    camera.start_recording(output, format='mjpeg')
-    try:
-        address = ('', 8000)
-        server = StreamingServer(address, StreamingHandler)
-        server.serve_forever()
-    finally:
-        camera.stop_recording()
+
+picam2 = Picamera2()
+picam2.configure(picam2.create_video_configuration(main={"size": (640, 480)}))
+output = StreamingOutput()
+picam2.start_recording(JpegEncoder(), FileOutput(output))
+
+try:
+    address = ('', 8000)
+    server = StreamingServer(address, StreamingHandler)
+    server.serve_forever()
+finally:
+    picam2.stop_recording()

picamera2のフレームレートのデフォルト値は見てませんが、picameraでframerate=24を指定していたところをpicamera2で実装する場合は次のようになります。

picam2.configure(picam2.create_video_configuration(main={"size": (640, 480)},controll={"FrameRate": 24.0}))

以上から、スクリプトの移行自体はわりと苦労せずにサクっとできそうです。

動かしてみると……

メダカメラのスクリプトをPicamera2用に変更したバージョンを作って、Bullseye環境で実際に動かしてみました。現在のメダカメラはPi3Bで動かしているので、Bullseye環境も同様にPi3Bを使いました。動作自体はごく普通に、カメラの映像がMJPEGで配信されていて問題はなさそうでしたが、ふと気になってtopコマンドで負荷を覗くと、CPU使用率に差があることに気が付きました。

左のmedaka2.localが本番用、右のmedaka.localがテスト環境です。以下はまだだれもストリーミング画像を開いていない状態。左のPicamera環境が控えめなのに対して、右のPicamera2環境の負荷が高めなのがわかります。おそらく左環境ではGPU側で処理できているのに対して、右側はCPUで処理しているからではないかと推測します。

次にストリーミング画像を開いて少し置いた状態。左も右も少し負荷が上がりましたが、差はとくに変わらない気がしました。また、負荷に応じてCPUの温度が上昇しているのもわかります。なお、左の環境はヒートシンクなし、右の環境はヒートシンクありで、環境差がある点に注意してください。


うーん、まだ待ったほうが良い?

Pi3BやPi4Bで動かすぶんにはCPUで処理しても問題ないと判断されているのか、どこかしらの処理でlibcameraからGPUを使うための実装がまだなのか。いまいち想像がつきませんが、どちらにせよせっかく今までGPU側で処理できていたっぽい部分がCPU側で処理されているのは微妙な印象です。

じつは昨日までPiZero Wでメダカメラを動かしていて、若干カクつくので3Bに入れ替えたのですが、その浮いたPiZero WでPicamera2環境を動かしたら、だいぶ厳しい感じでした。映像も2秒遅れとかガックガクとかで、若干カクつくどころではありませんでした。ぐぬぬ

今後の開発でGPU処理に変われば良いでしょうが、そうでなければちょっと厳しいかなあという印象です。ディスプレイ出力周りの変更もそうですが、プロプライエタリな実装をオープンな実装に移行することが必ずしも便利になるばかりではないみたいなものを感じますね……。

まとめ

MJPEGストリーミングサーバーのスクリプトの移行自体は簡単でしたが、動かしてみると負荷の面でちょっと不安がありそうでした。今は無理をせずLegacyに切り替えてPicameraの利用を継続しつつ、動向をウォッチしておこうかなあと思います。Issueで質問とかしたほうが良い気もしつつ、それは面倒なのでやめておきたい(へたれ)