スマートフォン・ジン | Smartphone-Zine

引っ越し先→ https://smartphone-zine.com/

【ドラレコ】バックミラーみたいに出来ないか試してみる。

pygameを使えば、カメラの画像を取得してCUIの画面(フレームバッファ)に書き込めないか・・・

Raspberry Piのタッチパネル対応アプリ開発TIPS - karaage. [からあげ]

を試してみる。

pip3 install pygame

タッチディスプレイも使いたかったのですが、残念ながら、タッチディスプレイをCUIで使う方法がわわかりません。

evtest - 入力デバイスのイベントモニタおよびクエリツール

sudo apt install evtest

表示してみましょう。

$ sudo evtest 
No device specified, trying to scan all of /dev/input/event*
Available devices:
/dev/input/event0:  CS-USB-IMX307: UVC Camera
/dev/input/event1:  Mitsumi Electric Apple Extended USB Keyboard
/dev/input/event2:  Mitsumi Electric Apple Extended USB Keyboard System Control
/dev/input/event3:  Mitsumi Electric Apple Extended USB Keyboard Consumer Control
/dev/input/event4:  CS-USB-IMX307: UVC Camera
/dev/input/event5:  WaveShare WaveShare
Select the device event number [0-5]: 

今回のディスプレイのメーカ名が「/dev/input/event5: WaveShare WaveShare」として表示されています。どうもこのevent5ってのが怪しい。

Select the device event number [0-5]: 5 [enter]

とすると、イベントが表示されます。タッチパネルを触ると反応がありました!!やはり「/dev/input/event5」がタッチパネルのようです!!

ただし、このevent5と限らず、再起動すると「/dev/input/event0: WaveShare WaveShare」となりました。

$ evtest 
No device specified, trying to scan all of /dev/input/event*
Not running as root, no devices may be available.
Available devices:
/dev/input/event0:  WaveShare WaveShare
/dev/input/event1:  CS-USB-IMX307: UVC Camera
/dev/input/event2:  CS-USB-IMX307: UVC Camera

うーん、pygameもうちょっと勉強しないとわからないかも。

ffmpeg使ってカメラの画像をリアルタイムに画面に表示できるか

v4l2-ctl -v width=1920,height=1080,pixelformat=H264 --device /dev/video0
v4l2-ctl -p 30 --device /dev/video0
v4l2-ctl --device /dev/video0 --stream-mmap=0 --stream-to=- | ffmpeg -y -i pipe:0 -pix_fmt bgra -s 1280x400 -f fbdev /dev/fb0

出るけど、なんかぼやけてる。

ffmpegだけならどうか?

ffmpeg -y -i /dev/video0 -input_format h264 -pix_fmt bgra -s 1280x400 -f fbdev /dev/fb0
Input #0, video4linux2,v4l2, from '/dev/video0':
  Duration: N/A, start: 6311.371615, bitrate: 110592 kb/s
  Stream #0:0: Video: rawvideo (YUY2 / 0x32595559), yuyv422, 640x360, 110592 kb/s, 30 fps, 30 tbr, 1000k tbn
Stream mapping:
  Stream #0:0 -> #0:0 (rawvideo (native) -> rawvideo (native))
Press [q] to stop, [?] for help
Output #0, fbdev, to '/dev/fb0':
  Metadata:
    encoder         : Lavf59.16.100
  Stream #0:0: Video: rawvideo (BGRA / 0x41524742), bgra(pc, gbr/unknown/unknown, progressive), 1280x400, q=2-31, 491520 kb/s, 30 fps, 30 tbn
    Metadata:
      encoder         : Lavc59.18.100 rawvideo

-input_format h264を指定していますが、全く効果ない感じです。相変わらずおかしい動きです。 しかし、yuyv422 640x360の入力ならばパフォーマンスは十分、ほぼリアルタイムでカメラ画像を画面に表示出来ています。

どうも、設定が悪かったようです。この構文で h264にも反応するようになりました。-f v4l2 を明示的に指定すればOKです。

ffmpeg \
-f v4l2 -input_format h264 -video_size 640x360 -framerate 30 -i /dev/video0 \
-f fbdev -s 1280x400 -vf hflip -pix_fmt bgra /dev/fb0

ただし、h264 だと遅延が1秒くらいはある感じで、使い物になりません。

 フォーマットをyuyv422にすればほとんど遅延なし。(このカメラのyuyv422対応解像度は640x360のみ)

ffmpeg \
-f v4l2 -input_format yuyv422 -video_size 640x360 -framerate 30 -i /dev/video0 \
-f fbdev -s 1280x400 -vf hflip -pix_fmt bgra /dev/fb0

解像度をmmpeg最大(1920x1080)にしてみる。

