Skip to content

Commit 14e27e1

Browse files
author
Robert Sachunsky
committed
resegment: implement alternative method=baseline
- calculate connected component analysis - find new line seeds based on the existing baselines (by applying dilation above) - propagate line seeds across connected components (by majority in case of conflict) - spread ccomps labels against each other into the background - for each line, * if enough background and foreground will be retained * find the hull polygon of the new line via alpha shape * annotate as new coordinates
1 parent 21cb7c9 commit 14e27e1

File tree

1 file changed

+86
-43
lines changed

1 file changed

+86
-43
lines changed

ocrd_cis/ocropy/resegment.py

Lines changed: 86 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
import os.path
44
import numpy as np
55
from skimage import draw
6-
from shapely.geometry import Polygon, asPolygon
6+
from shapely.geometry import Polygon, asPolygon, LineString
77
from shapely.prepared import prep
88
from shapely.ops import unary_union
99
import alphashape
@@ -20,6 +20,8 @@
2020
coordinates_of_segment,
2121
coordinates_for_segment,
2222
points_from_polygon,
23+
polygon_from_points,
24+
transform_coordinates,
2325
MIMETYPE_PAGE
2426
)
2527

@@ -58,25 +60,39 @@ def process(self):
5860
Open and deserialise PAGE input files and their respective images,
5961
then iterate over the element hierarchy down to the line level.
6062
61-
Next, get the image according to the layout annotation (from
63+
Next, get the page image according to the layout annotation (from
6264
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).
6666
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
7073
new line's foreground and background area, assign the one with the
7174
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
7780
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.
8096
8197
Produce a new output file by serialising the resulting hierarchy.
8298
"""
@@ -181,7 +197,8 @@ def _process_segment(self, parent, parent_image, parent_coords, page_id, zoom, l
181197
fullpage = False
182198
report = check_region(parent_bin, zoom)
183199
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)
185202
return
186203
# get existing line labels:
187204
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
203220
# only text region(s) may contain new text lines
204221
for i, segment in enumerate(set(line.parent_object_ for line in lines)):
205222
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)
207224
segment_polygon = coordinates_of_segment(segment, parent_image, parent_coords)
208225
segment_polygon = make_valid(Polygon(segment_polygon)).buffer(margin)
209226
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
212229
parent_bin.shape)] = False
213230
# mask/ignore overlapping neighbours
214231
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)
217234
segment_polygon = coordinates_of_segment(segment, parent_image, parent_coords)
218235
ignore_bin[draw.polygon(segment_polygon[:, 1],
219236
segment_polygon[:, 0],
@@ -222,12 +239,6 @@ def _process_segment(self, parent, parent_image, parent_coords, page_id, zoom, l
222239
LOG.debug('calculating connected component and distance transforms for "%s"', parent.id)
223240
bin = parent_bin & ~ ignore_bin
224241
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
231242
# estimate glyph scale (roughly)
232243
_, counts = np.unique(components, return_counts=True)
233244
if counts.shape[0] > 1:
@@ -237,15 +248,42 @@ def _process_segment(self, parent, parent_image, parent_coords, page_id, zoom, l
237248
LOG.debug("estimated scale: %d", scale)
238249
else:
239250
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,
241278
scale=scale, loc=parent.id, threshold=threshold)
242279
return
243280
try:
244281
new_line_labels, _, _, _, _, scale = compute_segmentation(
245282
parent_bin, seps=ignore_bin, zoom=zoom, fullpage=fullpage,
246283
maxseps=0, maxcolseps=len(ignore), maximages=0)
247284
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)
249287
return
250288
LOG.info("Found %d new line labels for %d existing lines on %s '%s'",
251289
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
375413
continue
376414
otherline.get_Coords().set_points(points_from_polygon(other_polygon))
377415

378-
def spread_dist(lines, labels, distances, binarized, components, coords,
416+
def spread_dist(lines, old_labels, new_labels, binarized, components, coords,
379417
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"""
382419
LOG = getLogger('processor.OcropyResegment')
383-
# use depth to flatten overlapping lines as seed labels
384-
max_labels = np.argmax(distances, axis=0)
385420
# allocate to connected components consistently (by majority,
386421
# 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)
389424
# 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)
391426
# find polygon hull and modify line coords
392427
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)
396433
if not count:
397434
LOG.warning("skipping zero-area line '%s'", line.id)
398435
continue
399-
covers = np.count_nonzero(mask) / count
436+
covers = np.count_nonzero(new_label) / count
400437
if covers < threshold / 3:
401438
LOG.debug("new line for '%s' only covers %.1f%% bg",
402439
line.id, covers * 100)
403440
continue
404-
count = np.count_nonzero(label * binarized)
441+
count = np.count_nonzero(old_label * binarized)
405442
if not count:
406443
LOG.warning("skipping binarizy-empty line '%s'", line.id)
407444
continue
408-
covers = np.count_nonzero(mask * binarized) / count
445+
covers = np.count_nonzero(new_label * binarized) / count
409446
if covers < threshold:
410447
LOG.debug("new line for '%s' only covers %.1f%% fg",
411448
line.id, covers * 100)
412449
continue
413450
LOG.debug('Black pixels before/after resegment of line "%s": %d/%d',
414451
line.id, count, covers * count)
415452
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)]
417454
#LOG.debug("joining %d subsegments for %s", len(contours), line.id)
418455
if len(contours) == 0:
419456
LOG.warning("no contours for %s - keeping", line.id)
420457
continue
421458
else:
422459
# get alpha shape
423460
poly = join_polygons([make_valid(Polygon(contour))
424-
for contour in contours], loc=loc)
461+
for contour in contours], loc=line.id)
425462
poly = poly.exterior.coords[:-1]
426463
polygon = coordinates_for_segment(poly, None, coords)
427464
polygon = polygon_for_parent(polygon, line.parent_object_)
@@ -475,3 +512,9 @@ def join_polygons(polygons, loc=''):
475512
jointp = asPolygon(np.round(jointp.exterior.coords))
476513
jointp = make_valid(jointp)
477514
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

Comments
 (0)