Skip to content

Commit dbc6850

Browse files
EchterAgoaxxel
andcommitted
Python: Remove numpy requirement, use buffer protocol instead
Partly taken from #283 (comment) This removes the requirement of `read_barcode` / `write_barcode` to have Numpy installed and uses [buffer protocol](https://pybind11.readthedocs.io/en/stable/advanced/pycpp/numpy.html#buffer-protocol) instead. Numpy arrays can already be converted to a buffer, for PIL images we get the `__array_interface__` and use its values to create a memory view with the right shape. The `write_barcode` function now returns a buffer instead of a Numpy array. This buffer also supports `__array_interface__` for easy conversion to PIL images or Numpy arrays. * For PIL: `img = Image.fromarray(result, "L")` * For Numpy: `img = np.array(result)` fixes #283 Co-authored-by: axxel <awagger@gmail.com>
1 parent 564dda9 commit dbc6850

File tree

3 files changed

+172
-42
lines changed

3 files changed

+172
-42
lines changed

wrappers/python/setup.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,6 @@ def build_extension(self, ext):
7676
"Topic :: Multimedia :: Graphics",
7777
],
7878
python_requires=">=3.6",
79-
install_requires=["numpy"],
8079
ext_modules=[CMakeExtension('zxingcpp')],
8180
cmdclass=dict(build_ext=CMakeBuild),
8281
zip_safe=False,

wrappers/python/test.py

Lines changed: 43 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import importlib.util
22
import unittest
3+
import math
34

45
import zxingcpp
56

@@ -16,7 +17,6 @@ def test_format(self):
1617
self.assertEqual(zxingcpp.barcode_formats_from_str('ITF, qrcode'), BF.ITF | BF.QRCode)
1718

1819

19-
@unittest.skipIf(not has_numpy, "need numpy for read/write tests")
2020
class TestReadWrite(unittest.TestCase):
2121

2222
def check_res(self, res, format, text):
@@ -60,21 +60,52 @@ def test_write_read_multi_cycle(self):
6060
res = zxingcpp.read_barcodes(img)[0]
6161
self.check_res(res, format, text)
6262

63-
def test_failed_read(self):
63+
@staticmethod
64+
def zeroes(shape):
65+
return memoryview(b"0" * math.prod(shape)).cast("B", shape=shape)
66+
67+
def test_failed_read_buffer(self):
68+
res = zxingcpp.read_barcode(
69+
self.zeroes((100, 100)), formats=BF.EAN8 | BF.Aztec, binarizer=zxingcpp.Binarizer.BoolCast
70+
)
71+
72+
self.assertEqual(res, None)
73+
74+
@unittest.skipIf(not has_numpy, "need numpy for read/write tests")
75+
def test_failed_read_numpy(self):
6476
import numpy as np
6577
res = zxingcpp.read_barcode(
6678
np.zeros((100, 100), np.uint8), formats=BF.EAN8 | BF.Aztec, binarizer=zxingcpp.Binarizer.BoolCast
6779
)
6880

6981
self.assertEqual(res, None)
7082

83+
def test_write_read_cycle_buffer(self):
84+
format = BF.QRCode
85+
text = "I have the best words."
86+
img = zxingcpp.write_barcode(format, text)
87+
88+
self.check_res(zxingcpp.read_barcode(img), format, text)
89+
90+
@unittest.skipIf(not has_numpy, "need numpy for read/write tests")
91+
def test_write_read_cycle_numpy(self):
92+
import numpy as np
93+
format = BF.QRCode
94+
text = "I have the best words."
95+
img = zxingcpp.write_barcode(format, text)
96+
img = np.array(img)
97+
#img = np.ndarray(img.shape, dtype=np.uint8, buffer=img)
98+
99+
self.check_res(zxingcpp.read_barcode(img), format, text)
100+
71101
@unittest.skipIf(not has_pil, "need PIL for read/write tests")
72102
def test_write_read_cycle_pil(self):
73103
from PIL import Image
74104
format = BF.QRCode
75105
text = "I have the best words."
76106
img = zxingcpp.write_barcode(format, text)
77107
img = Image.fromarray(img, "L")
108+
#img = Image.frombuffer("L", img.shape, img)
78109

79110
self.check_res(zxingcpp.read_barcode(img), format, text)
80111
self.check_res(zxingcpp.read_barcode(img.convert("RGB")), format, text)
@@ -84,13 +115,20 @@ def test_write_read_cycle_pil(self):
84115

85116
def test_read_invalid_type(self):
86117
self.assertRaisesRegex(
87-
TypeError, "Could not convert <class 'str'> to numpy array.", zxingcpp.read_barcode, "foo"
118+
TypeError, "Could not convert <class 'str'> to buffer.", zxingcpp.read_barcode, "foo"
119+
)
120+
121+
def test_read_invalid_numpy_array_channels_buffer(self):
122+
self.assertRaisesRegex(
123+
ValueError, "Unsupported number of channels for buffer: 4", zxingcpp.read_barcode,
124+
self.zeroes((100, 100, 4))
88125
)
89126