ffmpeg \
-f v4l2 -input_format mjpeg -video_size 1920x1080 -framerate 30 -i /dev/video0 \
-f fbdev -s 1280x400 -vf hflip -pix_fmt bgra /dev/fb0

これでは遅延と画面の崩れが酷く使い物にならない。

mmpegによるカメラのディスプレイ表示は、

  • h264 640x360 遅延と画面の崩れ
  • mmpeg 1920x1080 遅延と画面の崩れ
  • yuyv422 640x360 遅延、画面の崩れもなく良好。

このような結果になった。

色々調べた結果、「Corrupt JPEG data: premature end of data segment」というエラーが起きている。 これは、処理能力が足りず、JPEGを受信出来ていないようだ。ラズパイの限界なのかもしれない。

ffmpeg -y -i /dev/video0 -input_format h264 -pix_fmt bgra -s 1280x400 -f fbdev /dev/fb0

しかし、実際には、カメラはドラレコの為に1920x1080、H.264で録画中なわけなので、録画しつつ画面に表示する方法を模索しないといけない。

どうしたものか?

OpenCVはどうか?

OpenCVでカメラ画像を取得して画面に表示してはどうだろうか?

>>> import cv2
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
ModuleNotFoundError: No module named 'cv2'

おっと、opencvがインストールされていないようだ。インストールしましょう。

pip install --upgrade pip

バージョン指定しないと、インストールに失敗しました。

この問題を回避するため、バージョン指定でインストールが必要です。

sudo pip3 --default-timeout=1000 install opencv-contrib-python==3.4.14.51

しかし、import cv2でエラーになります。

$ python3
Python 3.9.2 (default, Mar 12 2021, 04:06:34) 
[GCC 10.2.1 20210110] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> import cv2
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/usr/local/lib/python3.9/dist-packages/cv2/__init__.py", line 5, in <module>
    from .cv2 import *
ImportError: libhdf5_serial.so.103: cannot open shared object file: No such file or directory

依存関係ライブラリのインストール

$ sudo apt install libhdf5-dev libhdf5-serial-dev libhdf5-103
$ sudo apt install libatlas-base-dev
$ sudo apt install libjasper-dev

import cv2して確認してみる。

$ python3
Python 3.9.2 (default, Mar 12 2021, 04:06:34) 
[GCC 10.2.1 20210110] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> import cv2
RuntimeError: module compiled against API version 0xe but this version of numpy is 0xd
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/usr/local/lib/python3.9/dist-packages/cv2/__init__.py", line 5, in <module>
    from .cv2 import *
ImportError: numpy.core.multiarray failed to import
>>> 

エラーになった。今度はnumpyのエラーだ。numpyをアップグレードする。

pip3 install numpy --upgrade

これでエラー解消された。

仮想ビデオデバイス

sudo apt install v4l2loopback-dkms

2つのデバイスを作成したい場合は次のようにします。

pi@raspberrypi:~ $ v4l2-ctl --list-devices
bcm2835-codec-decode (platform:bcm2835-codec):
    /dev/video10
    /dev/video11
    /dev/video12
    /dev/video18
    /dev/video31
    /dev/media2

bcm2835-isp (platform:bcm2835-isp):
    /dev/video13
    /dev/video14
    /dev/video15
    /dev/video16
    /dev/video20
    /dev/video21
    /dev/video22
    /dev/video23
    /dev/media0
    /dev/media1

CS-USB-IMX307: UVC Camera (usb-3f980000.usb-1.1):
    /dev/video2
    /dev/video3
    /dev/media4

CS-USB-IMX307: UVC Camera (usb-3f980000.usb-1.4):
    /dev/video0
    /dev/video1
    /dev/media3

42,43の2つの仮想デバイスを追加します。

pi@raspberrypi:~ $ sudo modprobe v4l2loopback video_nr=42,43

追加されたか確認します。

pi@raspberrypi:~ $ v4l2-ctl --list-devices
bcm2835-codec-decode (platform:bcm2835-codec):
    /dev/video10
    /dev/video11
    /dev/video12
    /dev/video18
    /dev/video31
    /dev/media2

bcm2835-isp (platform:bcm2835-isp):
    /dev/video13
    /dev/video14
    /dev/video15
    /dev/video16
    /dev/video20
    /dev/video21
    /dev/video22
    /dev/video23
    /dev/media0
    /dev/media1

Dummy video device (0x0000) (platform:v4l2loopback-000):
    /dev/video42

Dummy video device (0x0001) (platform:v4l2loopback-001):
    /dev/video43

CS-USB-IMX307: UVC Camera (usb-3f980000.usb-1.1):
    /dev/video2
    /dev/video3
    /dev/media4

CS-USB-IMX307: UVC Camera (usb-3f980000.usb-1.4):
    /dev/video0
    /dev/video1
    /dev/media3

追加されているのが確認できました。

あとは、再起動時にもこの設定で読み込まれるように設定しておきましょう。

