From 464113fd73589eee1e68cc3c61a64b80327bb885 Mon Sep 17 00:00:00 2001 From: The Cheaterman Date: Wed, 12 Aug 2020 21:15:20 +0200 Subject: [PATCH 1/5] Make QRcode decoding non-blocking by doing it in a thread --- src/kivy_garden/zbarcam/zbarcam.py | 35 ++++++++++++++++++++++++------ 1 file changed, 28 insertions(+), 7 deletions(-) diff --git a/src/kivy_garden/zbarcam/zbarcam.py b/src/kivy_garden/zbarcam/zbarcam.py index 1d85e7e..60afd97 100644 --- a/src/kivy_garden/zbarcam/zbarcam.py +++ b/src/kivy_garden/zbarcam/zbarcam.py @@ -1,4 +1,6 @@ import os +import threading +import queue from collections import namedtuple import PIL @@ -49,6 +51,9 @@ def _on_camera_ready(self, xcamera): Starts binding when the `xcamera._camera` instance is ready. """ xcamera._camera.bind(on_texture=self._on_texture) + self._decoding_frame = threading.Event() + self._symbols_queue = queue.Queue() + Clock.schedule_interval(self._update_symbols, 0) def _remove_shoot_button(self): """ @@ -59,13 +64,26 @@ def _remove_shoot_button(self): shoot_button = xcamera.children[0] xcamera.remove_widget(shoot_button) - def _on_texture(self, instance): - self.symbols = self._detect_qrcode_frame( - texture=instance.texture, code_types=self.code_types) + def _on_texture(self, xcamera): + if not self._decoding_frame.is_set(): + self._decoding_frame.set() + threading.Thread( + target=self._detect_qrcode_frame, + args=( + xcamera.texture, + xcamera.texture.pixels, + self.code_types, + ), + ).start() - @classmethod - def _detect_qrcode_frame(cls, texture, code_types): - image_data = texture.pixels + def _update_symbols(self, *args): + try: + self.symbols = self._symbols_queue.get_nowait() + except queue.Empty: + return + + def _detect_qrcode_frame(self, texture, pixels, code_types): + image_data = pixels size = texture.size # Fix for mode mismatch between texture.colorfmt and data returned by # texture.pixels. texture.pixels always returns RGBA, so that should @@ -76,10 +94,13 @@ def _detect_qrcode_frame(cls, texture, code_types): pil_image = fix_android_image(pil_image) symbols = [] codes = pyzbar.decode(pil_image, symbols=code_types) + for code in codes: symbol = ZBarCam.Symbol(type=code.type, data=code.data) symbols.append(symbol) - return symbols + + self._symbols_queue.put(symbols) + self._decoding_frame.clear() @property def xcamera(self): From 96a0aead815b8b73e189520de6ab882585e75fc1 Mon Sep 17 00:00:00 2001 From: The Cheaterman Date: Mon, 17 Aug 2020 15:00:49 +0200 Subject: [PATCH 2/5] Fix _detect_qrcode_frame signature in tests It takes pixels independently of texture because in normal usage the decoding will be done in a separate thread (which therefore can't make the required GL calls to access the pixels). --- tests/kivy_garden/zbarcam/test_zbarcam.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/kivy_garden/zbarcam/test_zbarcam.py b/tests/kivy_garden/zbarcam/test_zbarcam.py index 2c7370b..4c7f4a0 100644 --- a/tests/kivy_garden/zbarcam/test_zbarcam.py +++ b/tests/kivy_garden/zbarcam/test_zbarcam.py @@ -28,7 +28,7 @@ def test_detect_qrcode_frame_no_qrcode(self): fixture_path = os.path.join(FIXTURE_DIR, 'no_qr_code.png') texture = Image(fixture_path).texture code_types = self.zbarcam.code_types - symbols = self.zbarcam._detect_qrcode_frame(texture, code_types) + symbols = self.zbarcam._detect_qrcode_frame(texture, texture.pixels, code_types) assert symbols == [] def test_detect_qrcode_frame_one_qrcode(self): @@ -38,7 +38,7 @@ def test_detect_qrcode_frame_one_qrcode(self): fixture_path = os.path.join(FIXTURE_DIR, 'one_qr_code.png') texture = Image(fixture_path).texture code_types = self.zbarcam.code_types - symbols = self.zbarcam._detect_qrcode_frame(texture, code_types) + symbols = self.zbarcam._detect_qrcode_frame(texture, texture.pixels, code_types) assert symbols == [ ZBarCam.Symbol(type='QRCODE', data=b'zbarlight test qr code') ] @@ -50,7 +50,7 @@ def test_detect_qrcode_frame_one_qrcode_one_ean(self): fixture_path = os.path.join(FIXTURE_DIR, 'one_qr_code_and_one_ean.png') texture = Image(fixture_path).texture code_types = self.zbarcam.code_types - symbols = self.zbarcam._detect_qrcode_frame(texture, code_types) + symbols = self.zbarcam._detect_qrcode_frame(texture, texture.pixels, code_types) assert symbols == [ ZBarCam.Symbol(type='QRCODE', data=b'zbarlight test qr code'), ZBarCam.Symbol(type='UPCA', data=b'012345678905') @@ -63,7 +63,7 @@ def test_detect_qrcode_frame_two_qrcodes(self): fixture_path = os.path.join(FIXTURE_DIR, 'two_qr_codes.png') texture = Image(fixture_path).texture code_types = self.zbarcam.code_types - symbols = self.zbarcam._detect_qrcode_frame(texture, code_types) + symbols = self.zbarcam._detect_qrcode_frame(texture, texture.pixels, code_types) Symbol = ZBarCam.Symbol assert symbols == [ Symbol(type='QRCODE', data=b'second zbarlight test qr code'), From a295964f7b9766ad63e921ac6ec7ed5ae53346d9 Mon Sep 17 00:00:00 2001 From: The Cheaterman Date: Mon, 17 Aug 2020 15:10:41 +0200 Subject: [PATCH 3/5] Set _decoding_frame and _symbols_queue in __init__ for unit tests --- src/kivy_garden/zbarcam/zbarcam.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/kivy_garden/zbarcam/zbarcam.py b/src/kivy_garden/zbarcam/zbarcam.py index 60afd97..88cbe97 100644 --- a/src/kivy_garden/zbarcam/zbarcam.py +++ b/src/kivy_garden/zbarcam/zbarcam.py @@ -32,6 +32,10 @@ def __init__(self, **kwargs): # that way the `XCamera` import doesn't happen too early Builder.load_file(os.path.join(MODULE_DIRECTORY, "zbarcam.kv")) super().__init__(**kwargs) + + self._decoding_frame = threading.Event() + self._symbols_queue = queue.Queue() + Clock.schedule_once(lambda dt: self._setup()) def _setup(self): @@ -51,8 +55,6 @@ def _on_camera_ready(self, xcamera): Starts binding when the `xcamera._camera` instance is ready. """ xcamera._camera.bind(on_texture=self._on_texture) - self._decoding_frame = threading.Event() - self._symbols_queue = queue.Queue() Clock.schedule_interval(self._update_symbols, 0) def _remove_shoot_button(self): From 0b7d7dfc9f7b0795515fc5a1f5a4bef9840f74ac Mon Sep 17 00:00:00 2001 From: The Cheaterman Date: Mon, 17 Aug 2020 17:55:44 +0200 Subject: [PATCH 4/5] Rewrite threading to make it test-friendly --- src/kivy_garden/zbarcam/zbarcam.py | 15 ++++++++++----- tests/kivy_garden/zbarcam/test_zbarcam.py | 8 ++++---- 2 files changed, 14 insertions(+), 9 deletions(-) diff --git a/src/kivy_garden/zbarcam/zbarcam.py b/src/kivy_garden/zbarcam/zbarcam.py index 88cbe97..5e11224 100644 --- a/src/kivy_garden/zbarcam/zbarcam.py +++ b/src/kivy_garden/zbarcam/zbarcam.py @@ -70,7 +70,7 @@ def _on_texture(self, xcamera): if not self._decoding_frame.is_set(): self._decoding_frame.set() threading.Thread( - target=self._detect_qrcode_frame, + target=self._threaded_detect_qrcode_frame, args=( xcamera.texture, xcamera.texture.pixels, @@ -78,14 +78,20 @@ def _on_texture(self, xcamera): ), ).start() + def _threaded_detect_qrcode_frame(self, texture, pixels, code_types): + self._symbols_queue.put( + self._detect_qrcode_frame(texture, code_types, pixels) + ) + self._decoding_frame.clear() + def _update_symbols(self, *args): try: self.symbols = self._symbols_queue.get_nowait() except queue.Empty: return - def _detect_qrcode_frame(self, texture, pixels, code_types): - image_data = pixels + def _detect_qrcode_frame(self, texture, code_types, pixels=None): + image_data = pixels or texture.pixels # Use pixels kwarg for threading size = texture.size # Fix for mode mismatch between texture.colorfmt and data returned by # texture.pixels. texture.pixels always returns RGBA, so that should @@ -101,8 +107,7 @@ def _detect_qrcode_frame(self, texture, pixels, code_types): symbol = ZBarCam.Symbol(type=code.type, data=code.data) symbols.append(symbol) - self._symbols_queue.put(symbols) - self._decoding_frame.clear() + return symbols @property def xcamera(self): diff --git a/tests/kivy_garden/zbarcam/test_zbarcam.py b/tests/kivy_garden/zbarcam/test_zbarcam.py index 4c7f4a0..2c7370b 100644 --- a/tests/kivy_garden/zbarcam/test_zbarcam.py +++ b/tests/kivy_garden/zbarcam/test_zbarcam.py @@ -28,7 +28,7 @@ def test_detect_qrcode_frame_no_qrcode(self): fixture_path = os.path.join(FIXTURE_DIR, 'no_qr_code.png') texture = Image(fixture_path).texture code_types = self.zbarcam.code_types - symbols = self.zbarcam._detect_qrcode_frame(texture, texture.pixels, code_types) + symbols = self.zbarcam._detect_qrcode_frame(texture, code_types) assert symbols == [] def test_detect_qrcode_frame_one_qrcode(self): @@ -38,7 +38,7 @@ def test_detect_qrcode_frame_one_qrcode(self): fixture_path = os.path.join(FIXTURE_DIR, 'one_qr_code.png') texture = Image(fixture_path).texture code_types = self.zbarcam.code_types - symbols = self.zbarcam._detect_qrcode_frame(texture, texture.pixels, code_types) + symbols = self.zbarcam._detect_qrcode_frame(texture, code_types) assert symbols == [ ZBarCam.Symbol(type='QRCODE', data=b'zbarlight test qr code') ] @@ -50,7 +50,7 @@ def test_detect_qrcode_frame_one_qrcode_one_ean(self): fixture_path = os.path.join(FIXTURE_DIR, 'one_qr_code_and_one_ean.png') texture = Image(fixture_path).texture code_types = self.zbarcam.code_types - symbols = self.zbarcam._detect_qrcode_frame(texture, texture.pixels, code_types) + symbols = self.zbarcam._detect_qrcode_frame(texture, code_types) assert symbols == [ ZBarCam.Symbol(type='QRCODE', data=b'zbarlight test qr code'), ZBarCam.Symbol(type='UPCA', data=b'012345678905') @@ -63,7 +63,7 @@ def test_detect_qrcode_frame_two_qrcodes(self): fixture_path = os.path.join(FIXTURE_DIR, 'two_qr_codes.png') texture = Image(fixture_path).texture code_types = self.zbarcam.code_types - symbols = self.zbarcam._detect_qrcode_frame(texture, texture.pixels, code_types) + symbols = self.zbarcam._detect_qrcode_frame(texture, code_types) Symbol = ZBarCam.Symbol assert symbols == [ Symbol(type='QRCODE', data=b'second zbarlight test qr code'), From 5669e5ac28c693a9746b87bf4b7c77785fe33194 Mon Sep 17 00:00:00 2001 From: The Cheaterman Date: Tue, 18 Aug 2020 10:50:12 +0200 Subject: [PATCH 5/5] Make isort happy as well. --- src/kivy_garden/zbarcam/zbarcam.py | 6 +++--- tests/kivy_garden/zbarcam/test_zbarcam.py | 1 + 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/src/kivy_garden/zbarcam/zbarcam.py b/src/kivy_garden/zbarcam/zbarcam.py index 5e11224..741f0cc 100644 --- a/src/kivy_garden/zbarcam/zbarcam.py +++ b/src/kivy_garden/zbarcam/zbarcam.py @@ -1,7 +1,7 @@ +import collections import os -import threading import queue -from collections import namedtuple +import threading import PIL from kivy.clock import Clock @@ -23,7 +23,7 @@ class ZBarCam(AnchorLayout): resolution = ListProperty([640, 480]) symbols = ListProperty([]) - Symbol = namedtuple('Symbol', ['type', 'data']) + Symbol = collections.namedtuple('Symbol', ['type', 'data']) # checking all possible types by default code_types = ListProperty(set(pyzbar.ZBarSymbol)) diff --git a/tests/kivy_garden/zbarcam/test_zbarcam.py b/tests/kivy_garden/zbarcam/test_zbarcam.py index 2c7370b..414894b 100644 --- a/tests/kivy_garden/zbarcam/test_zbarcam.py +++ b/tests/kivy_garden/zbarcam/test_zbarcam.py @@ -4,6 +4,7 @@ from kivy.base import EventLoop from kivy.core.image import Image + from kivy_garden.zbarcam import ZBarCam FIXTURE_DIR = os.path.join(