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

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

新規登録して質問してみよう
ただいま回答率
85.35%
Raspbian

Raspbianは、DebianベースのRaspberry Pi用ディストリビューション。ハードウェア浮動小数点演算を有効にすることが可能で、Webブラウズなどの速度を向上できます。

Python 3.x

Python 3はPythonプログラミング言語の最新バージョンであり、2008年12月3日にリリースされました。

Tkinter

Tkinterは、GUIツールキットである“Tk”をPythonから利用できるようにした標準ライブラリである。

Raspberry Pi

Raspberry Piは、ラズベリーパイ財団が開発した、名刺サイズのLinuxコンピュータです。 学校で基本的なコンピュータ科学の教育を促進することを意図しています。

Q&A

解決済

1回答

4324閲覧

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

person

総合スコア224

Raspbian

Raspbianは、DebianベースのRaspberry Pi用ディストリビューション。ハードウェア浮動小数点演算を有効にすることが可能で、Webブラウズなどの速度を向上できます。

Python 3.x

Python 3はPythonプログラミング言語の最新バージョンであり、2008年12月3日にリリースされました。

Tkinter

Tkinterは、GUIツールキットである“Tk”をPythonから利用できるようにした標準ライブラリである。

Raspberry Pi

Raspberry Piは、ラズベリーパイ財団が開発した、名刺サイズのLinuxコンピュータです。 学校で基本的なコンピュータ科学の教育を促進することを意図しています。

1グッド

2クリップ

投稿2021/05/18 00:53

編集2021/05/19 04:31

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を入れてましたが、
ラズパイでやったらエラーになったので、ここでは指定しません。

Python