echo "options v4l2loopback video_nr=42,43" | sudo tee -a /etc/modprobe.d/v4l2loopback.conf
echo v4l2loopback | sudo tee -a /etc/modules-load.d/modules.conf
sudo systemctl restart systemd-modules-load.service
sudo reboot

ffmpegを使って、データをvideo42, video43に流し込みます。

ffmpeg \
-f v4l2 -input_format h264 -video_size 1920x1080 -framerate 30 -i /dev/video0 \
-f v4l2 -c:v copy /dev/video42 -f v4l2 -c:v copy /dev/video43

video42からデータを取得して、画面に表示してみる。

ffmpeg \
-f v4l2 -i /dev/video42 \
-f fbdev -vf hflip -s 1280x400 -pix_fmt bgra /dev/fb0

できた。しかし、やはり、 h264 1920x1080 のデータはリアルタイム表示には向かない。カクカクしている。

同じく、video43をデータ保存してみる。

ffmpeg \
-y \
-f v4l2 -i /dev/video43 \
-c:v copy testshot.mp4

出来た。

高解像度 → カクカクしてディスプレイ表示は出来ない。

低解像度 → ディスプレイ表示OK。しかしドラレコの録画としては画質が低すぎて事故の際の参考にしかならない。(もしもの時に相手のナンバーとか判別つかない)

うーん困った。高解像度で保存しつつ、ディスプレイ表示出来る方法を模索しなければならない。

案1:もう一台カメラを装着すれば万事解決。

案2:ffmpgeではなくPythonで自前のコードを書いてみたらどうか?

video42とvideo43に流す解像度をかえられないか。かえられる。しかし、高解像度だと間に合わない。

ffmpeg \ -f v4l2 -input_format mjpeg -video_size 1920x1080 -framerate 30 -i /dev/video0 \ -f v4l2 -c:v mjpeg -s 640x360 /dev/video42 \ -f v4l2 -c:v copy /dev/video43

1280x720ではどうか。

h264はやはり画像がぼやける。

ffmpeg \ -f v4l2 -input_format h264 -video_size 1280x720 -framerate 30 -i /dev/video0 \ -f v4l2 -c:v copy /dev/video42 \ -f v4l2 -c:v copy /dev/video43

mjpegは優秀だがたまに画面が崩れる。

ffmpeg \
-f v4l2 -input_format mjpeg -video_size 1280x720 -framerate 30 -i /dev/video0 \
-f v4l2 -c:v copy /dev/video42 \
-f v4l2 -c:v copy /dev/video43

ffmpeg \
-f v4l2 -input_format mjpeg -video_size 1280x720 -framerate 30 -i /dev/video2 \
-f v4l2 -c:v copy /dev/video42 \
-f v4l2 -c:v copy /dev/video43

色々試して、一番安定しているのがこれ。

 mjpegで1280x720に解像度を抑えめにする。

ffmpeg \
-re -f v4l2 -input_format mjpeg -video_size 1280x720 -framerate 30 -i /dev/video0 \
-f v4l2 -c:v copy /dev/video42 \
-f v4l2 -c:v copy /dev/video43

電子バックミラー側は、hflip, fps=15をつける。

  • hflipは左右反転して鏡像をつくる。
  • ミラーとしてはfps=15くらいで十分
ffmpeg \
-f v4l2 -i /dev/video42 \
-f fbdev -vf "hflip, fps=15" -pix_fmt bgra /dev/fb0

これで、だいぶ落ち着く。ただしmjpegエラーが発生する。

[mjpeg @ 0x1e77890] EOI missing, emulating
[mjpeg @ 0x1e77890] mjpeg_decode_dc: bad vlc: 0:0 (0x1e78f00)
[mjpeg @ 0x1e77890] error dc
[mjpeg @ 0x1e77890] error y=42 x=68
[mjpeg @ 0x1e77890] overread 8

あとは上記エラーがなくなれば実用レベルになりそうなのだが・・・

他のカメラだとどうだろうか。同じくmjpeg 1280x720 30fps出せるはずの、BUFFALO BSWHD06M USB Cameraを繋いで動作確認してみる。

あれ。こちらは一切エラーも起きないでとても安定動作している。うーん、AliExpressで買ったカメラに問題あるのかも。安いカメラだったからなぁ・・・

色々ためそう。今度はv4l2経由の1280x720

v4l2-ctl -v width=1280,height=720,pixelformat=MJPEG --device /dev/video0 v4l2-ctl -p 30 --device /dev/video0 v4l2-ctl --device /dev/video0 --stream-mmap=3 --stream-to=- | ffmpeg -y -i pipe:0 -pix_fmt bgra -s 1280x400 -f fbdev /dev/fb0

これもダメでした。

640x360ならどうか?

