PaPeRo i のカメラでQRコードを読む

 PaPeRo i のカメラでQRコードが読めるか試してみました。PaPeRo i 本体では難しそうなのでRaspberry Pi でアプリを動かす事にします。
ちょっと古くなってしまいましたが手持ちのRaspberry Pi 3B+を使用しました。OSはRaspberry Pi OS busterをapt upgradeして最新にして使用しました(2020/9時点)。

必要ライブラリ

 QRコードの読み取りにはzbarを使用します。Pythonから使えるようにするラッパーはいくつかあるようなのですがzbarlightを使いました。

$ sudo apt update
$ sudo apt upgrade
$ sudo reboot
$ sudo apt install libzbar0 libzbar-dev
$ sudo pip3 install pillow
$ sudo pip3 install zbarlight

 実は最初OpenCVを試したのですが、インストールは大変だったのにQRコードのVersion 5(37×37、誤り訂正レベルLで最大バイナリ106バイト)までしか読み取れなかったためあきらめました。OpenCVは他にも使い道があるので寄り道になりますがpip3コマンドでopencv-pythonパッケージをインストールする手順をご紹介します。これで2020/9時点でRaspberry Pi にopencv-python==4.4.0.42を1時間ほどでインストールできたのですがバージョンが変るとまた変ってしまうかも知れません。

$ # [参考] opencv-pythonのインストール手順 (※使いません)
$ sudo apt install libavutil56 libcairo-gobject2 libgtk-3-0 libqtgui4 libpango-1.0-0 libqtcore4 libavcodec58 libcairo2 libswscale5 libtiff5 \
    libqt4-test libatk1.0-0 libavformat58 libgdk-pixbuf2.0-0 libilmbase23 libjasper1 libopenexr23 libpangocairo-1.0-0 libwebp6
$ sudo apt install ninja-build libatlas-base-dev
$ sudo apt install python3-numpy python-numpy
$ sudo apt install clang
$ sudo su
# export PYTHON3_EXECUTABLE="$(which python3)"
# export PYTHON3_INCLUDE_DIR="$(python3 -c 'from distutils.sysconfig import get_python_inc; print(get_python_inc())')"
# export PYTHON3_NUMPY_INCLUDE_DIRS="$(python3 -c 'import os, numpy.distutils; print(os.pathsep.join(numpy.distutils.misc_util.get_numpy_include_dirs()))')"
# export PYTHON3_LIBRARY="$(python3 -c 'import distutils.sysconfig as sysconfig; print(sysconfig.get_config_var("LIBDIR") + "/" + sysconfig.get_config_var("LDLIBRARY"))')"
# export PYTHON2_EXECUTABLE="$(which python2)"
# export PYTHON2_INCLUDE_DIR="$(python2 -c 'from distutils.sysconfig import get_python_inc; print(get_python_inc())')"
# export PYTHON2_NUMPY_INCLUDE_DIRS="$(python2 -c 'import os, numpy.distutils; print(os.pathsep.join(numpy.distutils.misc_util.get_numpy_include_dirs()))')"
# export PYTHON2_LIBRARY="$(python2 -c 'import distutils.sysconfig as sysconfig; print(sysconfig.get_config_var("LIBDIR") + "/" + sysconfig.get_config_var("LDLIBRARY"))')"
# export CC=/usr/bin/clang
# export CXX=/usr/bin/clang++
# pip3 install opencv-python

画像の撮影

 カメラはSXGAカメラの方を使うことにします。PaPeRo i でSXGAカメラで撮影してJPEGファイルに保存するにはpypaperoのオブジェクトpaperoとすると、

papero.send_take_picture('JPEG', camera='SXGA', filename='test.jpg')

とします。test.jpgは/tmp下に作られます。

画像の転送

 撮影した画像ファイルをPaPeRo i 本体からRaspberry Pi に転送するにはparamiko/scpパッケージを使用します。

$ sudo pip3 install paramiko
$ sudo pip3 install scp

これらのパッケージでscpコマンドと同じ事がpythonコードでできる様になります。Raspberry Pi 側からPaPeRo i 本体のファイルをgetするコードは下記の様になります。

import paramiko
import scp

with paramiko.SSHClient() as cli:
    cli.set_missing_host_key_policy(paramiko.AutoAddPolicy())
    cli.connect(hostname=paperoip, port=22,
                username='cli', password=password)
    with scp.SCPClient(cli.get_transport()) as scpc:
        scpc.get(remote_path='/tmp/test.jpg',
                 local_path='/tmp/test.jpg')

QRコードの読み取り

 zbar/zbarlightでQRコードを読み取るには、

from PIL import  Image
import zbarlight

with open('/tmp/test.png', 'rb') as fh:
    image = Image.open(fh)
    image.load()
    codes = zbarlight.scan_codes(['qrcode'], image)
    if codes is not None and 0 < len(codes):
        c0 = codes[0]
        txt = c0.decode('utf-8')
        print('len={}: text={}'.format(len(txt), txt))

とします。
 参考までにopencv-pythonを使用する場合には、

# [参考] opencv-pythonでQRコードを読む場合(※使いません)
import cv2

img = cv2.imread('/tmp/test.jpg', cv2.IMREAD_GRAYSCALE)
qr = cv2.QRCodeDetector()
txt, p, s = qr.detectAndDecode(img)
print('len={}: text={}'.format(len(txt), txt))

