3
3
import os .path
4
4
import numpy as np
5
5
from skimage import draw
6
- from shapely .geometry import Polygon , asPolygon
6
+ from shapely .geometry import Polygon , asPolygon , LineString
7
7
from shapely .prepared import prep
8
8
from shapely .ops import unary_union
9
9
import alphashape
20
20
coordinates_of_segment ,
21
21
coordinates_for_segment ,
22
22
points_from_polygon ,
23
+ polygon_from_points ,
24
+ transform_coordinates ,
23
25
MIMETYPE_PAGE
24
26
)
25
27
@@ -58,25 +60,39 @@ def process(self):
58
60
Open and deserialise PAGE input files and their respective images,
59
61
then iterate over the element hierarchy down to the line level.
60
62
61
- Next, get the image according to the layout annotation (from
63
+ Next, get the page image according to the layout annotation (from
62
64
the alternative image of the page, or by cropping from annotated
63
- Border and rotating from annotated orientation), and compute a new
64
- line segmentation for that (as a label mask, suppressing all non-text
65
- regions' foreground), and polygonalize its contours.
65
+ Border and rotating from annotated orientation).
66
66
67
- Then calculate overlaps between the new and existing lines, i.e.
68
- which existing line polygons (or rectangles) contain most of each
69
- new line polygon: Among the existing lines covering most of each
67
+ \b
68
+ If ``method`` is `lineest`, then compute a new line segmentation
69
+ for that image (suppressing the foreground non-text regions), and
70
+ polygonalize its contours. Now calculate overlaps between the new
71
+ and old lines, i.e. which existing polygons (or rectangles) match
72
+ each new line polygon: Among the existing lines covering most of each
70
73
new line's foreground and background area, assign the one with the
71
74
largest share of the existing line. Next, for each existing line,
72
- calculate the hull polygon of all assigned new lines, and if the
73
- foreground and background overlap is sufficient, and no overlapping
74
- but yet unassigned lines would be lost, then annotate that polygon
75
- as new coordinates.
76
- Thus, at the end, all new and existing lines will have been used
75
+ calculate the concave hull polygon of all assigned new lines, and
76
+ if the foreground and background overlap is sufficient, and if no
77
+ overlapping but yet unassigned lines would be lost, then annotate
78
+ that polygon as new coordinates.
79
+ ( Thus, at the end, all new and existing lines will have been used
77
80
at most once, but not all existing lines might have been resegmented
78
- (either because there were no matches at all, or the loss would have
79
- been too large, either by fg/bg share or by unassigned line labels).
81
+ – either because there were no matches at all, or the loss would have
82
+ been too large in terms of fg/bg share.)
83
+
84
+ Otherwise, first compute a connected component analysis of that image.
85
+ Next, if ``method`` is `ccomps`, then calculate a distance transform
86
+ of the existing (overlapping) line labels and flatten them by selecting
87
+ the labels with maximum distance, respectively. Else if ``method`` is
88
+ `baseline`, then create a flat segmentation by applying dilation on the
89
+ top side of the baselines. Subsequently, regardless of the ``method``,
90
+ propagate these new line seeds to the connected components, with conflicts
91
+ taking the majority label. Spread these foreground labels into the
92
+ background and find contour polygons for them. For each line, calculate
93
+ the concave hull polygon of its constituent contours. If the foreground
94
+ and background overlap is sufficent, then annotate that polygon as new
95
+ coordinates.
80
96
81
97
Produce a new output file by serialising the resulting hierarchy.
82
98
"""
@@ -181,7 +197,8 @@ def _process_segment(self, parent, parent_image, parent_coords, page_id, zoom, l
181
197
fullpage = False
182
198
report = check_region (parent_bin , zoom )
183
199
if report :
184
- LOG .warning ('Invalid %s "%s": %s' , tag , parent .id , report )
200
+ LOG .warning ('Invalid %s "%s": %s' , tag ,
201
+ page_id if fullpage else parent .id , report )
185
202
return
186
203
# get existing line labels:
187
204
line_labels = np .zeros_like (parent_bin , np .bool )
@@ -203,7 +220,7 @@ def _process_segment(self, parent, parent_image, parent_coords, page_id, zoom, l
203
220
# only text region(s) may contain new text lines
204
221
for i , segment in enumerate (set (line .parent_object_ for line in lines )):
205
222
LOG .debug ('unmasking area of text region "%s" for "%s"' ,
206
- segment .id , parent .id )
223
+ segment .id , page_id if fullpage else parent .id )
207
224
segment_polygon = coordinates_of_segment (segment , parent_image , parent_coords )
208
225
segment_polygon = make_valid (Polygon (segment_polygon )).buffer (margin )
209
226
segment_polygon = np .array (segment_polygon .exterior , np .int )[:- 1 ]
@@ -212,8 +229,8 @@ def _process_segment(self, parent, parent_image, parent_coords, page_id, zoom, l
212
229
parent_bin .shape )] = False
213
230
# mask/ignore overlapping neighbours
214
231
for i , segment in enumerate (ignore ):
215
- LOG .debug ('masking area of %s "%s" for "%s"' ,
216
- type ( segment ). __name__ [: - 4 ], segment . id , parent .id )
232
+ LOG .debug ('masking area of %s "%s" for "%s"' , type ( segment ). __name__ [: - 4 ],
233
+ segment . id , page_id if fullpage else parent .id )
217
234
segment_polygon = coordinates_of_segment (segment , parent_image , parent_coords )
218
235
ignore_bin [draw .polygon (segment_polygon [:, 1 ],
219
236
segment_polygon [:, 0 ],
@@ -222,12 +239,6 @@ def _process_segment(self, parent, parent_image, parent_coords, page_id, zoom, l
222
239
LOG .debug ('calculating connected component and distance transforms for "%s"' , parent .id )
223
240
bin = parent_bin & ~ ignore_bin
224
241
components , _ = morph .label (bin )
225
- labels = np .insert (line_labels , 0 , ignore_bin , axis = 0 )
226
- distances = np .zeros_like (labels , np .uint8 )
227
- for i , label in enumerate (labels ):
228
- distances [i ] = morph .dist_labels (label .astype (np .uint8 ))
229
- # normalize the distances of all lines so larger ones do not displace smaller ones
230
- distances [i ] = distances [i ] / distances [i ].max () * 255
231
242
# estimate glyph scale (roughly)
232
243
_ , counts = np .unique (components , return_counts = True )
233
244
if counts .shape [0 ] > 1 :
@@ -237,15 +248,42 @@ def _process_segment(self, parent, parent_image, parent_coords, page_id, zoom, l
237
248
LOG .debug ("estimated scale: %d" , scale )
238
249
else :
239
250
scale = 43
240
- spread_dist (lines , line_labels , distances , parent_bin , components , parent_coords ,
251
+ if method == 'ccomps' :
252
+ labels = np .insert (line_labels , 0 , ignore_bin , axis = 0 )
253
+ distances = np .zeros_like (labels , np .uint8 )
254
+ for i , label in enumerate (labels ):
255
+ distances [i ] = morph .dist_labels (label .astype (np .uint8 ))
256
+ # normalize the distances of all lines so larger ones do not displace smaller ones
257
+ distances [i ] = distances [i ] / distances [i ].max () * 255
258
+ # use depth to flatten overlapping lines as seed labels
259
+ new_labels = np .argmax (distances , axis = 0 )
260
+ else :
261
+ new_labels = np .zeros_like (parent_bin , np .uint8 )
262
+ for i , line in enumerate (lines ):
263
+ if line .Baseline is None :
264
+ LOG .warning ("Skipping '%s' without baseline" , line .id )
265
+ new_labels [line_labels [i ]] = i + 1
266
+ continue
267
+ line_polygon = baseline_of_segment (line , parent_coords )
268
+ line_ltr = line_polygon [0 ,0 ] < line_polygon [- 1 ,0 ]
269
+ line_polygon = make_valid (LineString (line_polygon ).buffer (
270
+ # left-hand side if left-to-right, and vice versa
271
+ scale * (- 1 ) ** line_ltr , single_sided = True ))
272
+ line_polygon = np .array (line_polygon .exterior , np .int )[:- 1 ]
273
+ line_y , line_x = draw .polygon (line_polygon [:, 1 ],
274
+ line_polygon [:, 0 ],
275
+ parent_bin .shape )
276
+ new_labels [line_y , line_x ] = i + 1
277
+ spread_dist (lines , line_labels , new_labels , parent_bin , components , parent_coords ,
241
278
scale = scale , loc = parent .id , threshold = threshold )
242
279
return
243
280
try :
244
281
new_line_labels , _ , _ , _ , _ , scale = compute_segmentation (
245
282
parent_bin , seps = ignore_bin , zoom = zoom , fullpage = fullpage ,
246
283
maxseps = 0 , maxcolseps = len (ignore ), maximages = 0 )
247
284
except Exception as err :
248
- LOG .warning ('Cannot line-segment %s "%s": %s' , tag , parent .id , err )
285
+ LOG .warning ('Cannot line-segment %s "%s": %s' ,
286
+ tag , page_id if fullpage else parent .id , err )
249
287
return
250
288
LOG .info ("Found %d new line labels for %d existing lines on %s '%s'" ,
251
289
new_line_labels .max (), len (lines ), tag , parent .id )
@@ -375,53 +413,52 @@ def _process_segment(self, parent, parent_image, parent_coords, page_id, zoom, l
375
413
continue
376
414
otherline .get_Coords ().set_points (points_from_polygon (other_polygon ))
377
415
378
- def spread_dist (lines , labels , distances , binarized , components , coords ,
416
+ def spread_dist (lines , old_labels , new_labels , binarized , components , coords ,
379
417
scale = 43 , loc = '' , threshold = 0.9 ):
380
- """redefine line coordinates by contourizing spread of connected components with max-distance of existing labels
381
- """
418
+ """redefine line coordinates by contourizing spread of connected components propagated from new labels"""
382
419
LOG = getLogger ('processor.OcropyResegment' )
383
- # use depth to flatten overlapping lines as seed labels
384
- max_labels = np .argmax (distances , axis = 0 )
385
420
# allocate to connected components consistently (by majority,
386
421
# ignoring smallest components like punctuation)
387
- #max_ccomps = morph.propagate_labels_majority(binarized, max_labels )
388
- max_ccomps = morph .propagate_labels_majority (components > 0 , max_labels )
422
+ #new_labels = morph.propagate_labels_majority(binarized, new_labels )
423
+ new_labels = morph .propagate_labels_majority (components > 0 , new_labels )
389
424
# dilate/grow labels from connected components against each other and bg
390
- max_ccomps = morph .spread_labels (max_ccomps , maxdist = scale / 2 )
425
+ new_labels = morph .spread_labels (new_labels , maxdist = scale / 2 )
391
426
# find polygon hull and modify line coords
392
427
for i , line in enumerate (lines ):
393
- mask = max_ccomps == i + 1
394
- label = labels [i ]
395
- count = np .count_nonzero (label )
428
+ new_label = new_labels == i + 1
429
+ old_label = old_labels [i ]
430
+ if np .equal (new_label , old_label ).all ():
431
+ continue
432
+ count = np .count_nonzero (old_label )
396
433
if not count :
397
434
LOG .warning ("skipping zero-area line '%s'" , line .id )
398
435
continue
399
- covers = np .count_nonzero (mask ) / count
436
+ covers = np .count_nonzero (new_label ) / count
400
437
if covers < threshold / 3 :
401
438
LOG .debug ("new line for '%s' only covers %.1f%% bg" ,
402
439
line .id , covers * 100 )
403
440
continue
404
- count = np .count_nonzero (label * binarized )
441
+ count = np .count_nonzero (old_label * binarized )
405
442
if not count :
406
443
LOG .warning ("skipping binarizy-empty line '%s'" , line .id )
407
444
continue
408
- covers = np .count_nonzero (mask * binarized ) / count
445
+ covers = np .count_nonzero (new_label * binarized ) / count
409
446
if covers < threshold :
410
447
LOG .debug ("new line for '%s' only covers %.1f%% fg" ,
411
448
line .id , covers * 100 )
412
449
continue
413
450
LOG .debug ('Black pixels before/after resegment of line "%s": %d/%d' ,
414
451
line .id , count , covers * count )
415
452
contours = [contour [:,::- 1 ] # get x,y order again
416
- for contour , area in morph .find_contours (mask )]
453
+ for contour , area in morph .find_contours (new_label )]
417
454
#LOG.debug("joining %d subsegments for %s", len(contours), line.id)
418
455
if len (contours ) == 0 :
419
456
LOG .warning ("no contours for %s - keeping" , line .id )
420
457
continue
421
458
else :
422
459
# get alpha shape
423
460
poly = join_polygons ([make_valid (Polygon (contour ))
424
- for contour in contours ], loc = loc )
461
+ for contour in contours ], loc = line . id )
425
462
poly = poly .exterior .coords [:- 1 ]
426
463
polygon = coordinates_for_segment (poly , None , coords )
427
464
polygon = polygon_for_parent (polygon , line .parent_object_ )
@@ -475,3 +512,9 @@ def join_polygons(polygons, loc=''):
475
512
jointp = asPolygon (np .round (jointp .exterior .coords ))
476
513
jointp = make_valid (jointp )
477
514
return jointp
515
+
516
+ # zzz should go into core ocrd_utils
517
+ def baseline_of_segment (segment , coords ):
518
+ line = np .array (polygon_from_points (segment .get_Baseline ().points ))
519
+ line = transform_coordinates (line , coords ['transform' ])
520
+ return np .round (line ).astype (np .int32 )
0 commit comments