layout/writer: simplify and refactor using dataclasses for segments…

- `utils`:
  - add dataclasses `Region` (including contour, skew, conf,
    centers and area – the latter computed during init, but only once
    instead of over and over) and `TextRegion` (ditto, but with a list
    of lines) to encapsulate intermediate results more succinctly
  - drop `check_any_text_region_in_model_one_is_main_or_header`
- `run_single()`:
  - instantiate `Region` and `TextRegion` lists for separator lines,
    drop-caps, marginals, paragraphs, headings, images, tables instead
    of multiple sets of identifiers for these segment types, name them
    more consistently
  - apply contour "dilation" (really Shapely polygon instantiation,
    repair/join and buffering) only once in the writer after rescaling
    to original/final image size
- `filter_contours_inside_a_bigger_one()`: separate and rename
  - → `filter_small_regions()`: taking lists of `Region` (original and
    deskewed) for paragraphs and marginals, filtering small regions
    that are contained in large regions or marginals in-place
  - → `filter_small_textlines()`: taking a list of `TextRegion` and
    filtering lines that are contained in larger lines (of possibly
    other regions) in-place
- `filter_contours_without_textlines_inside()`: simplify and rename
  → `filter_textregions_without_textlines()`
- rename `separate_marginals_to_left_and_right_and_order_from_top_to_down`
  → `separate_marginals_and_order()` and simplify
- `get_slopes_and_deskew*`: work on list of `Region` in-place
  (modifying their `.skew` and `.lines`), extract line confidence, too
