11from __future__ import absolute_import
22
33import os .path
4- from itertools import chain
4+ import itertools
55import 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
109from ocrd import Processor
1110from 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
238235def 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
0 commit comments