1from datetime import datetime 2from PIL import Image, ImageTk 3from pyzbar.pyzbar import decode 4import cv2 5import queue 6import threading 7import time 8import tkinter as tk 9 10decode_cnt = 0 11decode_span = 20 12 13class App: 14 def __init__(self, win): 15 video_source = 0 16 self.vcap = cv2.VideoCapture(video_source) 17 self.vcap.set(cv2.CAP_PROP_FOURCC, cv2.VideoWriter_fourcc("H", "2", "6", "4")) 18 self.vcap.set(cv2.CAP_PROP_FRAME_WIDTH, 720) 19 self.vcap.set(cv2.CAP_PROP_FRAME_HEIGHT, 480) 20 #self.vcap.set(cv2.CAP_PROP_FPS, 30) 21 self.fourcc = self.vcap.get(cv2.CAP_PROP_FOURCC) 22 self.width = self.vcap.get(cv2.CAP_PROP_FRAME_WIDTH) 23 self.height = self.vcap.get(cv2.CAP_PROP_FRAME_HEIGHT) 24 #self.fps = self.vcap.get(cv2.CAP_PROP_FPS) 25 print("width: {}".format(self.width)) 26 print("height: {}".format(self.height)) 27 self.font = cv2.FONT_HERSHEY_SIMPLEX 28 29 self.label_header = "バーコード:" 30 self.barcodeData = "" 31 32 self.queue_to_subthread = queue.Queue() 33 self.queue_to_mainthread = queue.Queue() 34 35 self.win = win 36 self.win.title("Camera") 37 self.thread = threading.Thread(target=self.get_queue, args=(self.queue_to_mainthread,)) 38 self.thread.start() 39 size_str = str(int(self.width + 100)) + "x" + str(int(self.height + 100)) 40 self.win.geometry(size_str) 41 #self.disp_center(self.win, int(self.width) + 100, int(self.height) + 100) 42 #self.disp_center(self.win, 720, 480) 43 self.win.protocol("WM_DELETE_WINDOW", self.close) 44 self.create_view() 45 self.bind_event() 46 #self.update_by_timer() 47 self.canvas.after(15, self.update_by_timer) 48 49 50 def disp_center(self, win, wx=400, wy=300): 51 #win.resizable(0, 0) 52 dx = win.winfo_screenwidth() 53 dy = win.winfo_screenheight() 54 win_size = str(wx) + "x" + str(wy) + \ 55 "+" + str(int(dx/2 - wx/2)) + "+" + str(int(dy/2 - wy/2)) 56 win.geometry(win_size) 57 58 59 def close(self, e=None): 60 self.queue_to_subthread.put(["close", None]) 61 self.thread.join() 62 self.win.destroy() 63 self.vcap.release() 64 #cv2.destroyAllWindows() 65 66 67 def create_view(self): 68 self.win.rowconfigure(0, weight=1) 69 self.win.columnconfigure(0, weight=1) 70 71 self.frame = tk.Frame(self.win, relief="sunken", bd=1) 72 self.frame.grid(row=0, column=0, sticky="nsew") 73 self.frame.rowconfigure(0, weight=1) 74 self.frame.columnconfigure(0, weight=1) 75 76 self.canvas = tk.Canvas(self.frame) 77 self.canvas.grid(row=0, column=0, sticky="nsew", padx=10, pady=10) 78 79 ret, frm = self.vcap.read() 80 #print("ret: {}".format(ret)) 81 #print("frm: {}".format(frm)) 82 self.frame = cv2.cvtColor(frm, cv2.COLOR_BGR2RGB) 83 self.photo = ImageTk.PhotoImage(image = Image.fromarray(self.frame)) 84 self.canvas.update() 85 cv_w = self.canvas.winfo_reqwidth() 86 cv_h = self.canvas.winfo_reqheight() 87 self.img_id = self.canvas.create_image(cv_w // 2, cv_h // 2, image=self.photo, anchor="center") 88 89 90 """ 91 self.canvas.update() 92 cv_w = self.canvas.winfo_width() 93 cv_h = self.canvas.winfo_height() 94 95 rec_id = self.canvas.create_rectangle(0, 0, cv_w, cv_h, fill="black") 96 97 98 # 長方形の座標取得 99 rec_pos = self.canvas.coords(rec_id) 100 # テキストを描画(位置は適当) 101 text_id = self.canvas.create_text(0, 0, text="NO SIGNAL", font=("", 20), fill="white") 102 # テキストのサイズ取得 103 text_size = self.canvas.bbox(text_id) 104 # テキストの座標移動 105 rc_x = rec_pos[2] / 2 106 rc_y = rec_pos[2] / 3 107 tc_y = text_size[3] / 2 108 self.canvas.move(text_id, rc_x - (rec_pos[0] / 2), rc_y - tc_y) 109 """ 110 111 self.label = tk.Label(self.win) 112 self.label["text"] = self.label_header 113 self.label.grid(row=1, column=0, sticky="", pady=10) 114 115 self.button = tk.Button(self.win, text="Snapshot", command=self.pushed_button) 116 self.button.grid(row=2, column=0, sticky="", pady=10) 117 118 119 def pushed_button(self): 120 self.queue_to_subthread.put(["snapshot", None]) 121 122 123 def update_by_timer(self): 124 global decode_cnt, decode_span 125 ret, self.frame = self.vcap.read() 126 #print("ret: {}".format(ret)) 127 #print("self.frame: {}".format(self.frame)) 128 # cv2はBGR、pillow(PIL)はRGB。色の構成順が異なる 129 self.frame = cv2.cvtColor(self.frame, cv2.COLOR_BGR2RGB) # フルカラー(色の構成順をRGBへ) 130 #self.frame = cv2.cvtColor(self.frame, cv2.COLOR_BGR2GRAY) # グレースケール 131 132 # 2値化(単純な閾値処理) 133 # https://axa.biopapyrus.jp/ia/opencv/threshold.html 134 #_, self.frame = cv2.threshold(self.frame, 120, 255, cv2.THRESH_BINARY) # 2値化 135 136 # 2値化(適応的閾値処理) 137 # 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 138 #self.frame = cv2.adaptiveThreshold(self.frame, 255, cv2.ADAPTIVE_THRESH_GAUSSIAN_C, cv2.THRESH_BINARY, 11, 3) 139 140 141 142 if ret and decode_cnt % decode_span == 0: 143 self.queue_to_subthread.put(["decode", self.frame]) 144 145 try: 146 msg, d = self.queue_to_mainthread.get_nowait() 147 #print("data: \n{}".format(d)) 148 print(d) 149 x,y,w,h = (0,0,0,0) 150 if d: 151 for barcode in d: 152 x,y,w,h = barcode.rect 153 self.barcodeData = barcode.data.decode("utf-8") 154 155 156 else: 157 self.barcodeData = "" 158 self.label["text"] = self.label_header + self.barcodeData 159 cv2.rectangle(self.frame,(x,y),(x+w,y+h),(0,0,255),2) 160 frame = cv2.putText(self.frame,self.barcodeData,(x,y-10),self.font,.5,(0,0,255),2,cv2.LINE_AA) 161 162 163 except queue.Empty: 164 pass 165 166 self.photo = ImageTk.PhotoImage(image = Image.fromarray(self.frame)) 167 self.canvas.itemconfig(self.img_id, image=self.photo) 168 decode_cnt += 1 169 self.canvas.after(15, self.update_by_timer) 170 171 172 def update_by_resize(self, e): 173 self.canvas.itemconfig(self.img_id, image=self.photo, anchor="center") 174 cv_w = self.canvas.winfo_width() 175 cv_h = self.canvas.winfo_height() 176 self.canvas.coords(self.img_id, cv_w // 2, cv_h // 2) 177 #pass 178 179 180 def bind_event(self): 181 self.win.bind("<Escape>", self.close) 182 self.canvas.bind("<Configure>", self.update_by_resize) 183 184 185 def get_queue(self, queue_to_mainthread): 186 while True: 187 msg, data = self.queue_to_subthread.get() 188 if msg == "decode": 189 d = decode(data) 190 if d: 191 queue_to_mainthread.put(["result", d]) 192 else: 193 queue_to_mainthread.put(["result", None]) 194 elif msg == "snapshot": 195 self.snapshot() 196 elif msg == "close": 197 break 198 else: 199 print("----- Undefined msg -----") 200 break 201 202 def snapshot(self): 203 filename = "/home/pi/デスクトップ/img/frame-" + datetime.now().strftime("%Y-%m-%d-%H-%M-%S_%f") + ".jpg" 204 cv2.imwrite(filename, 205 cv2.cvtColor(self.frame, cv2.COLOR_BGR2RGB)) 206 print(filename) 207 208 209def main(): 210 win = tk.Tk() 211 App(win) 212 win.mainloop() 213 214if __name__ == "__main__": 215 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 (他省略)
teamikl👍を押しています