とします。

QRコードを読めたら発話するサンプルアプリ

 座布団ボタンで撮影してQRコードを読み取り、読み取れたら発話するサンプルアプリを作ってみました。pypaperoのラッパークラスstpypaperoを使用しています。

import os
import sys
import time
from logging import (getLogger, Formatter, debug, info, warn, error, critical,
                     DEBUG, INFO, WARN, ERROR, CRITICAL, basicConfig)

import paramiko
import scp
from PIL import  Image
import zbarlight

import pypapero
from stpypapero import StPyPaperoThread

logger = getLogger(__name__)


class App1(StPyPaperoThread):
    def initialize(self):
        # in_initから開始する
        self.handler = self.in_init
        self.picture_format = 'JPEG'
        self.camera = 'SXGA'
        #self.camera = 'VGA'
        self.picture_fn = 'test.jpg'
        self.picture_local = '/tmp/{}'.format(self.picture_fn)

    def in_init(self, name, msg):
        # 初期状態
        if name == 'Ready':
            # 準備完了
            logger.info('{}'.format(name))
            self.trans_state(self.in_idle)

    def in_idle(self, name, msg):
        # アイドル状態
        if name == 'detectButton':
            val = msg.get('Status')
            if  val == 'C':
                self.take_picture()
        elif name == 'takePictureRes':
            self.picture_wait = False
            ret = msg.get('Return')
            if ret != 0:
                logger.error('{}: res={}'.format(name, ret))
                return
            self.scp_jpg()
            self.detect_qr()

    def take_picture(self):
        self.papero.send_take_picture(
            self.picture_format,
            filename=self.picture_fn,
            camera=self.camera,
        )

    def scp_jpg(self):
        paperoip = '192.168.5.1'
        picture_remote = '/tmp/{}'.format(self.picture_fn)
        picture_local = self.picture_local
        logger.info('scp. paperoip={} remote={} local={}'.format(
            paperoip, picture_remote, picture_local
        ))
        try:
            with paramiko.SSHClient() as cli:
                cli.set_missing_host_key_policy(paramiko.AutoAddPolicy())
                cli.connect(hostname=paperoip, port=22,
                            username='cli', password='1234')
                with scp.SCPClient(cli.get_transport()) as scpc:
                    scpc.get(remote_path=picture_remote,
                             local_path=picture_local)
                    logger.info('got by scp: {}'.format(picture_local))
                    return True
        except Exception as e:
            logger.error(e)
            return False

    def detect_qr(self):
        logger.info('start')
        try:
            with open(self.picture_local, 'rb') as fh:
                image = Image.open(fh)
                image.load()
                codes = zbarlight.scan_codes(['qrcode'], image)
                if codes is not None and 0 < len(codes):
                    c0 = codes[0]
                    txt = c0.decode('utf-8')
                    if txt is not None and 0 < len(txt):
                        logger.info('QRデータ: {}'.format(txt))
                        # self.speech(txt)
                        self.speech('{}文字読めました'.format(len(txt)))
                else:
                    logger.info('QRコード読込み失敗')
        except Exception as e:
            logger.error(e)


def main():
    logger.info('start.')

    simid, simname, papeurl = pypapero.get_params_from_commandline(sys.argv)
    # ipaddr = 'localhost'
    if simid == '' and papeurl == '':
        papeurl = 'ws://192.168.5.1:8088/papero'
    try:
        app1 = App1(simid='', simname='', url=papeurl)
        papero = app1.papero
        if papero.errOccurred != 0:
            logger.error('errOccurred: {}'.format(papero.errOccurred))
            return
        app1.start()
    logger.info('end.')


if __name__ == "__main__":
    loglevel = INFO
    logfmt = '%(asctime)s %(levelname)s %(name)s.%(funcName)s %(message)s'
    basicConfig(level=loglevel, format=logfmt)
    main()

動作結果

 PaPeRo i のカメラの焦点距離は40cm~∞なので普通の印刷されたサイズのQRコードを読み取るのは困難です。そこで4.7インチのスマホ画面いっぱいにQRコード(誤り訂正レベルL)を表示しQRコードを色々変えて読み取れるか試したところ、下表の様な結果になりました。

Version Module 誤り訂正レベルLのときのバイナリバイト数 実用可能性
1 ~ 8 21×21 ~ 49×49 17 ~ 192 工夫次第で実用可能?
9 ~ 18 53×53 ~ 89×89 230 ~ 718 読み取れる事はあるが実用は無理そう
19 ~ 40 93×93 ~ 177×177 792 ~ 2953 読み取れない

(QRコードについて参考)

読み取りにかかる時間は約1.7秒(Version=8)で、内訳は撮影約0.3秒+転送約1.1秒+QRコード読み取り約0.3秒となってしました。またカメラの焦点距離は40cm~∞ですが、読み取れる率が高い距離はVersionによらず14cm~17cmあたりのように思われました。
なお、
・スマホを手で持ったままだと画像がぶれてしまうことが多い
・距離にシビア
・背景の明るさ次第でスマホ画面が明るすぎてつぶれたり真っ黒に写ったりする
といったことがあるため、手でかざすような使い方は難しく、背景のスクリーンやスマホを置く台を設置してそこに置いて貰って読み取る、といった工夫が必要そうです。


0