Skip to content

Commit 13d4510

Browse files
author
Robert Sachunsky
committed
repair/project: join polygons directly instead of alphashape
1 parent 43e4c9c commit 13d4510

4 files changed

Lines changed: 42 additions & 41 deletions

File tree

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,10 @@ Versioned according to [Semantic Versioning](http://semver.org/).
44

55
## Unreleased
66

7+
Fixed:
8+
9+
* repair/project: join polygons directly instead of alphashape
10+
711
## [0.1.20] - 2022-04-27
812

913
Fixed:

ocrd_segment/project.py

Lines changed: 36 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,10 @@
11
from __future__ import absolute_import
22

33
import os.path
4-
from itertools import chain
4+
import itertools
55
import numpy as np
6-
from shapely.geometry import Polygon
7-
from shapely.ops import unary_union
8-
import alphashape
6+
from shapely.geometry import Polygon, LineString
7+
from shapely.ops import unary_union, nearest_points
98

109
from ocrd import Processor
1110
from ocrd_utils import (
@@ -144,39 +143,38 @@ def _process_segment(self, segment, constituents, page_id):
144143
else:
145144
segment.set_Coords(coords)
146145

147-
def join_polygons(polygons, loc='', scale=20):
148-
"""construct concave hull (alpha shape) from input polygons"""
149-
LOG = getLogger('processor.ProjectHull')
146+
def pairwise(iterable):
147+
a, b = itertools.tee(iterable)
148+
next(b, None)
149+
return zip(a, b)
150+
151+
def join_polygons(polygons, scale=20):
152+
"""construct concave hull (alpha shape) from input polygons by connecting their pairwise nearest points"""
150153
# ensure input polygons are simply typed
151-
polygons = list(chain.from_iterable([
154+
polygons = list(itertools.chain.from_iterable([
152155
poly.geoms if poly.type in ['MultiPolygon', 'GeometryCollection']
153156
else [poly]
154157
for poly in polygons]))
155-
if len(polygons) == 1:
158+
npoly = len(polygons)
159+
if npoly == 1:
156160
return polygons[0]
157-
# get equidistant list of points along hull
158-
# (otherwise alphashape will jump across the interior)
159-
points = [poly.exterior.interpolate(dist).coords[0] # .xy
160-
for poly in polygons
161-
for dist in np.arange(0, poly.length, min(scale / 2, poly.length / 4))]
162-
#alpha = alphashape.optimizealpha(points) # too slow
163-
alpha = 0.01
164-
jointp = alphashape.alphashape(points, alpha)
165-
tries = 0
166-
# from descartes import PolygonPatch
167-
# import matplotlib.pyplot as plt
168-
while jointp.is_empty or jointp.area == 0.0 or jointp.type in ['MultiPolygon', 'GeometryCollection'] or len(jointp.interiors):
169-
# plt.figure()
170-
# plt.gca().scatter(*zip(*points))
171-
# for geom in jointp.geoms:
172-
# plt.gca().add_patch(PolygonPatch(geom, alpha=0.2))
173-
# plt.show()
174-
alpha *= 0.7
175-
tries += 1
176-
if tries > 10:
177-
LOG.warning("cannot find alpha for concave hull on '%s'", loc)
178-
alpha = 0
179-
jointp = alphashape.alphashape(points, alpha)
161+
# find min-dist path through all polygons (travelling salesman)
162+
pairs = itertools.combinations(range(npoly), 2)
163+
paths = list(itertools.permutations(range(npoly)))
164+
dists = np.eye(npoly, dtype=float)
165+
for i, j in pairs:
166+
dists[i, j] = polygons[i].distance(polygons[j])
167+
dists[j, i] = dists[i, j]
168+
dists = [sum(dists[i, j] for i, j in pairwise(path))
169+
for path in paths]
170+
path = paths[min(enumerate(dists), key=lambda x: x[1])[0]]
171+
polygons = [polygons[i] for i in path]
172+
# iteratively join to next nearest neighbour
173+
jointp = polygons[0]
174+
for thisp, nextp in pairwise(polygons):
175+
nearest = nearest_points(jointp, nextp)
176+
bridgep = LineString(nearest).buffer(max(1, scale/5), resolution=1)
177+
jointp = unary_union([jointp, bridgep, nextp])
180178
if jointp.minimum_clearance < 1.0:
181179
# follow-up calculations will necessarily be integer;
182180
# so anticipate rounding here and then ensure validity
@@ -226,8 +224,7 @@ def make_intersection(poly1, poly2):
226224
interp = unary_union([geom for geom in interp.geoms if geom.area > 0])
227225
if interp.type == 'MultiPolygon':
228226
# homogeneous result: construct convex hull to connect
229-
# FIXME: construct concave hull / alpha shape
230-
interp = interp.convex_hull
227+
interp = join_polygons(interp.geoms)
231228
if interp.minimum_clearance < 1.0:
232229
# follow-up calculations will necessarily be integer;
233230
# so anticipate rounding here and then ensure validity
@@ -236,15 +233,16 @@ def make_intersection(poly1, poly2):
236233
return interp
237234

238235
def make_valid(polygon):
239-
for split in range(1, len(polygon.exterior.coords)-1):
236+
points = list(polygon.exterior.coords)
237+
for split in range(1, len(points)):
240238
if polygon.is_valid or polygon.simplify(polygon.area).is_valid:
241239
break
242240
# simplification may not be possible (at all) due to ordering
243241
# in that case, try another starting point
244-
polygon = Polygon(polygon.exterior.coords[-split:]+polygon.exterior.coords[:-split])
245-
for tolerance in range(1, int(polygon.area)):
242+
polygon = Polygon(points[-split:]+points[:-split])
243+
for tolerance in range(int(polygon.area)):
246244
if polygon.is_valid:
247245
break
248246
# simplification may require a larger tolerance
249-
polygon = polygon.simplify(tolerance)
247+
polygon = polygon.simplify(tolerance + 1)
250248
return polygon

ocrd_segment/repair.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -162,7 +162,7 @@ def process(self):
162162
try:
163163
ensure_consistent(element)
164164
except Exception as e:
165-
LOG.error(str(e))
165+
LOG.error(str(e)) # exc_info=e
166166
report.add_error(error)
167167
continue
168168
else:
@@ -556,7 +556,7 @@ def shrink_regions(page_image, page_coords, page, page_id, padding=0):
556556
# pick contour and convert to absolute:
557557
region_polygon = join_polygons([make_valid(Polygon(contour[:, 0, ::]))
558558
for contour in contours
559-
if len(contour) >= 3], loc=region.id, scale=scale)
559+
if len(contour) >= 3], scale=scale)
560560
if padding:
561561
region_polygon = region_polygon.buffer(padding)
562562
region_polygon = coordinates_for_segment(region_polygon.exterior.coords[:-1], page_image, page_coords)

requirements.txt

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,4 +13,3 @@ opencv-python-headless
1313
# Is not allowed on PyPI:
1414
#pycocotools@git+https://github.com/bertsky/pycocotools#subdirectory=PythonAPI
1515
ocrd-fork-pycocotools
16-
alphashape

0 commit comments

Comments
 (0)