気になる質問をクリップする

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

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

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

guest

回答1

0

ベストアンサー

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

python

1 2import time 3 4start_time = time.perf_counter() 5 6# 計測したい処理 7 8end_time = time.perf_counter() 9print(f"decode() took {end_time - start_time} seconds") 10

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


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

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

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


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

  • update_by_timer で self.frame に代入し
  • pushed_button から self.frame を参照。

 同一スレッド内なので問題ありません。

  • snapshot から self.frame の参照

 別スレッドなので、スレッドセーフではありません。

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

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

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

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

投稿2021/05/18 05:38

teamikl

総合スコア8760

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

person

2021/05/18 07:20

> 実際に、何処でどのくらい時間が掛かっているかを計測して見て下さい。 after()で何度も呼んでいる、update()の中の処理について cv2.VideoCaputure().read()は0.007~0.011秒 cv2.cvtColor()は0.002~0.004秒 pyzbar.decode()は0.25~0.26秒 barcode.rectは 1.5313000403693877e-05[s] 1.4479999663308263e-05[s] 1.4322999049909413e-05[s] ・・・ cv2.rectangle()とlabel["text"]変更は約0.0003秒 cv2.putText()は約0.005秒 PhotoImage()は0.015秒~0.030秒 canvas.itemconfig()は0.06~0.08秒 目立つのはdecode()とrect()といったところですかね。
teamikl