- writer:
  - expect `Region` and `TextRegion` instances for various segment types
    instead of wild list of identifiers
  - simplify (a lot)
  - merge `build_pagexml_no_full_layout` and build_pagexml_full_layout`
    → `build_pagexml()` (w/ empty lists as default)
  - also annotate textline confidence
  - enlarge/dilate/buffer coordinates differently for various segment types:
    6px for text and tables, 2px for images and seplines
- add more type hints
This commit is contained in:
Robert Sachunsky 2026-07-19 14:07:20 +02:00
parent 5e3fde31d9
commit beb5545899
5 changed files with 547 additions and 808 deletions

File diff suppressed because it is too large Load diff

View file

@ -1,10 +1,11 @@
from typing import Optional
import os.path
try: try:
import matplotlib.pyplot as plt import matplotlib.pyplot as plt
import matplotlib.patches as mpatches import matplotlib.patches as mpatches
except ImportError: except ImportError:
plt = mpatches = None plt = mpatches = None
import numpy as np import numpy as np
import os.path
import cv2 import cv2
from scipy.ndimage import gaussian_filter1d from scipy.ndimage import gaussian_filter1d
@ -21,11 +22,11 @@ class EynollahPlotter:
self, self,
*, *,
dir_out, dir_out,
dir_of_all, dir_of_all: Optional[str] = None,
dir_save_page, dir_save_page: Optional[str] = None,
dir_of_deskewed, dir_of_deskewed: Optional[str] = None,
dir_of_layout, dir_of_layout: Optional[str] = None,
dir_of_cropped_images, dir_of_cropped_images: Optional[str] = None,
): ):
self.dir_out = dir_out self.dir_out = dir_out
self.dir_of_all = dir_of_all self.dir_of_all = dir_of_all
@ -172,6 +173,8 @@ class EynollahPlotter:
x, y, w, h = cv2.boundingRect(cont_ind) x, y, w, h = cv2.boundingRect(cont_ind)
box = [x, y, w, h] box = [x, y, w, h]
image, _ = crop_image_inside_box(box, image_page) image, _ = crop_image_inside_box(box, image_page)
if not image.size:
continue
image = resize_image(image, image = resize_image(image,
int(image.shape[0] / scale_y), int(image.shape[0] / scale_y),
int(image.shape[1] / scale_x)) int(image.shape[1] / scale_x))

View file

@ -2,7 +2,8 @@ from typing import Iterable, List, Tuple
from logging import getLogger from logging import getLogger
import time import time
import math import math
from itertools import islice from dataclasses import dataclass, field
from itertools import islice, compress
try: try:
import matplotlib.pyplot as plt import matplotlib.pyplot as plt
@ -10,16 +11,18 @@ try:
except ImportError: except ImportError:
plt = None plt = None
import numpy as np import numpy as np
from shapely import geometry from shapely import geometry, prepare, ops, plotting
import cv2 import cv2
from scipy.signal import find_peaks from scipy.signal import find_peaks
from scipy.ndimage import gaussian_filter1d from scipy.ndimage import gaussian_filter1d
from skimage import morphology from skimage import morphology
from .is_nan import isNaN from .is_nan import isNaN
from .contour import (contours_in_same_horizon, from .contour import (contour2polygon,
contours_in_same_horizon,
find_center_of_contours, find_center_of_contours,
find_new_features_of_contours, find_new_features_of_contours,
polygon2contour,
return_contours_of_image, return_contours_of_image,
return_parent_contours) return_parent_contours)
@ -39,6 +42,32 @@ def batched(iterable, n):
while batch := tuple(islice(iterator, n)): while batch := tuple(islice(iterator, n)):
yield batch yield batch
def itemgetter(seq):
# replacement for operator.itemgetter
# (which is ambiguous about its return type:
# single-item if 1 argument, tuple otherwise)
def fun(obj):
return list(obj[idx] for idx in seq)
return fun
@dataclass
class Region:
contour: np.ndarray
area: int = 0
cx: float = 0
cy: float = 0
skew: float = 0
conf: float = 0
def __post_init__(self):
self.area = cv2.contourArea(self.contour)
moments = cv2.moments(self.contour)
self.cx = moments['m10'] / (moments['m00'] or 1e-32)
self.cy = moments['m01'] / (moments['m00'] or 1e-32)
@dataclass
class TextRegion(Region):
lines: List[Region] = field(default_factory=list)
def return_multicol_separators_x_start_end( def return_multicol_separators_x_start_end(
regions_without_separators, peak_points, top, bot, regions_without_separators, peak_points, top, bot,
x_min_hor_some, x_max_hor_some, cy_hor_some, y_min_hor_some, y_max_hor_some): x_min_hor_some, x_max_hor_some, cy_hor_some, y_min_hor_some, y_max_hor_some):
@ -800,93 +829,16 @@ def fill_bb_of_drop_capitals(
return full_prediction == label_drop_fl_model return full_prediction == label_drop_fl_model
def check_any_text_region_in_model_one_is_main_or_header(
regions_model_1, regions_model_full,
contours_only_text_parent,
all_box_coord, all_found_textline_polygons,
slopes,
contours_only_text_parent_d_ordered, conf_contours):
cx_main, cy_main, x_min_main, x_max_main, y_min_main, y_max_main, y_corr_x_min_from_argmin = \
find_new_features_of_contours(contours_only_text_parent)
length_con=x_max_main-x_min_main
height_con=y_max_main-y_min_main
all_found_textline_polygons_main=[]
all_found_textline_polygons_head=[]
all_box_coord_main=[]
all_box_coord_head=[]
slopes_main=[]
slopes_head=[]
contours_only_text_parent_main=[]
contours_only_text_parent_head=[]
conf_contours_main=[]
conf_contours_head=[]
contours_only_text_parent_main_d=[]
contours_only_text_parent_head_d=[]
for ii, con in enumerate(contours_only_text_parent):
img = np.zeros(regions_model_1.shape[:2])
img = cv2.fillPoly(img, pts=[con], color=255)
all_pixels=((img == 255)*1).sum()
pixels_header=( ( (img == 255) & (regions_model_full[:,:,0]==2) )*1 ).sum()
pixels_main=all_pixels-pixels_header
if (pixels_header>=pixels_main) and ( (length_con[ii]/float(height_con[ii]) )>=1.3 ):
regions_model_1[:,:][(regions_model_1[:,:]==1) & (img == 255) ]=2
contours_only_text_parent_head.append(con)
if len(contours_only_text_parent_d_ordered):
contours_only_text_parent_head_d.append(contours_only_text_parent_d_ordered[ii])
all_box_coord_head.append(all_box_coord[ii])
slopes_head.append(slopes[ii])
all_found_textline_polygons_head.append(all_found_textline_polygons[ii])
conf_contours_head.append(None)
else:
regions_model_1[:,:][(regions_model_1[:,:]==1) & (img == 255) ]=1
contours_only_text_parent_main.append(con)
conf_contours_main.append(conf_contours[ii])
if len(contours_only_text_parent_d_ordered):
contours_only_text_parent_main_d.append(contours_only_text_parent_d_ordered[ii])
all_box_coord_main.append(all_box_coord[ii])
slopes_main.append(slopes[ii])
all_found_textline_polygons_main.append(all_found_textline_polygons[ii])
#print(all_pixels,pixels_main,pixels_header)
return (regions_model_1,
contours_only_text_parent_main,
contours_only_text_parent_head,
all_box_coord_main,
all_box_coord_head,
all_found_textline_polygons_main,
all_found_textline_polygons_head,
slopes_main,
slopes_head,
contours_only_text_parent_main_d,
contours_only_text_parent_head_d,
conf_contours_main,
conf_contours_head)
def split_textregion_main_vs_head( def split_textregion_main_vs_head(
regions_model_1, regions_model_1: np.ndarray,
regions_model_full, regions_model_full: np.ndarray,
polygons_of_textregions, textregions: List[Region],
polygons_of_textregions_d, textregions_d: List[Region],
all_found_textline_polygons,
slopes,
conf_textregions,
label_text=1, label_text=1,
label_head_full=2, label_head_full=2,
label_head_final=2, label_head_final=2,
label_main_final=1, label_main_final=1,
): ) -> Tuple[np.ndarray, List[Region], List[Region], List[Region], List[Region]]:
### to make it faster ### to make it faster
h_o = regions_model_1.shape[0] h_o = regions_model_1.shape[0]
@ -900,19 +852,12 @@ def split_textregion_main_vs_head(
(regions_model_full.shape[1] // zoom, (regions_model_full.shape[1] // zoom,
regions_model_full.shape[0] // zoom), regions_model_full.shape[0] // zoom),
interpolation=cv2.INTER_NEAREST) interpolation=cv2.INTER_NEAREST)
contours_z = [contour // zoom contours_z = [textregion.contour // zoom
for contour in polygons_of_textregions] for textregion in textregions]
### main = np.ones(len(contours_z), dtype=bool)
_, _, x_min_main, x_max_main, y_min_main, y_max_main, _ = \
find_new_features_of_contours(contours_z)
length_con=x_max_main-x_min_main
height_con=y_max_main-y_min_main
main = []
head = []
for ii, con in enumerate(contours_z): for ii, con in enumerate(contours_z):
width, height = cv2.boundingRect(con)[2:]
parent = np.zeros_like(regions_model_1) parent = np.zeros_like(regions_model_1)
parent = cv2.fillPoly(parent, pts=[con], color=1) parent = cv2.fillPoly(parent, pts=[con], color=1)
@ -920,16 +865,14 @@ def split_textregion_main_vs_head(
pixels_main = parent.sum() - pixels_head pixels_main = parent.sum() - pixels_head
if (( pixels_head >= 0.6 * pixels_main and if (( pixels_head >= 0.6 * pixels_main and
length_con[ii] >= 1.3 * height_con[ii] and width >= 1.3 * height and
length_con[ii] <= 3 * height_con[ii] ) or width <= 3 * height ) or
( pixels_head >= 0.3 * pixels_main and ( pixels_head >= 0.3 * pixels_main and
length_con[ii] >= 3 * height_con[ii] )): width >= 3 * height )):
head.append(ii) main[ii] = False
label = label_head_final label = label_head_final
else: else:
main.append(ii)
label = label_main_final label = label_main_final
regions_model_1[(regions_model_1 == label_text) & (parent > 0)] = label regions_model_1[(regions_model_1 == label_text) & (parent > 0)] = label
@ -941,35 +884,26 @@ def split_textregion_main_vs_head(
# interpolation=cv2.INTER_NEAREST) # interpolation=cv2.INTER_NEAREST)
### ###
def select(lis, indexes):
if not len(lis):
return []
return [lis[ind] for ind in indexes]
return (regions_model_1, return (regions_model_1,
select(polygons_of_textregions, main), list(compress(textregions, main)),
select(polygons_of_textregions, head), list(compress(textregions, ~main)),
select(polygons_of_textregions_d, main), list(compress(textregions_d, main)),
select(polygons_of_textregions_d, head), list(compress(textregions_d, ~main)),
select(all_found_textline_polygons, main),
select(all_found_textline_polygons, head),
select(slopes, main),
select(slopes, head),
select(conf_textregions, main),
select(conf_textregions, head),
) )
def small_textlines_to_parent_adherence2(textlines_con, textline_mask, num_col): def small_textlines_to_parent_adherence2(
textregions: List[TextRegion],
area_factor: float,
num_col: int,
) -> None:
""" """
for each region, split up textlines into small and large areas; for each region, split up textlines into small and large;
keep only the ones with large area, but expanded (by merging keep only the ones with large area, but expanded (by merging
contours) by all intersecting lines with small area contours) by all intersecting lines with small area
""" """
textlines_con_new = [] textlines_con_new = []
for region in textlines_con: for textregion in textregions:
areas_cnt_text = np.array(list(map(cv2.contourArea, region))) areas = np.array([line.area for line in textregion.lines]) * area_factor
areas_cnt_text = areas_cnt_text / float(textline_mask.size)
indexes_textlines = np.arange(len(region))
if num_col == 0: if num_col == 0:
min_area = 0.0004 min_area = 0.0004
@ -977,52 +911,43 @@ def small_textlines_to_parent_adherence2(textlines_con, textline_mask, num_col):
min_area = 0.0003 min_area = 0.0003
else: else:
min_area = 0.0001 min_area = 0.0001
indexes_textlines_small = indexes_textlines[areas_cnt_text < min_area] textlines_small = list(compress(textregion.lines, areas < min_area))
indexes_textlines_large = indexes_textlines[areas_cnt_text >= min_area] textlines_large = list(compress(textregion.lines, areas >= min_area))
textlines_small_poly = [contour2polygon(line.contour) for line in textlines_small]
textlines_large_poly = [contour2polygon(line.contour) for line in textlines_large]
textregion.lines = textlines_large
textlines_small = [region[i] for i in indexes_textlines_small] if geometry.MultiPolygon(textlines_small_poly).intersects(
textlines_large = [region[i] for i in indexes_textlines_large] geometry.MultiPolygon(textlines_large_poly)):
# FIXME: also consider confidence (less certain lines replaced by better ones)...
img_small = np.zeros_like(textline_mask) textlines_large_prep = [prepare(poly) for poly in textlines_large_poly]
img_small = cv2.fillPoly(img_small, pts=textlines_small, color=1) textlines_small_indexes_interlarge = []
img_large = np.zeros_like(textline_mask) for small_poly in textlines_small_poly:
img_large = cv2.fillPoly(img_large, pts=textlines_large, color=1) intersections = [small_poly.intersection(prep).area
img_inter = img_small + img_large == 2 if prep.intersects(small_poly) else 0
if np.any(img_inter): for prep in textlines_large_prep]
indexes_textlines_inter = []
for contour_small in textlines_small:
intersections = []
for contour_large in textlines_large:
img0_small = np.zeros_like(textline_mask)
img0_small = cv2.fillPoly(img0_small, pts=[contour_small], color=1)
img0_large = np.zeros_like(textline_mask)
img0_large = cv2.fillPoly(img0_large, pts=[contour_large], color=1)
img0_inter = img0_small + img0_large == 2
intersections.append(np.count_nonzero(img0_inter))
idx_large = np.argmax(intersections) idx_large = np.argmax(intersections)
if intersections[idx_large] <= 0: if intersections[idx_large] == 0:
idx_large = -1 idx_large = -1
indexes_textlines_inter.append(idx_large) textlines_small_indexes_interlarge.append(idx_large)
textlines_small_indexes_interlarge = np.array(textlines_small_indexes_interlarge)
indexes_textlines_inter = np.array(indexes_textlines_inter) for idx_large in set(textlines_small_indexes_interlarge):
for idx_large in set(indexes_textlines_inter):
if idx_large < 0: if idx_large < 0:
continue continue
img0_union = np.zeros_like(textline_mask) large_poly = textlines_large_poly[idx_large]
img0_union = cv2.fillPoly(img0_union, pts=[textlines_large[idx_large]], color=255) indexes_small = np.flatnonzero(textlines_small_indexes_interlarge == idx_large)
indexes_inter_small = np.flatnonzero(indexes_textlines_inter == idx_large) for idx_small in indexes_small:
for idx_small in indexes_inter_small: large_poly = large_poly.union(textlines_small_poly[idx_small])
img0_union = cv2.fillPoly(img0_union, pts=[textlines_small[idx_small]], color=255) if large_poly.geom_type == 'GeometryCollection':
# hetergeneous: filter lines and points
_, thresh = cv2.threshold(img0_union, 0, 255, 0) large_poly = ops.unary_union([geom for geom in large_poly.geoms
contours_union, _ = cv2.findContours(thresh.astype(np.uint8), if geom.area > 0])
cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) if large_poly.geom_type == 'MultiPolygon':
areas_union = np.array(list(map(cv2.contourArea, contours_union))) # disjoint: pick largest part
contour_union = contours_union[np.argmax(areas_union)] #contours_union[0] idx_large = np.argmax([geom.area for geom in large_poly.geoms])
textlines_large[idx_large] = contour_union large_poly = large_poly.geoms[idx_large]
# replace original
textlines_con_new.append(textlines_large) textregion.lines[idx_large].contour = polygon2contour(large_poly)
return textlines_con_new
def order_of_regions(textline_mask, contours_main, contours_head, contours_drop, y_ref, x_ref): def order_of_regions(textline_mask, contours_main, contours_head, contours_drop, y_ref, x_ref):
""" """
@ -1051,7 +976,7 @@ def order_of_regions(textline_mask, contours_main, contours_head, contours_drop,
total = len(contours_main) + len(contours_head) + len(contours_drop) total = len(contours_main) + len(contours_head) + len(contours_drop)
assert total == 0 or np.any(textline_mask) assert total == 0 or np.any(textline_mask)
# ax1 = plt.subplot(2, 1, 1, title="order_of_regions textline_mask") # ax1 = plt.subplot(1, 2, 1, title="order_of_regions textline_mask")
# plt.imshow(textline_mask, aspect='auto') # plt.imshow(textline_mask, aspect='auto')
y = textline_mask.sum(axis=1) # horizontal projection profile y = textline_mask.sum(axis=1) # horizontal projection profile
y_padded = np.zeros(len(y) + 40) y_padded = np.zeros(len(y) + 40)
@ -1061,15 +986,21 @@ def order_of_regions(textline_mask, contours_main, contours_head, contours_drop,
#z = gaussian_filter1d(y_padded, sigma_gaus) #z = gaussian_filter1d(y_padded, sigma_gaus)
#peaks, _ = find_peaks(z, height=0) #peaks, _ = find_peaks(z, height=0)
#peaks = peaks - 20 #peaks = peaks - 20
# ax2 = plt.subplot(2, 1, 2, title="smoothed horizontal projection", sharex=ax1) # ax2 = plt.subplot(1, 2, 2, title="smoothed horizontal projection", sharey=ax1)
# plt.plot(y) # ax2plot = plt.plot(y)[0]
# xdata = ax2plot.get_xdata()
# ydata = ax2plot.get_ydata()
# ax2plot.set_xdata(ydata)
# ax2plot.set_ydata(xdata)
# ax2.set_xlim(*ax1.get_xlim())
# ax2.set_ylim(*ax1.get_ylim())
zneg_rev = np.max(y_padded) - y_padded zneg_rev = np.max(y_padded) - y_padded
zneg = np.zeros(len(zneg_rev) + 40) zneg = np.zeros(len(zneg_rev) + 40)
zneg[20 : len(zneg_rev) + 20] = zneg_rev zneg[20 : len(zneg_rev) + 20] = zneg_rev
zneg = gaussian_filter1d(zneg, sigma_gaus) zneg = gaussian_filter1d(zneg, sigma_gaus)
peaks_neg, _ = find_peaks(zneg, height=0) peaks_neg, _ = find_peaks(zneg, height=0)
# plt.vlines(peaks_neg - 40, 0, None, label="peaks") # plt.hlines(peaks_neg - 40, 0, textline_mask.shape[1], label="peaks")
# plt.show() # plt.show()
peaks_neg = peaks_neg - 20 - 20 peaks_neg = peaks_neg - 20 - 20

View file

@ -1,4 +1,4 @@
from typing import Sequence, Union from typing import List, Sequence, Union, Tuple
from numbers import Number from numbers import Number
from functools import partial from functools import partial
import itertools import itertools
@ -167,26 +167,34 @@ def dilate_textregion_contours(all_found_textregion_polygons):
[polygon2contour(contour2polygon(contour, dilate=6)) [polygon2contour(contour2polygon(contour, dilate=6))
for contour in all_found_textregion_polygons]) for contour in all_found_textregion_polygons])
def match_deskewed_contours(slope_deskew, contours_o, contours_d, shape_o, shape_d): def match_deskewed_contours(
slope_deskew: float,
regions_o, #: List[Region], # (cyclic import)
regions_d, #: List[Region], # (cyclic import)
shape_o: Tuple[int, int],
shape_d: Tuple[int, int],
) -> np.ndarray:
from . import ensure_array from . import ensure_array
cntareas_o = np.array([cv2.contourArea(contour) for contour in contours_o]) centers_o = np.array([[region.cx, region.cy] for region in regions_o]) # [N, 2]
cntareas_d = np.array([cv2.contourArea(contour) for contour in contours_d]) centers_d = np.array([[region.cx, region.cy] for region in regions_d]) # [N, 2]
cntareas_o = np.array([region.area for region in regions_o])
cntareas_d = np.array([region.area for region in regions_d])
cntareas_o = cntareas_o / float(np.prod(shape_o[:2])) cntareas_o = cntareas_o / float(np.prod(shape_o[:2]))
cntareas_d = cntareas_d / float(np.prod(shape_d[:2])) cntareas_d = cntareas_d / float(np.prod(shape_d[:2]))
contours_o = ensure_array(contours_o) contours_o = ensure_array([region.contour for region in regions_o])
contours_d = ensure_array(contours_d) contours_d = ensure_array([region.contour for region in regions_d])
sort_o = np.argsort(cntareas_o) sort_o = np.argsort(cntareas_o)
sort_d = np.argsort(cntareas_d) sort_d = np.argsort(cntareas_d)
centers_o = centers_o[sort_o].T # [2, N]
centers_d = centers_d[sort_d].T # [2, N]
contours_o = contours_o[sort_o] contours_o = contours_o[sort_o]
contours_d = contours_d[sort_d] contours_d = contours_d[sort_d]
cntareas_o = cntareas_o[sort_o] cntareas_o = cntareas_o[sort_o]
cntareas_d = cntareas_d[sort_d] cntareas_d = cntareas_d[sort_d]
centers_o = np.stack(find_center_of_contours(contours_o)) # [2, N]
centers_d = np.stack(find_center_of_contours(contours_d)) # [2, N]
center0_o = centers_o[:, -1:] # [2, 1] center0_o = centers_o[:, -1:] # [2, 1]
center0_d = centers_d[:, -1:] # [2, 1] center0_d = centers_d[:, -1:] # [2, 1]

View file

@ -3,36 +3,44 @@
from pathlib import Path from pathlib import Path
import os.path import os.path
import logging import logging
from typing import Optional from typing import Optional, List, Tuple
import numpy as np import numpy as np
from shapely import affinity, clip_by_rect from shapely import affinity, clip_by_rect
from ocrd_utils import points_from_polygon from ocrd_utils import points_from_polygon
from ocrd_models.ocrd_page import ( from ocrd_models.ocrd_page import (
BorderType, BorderType,
CoordsType, CoordsType,
TextLineType, TextLineType,
TextEquivType, TextEquivType,
TextRegionType, TextRegionType,
ImageRegionType, ImageRegionType,
TableRegionType, TableRegionType,
SeparatorRegionType, SeparatorRegionType,
to_xml PcGtsType,
) to_xml
)
from .utils import Region, TextRegion
from .utils.xml import create_page_xml, xml_reading_order from .utils.xml import create_page_xml, xml_reading_order
from .utils.counter import EynollahIdCounter from .utils.counter import EynollahIdCounter
from .utils.contour import contour2polygon, make_valid from .utils.contour import contour2polygon, make_valid
class EynollahXmlWriter: class EynollahXmlWriter:
def __init__(self, *, dir_out, image_filename, image_width, image_height, curved_line, pcgts=None): def __init__(
self, *,
dir_out: Optional[str],
image_filename: str,
image_width: int,
image_height: int,
pcgts: Optional[PcGtsType] = None,
):
self.logger = logging.getLogger('eynollah.writer') self.logger = logging.getLogger('eynollah.writer')
self.counter = EynollahIdCounter() self.counter = EynollahIdCounter()
self.dir_out = dir_out self.dir_out = dir_out
self.image_filename = image_filename self.image_filename = image_filename
self.output_filename = os.path.join(self.dir_out or "", self.image_filename_stem) + ".xml" self.output_filename = os.path.join(self.dir_out or "", self.image_filename_stem) + ".xml"
self.curved_line = curved_line
self.pcgts = pcgts self.pcgts = pcgts
self.image_height = image_height self.image_height = image_height
self.image_width = image_width self.image_width = image_width
@ -40,132 +48,56 @@ class EynollahXmlWriter:
self.scale_y = 1.0 self.scale_y = 1.0
@property @property
def image_filename_stem(self): def image_filename_stem(self) -> str:
return Path(Path(self.image_filename).name).stem return Path(Path(self.image_filename).name).stem
def calculate_points(self, contour, offset=None): def calculate_points(
poly = contour2polygon(contour) self, contour: np.ndarray,
offset: Optional[List[int]] = None,
dilate: int = 0,
) -> str:
poly = contour2polygon(contour, dilate=dilate)
if offset is not None: if offset is not None:
poly = affinity.translate(poly, *offset) poly = affinity.translate(poly, *offset)
poly = affinity.scale(poly, xfact=1 / self.scale_x, yfact=1 / self.scale_y, origin=(0, 0)) poly = affinity.scale(poly, xfact=1 / self.scale_x, yfact=1 / self.scale_y, origin=(0, 0))
poly = make_valid(clip_by_rect(poly, 0, 0, self.image_width, self.image_height)) poly = make_valid(clip_by_rect(poly, 0, 0, self.image_width, self.image_height))
return points_from_polygon(poly.exterior.coords[:-1]) return points_from_polygon(poly.exterior.coords[:-1])
def serialize_lines_in_region(self, text_region, all_found_textline_polygons, region_idx, page_coord, slopes, counter, ocr_all_textlines_textregion): def serialize_lines_in_region(
for j, polygon_textline in enumerate(all_found_textline_polygons[region_idx]): self, text_region: TextRegionType,
coords = CoordsType() offset: List[int],
textline = TextLineType(id=counter.next_line_id, Coords=coords) counter: EynollahIdCounter,
if ocr_all_textlines_textregion: lines: List[Region],
# FIXME: add OCR confidence ) -> None:
textline.set_TextEquiv([TextEquivType(Unicode=ocr_all_textlines_textregion[j])]) for line in lines:
textline = TextLineType(
id=counter.next_line_id,
Coords=CoordsType(points=self.calculate_points(line.contour, offset, 5),
conf=line.conf)
)
text_region.add_TextLine(textline) text_region.add_TextLine(textline)
text_region.set_orientation(-slopes[region_idx])
offset = [page_coord[2], page_coord[0]]
coords.set_points(self.calculate_points(polygon_textline, offset))
def write_pagexml(self, pcgts): def write_pagexml(self, pcgts):
self.logger.info("output filename: '%s'", self.output_filename) self.logger.info("output filename: '%s'", self.output_filename)
with open(self.output_filename, 'w') as f: with open(self.output_filename, 'w') as f:
f.write(to_xml(pcgts)) f.write(to_xml(pcgts))
def build_pagexml_no_full_layout( def build_pagexml(
self, self,
*, *,
num_col, num_col=1,
found_polygons_text_region, page_coord: Optional[List[int]] = None,
page_coord, page_contour: Optional[np.ndarray] = None,
page_slope, page_skew: float = 0.,
order_of_texts, order_of_texts: List[int] = [],
all_found_textline_polygons, textregions: List[TextRegion] = [],
found_polygons_images, textregions_h: List[TextRegion] = [],
found_polygons_tables, images: List[Region] = [],
found_polygons_marginals_left, tables: List[Region] = [],
found_polygons_marginals_right, drop_caps: List[Region] = [],
all_found_textline_polygons_marginals_left, marginals_left: List[TextRegion] = [],
all_found_textline_polygons_marginals_right, marginals_right: List[TextRegion] = [],
slopes, seplines: List[Region] = [],
slopes_marginals_left,
slopes_marginals_right,
cont_page,
polygons_seplines,
ocr_all_textlines=None,
ocr_all_textlines_marginals_left=None,
ocr_all_textlines_marginals_right=None,
ocr_all_textlines_drop=None,
conf_textregions=None,
conf_marginals_left=None,
conf_marginals_right=None,
conf_images=None,
conf_tables=None,
):
return self.build_pagexml_full_layout(
num_col=num_col,
found_polygons_text_region=found_polygons_text_region,
found_polygons_text_region_h=[],
page_coord=page_coord,
page_slope=page_slope,
order_of_texts=order_of_texts,
all_found_textline_polygons=all_found_textline_polygons,
all_found_textline_polygons_h=[],
found_polygons_images=found_polygons_images,
found_polygons_tables=found_polygons_tables,
found_polygons_drop_capitals=[],
found_polygons_marginals_left=found_polygons_marginals_left,
found_polygons_marginals_right=found_polygons_marginals_right,
all_found_textline_polygons_marginals_left=all_found_textline_polygons_marginals_left,
all_found_textline_polygons_marginals_right=all_found_textline_polygons_marginals_right,
slopes=slopes,
slopes_h=[],
slopes_marginals_left=slopes_marginals_left,
slopes_marginals_right=slopes_marginals_right,
cont_page=cont_page,
polygons_seplines=polygons_seplines,
ocr_all_textlines=ocr_all_textlines,
ocr_all_textlines_marginals_left=ocr_all_textlines_marginals_left,
ocr_all_textlines_marginals_right=ocr_all_textlines_marginals_right,
conf_textregions=conf_textregions,
conf_marginals_left=conf_marginals_left,
conf_marginals_right=conf_marginals_right,
conf_images=conf_images,
conf_tables=conf_tables,
)
def build_pagexml_full_layout(
self,
*,
num_col,
found_polygons_text_region,
found_polygons_text_region_h,
page_coord,
page_slope,
order_of_texts,
all_found_textline_polygons,
all_found_textline_polygons_h,
found_polygons_images,
found_polygons_tables,
found_polygons_drop_capitals,
found_polygons_marginals_left,
found_polygons_marginals_right,
all_found_textline_polygons_marginals_left,
all_found_textline_polygons_marginals_right,
slopes,
slopes_h,
slopes_marginals_left,
slopes_marginals_right,
cont_page,
polygons_seplines,
ocr_all_textlines=None,
ocr_all_textlines_h=None,
ocr_all_textlines_marginals_left=None,
ocr_all_textlines_marginals_right=None,
ocr_all_textlines_drop=None,
conf_textregions=None,
conf_textregions_h=None,
conf_marginals_left=None,
conf_marginals_right=None,
conf_images=None,
conf_tables=None,
conf_drops=None,
): ):
self.logger.debug('enter build_pagexml') self.logger.debug('enter build_pagexml')
@ -175,118 +107,92 @@ class EynollahXmlWriter:
page = pcgts.get_Page() page = pcgts.get_Page()
pcgts.Metadata.Comments = "num_col %d" % num_col pcgts.Metadata.Comments = "num_col %d" % num_col
page.set_custom('layout {num_col:%d;} ' % num_col) page.set_custom('layout {num_col:%d;} ' % num_col)
page.set_orientation(-page_slope) page.set_orientation(-page_skew)
if len(cont_page): if page_contour is not None:
page.set_Border(BorderType(Coords=CoordsType(points=self.calculate_points(cont_page[0])))) page.set_Border(BorderType(Coords=CoordsType(points=self.calculate_points(page_contour))))
if page_coord is None:
offset = [page_coord[2], page_coord[0]] offset = [0, 0]
else:
offset = [page_coord[2], page_coord[0]]
counter = EynollahIdCounter() counter = EynollahIdCounter()
if len(order_of_texts): if len(order_of_texts):
_counter_marginals = EynollahIdCounter(region_idx=len(order_of_texts)) _counter_marginals = EynollahIdCounter(region_idx=len(order_of_texts))
id_of_marginalia_left = [_counter_marginals.next_region_id id_of_marginalia_left = [_counter_marginals.next_region_id
for _ in found_polygons_marginals_left] for _ in marginals_left]
id_of_marginalia_right = [_counter_marginals.next_region_id id_of_marginalia_right = [_counter_marginals.next_region_id
for _ in found_polygons_marginals_right] for _ in marginals_right]
xml_reading_order(page, order_of_texts, id_of_marginalia_left, id_of_marginalia_right) xml_reading_order(page, order_of_texts, id_of_marginalia_left, id_of_marginalia_right)
for mm, region_contour in enumerate(found_polygons_text_region): for region in textregions:
textregion = TextRegionType( textregion = TextRegionType(
id=counter.next_region_id, type_='paragraph', id=counter.next_region_id, type_='paragraph',
Coords=CoordsType(points=self.calculate_points(region_contour, offset)) Coords=CoordsType(points=self.calculate_points(region.contour, offset, 6),
conf=region.conf),
orientation=-region.skew
) )
assert textregion.Coords self.serialize_lines_in_region(textregion, offset, counter, region.lines)
if conf_textregions:
textregion.Coords.set_conf(conf_textregions[mm])
page.add_TextRegion(textregion) page.add_TextRegion(textregion)
if ocr_all_textlines:
ocr_textlines = ocr_all_textlines[mm]
else:
ocr_textlines = None
self.serialize_lines_in_region(textregion, all_found_textline_polygons, mm, page_coord,
slopes, counter, ocr_textlines)
self.logger.debug('len(found_polygons_text_region_h) %s', len(found_polygons_text_region_h)) self.logger.debug('len(textregions_h) %s', len(textregions_h))
for mm, region_contour in enumerate(found_polygons_text_region_h): for region in textregions_h:
textregion = TextRegionType( textregion = TextRegionType(
id=counter.next_region_id, type_='heading', id=counter.next_region_id, type_='heading',
Coords=CoordsType(points=self.calculate_points(region_contour, offset)) Coords=CoordsType(points=self.calculate_points(region.contour, offset, 6),
conf=region.conf),
orientation=-region.skew
) )
assert textregion.Coords self.serialize_lines_in_region(textregion, offset, counter, region.lines)
if conf_textregions_h:
textregion.Coords.set_conf(conf_textregions_h[mm])
page.add_TextRegion(textregion) page.add_TextRegion(textregion)
if ocr_all_textlines_h:
ocr_textlines = ocr_all_textlines_h[mm]
else:
ocr_textlines = None
self.serialize_lines_in_region(textregion, all_found_textline_polygons_h, mm, page_coord,
slopes_h, counter, ocr_textlines)
for mm, region_contour in enumerate(found_polygons_drop_capitals): for region in drop_caps:
dropcapital = TextRegionType( textregion = TextRegionType(
id=counter.next_region_id, type_='drop-capital', id=counter.next_region_id, type_='drop-capital',
Coords=CoordsType(points=self.calculate_points(region_contour, offset)) Coords=CoordsType(points=self.calculate_points(region.contour, offset, 6),
conf=region.conf),
orientation=-region.skew
) )
if conf_drops: self.serialize_lines_in_region(textregion, offset, counter, [region])
dropcapital.Coords.set_conf(conf_drops[mm]) page.add_TextRegion(textregion)
page.add_TextRegion(dropcapital)
slopes_drop = [0]
if ocr_all_textlines_drop:
ocr_textlines = ocr_all_textlines_drop[mm]
else:
ocr_textlines = None
self.serialize_lines_in_region(dropcapital, [[found_polygons_drop_capitals[mm]]], 0, page_coord,
slopes_drop, counter, ocr_textlines)
for mm, region_contour in enumerate(found_polygons_marginals_left): for region in marginals_left:
marginal = TextRegionType( textregion = TextRegionType(
id=counter.next_region_id, type_='marginalia', id=counter.next_region_id, type_='marginalia',
Coords=CoordsType(points=self.calculate_points(region_contour, offset)) Coords=CoordsType(points=self.calculate_points(region.contour, offset, 6),
conf=region.conf),
orientation=-region.skew
) )
if conf_marginals_left: self.serialize_lines_in_region(textregion, offset, counter, region.lines)
marginal.Coords.set_conf(conf_marginals_left[mm]) page.add_TextRegion(textregion)
page.add_TextRegion(marginal)
if ocr_all_textlines_marginals_left:
ocr_textlines = ocr_all_textlines_marginals_left[mm]
else:
ocr_textlines = None
self.serialize_lines_in_region(marginal, all_found_textline_polygons_marginals_left, mm, page_coord,
slopes_marginals_left, counter, ocr_textlines)
for mm, region_contour in enumerate(found_polygons_marginals_right): for region in marginals_right:
marginal = TextRegionType( textregion = TextRegionType(
id=counter.next_region_id, type_='marginalia', id=counter.next_region_id, type_='marginalia',
Coords=CoordsType(points=self.calculate_points(region_contour, offset)) Coords=CoordsType(points=self.calculate_points(region.contour, offset, 6),
conf=region.conf),
orientation=-region.skew
) )
if conf_marginals_right: self.serialize_lines_in_region(textregion, offset, counter, region.lines)
marginal.Coords.set_conf(conf_marginals_right[mm]) page.add_TextRegion(textregion)
page.add_TextRegion(marginal)
if ocr_all_textlines_marginals_right:
ocr_textlines = ocr_all_textlines_marginals_right[mm]
else:
ocr_textlines = None
self.serialize_lines_in_region(marginal, all_found_textline_polygons_marginals_right, mm, page_coord,
slopes_marginals_right, counter, ocr_textlines)
for mm, region_contour in enumerate(found_polygons_images): for region in images:
image = ImageRegionType( image = ImageRegionType(
id=counter.next_region_id, id=counter.next_region_id,
Coords=CoordsType(points=self.calculate_points(region_contour, offset))) Coords=CoordsType(points=self.calculate_points(region.contour, offset, 2),
if conf_images: conf=region.conf))
image.Coords.set_conf(conf_images[mm])
page.add_ImageRegion(image) page.add_ImageRegion(image)
for region_contour in polygons_seplines: for region in seplines:
page.add_SeparatorRegion( page.add_SeparatorRegion(
SeparatorRegionType(id=counter.next_region_id, SeparatorRegionType(
Coords=CoordsType(points=self.calculate_points(region_contour, offset)))) id=counter.next_region_id,
Coords=CoordsType(points=self.calculate_points(region.contour, offset, 2),
conf=region.conf)))
for mm, region_contour in enumerate(found_polygons_tables): for region in tables:
table = TableRegionType( table = TableRegionType(
id=counter.next_region_id, id=counter.next_region_id,
Coords=CoordsType(points=self.calculate_points(region_contour, offset))) Coords=CoordsType(points=self.calculate_points(region.contour, offset, 6),
if conf_tables: conf=region.conf))
table.Coords.set_conf(conf_tables[mm])
page.add_TableRegion(table) page.add_TableRegion(table)
return pcgts return pcgts