ffmpeg \
-re -f v4l2 -input_format mjpeg -video_size 640x360 -framerate 30 -i /dev/video0 \
-f v4l2 -c:v copy /dev/video42 \
-f v4l2 -c:v copy /dev/video43

ffmpeg \
-f v4l2 -i /dev/video42 \
-f fbdev -vf "hflip, fps=15" -pix_fmt bgra /dev/fb0

撮影した動画をコンソールで確認

ffmpeg -re -i out.mp4 -c:v rawvideo -pix_fmt bgra -f fbdev /dev/fb0

OpenCVでカメラ画像を画面に表示

これもうまくいかない。同じように画面が時折崩れる。

そこで、このカメラを、パソコンに繋いでみよう。

  • Macに繋いで、QuickTime Playerで表示してみる。→問題ない。
  • ノートパソコン上のUbuntuで、guvcviewで表示してみる→問題ない。

  • Raspberry pi 4 で、uvcviewで表示してみる→画像がときおり崩れる。JPEGエラー。

  • Raspberry pi Zero 2 W で、uvcviewで表示してみる→画像がときおり崩れる。JPEGエラー。

つまり、PCでは大丈夫だが、ラズパイだと崩れる。ラズパイでも解像度が低ければ崩れない。

これらから、単純に「ラズパイのCPU・GPUでは、能力的にカメラの高解像度データをリアルタイムに画面に表示できない」と結論付ける事ができる。

というわけで、 「ラズパイゼロ2で、電子バックミラー(高解像度)は無理、解像度落とせばOK」 という結果となりました。 ただ、ドラレコとしてはバックモニターの解像度を落とすわけにはいかないです。もしもの事故の時の証拠がメインの目的であり、電子バックミラーはオマケ機能なのです。

電子バックミラーの選択肢としては

  • PCクラスのCPUを用意する。
  • 電子バックミラー用にもう1台カメラ用意する → 割と現実的かも。ただし3台のカメラの通信量耐えられるか心配ではある。
  • ちらつくけど我慢して使う。→ バックミラーとして致命的なので無理。
  • 潔く諦める。

今回は、ドラレコ作成、バックミラーは目的ではありませんので、一旦見送ってもいいかな。

複数のUSBカメラがある場合、どれが後方カメラなのか判断つかない問題

複数のUSBカメラをさした場合、指した順番でvideo0, video2に割り振られる。

再起動すると、video0, video2が入れ替わる可能性もある。

どちらが前方カメラで、どちらが後方カメラなのかを認識して、後方カメラをディスプレイに映したい(バックミラーなので)

そこで下記の記事を参考に、後方カメラを判定する。

dev.classmethod.jp

私の環境ではデバイス名の箇所がそのままでは動作しなかったので修正して使用している。

vim usbVideoDevice.py
import subprocess

class UsbVideoDevice():
    def __init__(self):
        self.__deviceList = []

        try:
            cmd = 'ls -la /dev/v4l/by-id'
            res = subprocess.check_output(cmd.split())
            by_id = res.decode()
        except:
            return

        try:
            cmd = 'ls -la /dev/v4l/by-path'
            res = subprocess.check_output(cmd.split())
            by_path = res.decode()
        except:
            return

        # ポート番号取得
        for line in by_path.split('\n'):
            if('usb-0' in line):
                tmp = self.__split(line, '-usb-0:1.')
                tmp = self.__split(tmp[1], ':')
                port = int(tmp[0])
                tmp = self.__split(tmp[1], '../../video')
                deviceId = int(tmp[1])
                if deviceId % 2 == 0:
                    #name = deviceNames[str(deviceId)]
                    self.__deviceList.append((deviceId , port))
    
    def __split(self, str, val):
        tmp = str.split(val)
        if('' in tmp):
            tmp.remove('')
        return tmp

    # 認識しているVideoデバイスの一覧を表示する
    def disp(self):
        for (deviceId, port) in self.__deviceList:
            print("/dev/video{} port:{}".format(deviceId, port))

    # ポート番号(1..)を指定してVideoIDを取得する
    def getId(self, port):
        for (deviceId, p) in self.__deviceList:
            if(p == port):
                return deviceId
        return -1

実行してみる。

$ python3
Python 3.9.2 (default, Mar 12 2021, 04:06:34) 
[GCC 10.2.1 20210110] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> from usbVideoDevice import UsbVideoDevice
>>> usbVideoDevice = UsbVideoDevice()
>>> usbVideoDevice.disp()
/dev/video2 port:1
/dev/video0 port:4
>>> print(usbVideoDevice.getId(1))
2
>>> print(usbVideoDevice.getId(4))
0
>>> 

getIdでポート番号(USBの刺さっている場所)を渡せば、今そのポート番号に接続されているカメラの番号を返してくれている。 これで、ポート番号でカメラ指定出来るようになった。

今日はここまでです。