2021/05/18 07:50

windows でも計測して、時間を比較して見て下さい。 時間差が大きい所から改善策を検討していきましょう。 考えられる対策は、decode の頻度を下げる 2秒間隔くらいで実行するtkinterのタイマーを作り、 "decode" メッセージを定期的にキューに入れる。 サブスレッド側でdecode()処理を行う、等でしょうか。 barcode.rect 情報の更新は、毎回ではなく decode のタイミングのみで良いはずです。 計測された処理のうち、tkinter 関連の部分 label["text"]変更 と、PhotoImage() と canvas.itemconfig() は、 必ずメインスレッド側のタイマーイベント内で行う必要があります。 他のcv2関連の処理は、タイマーイベント内で行うと、描画更新の遅延に繋がる可能性有。 改善したいならサブスレッドで行う方が良いのですが、優先度は状況次第なので、 ここは時間を計測したうえで、必要な対策かどうか判断してください。
person

2021/05/18 08:18

Windowsの場合、tkinter以外の時間は下記のようになりました。 ラズパイと比較して速い遅いはありますが、大きく異なるところはpyzbar関係の部分といったところですかね。 cv2.VideoCaputure().read()は0.006~0.03秒 cv2.cvtColor()は0.0003~0.0005秒 pyzbar.decode()は0.014~0.025秒 barcode.rectは 1.7999999997186933e-06[s] 1.7000000003264404e-06[s] 1.4000000003733248e-06[s] ・・・ cv2.rectangle()は 1.5000000001208491e-05[s] 1.7899999999571037e-05[s] 1.8399999998308658e-05[s] ・・・ cv2.putText()は0.0003~0.0004秒
teamikl

2021/05/18 08:43

概ね、decodeの頻度を下げる、別スレッド側で行う方針で良いと思いますが、 見落とし、barcode.rect は 指数表記 1.5~e-05 なので、0.000015 です。 PhotoImage() と itemconfig は動かせないので、 改良が必要な箇所は pyzbar.decode() に絞ってよいと思います。
teamikl

2021/05/18 09:05

ラベルの更新頻度も、フレーム更新の度に毎回ではなくて、 デコード時のみで良いですね。 decode() をサブスレッド側で行う場合は、 サブスレッド側で直接GUI操作(ラベルの更新)は行わない方が良いので、 サブスレッド → メインスレッドへの通知が必要になります。
person

2021/05/19 00:54

すみません。 キューへの受け渡しを実装しようと思ったのですが、サブスレッドからメインスレッドへ解析後のフレームやバーコードのデータを渡そうと思いupdate()に取得部分を書いたら、canvas上の画像が止まってしまっており、プログラムも止まっています。 (ソースコードは質問文のものを変えました) おそらく自分の書き方が悪いのだと思いますが、サブ→メインの受け渡し部分はどのように書くとよいでしょうか? メイン→サブと同じようにget()ループさせる必要があるのでしょうkあ
teamikl

2021/05/19 01:32 編集

メイン側では、tkinter のイベントループを止めてはいけないので、 queue からの要素の所得は、get() ではなく ノンブロッキング版の get_nowait() を使います。 また、その場合は、キューが空の時の挙動が変わり、 get() であればキューにアイテムが入る迄待機だったのが、 ブロッキングしない場合は、例外を投げるので、queue.Empty 例外の補足が必要になります。
person

2021/05/19 02:13

get_nowait() に変更しました。 canvasのカメラの映像が巻き戻ったり進んだりするようになりました。 get_nowait()を使ったことで、メインスレッドの更新中のframeに過去のframeが割り込んできているのでしょうか?
teamikl

