Skip to content

Commit 21cb7c9

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

File tree

2 files changed

+90
-5
lines changed

2 files changed

+90
-5
lines changed

ocrd_cis/ocrd-tool.json

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -191,14 +191,20 @@
191191
"output_file_grp": [
192192
"OCR-D-SEG-LINE"
193193
],
194-
"description": "Resegment lines with ocropy (by shrinking annotated polygons)",
194+
"description": "Resegment text lines",
195195
"parameters": {
196196
"level-of-operation": {
197197
"type": "string",
198198
"enum": ["page", "region"],
199-
"description": "PAGE XML hierarchy level granularity to segment lines in",
199+
"description": "PAGE XML hierarchy level to segment textlines in ('region' abides by existing text region boundaries, 'page' optimises lines in the whole page once",
200200
"default": "page"
201201
},
202+
"method": {
203+
"type": "string",
204+
"enum": ["lineest", "baseline", "ccomps"],
205+
"description": "source for new line polygon candidates ('lineest' for line estimation, i.e. how Ocropy would have segmented text lines; 'baseline' tries to re-polygonize from the baseline annotation; 'ccomps' avoids crossing connected components by majority rule)",
206+
"default": "lineest"
207+
},
202208
"dpi": {
203209
"type": "number",
204210
"format": "float",
@@ -208,13 +214,13 @@
208214
"min_fraction": {
209215
"type": "number",
210216
"format": "float",
211-
"description": "share of foreground pixels that must be retained by the largest label",
217+
"description": "share of foreground pixels that must be retained by the output polygons",
212218
"default": 0.75
213219
},
214220
"extend_margins": {
215221
"type": "number",
216222
"format": "integer",
217-
"description": "number of pixels to extend the input polygons horizontally and vertically before intersecting",
223+
"description": "number of pixels to extend the input polygons in all directions",
218224
"default": 3
219225
}
220226
}

ocrd_cis/ocropy/resegment.py

Lines changed: 80 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,9 +24,10 @@
2424
)
2525

2626
from .. import get_ocrd_tool
27-
from .ocrolib import midrange
27+
from .ocrolib import midrange, morph
2828
from .common import (
2929
pil2array,
30+
odd,
3031
# DSAVE,
3132
# binarize,
3233
check_page,
@@ -165,6 +166,7 @@ def _process_segment(self, parent, parent_image, parent_coords, page_id, zoom, l
165166
LOG = getLogger('processor.OcropyResegment')
166167
threshold = self.parameter['min_fraction']
167168
margin = self.parameter['extend_margins']
169+
method = self.parameter['method']
168170
# prepare line segmentation
169171
parent_array = pil2array(parent_image)
170172
#parent_array, _ = common.binarize(parent_array, maxskew=0) # just in case still raw
@@ -216,6 +218,28 @@ def _process_segment(self, parent, parent_image, parent_coords, page_id, zoom, l
216218
ignore_bin[draw.polygon(segment_polygon[:, 1],
217219
segment_polygon[:, 0],
218220
parent_bin.shape)] = True
221+
if method != 'lineest':
222+
LOG.debug('calculating connected component and distance transforms for "%s"', parent.id)
223+
bin = parent_bin & ~ ignore_bin
224+
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+
# estimate glyph scale (roughly)
232+
_, counts = np.unique(components, return_counts=True)
233+
if counts.shape[0] > 1:
234+
counts = np.sqrt(3 * counts)
235+
scale = int(np.median(counts[(5/zoom < counts) & (counts < 100/zoom)]))
236+
components *= (counts > 15/zoom)[components]
237+
LOG.debug("estimated scale: %d", scale)
238+
else:
239+
scale = 43
240+
spread_dist(lines, line_labels, distances, parent_bin, components, parent_coords,
241+
scale=scale, loc=parent.id, threshold=threshold)
242+
return
219243
try:
220244
new_line_labels, _, _, _, _, scale = compute_segmentation(
221245
parent_bin, seps=ignore_bin, zoom=zoom, fullpage=fullpage,
@@ -351,6 +375,61 @@ def _process_segment(self, parent, parent_image, parent_coords, page_id, zoom, l
351375
continue
352376
otherline.get_Coords().set_points(points_from_polygon(other_polygon))
353377

378+
def spread_dist(lines, labels, distances, binarized, components, coords,
379+
scale=43, loc='', threshold=0.9):
380+
"""redefine line coordinates by contourizing spread of connected components with max-distance of existing labels
381+
"""
382+
LOG = getLogger('processor.OcropyResegment')
383+
# use depth to flatten overlapping lines as seed labels
384+
max_labels = np.argmax(distances, axis=0)
385+
# allocate to connected components consistently (by majority,
386+
# 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)
389+
# dilate/grow labels from connected components against each other and bg
390+
max_ccomps = morph.spread_labels(max_ccomps, maxdist=scale/2)
391+
# find polygon hull and modify line coords
392+
for i, line in enumerate(lines):
393+
mask = max_ccomps == i + 1
394+
label = labels[i]
395+
count = np.count_nonzero(label)
396+
if not count:
397+
LOG.warning("skipping zero-area line '%s'", line.id)
398+
continue
399+
covers = np.count_nonzero(mask) / count
400+
if covers < threshold / 3:
401+
LOG.debug("new line for '%s' only covers %.1f%% bg",
402+
line.id, covers * 100)
403+
continue
404+
count = np.count_nonzero(label * binarized)
405+
if not count:
406+
LOG.warning("skipping binarizy-empty line '%s'", line.id)
407+
continue
408+
covers = np.count_nonzero(mask * binarized) / count
409+
if covers < threshold:
410+
LOG.debug("new line for '%s' only covers %.1f%% fg",
411+
line.id, covers * 100)
412+
continue
413+
LOG.debug('Black pixels before/after resegment of line "%s": %d/%d',
414+
line.id, count, covers * count)
415+
contours = [contour[:,::-1] # get x,y order again
416+
for contour, area in morph.find_contours(mask)]
417+
#LOG.debug("joining %d subsegments for %s", len(contours), line.id)
418+
if len(contours) == 0:
419+
LOG.warning("no contours for %s - keeping", line.id)
420+
continue
421+
else:
422+
# get alpha shape
423+
poly = join_polygons([make_valid(Polygon(contour))
424+
for contour in contours], loc=loc)
425+
poly = poly.exterior.coords[:-1]
426+
polygon = coordinates_for_segment(poly, None, coords)
427+
polygon = polygon_for_parent(polygon, line.parent_object_)
428+
if polygon is None:
429+
LOG.warning("Ignoring extant line for %s", line.id)
430+
continue
431+
line.get_Coords().set_points(points_from_polygon(polygon))
432+
354433
def diff_polygons(poly1, poly2):
355434
poly = poly1.difference(poly2)
356435
if poly.type == 'MultiPolygon':

0 commit comments

Comments
 (0)