90-
def test_read_invalid_numpy_array_channels(self):
127+
@unittest.skipIf(not has_numpy, "need numpy for read/write tests")
128+
def test_read_invalid_numpy_array_channels_numpy(self):
91129
import numpy as np
92130
self.assertRaisesRegex(
93-
ValueError, "Unsupported number of channels for numpy array: 4", zxingcpp.read_barcode,
131+
ValueError, "Unsupported number of channels for buffer: 4", zxingcpp.read_barcode,
94132
np.zeros((100, 100, 4), np.uint8)
95133
)
96134

wrappers/python/zxing.cpp

Lines changed: 129 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -15,19 +15,18 @@
1515
#include "BitMatrix.h"
1616
#include "MultiFormatWriter.h"
1717

18-
#include <pybind11/numpy.h>
1918
#include <pybind11/pybind11.h>
2019
#include <pybind11/stl.h>
2120
#include <optional>
2221
#include <memory>
2322
#include <vector>
23+
#include <sstream>
24+
#include <functional>
25+
#include <list>
2426

2527
using namespace ZXing;
2628
namespace py = pybind11;
2729

28-
// Numpy array wrapper class for images (either BGR or GRAYSCALE)
29-
using Image = py::array_t<uint8_t, py::array::c_style>;
30-
3130
std::ostream& operator<<(std::ostream& os, const Position& points) {
3231
for (const auto& p : points)
3332
os << p.x << "x" << p.y << " ";
@@ -49,47 +48,91 @@ auto read_barcodes_impl(py::object _image, const BarcodeFormats& formats, bool t
4948
.setMaxNumberOfSymbols(max_number_of_symbols)
5049
.setEanAddOnSymbol(ean_add_on_symbol);
5150
const auto _type = std::string(py::str(py::type::of(_image)));
52-
Image image;
51+
py::buffer buffer;
5352
ImageFormat imgfmt = ImageFormat::None;
5453
try {
55-
if (_type.find("PIL.") != std::string::npos) {
56-
_image.attr("load")();
57-
const auto mode = _image.attr("mode").cast<std::string>();
58-
if (mode == "L")
59-
imgfmt = ImageFormat::Lum;
60-
else if (mode == "RGB")
61-
imgfmt = ImageFormat::RGB;
62-
else if (mode == "RGBA")
63-
imgfmt = ImageFormat::RGBX;
64-
else {
65-
// Unsupported mode in ImageFormat. Let's do conversion to L mode with PIL.
66-
_image = _image.attr("convert")("L");
67-
imgfmt = ImageFormat::Lum;
54+
if (py::hasattr(_image, "__array_interface__")) {
55+
if (_type.find("PIL.") != std::string::npos) {
56+
_image.attr("load")();
57+
const auto mode = _image.attr("mode").cast<std::string>();
58+
if (mode == "L")
59+
imgfmt = ImageFormat::Lum;
60+
else if (mode == "RGB")
61+
imgfmt = ImageFormat::RGB;
62+
else if (mode == "RGBA")
63+
imgfmt = ImageFormat::RGBX;
64+
else {
65+
// Unsupported mode in ImageFormat. Let's do conversion to L mode with PIL.
66+
_image = _image.attr("convert")("L");
67+
imgfmt = ImageFormat::Lum;
68+
}
69+
}
70+
71+
auto ai = _image.attr("__array_interface__").cast<py::dict>();
72+
auto ashape = ai["shape"].cast<py::tuple>();
73+
74+
if (ai.contains("data")) {
75+
auto adata = ai["data"];
76+
77+
if (py::isinstance<py::tuple>(adata)) {
78+
auto data_tuple = adata.cast<py::tuple>();
79+
auto ashape_std = ashape.cast<std::list<py::size_t>>();
80+
auto data_len = Reduce(ashape_std, py::size_t(1u), std::multiplies<py::size_t>());
81+
buffer = py::memoryview::from_memory(reinterpret_cast<void*>(data_tuple[0].cast<py::size_t>()), data_len, true);
82+
} else if (py::isinstance<py::buffer>(adata)) {
83+
// Numpy and our own __array_interface__ passes data as a buffer/bytes object
84+
buffer = adata.cast<py::buffer>();
85+
} else {
86+
throw py::type_error("No way to get data from __array_interface__");
87+
}
88+
} else {
89+
buffer = _image.cast<py::buffer>();
90+
}
91+
92+
py::tuple bshape;
93+
if (py::hasattr(buffer, "shape")) {
94+
bshape = buffer.attr("shape").cast<py::tuple>();
6895
}
96+
97+
if (!ashape.equal(bshape)) {
98+
auto bufferview = py::memoryview(buffer);
99+
buffer = bufferview.attr("cast")("B", ashape).cast<py::buffer>();
100+
}
101+
} else {
102+
buffer = _image.cast<py::buffer>();
69103
}
70-
image = _image.cast<Image>();
71104
#if PYBIND11_VERSION_HEX > 0x02080000 // py::raise_from is available starting from 2.8.0
72105
} catch (py::error_already_set &e) {
73-
py::raise_from(e, PyExc_TypeError, ("Could not convert " + _type + " to numpy array of dtype 'uint8'.").c_str());
106+
py::raise_from(e, PyExc_TypeError, ("Could not convert " + _type + " to buffer.").c_str());
74107
throw py::error_already_set();
75108
#endif
76109
} catch (...) {
77-
throw py::type_error("Could not convert " + _type + " to numpy array. Expecting a PIL Image or numpy array.");
110+
throw py::type_error("Could not convert " + _type + " to buffer. Expecting a PIL Image or buffer.");
78111
}
79-
const auto height = narrow_cast<int>(image.shape(0));
80-
const auto width = narrow_cast<int>(image.shape(1));
81-
const auto channels = image.ndim() == 2 ? 1 : narrow_cast<int>(image.shape(2));
112+
113+
/* Request a buffer descriptor from Python */
114+
py::buffer_info info = buffer.request();
115+
116+
if (info.format != py::format_descriptor<uint8_t>::format())
117+
throw py::type_error("Incompatible format: expected a uint8_t array.");
118+
119+
if (info.ndim != 2 && info.ndim != 3)
120+
throw py::type_error("Incompatible buffer dimension.");
121+
122+
const auto height = narrow_cast<int>(info.shape[0]);
123+
const auto width = narrow_cast<int>(info.shape[1]);
124+
const auto channels = info.ndim == 2 ? 1 : narrow_cast<int>(info.shape[2]);
82125
if (imgfmt == ImageFormat::None) {
83126
// Assume grayscale or BGR image depending on channels number
84127
if (channels == 1)
85128
imgfmt = ImageFormat::Lum;
86129
else if (channels == 3)
87130
imgfmt = ImageFormat::BGR;
88131
else
89-
throw py::value_error("Unsupported number of channels for numpy array: " + std::to_string(channels));
132+
throw py::value_error("Unsupported number of channels for buffer: " + std::to_string(channels));
90133
}
91134

92-
const auto bytes = image.data();
135+
const auto bytes = static_cast<uint8_t*>(info.ptr);
93136
// Disables the GIL during zxing processing (restored automatically upon completion)
94137
py::gil_scoped_release release;
95138
return ReadBarcodes({bytes, width, height, imgfmt, width * channels, channels}, hints);
@@ -108,17 +151,60 @@ Results read_barcodes(py::object _image, const BarcodeFormats& formats, bool try
108151
return read_barcodes_impl(_image, formats, try_rotate, try_downscale, text_mode, binarizer, is_pure, ean_add_on_symbol);
109152
}
110153

111-
Image write_barcode(BarcodeFormat format, std::string text, int width, int height, int quiet_zone, int ec_level)
154+
class WriteResult
155+
{
156+
public:
157+
WriteResult(std::vector<char>&& result_data_, std::vector<py::ssize_t>&& shape_)
158+
{
159+
result_data = result_data_;
160+
shape = shape_;
161+
ndim = shape.size();
162+
}
163+
164+
WriteResult(WriteResult const& o) : ndim(o.ndim), shape(o.shape), result_data(o.result_data) {}
165+
166+
const std::vector<char>& get_result_data() const { return result_data; }
167+
168+
static py::buffer_info get_buffer(const WriteResult& wr)
169+
{
170+
return py::buffer_info(
171+
reinterpret_cast<void*>(const_cast<char*>(wr.result_data.data())),
172+
sizeof(char), "B", wr.shape.size(), wr.shape, { wr.shape[0], py::ssize_t(1) }
173+
);
174+
}
175+
176+
const std::vector<py::ssize_t> get_shape() const { return shape; }
177+
178+
private:
179+
py::ssize_t ndim;
180+
std::vector<py::ssize_t> shape;
181+
std::vector<char> result_data;
182+
};
183+
184+
py::object write_barcode(BarcodeFormat format, std::string text, int width, int height, int quiet_zone, int ec_level)
112185
{
113186
auto writer = MultiFormatWriter(format).setEncoding(CharacterSet::UTF8).setMargin(quiet_zone).setEccLevel(ec_level);
114187
auto bitmap = writer.encode(text, width, height);
115188

116-
auto result = Image({bitmap.height(), bitmap.width()});
117-
auto r = result.mutable_unchecked<2>();
118-
for (py::ssize_t y = 0; y < r.shape(0); y++)
119-
for (py::ssize_t x = 0; x < r.shape(1); x++)
120-
r(y, x) = bitmap.get(narrow_cast<int>(x), narrow_cast<int>(y)) ? 0 : 255;
121-
return result;
189+
std::vector<char> result_data(bitmap.width() * bitmap.height());
190+
for (py::ssize_t y = 0; y < bitmap.height(); y++)
191+
for (py::ssize_t x = 0; x < bitmap.width(); x++)
192+
result_data[x + (y * bitmap.width())] = bitmap.get(narrow_cast<int>(x), narrow_cast<int>(y)) ? 0 : 255;
193+
194+
WriteResult res(std::move(result_data), {bitmap.height(), bitmap.width()});
195+
196+
auto resobj = py::cast(res);
197+
198+
// We add an __array_interface__ here to make the returned type easily convertible to PIL images,
199+
// Numpy arrays and other libraries that spport the interface.
200+
py::dict array_interface;
201+
array_interface["version"] = py::cast(3);
202+
array_interface["data"] = resobj;
203+
array_interface["shape"] = res.get_shape();
204+
array_interface["typestr"] = py::cast("|u1");
205+
resobj.attr("__array_interface__") = array_interface;
206+
207+
return resobj;
122208
}
123209

124210

@@ -213,6 +299,10 @@ PYBIND11_MODULE(zxingcpp, m)
213299
oss << pos;
214300
return oss.str();
215301
});
302+
py::class_<WriteResult>(m, "WriteResult", "Result of barcode writing", py::buffer_protocol(), py::dynamic_attr())
303+
.def_property_readonly("shape", &WriteResult::get_shape,
304+
":rtype: tuple")
305+
.def_buffer(&WriteResult::get_buffer);
216306
py::class_<Result>(m, "Result", "Result of barcode reading")
217307
.def_property_readonly("valid", &Result::isValid,
218308
":return: whether or not result is valid (i.e. a symbol was found)\n"
@@ -265,8 +355,9 @@ PYBIND11_MODULE(zxingcpp, m)
265355
py::arg("is_pure") = false,
266356
py::arg("ean_add_on_symbol") = EanAddOnSymbol::Ignore,
267357
"Read (decode) a barcode from a numpy BGR or grayscale image array or from a PIL image.\n\n"
268-
":type image: numpy.ndarray|PIL.Image.Image\n"
358+
":type image: buffer|numpy.ndarray|PIL.Image.Image\n"
269359
":param image: The image object to decode. The image can be either:\n"
360+
" - a buffer with the correct shape, use .cast on memory view to convert\n"
270361
" - a numpy array containing image either in grayscale (1 byte per pixel) or BGR mode (3 bytes per pixel)\n"
271362
" - a PIL Image\n"
272363
":type formats: zxing.BarcodeFormat|zxing.BarcodeFormats\n"
@@ -302,8 +393,9 @@ PYBIND11_MODULE(zxingcpp, m)
302393
py::arg("is_pure") = false,
303394
py::arg("ean_add_on_symbol") = EanAddOnSymbol::Ignore,
304395
"Read (decode) multiple barcodes from a numpy BGR or grayscale image array or from a PIL image.\n\n"
305-
":type image: numpy.ndarray|PIL.Image.Image\n"
396+
":type image: buffer|numpy.ndarray|PIL.Image.Image\n"
306397
":param image: The image object to decode. The image can be either:\n"
398+
" - a buffer with the correct shape, use .cast on memory view to convert\n"
307399
" - a numpy array containing image either in grayscale (1 byte per pixel) or BGR mode (3 bytes per pixel)\n"
308400
" - a PIL Image\n"
309401
":type formats: zxing.BarcodeFormat|zxing.BarcodeFormats\n"
@@ -336,7 +428,7 @@ PYBIND11_MODULE(zxingcpp, m)
336428
py::arg("height") = 0,
337429
py::arg("quiet_zone") = -1,
338430
py::arg("ec_level") = -1,
339-
"Write (encode) a text into a barcode and return numpy (grayscale) image array\n\n"
431+
"Write (encode) a text into a barcode and return 8-bit grayscale bitmap buffer\n\n"
340432
":type format: zxing.BarcodeFormat\n"
341433
":param format: format of the barcode to create\n"
342434
":type text: str\n"
@@ -353,5 +445,6 @@ PYBIND11_MODULE(zxingcpp, m)
353445
":type ec_level: int\n"
354446
":param ec_level: error correction level of the barcode\n"
355447
" (Used for Aztec, PDF417, and QRCode only)."
448+
":rtype: zxing.WriteResult\n"
356449
);
357450
}

0 commit comments

Comments
 (0)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy