Skip to content

Commit

Permalink
#21 Polygons around barcodes
Browse files Browse the repository at this point in the history
  • Loading branch information
quicklizard99 committed Mar 21, 2018
1 parent 7b4441c commit c4f45d5
Show file tree
Hide file tree
Showing 6 changed files with 220 additions and 40 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
### v0.1.6

* #19 Improve error messages
* #21 Polygons around barcodes

### v0.1.5

Expand Down
96 changes: 87 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,8 +49,24 @@ The `decode` function accepts instances of `PIL.Image`.
>>> from pyzbar.pyzbar import decode
>>> from PIL import Image
>>> decode(Image.open('pyzbar/tests/code128.png'))
[Decoded(data=b'Foramenifera', type='CODE128', rect=Rect(left=37, top=550, width=324, height=76)),
Decoded(data=b'Rana temporaria', type='CODE128', rect=Rect(left=4, top=0, width=390, height=76))]
[
Decoded(
data=b'Foramenifera', type='CODE128',
rect=Rect(left=37, top=550, width=324, height=76),
polygon=[
Point(x=37, y=551), Point(x=37, y=625), Point(x=361, y=626),
Point(x=361, y=550)
]
)
Decoded(
data=b'Rana temporaria', type='CODE128',
rect=Rect(left=4, top=0, width=390, height=76),
polygon=[
Point(x=4, y=1), Point(x=4, y=75), Point(x=394, y=76),
Point(x=394, y=0)
]
)
]
```

It also accepts instances of `numpy.ndarray`, which might come from loading
Expand All @@ -59,8 +75,24 @@ images using [OpenCV](http://opencv.org/).
```
>>> import cv2
>>> decode(cv2.imread('pyzbar/tests/code128.png'))
[Decoded(data=b'Foramenifera', type='CODE128', rect=Rect(left=37, top=550, width=324, height=76)),
Decoded(data=b'Rana temporaria', type='CODE128', rect=Rect(left=4, top=0, width=390, height=76))]
[
Decoded(
data=b'Foramenifera', type='CODE128',
rect=Rect(left=37, top=550, width=324, height=76),
polygon=[
Point(x=37, y=551), Point(x=37, y=625), Point(x=361, y=626),
Point(x=361, y=550)
]
)
Decoded(
data=b'Rana temporaria', type='CODE128',
rect=Rect(left=4, top=0, width=390, height=76),
polygon=[
Point(x=4, y=1), Point(x=4, y=75), Point(x=394, y=76),
Point(x=394, y=0)
]
)
]
```

You can also provide a tuple `(pixels, width, height)`, where the image data
Expand All @@ -72,14 +104,46 @@ is eight bits-per-pixel.
>>> # 8 bpp by considering just the blue channel
>>> decode((image[:, :, 0].astype('uint8').tobytes(), width, height))
[Decoded(data=b'Foramenifera', type='CODE128', rect=Rect(left=37, top=550, width=324, height=76)),
Decoded(data=b'Rana temporaria', type='CODE128', rect=Rect(left=4, top=0, width=390, height=76))]
[
Decoded(
data=b'Foramenifera', type='CODE128',
rect=Rect(left=37, top=550, width=324, height=76),
polygon=[
Point(x=37, y=551), Point(x=37, y=625), Point(x=361, y=626),
Point(x=361, y=550)
]
)
Decoded(
data=b'Rana temporaria', type='CODE128',
rect=Rect(left=4, top=0, width=390, height=76),
polygon=[
Point(x=4, y=1), Point(x=4, y=75), Point(x=394, y=76),
Point(x=394, y=0)
]
)
]
>>> # 8 bpp by converting image to greyscale
>>> grey = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
>>> decode((grey.tobytes(), width, height))
[Decoded(data=b'Foramenifera', type='CODE128', rect=Rect(left=37, top=550, width=324, height=76)),
Decoded(data=b'Rana temporaria', type='CODE128', rect=Rect(left=4, top=0, width=390, height=76))]
[
Decoded(
data=b'Foramenifera', type='CODE128',
rect=Rect(left=37, top=550, width=324, height=76),
polygon=[
Point(x=37, y=551), Point(x=37, y=625), Point(x=361, y=626),
Point(x=361, y=550)
]
)
Decoded(
data=b'Rana temporaria', type='CODE128',
rect=Rect(left=4, top=0, width=390, height=76),
polygon=[
Point(x=4, y=1), Point(x=4, y=75), Point(x=394, y=76),
Point(x=394, y=0)
]
)
]
>>> # If you don't provide 8 bpp
>>> decode((image.tobytes(), width, height))
Expand All @@ -97,13 +161,27 @@ symbol types
>>> from pyzbar.pyzbar import ZBarSymbol
>>> # Look for just qrcode
>>> decode(Image.open('pyzbar/tests/qrcode.png'), symbols=[ZBarSymbol.QRCODE])
[Decoded(data=b'Thalassiodracon', type='QRCODE', rect=Rect(left=27, top=27, width=145, height=145))]
[
Decoded(
data=b'Thalassiodracon', type='QRCODE',
rect=Rect(left=27, top=27, width=145, height=145),
polygon=[
Point(x=27, y=27), Point(x=27, y=172), Point(x=172, y=172),
Point(x=172, y=27)
]
)
]
>>> # If we look for just code128, the qrcodes in the image will not be detected
>>> decode(Image.open('pyzbar/tests/qrcode.png'), symbols=[ZBarSymbol.CODE128])
[]
```

## Bounding boxes and polygon

**TODO** Description and graphical representation

## Windows error message
If you see an ugly `ImportError` when importing `pyzbar` on Windows you will
most likely need the
Expand Down
68 changes: 68 additions & 0 deletions pyzbar/locations.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
from collections import namedtuple
from itertools import chain
from operator import itemgetter


__all__ = ['bounding_box', 'convex_hull', 'Rect']


Point = namedtuple('Point', ['x', 'y'])
Rect = namedtuple('Rect', ['left', 'top', 'width', 'height'])


def bounding_box(locations):
"""Computes the bounding box of an iterable of (x, y) coordinates.
Args:
locations: iterable of (x, y) tuples.
Returns:
`Rect`: Coordinates of the bounding box.
"""
x_values = list(map(itemgetter(0), locations))
x_min, x_max = min(x_values), max(x_values)
y_values = list(map(itemgetter(1), locations))
y_min, y_max = min(y_values), max(y_values)
return Rect(x_min, y_min, x_max - x_min, y_max - y_min)


def convex_hull(points):
"""Computes the convex hull of an iterable of (x, y) coordinates.
Args:
locations: iterable of (x, y) tuples.
Returns:
`list`: instances of `Point` - vertices of the convex hull in
counter-clockwise order, starting from the vertex with the
lexicographically smallest coordinates.
Andrew's monotone chain algorithm. O(n log n) complexity.
https://en.wikibooks.org/wiki/Algorithm_Implementation/Geometry/Convex_hull/Monotone_chain
"""

def is_not_clockwise(p0, p1, p2):
return 0 <= (
(p1[0] - p0[0]) * (p2[1] - p0[1]) -
(p1[1] - p0[1]) * (p2[0] - p0[0])
)

def go(points):
res = []
for p in points:
while 1 < len(res) and is_not_clockwise(res[-2], res[-1], p):
res.pop()
res.append(p)

# The last point in each list is the first point in the other list
res.pop()

return res

# Discard duplicates and sort by x then y
points = sorted(set(points))

if len(points) < 2:
return points
else:
return list(map(Point._make, chain(go(points), go(reversed(points)))))
34 changes: 7 additions & 27 deletions pyzbar/pyzbar.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@
from collections import namedtuple
from contextlib import contextmanager
from ctypes import cast, c_void_p, string_at
from operator import itemgetter

from .locations import bounding_box, convex_hull, Point, Rect
from .pyzbar_error import PyZbarError
from .wrapper import (
zbar_image_scanner_set_config,
Expand All @@ -16,14 +16,10 @@
zbar_symbol_next, ZBarConfig, ZBarSymbol, EXTERNAL_DEPENDENCIES
)

__all__ = ['decode', 'EXTERNAL_DEPENDENCIES']
__all__ = ['decode', 'Point', 'Rect', 'Decoded', 'EXTERNAL_DEPENDENCIES']


# A rectangle
Rect = namedtuple('Rect', ['left', 'top', 'width', 'height'])

# Results of reading a barcode
Decoded = namedtuple('Decoded', ['data', 'type', 'rect'])
Decoded = namedtuple('Decoded', ['data', 'type', 'rect', 'polygon'])

# ZBar's magic 'fourcc' numbers that represent image formats
_FOURCC = {
Expand Down Expand Up @@ -76,23 +72,6 @@ def _image_scanner():
zbar_image_scanner_destroy(scanner)


def _bounding_box_of_locations(locations):
"""Computes a bounding box from scan locations.
Args:
locations: iterable of tuples of ints (x, y).
Returns:
`Rect`: Coordinates of the bounding box.
"""
x_values = list(map(itemgetter(0), locations))
x_min, x_max = min(x_values), max(x_values)
y_values = list(map(itemgetter(1), locations))
y_min, y_max = min(y_values), max(y_values)
return Rect(x_min, y_min, x_max - x_min, y_max - y_min)


def _symbols_for_image(image):
"""Generator of symbols.
Expand Down Expand Up @@ -121,18 +100,19 @@ def _decode_symbols(symbols):
data = string_at(zbar_symbol_get_data(symbol))
# The 'type' int in a value in the ZBarSymbol enumeration
symbol_type = ZBarSymbol(symbol.contents.type).name
locations = [
polygon = convex_hull(
(
zbar_symbol_get_loc_x(symbol, index),
zbar_symbol_get_loc_y(symbol, index)
)
for index in _RANGEFN(zbar_symbol_get_loc_size(symbol))
]
)

yield Decoded(
data=data,
type=symbol_type,
rect=_bounding_box_of_locations(locations),
rect=bounding_box(polygon),
polygon=polygon
)


Expand Down
50 changes: 50 additions & 0 deletions pyzbar/tests/test_locations.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import unittest

from pyzbar.locations import bounding_box, convex_hull, Rect


class TestLocations(unittest.TestCase):
def test_bounding_box(self):
self.assertRaises(ValueError, bounding_box, [])
self.assertEqual(
Rect(left=0, top=0, width=0, height=0),
bounding_box([(0, 0)])
)
self.assertEqual(
Rect(left=37, top=550, width=324, height=76),
bounding_box([(37, 551), (37, 625), (361, 626), (361, 550)])
)

def test_convex_hull_empty(self):
self.assertEqual([], convex_hull([]))

def test_convex_square(self):
points = [(0, 0), (0, 1), (1, 1), (1, 0)]
self.assertEqual(points, convex_hull(points)),

def test_convex_duplicates(self):
points = [(0, 0), (0, 1), (1, 1), (1, 0)]
self.assertEqual(points, convex_hull(points * 10)),

def test_other(self):
# Taken from
# https://codegolf.stackexchange.com/questions/11035/find-the-convex-hull-of-a-set-of-2d-points
res = convex_hull([(1, 1), (2, 2), (3, 3), (1, 3)])
self.assertEqual([(1, 1), (1, 3), (3, 3)], res)

res = convex_hull([
(4.4, 14), (6.7, 15.25), (6.9, 12.8), (2.1, 11.1), (9.5, 14.9),
(13.2, 11.9), (10.3, 12.3), (6.8, 9.5), (3.3, 7.7), (0.6, 5.1),
(5.3, 2.4), (8.45, 4.7), (11.5, 9.6), (13.8, 7.3), (12.9, 3.1),
(11, 1.1)
])

expected = [
(0.6, 5.1), (2.1, 11.1), (4.4, 14), (6.7, 15.25), (9.5, 14.9),
(13.2, 11.9), (13.8, 7.3), (12.9, 3.1), (11, 1.1), (5.3, 2.4)
]
self.assertEqual(expected, res)


if __name__ == '__main__':
unittest.main()
11 changes: 7 additions & 4 deletions pyzbar/tests/test_pyzbar.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@


from pyzbar.pyzbar import (
decode, Rect, Decoded, ZBarSymbol, EXTERNAL_DEPENDENCIES
decode, Decoded, Rect, ZBarSymbol, EXTERNAL_DEPENDENCIES
)
from pyzbar.pyzbar_error import PyZbarError

Expand All @@ -33,20 +33,23 @@ class TestDecode(unittest.TestCase):
Decoded(
data=b'Foramenifera',
type='CODE128',
rect=Rect(left=37, top=550, width=324, height=76)
rect=Rect(left=37, top=550, width=324, height=76),
polygon=[(37, 551), (37, 625), (361, 626), (361, 550)]
),
Decoded(
data=b'Rana temporaria',
type='CODE128',
rect=Rect(left=4, top=0, width=390, height=76)
rect=Rect(left=4, top=0, width=390, height=76),
polygon=[(4, 1), (4, 75), (394, 76), (394, 0)]
)
]

EXPECTED_QRCODE = [
Decoded(
b'Thalassiodracon',
type='QRCODE',
rect=Rect(left=27, top=27, width=145, height=145)
rect=Rect(left=27, top=27, width=145, height=145),
polygon=[(27, 27), (27, 172), (172, 172), (172, 27)]
)
]

Expand Down

0 comments on commit c4f45d5

Please sign in to comment.