mirror of
https://github.com/qurator-spk/eynollah.git
synced 2026-07-25 21:19:18 +02:00
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:
parent
5e3fde31d9
commit
beb5545899
5 changed files with 547 additions and 808 deletions
File diff suppressed because it is too large
Load diff
|
|
@ -1,10 +1,11 @@
|
|||
from typing import Optional
|
||||
import os.path
|
||||
try:
|
||||
import matplotlib.pyplot as plt
|
||||
import matplotlib.patches as mpatches
|
||||
except ImportError:
|
||||
plt = mpatches = None
|
||||
import numpy as np
|
||||
import os.path
|
||||
import cv2
|
||||
from scipy.ndimage import gaussian_filter1d
|
||||
|
||||
|
|
@ -21,11 +22,11 @@ class EynollahPlotter:
|
|||
self,
|
||||
*,
|
||||
dir_out,
|
||||
dir_of_all,
|
||||
dir_save_page,
|
||||
dir_of_deskewed,
|
||||
dir_of_layout,
|
||||
dir_of_cropped_images,
|
||||
dir_of_all: Optional[str] = None,
|
||||
dir_save_page: Optional[str] = None,
|
||||
dir_of_deskewed: Optional[str] = None,
|
||||
dir_of_layout: Optional[str] = None,
|
||||
dir_of_cropped_images: Optional[str] = None,
|
||||
):
|
||||
self.dir_out = dir_out
|
||||
self.dir_of_all = dir_of_all
|
||||
|
|
@ -172,6 +173,8 @@ class EynollahPlotter:
|
|||
x, y, w, h = cv2.boundingRect(cont_ind)
|
||||
box = [x, y, w, h]
|
||||
image, _ = crop_image_inside_box(box, image_page)
|
||||
if not image.size:
|
||||
continue
|
||||
image = resize_image(image,
|
||||
int(image.shape[0] / scale_y),
|
||||
int(image.shape[1] / scale_x))
|
||||
|
|
|
|||
|
|
@ -2,7 +2,8 @@ from typing import Iterable, List, Tuple
|
|||
from logging import getLogger
|
||||
import time
|
||||
import math
|
||||
from itertools import islice
|
||||
from dataclasses import dataclass, field
|
||||
from itertools import islice, compress
|
||||
|
||||
try:
|
||||
import matplotlib.pyplot as plt
|
||||
|
|
@ -10,16 +11,18 @@ try:
|
|||
except ImportError:
|
||||
plt = None
|
||||
import numpy as np
|
||||
from shapely import geometry
|
||||
from shapely import geometry, prepare, ops, plotting
|
||||
import cv2
|
||||
from scipy.signal import find_peaks
|
||||
from scipy.ndimage import gaussian_filter1d
|
||||
from skimage import morphology
|
||||
|
||||
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_new_features_of_contours,
|
||||
polygon2contour,
|
||||
return_contours_of_image,
|
||||
return_parent_contours)
|
||||
|
||||
|
|
@ -39,6 +42,32 @@ def batched(iterable, n):
|
|||
while batch := tuple(islice(iterator, n)):
|
||||
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(
|
||||
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):
|
||||
|
|
@ -800,93 +829,16 @@ def fill_bb_of_drop_capitals(
|
|||
|
||||
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(
|
||||
regions_model_1,
|
||||
regions_model_full,
|
||||
polygons_of_textregions,
|
||||
polygons_of_textregions_d,
|
||||
all_found_textline_polygons,
|
||||
slopes,
|
||||
conf_textregions,
|
||||
regions_model_1: np.ndarray,
|
||||
regions_model_full: np.ndarray,
|
||||
textregions: List[Region],
|
||||
textregions_d: List[Region],
|
||||
label_text=1,
|
||||
label_head_full=2,
|
||||
label_head_final=2,
|
||||
label_main_final=1,
|
||||
):
|
||||
) -> Tuple[np.ndarray, List[Region], List[Region], List[Region], List[Region]]:
|
||||
|
||||
### to make it faster
|
||||
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[0] // zoom),
|
||||
interpolation=cv2.INTER_NEAREST)
|
||||
contours_z = [contour // zoom
|
||||
for contour in polygons_of_textregions]
|
||||
contours_z = [textregion.contour // zoom
|
||||
for textregion in textregions]
|
||||
|
||||
###
|
||||
_, _, 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 = []
|
||||
main = np.ones(len(contours_z), dtype=bool)
|
||||
for ii, con in enumerate(contours_z):
|
||||
width, height = cv2.boundingRect(con)[2:]
|
||||
parent = np.zeros_like(regions_model_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
|
||||
|
||||
if (( pixels_head >= 0.6 * pixels_main and
|
||||
length_con[ii] >= 1.3 * height_con[ii] and
|
||||
length_con[ii] <= 3 * height_con[ii] ) or
|
||||
width >= 1.3 * height and
|
||||
width <= 3 * height ) or
|
||||
( 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
|
||||
|
||||
else:
|
||||
main.append(ii)
|
||||
label = label_main_final
|
||||
|
||||
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)
|
||||
###
|
||||
|
||||
def select(lis, indexes):
|
||||
if not len(lis):
|
||||
return []
|
||||
return [lis[ind] for ind in indexes]
|
||||
|
||||
return (regions_model_1,
|
||||
select(polygons_of_textregions, main),
|
||||
select(polygons_of_textregions, head),
|
||||
select(polygons_of_textregions_d, main),
|
||||
select(polygons_of_textregions_d, head),
|
||||
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),
|
||||
list(compress(textregions, main)),
|
||||
list(compress(textregions, ~main)),
|
||||
list(compress(textregions_d, main)),
|
||||
list(compress(textregions_d, ~main)),
|
||||
)
|
||||
|
||||
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
|
||||
contours) by all intersecting lines with small area
|
||||
"""
|
||||
textlines_con_new = []
|
||||
for region in textlines_con:
|
||||
areas_cnt_text = np.array(list(map(cv2.contourArea, region)))
|
||||
areas_cnt_text = areas_cnt_text / float(textline_mask.size)
|
||||
indexes_textlines = np.arange(len(region))
|
||||
for textregion in textregions:
|
||||
areas = np.array([line.area for line in textregion.lines]) * area_factor
|
||||
|
||||
if num_col == 0:
|
||||
min_area = 0.0004
|
||||
|
|
@ -977,52 +911,43 @@ def small_textlines_to_parent_adherence2(textlines_con, textline_mask, num_col):
|
|||
min_area = 0.0003
|
||||
else:
|
||||
min_area = 0.0001
|
||||
indexes_textlines_small = indexes_textlines[areas_cnt_text < min_area]
|
||||
indexes_textlines_large = indexes_textlines[areas_cnt_text >= min_area]
|
||||
textlines_small = list(compress(textregion.lines, areas < 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]
|
||||
textlines_large = [region[i] for i in indexes_textlines_large]
|
||||
|
||||
img_small = np.zeros_like(textline_mask)
|
||||
img_small = cv2.fillPoly(img_small, pts=textlines_small, color=1)
|
||||
img_large = np.zeros_like(textline_mask)
|
||||
img_large = cv2.fillPoly(img_large, pts=textlines_large, color=1)
|
||||
img_inter = img_small + img_large == 2
|
||||
if np.any(img_inter):
|
||||
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))
|
||||
if geometry.MultiPolygon(textlines_small_poly).intersects(
|
||||
geometry.MultiPolygon(textlines_large_poly)):
|
||||
# FIXME: also consider confidence (less certain lines replaced by better ones)...
|
||||
textlines_large_prep = [prepare(poly) for poly in textlines_large_poly]
|
||||
textlines_small_indexes_interlarge = []
|
||||
for small_poly in textlines_small_poly:
|
||||
intersections = [small_poly.intersection(prep).area
|
||||
if prep.intersects(small_poly) else 0
|
||||
for prep in textlines_large_prep]
|
||||
idx_large = np.argmax(intersections)
|
||||
if intersections[idx_large] <= 0:
|
||||
if intersections[idx_large] == 0:
|
||||
idx_large = -1
|
||||
indexes_textlines_inter.append(idx_large)
|
||||
|
||||
indexes_textlines_inter = np.array(indexes_textlines_inter)
|
||||
for idx_large in set(indexes_textlines_inter):
|
||||
textlines_small_indexes_interlarge.append(idx_large)
|
||||
textlines_small_indexes_interlarge = np.array(textlines_small_indexes_interlarge)
|
||||
for idx_large in set(textlines_small_indexes_interlarge):
|
||||
if idx_large < 0:
|
||||
continue
|
||||
img0_union = np.zeros_like(textline_mask)
|
||||
img0_union = cv2.fillPoly(img0_union, pts=[textlines_large[idx_large]], color=255)
|
||||
indexes_inter_small = np.flatnonzero(indexes_textlines_inter == idx_large)
|
||||
for idx_small in indexes_inter_small:
|
||||
img0_union = cv2.fillPoly(img0_union, pts=[textlines_small[idx_small]], color=255)
|
||||
|
||||
_, thresh = cv2.threshold(img0_union, 0, 255, 0)
|
||||
contours_union, _ = cv2.findContours(thresh.astype(np.uint8),
|
||||
cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
|
||||
areas_union = np.array(list(map(cv2.contourArea, contours_union)))
|
||||
contour_union = contours_union[np.argmax(areas_union)] #contours_union[0]
|
||||
textlines_large[idx_large] = contour_union
|
||||
|
||||
textlines_con_new.append(textlines_large)
|
||||
return textlines_con_new
|
||||
large_poly = textlines_large_poly[idx_large]
|
||||
indexes_small = np.flatnonzero(textlines_small_indexes_interlarge == idx_large)
|
||||
for idx_small in indexes_small:
|
||||
large_poly = large_poly.union(textlines_small_poly[idx_small])
|
||||
if large_poly.geom_type == 'GeometryCollection':
|
||||
# hetergeneous: filter lines and points
|
||||
large_poly = ops.unary_union([geom for geom in large_poly.geoms
|
||||
if geom.area > 0])
|
||||
if large_poly.geom_type == 'MultiPolygon':
|
||||
# disjoint: pick largest part
|
||||
idx_large = np.argmax([geom.area for geom in large_poly.geoms])
|
||||
large_poly = large_poly.geoms[idx_large]
|
||||
# replace original
|
||||
textregion.lines[idx_large].contour = polygon2contour(large_poly)
|
||||
|
||||
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)
|
||||
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')
|
||||
y = textline_mask.sum(axis=1) # horizontal projection profile
|
||||
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)
|
||||
#peaks, _ = find_peaks(z, height=0)
|
||||
#peaks = peaks - 20
|
||||
# ax2 = plt.subplot(2, 1, 2, title="smoothed horizontal projection", sharex=ax1)
|
||||
# plt.plot(y)
|
||||
# ax2 = plt.subplot(1, 2, 2, title="smoothed horizontal projection", sharey=ax1)
|
||||
# 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 = np.zeros(len(zneg_rev) + 40)
|
||||
zneg[20 : len(zneg_rev) + 20] = zneg_rev
|
||||
zneg = gaussian_filter1d(zneg, sigma_gaus)
|
||||
|
||||
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()
|
||||
peaks_neg = peaks_neg - 20 - 20
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
from typing import Sequence, Union
|
||||
from typing import List, Sequence, Union, Tuple
|
||||
from numbers import Number
|
||||
from functools import partial
|
||||
import itertools
|
||||
|
|
@ -167,26 +167,34 @@ def dilate_textregion_contours(all_found_textregion_polygons):
|
|||
[polygon2contour(contour2polygon(contour, dilate=6))
|
||||
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
|
||||
|
||||
cntareas_o = np.array([cv2.contourArea(contour) for contour in contours_o])
|
||||
cntareas_d = np.array([cv2.contourArea(contour) for contour in contours_d])
|
||||
centers_o = np.array([[region.cx, region.cy] for region in regions_o]) # [N, 2]
|
||||
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_d = cntareas_d / float(np.prod(shape_d[:2]))
|
||||
|
||||
contours_o = ensure_array(contours_o)
|
||||
contours_d = ensure_array(contours_d)
|
||||
contours_o = ensure_array([region.contour for region in regions_o])
|
||||
contours_d = ensure_array([region.contour for region in regions_d])
|
||||
|
||||
sort_o = np.argsort(cntareas_o)
|
||||
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_d = contours_d[sort_d]
|
||||
cntareas_o = cntareas_o[sort_o]
|
||||
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_d = centers_d[:, -1:] # [2, 1]
|
||||
|
||||
|
|
|
|||
|
|
@ -3,36 +3,44 @@
|
|||
from pathlib import Path
|
||||
import os.path
|
||||
import logging
|
||||
from typing import Optional
|
||||
from typing import Optional, List, Tuple
|
||||
import numpy as np
|
||||
from shapely import affinity, clip_by_rect
|
||||
|
||||
from ocrd_utils import points_from_polygon
|
||||
from ocrd_models.ocrd_page import (
|
||||
BorderType,
|
||||
CoordsType,
|
||||
TextLineType,
|
||||
TextEquivType,
|
||||
TextRegionType,
|
||||
ImageRegionType,
|
||||
TableRegionType,
|
||||
SeparatorRegionType,
|
||||
to_xml
|
||||
)
|
||||
BorderType,
|
||||
CoordsType,
|
||||
TextLineType,
|
||||
TextEquivType,
|
||||
TextRegionType,
|
||||
ImageRegionType,
|
||||
TableRegionType,
|
||||
SeparatorRegionType,
|
||||
PcGtsType,
|
||||
to_xml
|
||||
)
|
||||
|
||||
from .utils import Region, TextRegion
|
||||
from .utils.xml import create_page_xml, xml_reading_order
|
||||
from .utils.counter import EynollahIdCounter
|
||||
from .utils.contour import contour2polygon, make_valid
|
||||
|
||||
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.counter = EynollahIdCounter()
|
||||
self.dir_out = dir_out
|
||||
self.image_filename = image_filename
|
||||
self.output_filename = os.path.join(self.dir_out or "", self.image_filename_stem) + ".xml"
|
||||
self.curved_line = curved_line
|
||||
self.pcgts = pcgts
|
||||
self.image_height = image_height
|
||||
self.image_width = image_width
|
||||
|
|
@ -40,132 +48,56 @@ class EynollahXmlWriter:
|
|||
self.scale_y = 1.0
|
||||
|
||||
@property
|
||||
def image_filename_stem(self):
|
||||
def image_filename_stem(self) -> str:
|
||||
return Path(Path(self.image_filename).name).stem
|
||||
|
||||
def calculate_points(self, contour, offset=None):
|
||||
poly = contour2polygon(contour)
|
||||
def calculate_points(
|
||||
self, contour: np.ndarray,
|
||||
offset: Optional[List[int]] = None,
|
||||
dilate: int = 0,
|
||||
) -> str:
|
||||
poly = contour2polygon(contour, dilate=dilate)
|
||||
if offset is not None:
|
||||
poly = affinity.translate(poly, *offset)
|
||||
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))
|
||||
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):
|
||||
for j, polygon_textline in enumerate(all_found_textline_polygons[region_idx]):
|
||||
coords = CoordsType()
|
||||
textline = TextLineType(id=counter.next_line_id, Coords=coords)
|
||||
if ocr_all_textlines_textregion:
|
||||
# FIXME: add OCR confidence
|
||||
textline.set_TextEquiv([TextEquivType(Unicode=ocr_all_textlines_textregion[j])])
|
||||
def serialize_lines_in_region(
|
||||
self, text_region: TextRegionType,
|
||||
offset: List[int],
|
||||
counter: EynollahIdCounter,
|
||||
lines: List[Region],
|
||||
) -> None:
|
||||
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.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):
|
||||
self.logger.info("output filename: '%s'", self.output_filename)
|
||||
with open(self.output_filename, 'w') as f:
|
||||
f.write(to_xml(pcgts))
|
||||
|
||||
def build_pagexml_no_full_layout(
|
||||
def build_pagexml(
|
||||
self,
|
||||
*,
|
||||
num_col,
|
||||
found_polygons_text_region,
|
||||
page_coord,
|
||||
page_slope,
|
||||
order_of_texts,
|
||||
all_found_textline_polygons,
|
||||
found_polygons_images,
|
||||
found_polygons_tables,
|
||||
found_polygons_marginals_left,
|
||||
found_polygons_marginals_right,
|
||||
all_found_textline_polygons_marginals_left,
|
||||
all_found_textline_polygons_marginals_right,
|
||||
slopes,
|
||||
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,
|
||||
num_col=1,
|
||||
page_coord: Optional[List[int]] = None,
|
||||
page_contour: Optional[np.ndarray] = None,
|
||||
page_skew: float = 0.,
|
||||
order_of_texts: List[int] = [],
|
||||
textregions: List[TextRegion] = [],
|
||||
textregions_h: List[TextRegion] = [],
|
||||
images: List[Region] = [],
|
||||
tables: List[Region] = [],
|
||||
drop_caps: List[Region] = [],
|
||||
marginals_left: List[TextRegion] = [],
|
||||
marginals_right: List[TextRegion] = [],
|
||||
seplines: List[Region] = [],
|
||||
):
|
||||
self.logger.debug('enter build_pagexml')
|
||||
|
||||
|
|
@ -175,118 +107,92 @@ class EynollahXmlWriter:
|
|||
page = pcgts.get_Page()
|
||||
pcgts.Metadata.Comments = "num_col %d" % num_col
|
||||
page.set_custom('layout {num_col:%d;} ' % num_col)
|
||||
page.set_orientation(-page_slope)
|
||||
if len(cont_page):
|
||||
page.set_Border(BorderType(Coords=CoordsType(points=self.calculate_points(cont_page[0]))))
|
||||
|
||||
offset = [page_coord[2], page_coord[0]]
|
||||
page.set_orientation(-page_skew)
|
||||
if page_contour is not None:
|
||||
page.set_Border(BorderType(Coords=CoordsType(points=self.calculate_points(page_contour))))
|
||||
if page_coord is None:
|
||||
offset = [0, 0]
|
||||
else:
|
||||
offset = [page_coord[2], page_coord[0]]
|
||||
counter = EynollahIdCounter()
|
||||
if len(order_of_texts):
|
||||
_counter_marginals = EynollahIdCounter(region_idx=len(order_of_texts))
|
||||
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
|
||||
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)
|
||||
|
||||
for mm, region_contour in enumerate(found_polygons_text_region):
|
||||
for region in textregions:
|
||||
textregion = TextRegionType(
|
||||
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
|
||||
if conf_textregions:
|
||||
textregion.Coords.set_conf(conf_textregions[mm])
|
||||
self.serialize_lines_in_region(textregion, offset, counter, region.lines)
|
||||
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))
|
||||
for mm, region_contour in enumerate(found_polygons_text_region_h):
|
||||
self.logger.debug('len(textregions_h) %s', len(textregions_h))
|
||||
for region in textregions_h:
|
||||
textregion = TextRegionType(
|
||||
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
|
||||
if conf_textregions_h:
|
||||
textregion.Coords.set_conf(conf_textregions_h[mm])
|
||||
self.serialize_lines_in_region(textregion, offset, counter, region.lines)
|
||||
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):
|
||||
dropcapital = TextRegionType(
|
||||
for region in drop_caps:
|
||||
textregion = TextRegionType(
|
||||
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:
|
||||
dropcapital.Coords.set_conf(conf_drops[mm])
|
||||
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)
|
||||
self.serialize_lines_in_region(textregion, offset, counter, [region])
|
||||
page.add_TextRegion(textregion)
|
||||
|
||||
for mm, region_contour in enumerate(found_polygons_marginals_left):
|
||||
marginal = TextRegionType(
|
||||
for region in marginals_left:
|
||||
textregion = TextRegionType(
|
||||
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:
|
||||
marginal.Coords.set_conf(conf_marginals_left[mm])
|
||||
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)
|
||||
self.serialize_lines_in_region(textregion, offset, counter, region.lines)
|
||||
page.add_TextRegion(textregion)
|
||||
|
||||
for mm, region_contour in enumerate(found_polygons_marginals_right):
|
||||
marginal = TextRegionType(
|
||||
for region in marginals_right:
|
||||
textregion = TextRegionType(
|
||||
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:
|
||||
marginal.Coords.set_conf(conf_marginals_right[mm])
|
||||
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)
|
||||
self.serialize_lines_in_region(textregion, offset, counter, region.lines)
|
||||
page.add_TextRegion(textregion)
|
||||
|
||||
for mm, region_contour in enumerate(found_polygons_images):
|
||||
for region in images:
|
||||
image = ImageRegionType(
|
||||
id=counter.next_region_id,
|
||||
Coords=CoordsType(points=self.calculate_points(region_contour, offset)))
|
||||
if conf_images:
|
||||
image.Coords.set_conf(conf_images[mm])
|
||||
Coords=CoordsType(points=self.calculate_points(region.contour, offset, 2),
|
||||
conf=region.conf))
|
||||
page.add_ImageRegion(image)
|
||||
|
||||
for region_contour in polygons_seplines:
|
||||
for region in seplines:
|
||||
page.add_SeparatorRegion(
|
||||
SeparatorRegionType(id=counter.next_region_id,
|
||||
Coords=CoordsType(points=self.calculate_points(region_contour, offset))))
|
||||
SeparatorRegionType(
|
||||
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(
|
||||
id=counter.next_region_id,
|
||||
Coords=CoordsType(points=self.calculate_points(region_contour, offset)))
|
||||
if conf_tables:
|
||||
table.Coords.set_conf(conf_tables[mm])
|
||||
Coords=CoordsType(points=self.calculate_points(region.contour, offset, 6),
|
||||
conf=region.conf))
|
||||
page.add_TableRegion(table)
|
||||
|
||||
return pcgts
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue