質問をすることでしか得られない、回答やアドバイスがある。

15分調べてもわからないことは、質問しよう!

ただいまの
回答率

87.92%

Python3 Tkinter Raspberry Piでカメラからバーコードを読もうとしたときに処理速度がだいぶ遅くなった

解決済

回答 1

投稿 編集

  • 評価
  • クリップ 2
  • VIEW 328

score 126

Python3 Tkinter カメラの映像からバーコードやQRコードを読み取りたい
にて作成したソースコードをラズパイで動かしたときに動作が重くなりました。

Windowsで確認した際はラグはなかったのですが、ラズパイで確認するとtkinterのcanvasの映像が体感的に3秒ほど遅れています。
after()の中にprint()にてタイムスタンプ表示を行うと、指定[ms]に対して少し誤差はあるものの、映像表示ほどの遅れはありません。
指定時間[ms]は10[ms]、100[ms]、1000[ms]で試験しましたが、どれも同じくらい遅いです。

canvasの描画内容をコメントアウトしてバーコードの読み取りのみに変更しても、読み取るのに要する時間(バーコードをカメラの前にもっていってから、tkinterのラベルに表示するまでの時間)が2~3秒と長いような気がします。(単にピントが合っておらず、時間がかかっているときもありますが)

画像一枚に対してのバーコード読み取りや描画処理がラズパイには負担が大きく、処理としてついていけないのでしょうか?

ソースコード

Windowsで試験した際はエラー回避のためcv2.VideoCaputure()の第2引数にcv2.CAP_DSHOWを入れてましたが、
ラズパイでやったらエラーになったので、ここでは指定しません。

from datetime import datetime
from PIL import Image, ImageTk
from pyzbar.pyzbar import decode
import cv2
import queue
import threading
import time
import tkinter as tk

decode_cnt = 0
decode_span = 20

class App:
    def __init__(self, win):
        video_source = 0
        self.vcap = cv2.VideoCapture(video_source)
        self.vcap.set(cv2.CAP_PROP_FOURCC, cv2.VideoWriter_fourcc("H", "2", "6", "4"))
        self.vcap.set(cv2.CAP_PROP_FRAME_WIDTH, 720)
        self.vcap.set(cv2.CAP_PROP_FRAME_HEIGHT, 480)
        #self.vcap.set(cv2.CAP_PROP_FPS, 30)
        self.fourcc = self.vcap.get(cv2.CAP_PROP_FOURCC)
        self.width = self.vcap.get(cv2.CAP_PROP_FRAME_WIDTH)
        self.height = self.vcap.get(cv2.CAP_PROP_FRAME_HEIGHT)
        #self.fps = self.vcap.get(cv2.CAP_PROP_FPS)
        print("width: {}".format(self.width))
        print("height: {}".format(self.height))
        self.font = cv2.FONT_HERSHEY_SIMPLEX

        self.label_header = "バーコード:"
        self.barcodeData = ""

        self.queue_to_subthread = queue.Queue()
        self.queue_to_mainthread = queue.Queue()

        self.win = win
        self.win.title("Camera")
        self.thread = threading.Thread(target=self.get_queue, args=(self.queue_to_mainthread,))
        self.thread.start()
        size_str = str(int(self.width + 100)) + "x" + str(int(self.height + 100))
        self.win.geometry(size_str)
        #self.disp_center(self.win, int(self.width) + 100, int(self.height) + 100)
        #self.disp_center(self.win, 720, 480)
        self.win.protocol("WM_DELETE_WINDOW", self.close)
        self.create_view()
        self.bind_event()
        #self.update_by_timer()
        self.canvas.after(15, self.update_by_timer)


    def disp_center(self, win, wx=400, wy=300):
        #win.resizable(0, 0)
        dx = win.winfo_screenwidth()
        dy = win.winfo_screenheight()
        win_size = str(wx) + "x" + str(wy) + \
            "+" + str(int(dx/2 - wx/2)) + "+" + str(int(dy/2 - wy/2))
        win.geometry(win_size)


    def close(self, e=None):
        self.queue_to_subthread.put(["close", None])
        self.thread.join()
        self.win.destroy()
        self.vcap.release()
        #cv2.destroyAllWindows()


    def create_view(self):
        self.win.rowconfigure(0, weight=1)
        self.win.columnconfigure(0, weight=1)

        self.frame = tk.Frame(self.win, relief="sunken", bd=1)
        self.frame.grid(row=0, column=0, sticky="nsew")
        self.frame.rowconfigure(0, weight=1)
        self.frame.columnconfigure(0, weight=1)

        self.canvas = tk.Canvas(self.frame)
        self.canvas.grid(row=0, column=0, sticky="nsew", padx=10, pady=10)

        ret, frm = self.vcap.read()
        #print("ret: {}".format(ret))
        #print("frm: {}".format(frm))
        self.frame = cv2.cvtColor(frm, cv2.COLOR_BGR2RGB)
        self.photo = ImageTk.PhotoImage(image = Image.fromarray(self.frame))
        self.canvas.update()
        cv_w = self.canvas.winfo_reqwidth()
        cv_h = self.canvas.winfo_reqheight()
        self.img_id = self.canvas.create_image(cv_w // 2, cv_h // 2, image=self.photo, anchor="center")


        """
        self.canvas.update()
        cv_w = self.canvas.winfo_width()
        cv_h = self.canvas.winfo_height()

        rec_id = self.canvas.create_rectangle(0, 0, cv_w, cv_h, fill="black")


        # 長方形の座標取得
        rec_pos = self.canvas.coords(rec_id)
        # テキストを描画(位置は適当)
        text_id = self.canvas.create_text(0, 0, text="NO SIGNAL", font=("", 20), fill="white")
        # テキストのサイズ取得
        text_size = self.canvas.bbox(text_id)
        # テキストの座標移動
        rc_x = rec_pos[2] / 2
        rc_y = rec_pos[2] / 3  
        tc_y = text_size[3] / 2
        self.canvas.move(text_id, rc_x - (rec_pos[0] / 2), rc_y - tc_y)
        """

        self.label = tk.Label(self.win)
        self.label["text"] = self.label_header
        self.label.grid(row=1, column=0, sticky="", pady=10)

        self.button = tk.Button(self.win, text="Snapshot", command=self.pushed_button)
        self.button.grid(row=2, column=0, sticky="", pady=10)


    def pushed_button(self):
        self.queue_to_subthread.put(["snapshot", None])


    def update_by_timer(self):
        global decode_cnt, decode_span
        ret, self.frame = self.vcap.read()
        #print("ret: {}".format(ret))
        #print("self.frame: {}".format(self.frame))
        # cv2はBGR、pillow(PIL)はRGB。色の構成順が異なる
        self.frame = cv2.cvtColor(self.frame, cv2.COLOR_BGR2RGB) # フルカラー(色の構成順をRGBへ)
        #self.frame = cv2.cvtColor(self.frame, cv2.COLOR_BGR2GRAY) # グレースケール

        # 2値化(単純な閾値処理)
        # https://axa.biopapyrus.jp/ia/opencv/threshold.html
        #_, self.frame = cv2.threshold(self.frame, 120, 255, cv2.THRESH_BINARY) # 2値化

        # 2値化(適応的閾値処理)
        # https://webcache.googleusercontent.com/search?q=cache:d8CkW7wbZ5AJ:https://algorithm.joho.info/programming/python/opencv-adaptive-thresholding-py/+&cd=2&hl=ja&ct=clnk&gl=jp
        #self.frame = cv2.adaptiveThreshold(self.frame, 255, cv2.ADAPTIVE_THRESH_GAUSSIAN_C, cv2.THRESH_BINARY, 11, 3)



        if ret and decode_cnt % decode_span == 0:
            self.queue_to_subthread.put(["decode", self.frame])

        try:
            msg, d = self.queue_to_mainthread.get_nowait()
            #print("data: \n{}".format(d))
            print(d)
            x,y,w,h = (0,0,0,0)
            if d:
                for barcode in d:
                    x,y,w,h = barcode.rect
                    self.barcodeData = barcode.data.decode("utf-8")


            else:
                self.barcodeData = ""
            self.label["text"] = self.label_header + self.barcodeData
            cv2.rectangle(self.frame,(x,y),(x+w,y+h),(0,0,255),2)
            frame = cv2.putText(self.frame,self.barcodeData,(x,y-10),self.font,.5,(0,0,255),2,cv2.LINE_AA)


        except queue.Empty:
            pass

        self.photo = ImageTk.PhotoImage(image = Image.fromarray(self.frame))
        self.canvas.itemconfig(self.img_id, image=self.photo)
        decode_cnt += 1
        self.canvas.after(15, self.update_by_timer)


    def update_by_resize(self, e):
        self.canvas.itemconfig(self.img_id, image=self.photo, anchor="center")
        cv_w = self.canvas.winfo_width()
        cv_h = self.canvas.winfo_height()
        self.canvas.coords(self.img_id, cv_w // 2, cv_h // 2)
        #pass


    def bind_event(self):
        self.win.bind("<Escape>", self.close)
        self.canvas.bind("<Configure>", self.update_by_resize)


    def get_queue(self, queue_to_mainthread):
        while True:
            msg, data = self.queue_to_subthread.get()
            if msg == "decode":
                d = decode(data)
                if d:
                    queue_to_mainthread.put(["result", d])
                else:
                    queue_to_mainthread.put(["result", None])
            elif msg == "snapshot":
                self.snapshot()
            elif msg == "close":
                break
            else:
                print("----- Undefined msg -----")
                break

    def snapshot(self):
        filename = "/home/pi/デスクトップ/img/frame-" + datetime.now().strftime("%Y-%m-%d-%H-%M-%S_%f") + ".jpg"
        cv2.imwrite(filename,
                     cv2.cvtColor(self.frame, cv2.COLOR_BGR2RGB))
        print(filename)


def main():
    win = tk.Tk()
    App(win)
    win.mainloop()

if __name__ == "__main__":
    main()

ラズパイのバージョン

 $ lsb_release -a
No LSB modules are available.
Distributor ID:    Raspbian
Description:    Raspbian GNU/Linux 9.13 (stretch)
Release:    9.13
Codename:    stretch

パッケージインストール方法等

$ sudo pip3 install opencv-python
$ sudo pip3 install pyzbar

$ sudo pip3 list
opencv-python==4.4.0.42
pyzbar==0.1.8
(他省略)
  • 気になる質問をクリップする

    クリップした質問は、後からいつでもマイページで確認できます。

    またクリップした質問に回答があった際、通知やメールを受け取ることができます。

    クリップを取り消します

  • 良い質問の評価を上げる

    以下のような質問は評価を上げましょう

    • 質問内容が明確
    • 自分も答えを知りたい
    • 質問者以外のユーザにも役立つ

    評価が高い質問は、TOPページの「注目」タブのフィードに表示されやすくなります。

    質問の評価を上げたことを取り消します

  • 評価を下げられる数の上限に達しました

    評価を下げることができません

    • 1日5回まで評価を下げられます
    • 1日に1ユーザに対して2回まで評価を下げられます

    質問の評価を下げる

    teratailでは下記のような質問を「具体的に困っていることがない質問」、「サイトポリシーに違反する質問」と定義し、推奨していません。

    • プログラミングに関係のない質問
    • やってほしいことだけを記載した丸投げの質問
    • 問題・課題が含まれていない質問
    • 意図的に内容が抹消された質問
    • 過去に投稿した質問と同じ内容の質問
    • 広告と受け取られるような投稿

    評価が下がると、TOPページの「アクティブ」「注目」タブのフィードに表示されにくくなります。

    質問の評価を下げたことを取り消します

    この機能は開放されていません

    評価を下げる条件を満たしてません

    評価を下げる理由を選択してください

    詳細な説明はこちら

    上記に当てはまらず、質問内容が明確になっていない質問には「情報の追加・修正依頼」機能からコメントをしてください。

    質問の評価を下げる機能の利用条件

    この機能を利用するためには、以下の事項を行う必要があります。

回答 1

checkベストアンサー

+1

実際に、何処でどのくらい時間が掛かっているかを計測して見て下さい。
時間計測には、time.perf_counter() を使います。

import time

start_time = time.perf_counter()

# 計測したい処理

end_time = time.perf_counter()
print(f"decode() took {end_time - start_time} seconds")

contextmanager にしておくと便利です。


前回のコメントに書きましたが、
バーコードの検出処理を毎フレーム行っているのは、
必要以上の頻度で行っているので、遅延に成り得ます。

基本的な方針としては、
GUIと関係ない処理(バーコードの検出)を、
別スレッドで行う事で改善できるはずですが、

描画更新の頻度に処理が追い付かない場合は、
解像度やフレームレートを落とす事も検討してください。


マルチスレッドでのデータの受け渡しに付いて注意点

  • update_by_timer で self.frame に代入し
  • pushed_button から self.frame を参照。
    同一スレッド内なので問題ありません。
  • snapshot から self.frame の参照
    別スレッドなので、スレッドセーフではありません。

以前のコードでは pushed_button でも read() を行っていたので、
インスタンス変数にして共有する提案をしたと思いますが、

別スレッドからの変数の共有は注意が必要で、
同時アクセスがあった場合に問題になるかどうかを
注意深く調べる必要が出てきます。

解決策としては単純で、
同期キューを使ったメッセージ受け渡しの枠組みは既に出来てるので、
キューに引数として渡す事で解消できますが、

この種の問題は、タイミング次第で問題が起きたり起きなかったりするので、
短期間のテストでは発生しないが、長期間の運用で顕在したりするので注意。

投稿

  • 回答の評価を上げる

    以下のような回答は評価を上げましょう

    • 正しい回答
    • わかりやすい回答
    • ためになる回答

    評価が高い回答ほどページの上位に表示されます。

  • 回答の評価を下げる

    下記のような回答は推奨されていません。

    • 間違っている回答
    • 質問の回答になっていない投稿
    • スパムや攻撃的な表現を用いた投稿

    評価を下げる際はその理由を明確に伝え、適切な回答に修正してもらいましょう。

  • 2021/05/19 13:35

    インスタンス変数で毎フレームputText()に変更しました。
    質問文に記載。
    数秒おきに読み取ります(おそらく設定した頻度)

    全部メインスレッドでやった時と比べてそこまで時間差があるわけではないですが、妥協せざるを得ないですかね。
    あるいは、ラズパイとカメラの組み合わせ以外の方法で読み取るか・・・。

    キャンセル

  • 2021/05/19 13:58 編集

    後は、フレームの描画更新頻度、カメラのフォーマット指定、解像度・FPS を落とすくらいですね。
    self.canvas.after(15, self.update_by_timer) だと FPS=60 (1000ms//60の近似値) 相当なので
    カメラの FPS に合わせてみてはどうでしょう。厳密には、更新間隔 - 処理にかかる時間を設定

    参考: カメラのFPS設定値の所得と実測値 (Raspberry Pi)
    https://ymgsapo.com/2019/04/06/opencv-fps/

    decode() は 15x20 = 300ms(0.3 秒) 毎なので、もう少し猶予見ても良いかもしれませんが、
    ここはサブスレッドに移した時点で解消してるはずなので、遅延には影響ないはずです。

    キャンセル

  • 2021/05/19 14:11

    > 後は、フレームの描画更新頻度、カメラのフォーマット指定、解像度・FPS を落とすくらいですね。

    フレームの更新頻度はFPSでいう60→30にしてみます(倍の30ms)。見た目は影響なく、CPUとかの負担も小さいほうがいいのでとりあえずこれにします。
    フォーマットは
    https://qiita.com/iwatake2222/items/b8c442a9ec0406883950
    を参考に、H264に設定しました。
    解像度は、小さくしすぎると単に見づらいのでいったんこれくらいの大きさにします。

    ありがとうございました。

    キャンセル

15分調べてもわからないことは、teratailで質問しよう!

  • ただいまの回答率 87.92%
  • 質問をまとめることで、思考を整理して素早く解決
  • テンプレート機能で、簡単に質問をまとめられる

関連した質問

同じタグがついた質問を見る