From d19dc952b611bf2bea9d38a5dc542b00b7f0afe0 Mon Sep 17 00:00:00 2001 From: Nick Date: Sun, 31 Mar 2019 03:14:01 -0400 Subject: [PATCH 1/3] Expose barcode metadata through the API. --- zxinglight/__init__.py | 29 ++++++++++++++++++++++++++--- zxinglight/_zxinglight.cpp | 33 ++++++++++++++++++--------------- 2 files changed, 44 insertions(+), 18 deletions(-) diff --git a/zxinglight/__init__.py b/zxinglight/__init__.py index 9ddac6b..fd72f98 100644 --- a/zxinglight/__init__.py +++ b/zxinglight/__init__.py @@ -67,9 +67,10 @@ class BarcodeType(IntEnum): UPC_EAN_EXTENSION = 17 -def read_codes(image, barcode_type=BarcodeType.NONE, try_harder=False, hybrid=False, multi=True): +def read_codes_full(image, barcode_type=BarcodeType.NONE, + try_harder=False, hybrid=False, multi=True): """ - Reads codes from a PIL Image. + Reads codes from a PIL Image and includes metadata about what was found. Args: image (PIL.Image.Image): Image to read barcodes from. @@ -80,7 +81,7 @@ def read_codes(image, barcode_type=BarcodeType.NONE, try_harder=False, hybrid=Fa multi (bool): Search for multiple barcodes in a single image. Returns: - A list of barcode values. + A list [(code, position, type), ...] containing each barcode found. .. _ZXing's documentation: https://zxing.github.io/zxing/apidocs/com/google/zxing/Binarizer.html @@ -98,3 +99,25 @@ def read_codes(image, barcode_type=BarcodeType.NONE, try_harder=False, hybrid=Fa width, height = grayscale_image.size return zxing_read_codes(raw_image, width, height, barcode_type, try_harder, hybrid, multi) + + +def read_codes(*args, **kwargs): + """ + Reads codes from a PIL Image. + + Args: + image (PIL.Image.Image): Image to read barcodes from. + barcode_type (zxinglight.BarcodeType): Barcode type to look for. + try_harder (bool): Spend more time trying to find a barcode. + hybrid (bool): Use Hybrid Binarizer instead of Global Binarizer. For more information, + see `ZXing's documentation`_. + multi (bool): Search for multiple barcodes in a single image. + + Returns: + A list of barcode contents found. + + .. _ZXing's documentation: + https://zxing.github.io/zxing/apidocs/com/google/zxing/Binarizer.html + """ + codes = read_codes_full(*args, **kwargs) + return [text for text, points, format in codes] diff --git a/zxinglight/_zxinglight.cpp b/zxinglight/_zxinglight.cpp index 60f7a45..aa97882 100644 --- a/zxinglight/_zxinglight.cpp +++ b/zxinglight/_zxinglight.cpp @@ -35,7 +35,7 @@ static void log_error(const string &msg) { PyObject_CallMethod(logger, "error", "O", PyUnicode_FromString(msg.c_str())); } -static vector *_zxing_read_codes( +static vector > *_zxing_read_codes( char *image, int image_size, int width, int height,int barcode_type, bool try_harder, bool hybrid, bool multi ) { @@ -67,24 +67,19 @@ static vector *_zxing_read_codes( Ref bitmap(new BinaryBitmap(binarizer)); - vector > results; + vector > *results; if (multi) { MultiFormatReader delegate; GenericMultipleBarcodeReader reader(delegate); - results = reader.decodeMultiple(bitmap, hints); + results = new vector >(reader.decodeMultiple(bitmap, hints)); } else { + // There is only one result, but wrap it in a vector anyway to give a consistent interface // Ref is an autodestructor; the `new` *does not leak*. Ref reader(new MultiFormatReader); - results = vector >(1, reader->decode(bitmap, hints)); + results = new vector >(1, reader->decode(bitmap, hints)); } - vector *codes = new vector(); - - for (const Ref &result : results) { - codes->push_back(result->getText()->getText()); - } - - return codes; + return results; } catch (const ReaderException &e) { log_error((string) "zxing::ReaderException: " + e.what()); } catch (const zxing::IllegalArgumentException &e) { @@ -113,17 +108,25 @@ static PyObject* zxing_read_codes(PyObject *self, PyObject *args) { PyBytes_AsStringAndSize(python_image, &image, &image_size); - vector *results = _zxing_read_codes( + vector > *results = _zxing_read_codes( image, image_size, width, height, barcode_type, try_harder, hybrid, multi ); PyObject *codes = PyList_New(0); if (results != NULL) { - for (const string &code : *results) { - PyList_Append(codes, PyUnicode_FromString(code.c_str())); - } + for (const Ref &result : *results) { + PyObject* text = PyUnicode_FromString(result->getText()->getText().c_str()); + PyObject* points = PyList_New(0); + for(auto point : result->getResultPoints()->values()) { + PyList_Append(points, Py_BuildValue("ff", point->getX(), point->getY())); + } + + PyObject* format = PyLong_FromUnsignedLong(result->getBarcodeFormat()); + + PyList_Append(codes, PyTuple_Pack(3, text, points, format)); + } delete results; } From 7f9be3b00af5de563d1a84be24ef1e253bc7a2bf Mon Sep 17 00:00:00 2001 From: Nick Date: Sun, 31 Mar 2019 03:34:45 -0400 Subject: [PATCH 2/3] Make cv2 a first-class citizen. cv2 is the fastest way to access a webcam (e.g. if you're trying to make a barcode scanner) in python, but the zxinglight API forces PIL, even though the QR library doesn't care. --- zxinglight/__init__.py | 35 +++++++++++++++++++++++++++-------- 1 file changed, 27 insertions(+), 8 deletions(-) diff --git a/zxinglight/__init__.py b/zxinglight/__init__.py index fd72f98..89e41a7 100644 --- a/zxinglight/__init__.py +++ b/zxinglight/__init__.py @@ -1,7 +1,15 @@ # -*- coding: utf-8 -*- from enum import IntEnum, unique -from PIL import Image +try: + from PIL import Image +except ImportError: + pass +try: + import cv2 + import numpy as np +except ImportError: + pass from ._zxinglight import zxing_read_codes @@ -87,17 +95,28 @@ def read_codes_full(image, barcode_type=BarcodeType.NONE, https://zxing.github.io/zxing/apidocs/com/google/zxing/Binarizer.html """ - if not Image.isImageType(image): - raise ValueError('Provided image is not a PIL image') + if "Image" in globals() and Image.isImageType(image): + grayscale_image = image.convert('L') + + raw_image = grayscale_image.tobytes() + width, height = grayscale_image.size + elif "cv2" in globals() and isinstance(image, np.ndarray): + if image.dtype != np.uint8: + raise TypeError("Provided cv2 image is not in integer bitdepth.") + if len(image.shape) == 2: + greyscale_image = image + elif len(image.shape) == 3 and image.shape[-1] == 3: + greyscale_image = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY) + else: + raise TypeError("Provided cv2 image is not in BGR nor greyscale formats.") + raw_image = greyscale_image.tobytes() + height, width = image.shape[:2] + else: + raise TypeError('Provided image is not a PIL nor cv2 image') if not isinstance(barcode_type, BarcodeType): raise ValueError('barcode_type is not an enum member of BarcodeType') - grayscale_image = image.convert('L') - - raw_image = grayscale_image.tobytes() - width, height = grayscale_image.size - return zxing_read_codes(raw_image, width, height, barcode_type, try_harder, hybrid, multi) From 118cc1f5d138bd87de69fbce12e602bdd8ab3e99 Mon Sep 17 00:00:00 2001 From: Nick Date: Sun, 31 Mar 2019 03:47:06 -0400 Subject: [PATCH 3/3] Mark Pillow as optional --- setup.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/setup.py b/setup.py index 8fdcea5..d3d7124 100644 --- a/setup.py +++ b/setup.py @@ -62,10 +62,13 @@ setup_requires=[ 'setuptools', ], - install_requires=[ - 'Pillow' - ], extras_require={ + 'PIL': [ + 'Pillow' + ], + 'cv2': [ + 'cv2' + ], 'docs': [ 'Sphinx', 'sphinx-autobuild',