2021/05/19 02:26 編集

カメラから読み込まれた self.frame をキューからのデータで上書きしてる為ですね。 カメラからのフレーム読込はメインスレッド側なので、 フレームへのcv2.putText は、メインスレッド側で行う必要があります。
person

2021/05/19 02:29

> フレームへのcv2.putText は、メインスレッド側で行う必要があります。 cv2.rectangleもメインスレッドで行ったほうがいいですか?
teamikl

2021/05/19 02:46 編集

> cv2.rectangleもメインスレッドで行ったほうがいいですか? そうですね。サブスレッドでは decode() のみ行い barcodeData を返す。 メインスレッドではカメラから所得したフレームに対して、 decode で得たデータを重ね合わせという流れになります。 ---- 他の解決策は、サブスレッド側でカメラ入力を得る、ですが サブスレッド側でカメラからread() する場合は、注意点が増えて queue が溢れないように queue のサイズ指定したり queue への put() がブロッキングしないように put_nowait() ~等、 もう少し作業が増えます。
person

2021/05/19 03:48 編集

cv2のフレームに対しての描画処理等をメインスレッドに移動しました。 バーコードは読み込めているようですが、cv2のバーコードを認識したことを表す描画が数フレーム遅れています。(おそらくバーコードのラベル表示のタイミングもこれと同じで遅れている。) cv2の描画部分の実装が必要かどうかは一旦置いておくとして、frameをスレッド間で受け渡すとやっぱり問題出てきますね。
teamikl

2021/05/19 03:53

decode() の頻度が戻ってしまっているのでは?コメントされてます > if ret: #and decode_cnt % decode_span == 0: >cv2のフレームに対しての描画処理等をメインスレッドに移動しました。 キューからの所得時のみになってますが、ラベルの更新はここで良いとして frame への描画 cv2.putText は、毎フレーム行う必要があるはずです。
person

2021/05/19 04:05

> decode() の頻度が戻ってしまっているのでは?コメントされてます 頻度戻しました。 > frame への描画 cv2.putText は、毎フレーム行う必要があるはずです。 putText()の描画内容はどこを参照するのでしょうか。 barcodeDataをグローバル変数にして毎回参照するような形でしょうか。
teamikl

2021/05/19 04:15 編集

>barcodeDataをグローバル変数にして毎回参照するような形でしょうか。 グローバル変数でも可能ですが、折角クラスを使ってるので キューから読み取った値は、インスタンス変数 (例: self.barcodeData) に保持しておきます。 ---- グローバル変数にした場合は、そのクラスの再利用が難しくなります。 現状ではApp() はひとつのみなので問題なさそうですが、 例えば複数のカメラ・複数のAppインスタンスとなった場合、 グローバル変数では値を共有してしまうので、 別カメラの情報を表示してしまう等の問題になります。
person

2021/05/19 04:35

インスタンス変数で毎フレームputText()に変更しました。 質問文に記載。 数秒おきに読み取ります(おそらく設定した頻度) 全部メインスレッドでやった時と比べてそこまで時間差があるわけではないですが、妥協せざるを得ないですかね。 あるいは、ラズパイとカメラの組み合わせ以外の方法で読み取るか・・・。
teamikl

2021/05/19 05:01 編集

後は、フレームの描画更新頻度、カメラのフォーマット指定、解像度・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 秒) 毎なので、もう少し猶予見ても良いかもしれませんが、 ここはサブスレッドに移した時点で解消してるはずなので、遅延には影響ないはずです。
person

2021/05/19 05:11

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

あなたの回答

tips

太字

斜体

打ち消し線

見出し

引用テキストの挿入

コードの挿入

リンクの挿入

リストの挿入

番号リストの挿入

表の挿入

水平線の挿入

プレビュー

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

ただいまの回答率
85.35%

質問をまとめることで
思考を整理して素早く解決

テンプレート機能で
簡単に質問をまとめる

質問する

関連した質問