This commit is contained in:
Robert Sachunsky 2026-07-21 09:13:25 +02:00 committed by GitHub
commit 879e479054
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
19 changed files with 1148 additions and 1811 deletions

View file

@ -83,7 +83,7 @@ smoke-test: tests/resources/2files/kant_aufklaerung_1784_0020.tif
eynollah -m $(CURDIR) layout -di $(<D) -o $(TMPDIR) eynollah -m $(CURDIR) layout -di $(<D) -o $(TMPDIR)
test -s $(TMPDIR)/euler_rechenkunst01_1738_0025.xml test -s $(TMPDIR)/euler_rechenkunst01_1738_0025.xml
# mbreorder, directory mode (overwrite): # mbreorder, directory mode (overwrite):
eynollah -m $(CURDIR) machine-based-reading-order -di $(<D) -o $(TMPDIR) eynollah -m $(CURDIR) reorder -mb -di $(<D) -o $(TMPDIR)
fgrep -q http://schema.primaresearch.org/PAGE/gts/pagecontent/2019-07-15 $(TMPDIR)/$(basename $(<F)).xml fgrep -q http://schema.primaresearch.org/PAGE/gts/pagecontent/2019-07-15 $(TMPDIR)/$(basename $(<F)).xml
fgrep -c -e RegionRefIndexed $(TMPDIR)/$(basename $(<F)).xml fgrep -c -e RegionRefIndexed $(TMPDIR)/$(basename $(<F)).xml
# binarize: # binarize:

View file

@ -10,7 +10,7 @@ from .cli_readingorder import readingorder_cli
main.add_command(binarize_cli, 'binarization') main.add_command(binarize_cli, 'binarization')
main.add_command(enhance_cli, 'enhancement') main.add_command(enhance_cli, 'enhancement')
main.add_command(layout_cli, 'layout') main.add_command(layout_cli, 'layout')
main.add_command(readingorder_cli, 'machine-based-reading-order') main.add_command(readingorder_cli, 'reorder')
main.add_command(models_cli, 'models') main.add_command(models_cli, 'models')
main.add_command(ocr_cli, 'ocr') main.add_command(ocr_cli, 'ocr')
main.add_command(extract_images_cli, 'extract-images') main.add_command(extract_images_cli, 'extract-images')

View file

@ -82,7 +82,7 @@ def extract_images_cli(
ignore_page_extraction, ignore_page_extraction,
): ):
""" """
Detect Layout (with optional image enhancement and reading order detection) Detect image regions only
""" """
assert enable_plotting or not save_images, "Plotting with -si also requires -ep" assert enable_plotting or not save_images, "Plotting with -si also requires -ep"
assert not enable_plotting or save_images, "Plotting with -ep also requires -si" assert not enable_plotting or save_images, "Plotting with -ep also requires -si"

View file

@ -3,6 +3,12 @@ import click
@click.command(context_settings=dict( @click.command(context_settings=dict(
help_option_names=['-h', '--help'], help_option_names=['-h', '--help'],
show_default=True)) show_default=True))
@click.option(
"--model_based",
"-mb",
help="use machine-learning model instead of heuristic rules",
is_flag=True,
)
@click.option( @click.option(
"--input", "--input",
"-i", "-i",
@ -15,6 +21,12 @@ import click
help="directory of PAGE-XML input files (instead of --input)", help="directory of PAGE-XML input files (instead of --input)",
type=click.Path(exists=True, file_okay=False), type=click.Path(exists=True, file_okay=False),
) )
@click.option(
"--dir_imgs",
"-dim",
help="directory of image input files (in addition to --dir_in or --input; filename stems must match the XML files, with image file format suffixes). Not needed for --model_based.",
type=click.Path(exists=True, file_okay=False),
)
@click.option( @click.option(
"--out", "--out",
"-o", "-o",
@ -22,17 +34,27 @@ import click
type=click.Path(exists=True, file_okay=False), type=click.Path(exists=True, file_okay=False),
required=True, required=True,
) )
@click.option(
"--overwrite",
"-O",
help="overwrite (instead of skipping) if output xml exists",
is_flag=True,
)
@click.pass_context @click.pass_context
def readingorder_cli(ctx, input, dir_in, out): def readingorder_cli(ctx, model_based, input, dir_in, dir_imgs, out, overwrite):
""" """
Generate ReadingOrder with a ML model Generate ReadingOrder for existing segmentation from ML model or from heuristic rules
""" """
from ..mb_ro_on_layout import Reorder from ..reorder import Reorder
assert bool(input) != bool(dir_in), "Either -i (single input) or -di (directory) must be provided, but not both." assert bool(input) != bool(dir_in), "Either -i (single input) or -di (directory) must be provided, but not both."
assert bool(model_based) or bool(dir_imgs), "For heuristic reading order, -dim must be provided, too."
orderer = Reorder(model_zoo=ctx.obj.model_zoo, orderer = Reorder(model_zoo=ctx.obj.model_zoo,
device=ctx.obj.device) device=ctx.obj.device,
orderer.run(xml_filename=input, model_based=model_based)
orderer.run(overwrite=overwrite,
xml_filename=input,
dir_in=dir_in, dir_in=dir_in,
dir_imgs=dir_imgs,
dir_out=out, dir_out=out,
) )

View file

@ -16,9 +16,11 @@ from eynollah.utils.contour import filter_contours_area_of_image, return_contour
from eynollah.utils.resize import resize_image from eynollah.utils.resize import resize_image
from .model_zoo.model_zoo import EynollahModelZoo from .model_zoo.model_zoo import EynollahModelZoo
from .writer import EynollahXmlWriter
from .eynollah import Eynollah from .eynollah import Eynollah
from .utils import box2rect, is_image_filename from .utils import box2rect, is_image_filename
from .plot import EynollahPlotter from .plot import EynollahPlotter
from .utils import Region
class EynollahImageExtractor(Eynollah): class EynollahImageExtractor(Eynollah):
@ -67,22 +69,29 @@ class EynollahImageExtractor(Eynollah):
self.setup_models() self.setup_models()
self.logger.info(f"Model initialization complete ({time.time() - t_start:.1f}s)") self.logger.info(f"Model initialization complete ({time.time() - t_start:.1f}s)")
def setup_models(self): def setup_models(self, device=''):
loadable = [ loadable = [
"col_classifier", "col_classifier",
"binarization",
"page", "page",
"extract_images", "extract_images",
] ]
self.model_zoo.load_models(*loadable) if self.input_binary:
loadable.append("binarization")
self.model_zoo.load_models(*loadable, device=device)
def get_regions_light_v_extract_only_images(self,img, num_col_classifier): def get_early_layout(
self,
img,
num_col_classifier,
label_text=1,
label_imgs=2,
label_seps=3,
):
self.logger.debug("enter get_regions_extract_images_only") self.logger.debug("enter get_regions_extract_images_only")
erosion_hurts = False erosion_hurts = False
img_org = np.copy(img) # already cropped
img_height_h = img_org.shape[0] img_height_h, img_width_h = img.shape[:2]
img_width_h = img_org.shape[1]
if num_col_classifier == 1: if num_col_classifier == 1:
img_w_new = 700 img_w_new = 700
@ -98,60 +107,47 @@ class EynollahImageExtractor(Eynollah):
img_w_new = 2500 img_w_new = 2500
else: else:
raise ValueError("num_col_classifier must be in range 1..6") raise ValueError("num_col_classifier must be in range 1..6")
img_h_new = int(img.shape[0] / float(img.shape[1]) * img_w_new) img_h_new = img_w_new * img_height_h // img_width_h
img_resized = resize_image(img,img_h_new, img_w_new ) img_resized = resize_image(img, img_h_new, img_w_new)
prediction_regions_org, _ = self.do_prediction_new_concept(True, img_resized, self.model_zoo.get("extract_images")) prediction_regions, _ = self.do_prediction_new_concept(
True, img_resized, self.model_zoo.get("extract_images"))
prediction_regions = resize_image(prediction_regions, img_height_h, img_width_h)
prediction_regions_org = resize_image(prediction_regions_org,img_height_h, img_width_h ) mask_texts_only = (prediction_regions == label_text).astype(np.uint8)
image_page, page_coord, cont_page = self.extract_page() mask_images_only = (prediction_regions == label_imgs).astype(np.uint8)
mask_seps_only = (prediction_regions == label_seps).astype(np.uint8)
prediction_regions_org = prediction_regions_org[page_coord[0] : page_coord[1], page_coord[2] : page_coord[3]] texts_only_cont = return_contours_of_interested_region(mask_texts_only,1,0.00001)
prediction_regions_org=prediction_regions_org[:,:,0] seps_only_cont = return_contours_of_interested_region(mask_seps_only,1,0.00001)
mask_seps_only = (prediction_regions_org[:,:] ==3)*1 text_regions_p = np.zeros_like(prediction_regions)
mask_texts_only = (prediction_regions_org[:,:] ==1)*1 text_regions_p = cv2.fillPoly(text_regions_p, pts=seps_only_cont, color=label_seps)
mask_images_only=(prediction_regions_org[:,:] ==2)*1 text_regions_p[mask_images_only == 1] = label_imgs
text_regions_p = cv2.fillPoly(text_regions_p, pts=texts_only_cont, color=label_text)
polygons_seplines, hir_seplines = return_contours_of_image(mask_seps_only) # rs: why?
polygons_seplines = filter_contours_area_of_image( text_regions_p[-15:] = 0
mask_seps_only, polygons_seplines, hir_seplines, max_area=1, min_area=0.00001, dilate=1) text_regions_p[:, -15:] = 0
polygons_of_only_texts = return_contours_of_interested_region(mask_texts_only,1,0.00001) images_cont = return_contours_of_interested_region(text_regions_p, label_imgs, 0.001)
polygons_of_only_seps = return_contours_of_interested_region(mask_seps_only,1,0.00001)
text_regions_p_true = np.zeros(prediction_regions_org.shape) images_cont_fin = []
text_regions_p_true = cv2.fillPoly(text_regions_p_true, pts = polygons_of_only_seps, color=(3,3,3)) for cont in images_cont:
_, _, w, h = box = cv2.boundingRect(cont)
text_regions_p_true[:,:][mask_images_only[:,:] == 1] = 2
text_regions_p_true = cv2.fillPoly(text_regions_p_true, pts=polygons_of_only_texts, color=(1,1,1))
text_regions_p_true[text_regions_p_true.shape[0]-15:text_regions_p_true.shape[0], :] = 0
text_regions_p_true[:, text_regions_p_true.shape[1]-15:text_regions_p_true.shape[1]] = 0
##polygons_of_images = return_contours_of_interested_region(text_regions_p_true, 2, 0.0001)
polygons_of_images = return_contours_of_interested_region(text_regions_p_true, 2, 0.001)
polygons_of_images_fin = []
for ploy_img_ind in polygons_of_images:
box = _, _, w, h = cv2.boundingRect(ploy_img_ind)
if h < 150 or w < 150: if h < 150 or w < 150:
pass pass
else: else:
page_coord_img = box2rect(box) # type: ignore y1, y2, x1, x2 = box2rect(box) # type: ignore
polygons_of_images_fin.append(np.array([[page_coord_img[2], page_coord_img[0]], images_cont_fin.append(np.array([[[x1, y1]],
[page_coord_img[3], page_coord_img[0]], [[x2, y1]],
[page_coord_img[3], page_coord_img[1]], [[x2, y2]],
[page_coord_img[2], page_coord_img[1]]])) [[x1, y2]]]))
self.logger.debug("exit get_regions_extract_images_only") self.logger.debug("exit get_regions_extract_images_only")
return (text_regions_p_true, return (text_regions_p,
erosion_hurts, erosion_hurts,
polygons_seplines, images_cont_fin)
polygons_of_images_fin,
image_page,
page_coord,
cont_page)
def run(self, def run(self,
overwrite: bool = False, overwrite: bool = False,
@ -159,16 +155,12 @@ class EynollahImageExtractor(Eynollah):
dir_in: Optional[str] = None, dir_in: Optional[str] = None,
dir_out: Optional[str] = None, dir_out: Optional[str] = None,
dir_of_cropped_images: Optional[str] = None, dir_of_cropped_images: Optional[str] = None,
dir_of_layout: Optional[str] = None, **kwargs
dir_of_deskewed: Optional[str] = None,
dir_of_all: Optional[str] = None,
dir_save_page: Optional[str] = None,
): ):
""" """
Get image and scales, then extract the page of scanned image Get image and scales, then extract the page of scanned image
""" """
self.logger.debug("enter run") self.logger.debug("enter run")
t0_tot = time.time()
# Log enabled features directly # Log enabled features directly
enabled_modes = [] enabled_modes = []
if self.full_layout: if self.full_layout:
@ -181,12 +173,12 @@ class EynollahImageExtractor(Eynollah):
self.logger.info("Saving debug plots") self.logger.info("Saving debug plots")
if dir_of_cropped_images: if dir_of_cropped_images:
self.logger.info(f"Saving cropped images to: {dir_of_cropped_images}") self.logger.info(f"Saving cropped images to: {dir_of_cropped_images}")
if dir_of_layout: self.plotter = EynollahPlotter(
self.logger.info(f"Saving layout plots to: {dir_of_layout}") dir_out=dir_out,
if dir_of_deskewed: dir_of_cropped_images=dir_of_cropped_images,
self.logger.info(f"Saving deskewed images to: {dir_of_deskewed}") )
if dir_in: if dir_in:
t0_tot = time.time()
ls_imgs = [os.path.join(dir_in, image_filename) ls_imgs = [os.path.join(dir_in, image_filename)
for image_filename in filter(is_image_filename, for image_filename in filter(is_image_filename,
os.listdir(dir_in))] os.listdir(dir_in))]
@ -196,79 +188,73 @@ class EynollahImageExtractor(Eynollah):
raise ValueError("run requires either a single image filename or a directory") raise ValueError("run requires either a single image filename or a directory")
for img_filename in ls_imgs: for img_filename in ls_imgs:
self.logger.info(img_filename) self.run_single(img_filename, dir_out=dir_out, overwrite=overwrite)
t0 = time.time()
self.reset_file_name_dir(img_filename, dir_out)
if self.enable_plotting:
self.plotter = EynollahPlotter(dir_out=dir_out,
dir_of_all=dir_of_all,
dir_save_page=dir_save_page,
dir_of_deskewed=dir_of_deskewed,
dir_of_cropped_images=dir_of_cropped_images,
dir_of_layout=dir_of_layout,
image_filename_stem=Path(img_filename).stem)
#print("text region early -11 in %.1fs", time.time() - t0)
if os.path.exists(self.writer.output_filename):
if overwrite:
self.logger.warning("will overwrite existing output file '%s'", self.writer.output_filename)
else:
self.logger.warning("will skip input for existing output file '%s'", self.writer.output_filename)
continue
pcgts = self.run_single()
self.logger.info("Job done in %.1fs", time.time() - t0)
self.writer.write_pagexml(pcgts)
if dir_in: if dir_in:
self.logger.info("All jobs done in %.1fs", time.time() - t0_tot) self.logger.info("All jobs done in %.1fs", time.time() - t0_tot)
def run_single(self): def run_single(self,
img_filename: str,
dir_out: Optional[str] = None,
overwrite: bool = False
) -> None:
t0 = time.time() t0 = time.time()
self.logger.info(img_filename)
self.logger.info(f"Processing file: {self.writer.image_filename}") image = self.cache_images(image_filename=img_filename)
writer = EynollahXmlWriter(
dir_out=dir_out,
image_filename=img_filename,
image_width=image['img'].shape[1],
image_height=image['img'].shape[0],
)
if os.path.exists(writer.output_filename):
if overwrite:
self.logger.warning("will overwrite existing output file '%s'", writer.output_filename)
else:
self.logger.warning("will skip input for existing output file '%s'", writer.output_filename)
return
self.logger.info(f"Processing file: {writer.image_filename}")
self.logger.info("Step 1/5: Image Enhancement") self.logger.info("Step 1/5: Image Enhancement")
img_res, is_image_enhanced, num_col_classifier, _ = \ num_col_classifier, _ = self.run_enhancement(image)
self.run_enhancement() writer.scale_x = image['scale_x']
writer.scale_y = image['scale_y']
self.logger.info(f"Image: {self.image.shape[1]}x{self.image.shape[0]}, "
f"{self.dpi} DPI, {num_col_classifier} columns")
if is_image_enhanced:
self.logger.info("Enhancement applied")
self.logger.info(f"Image: {image['img_res'].shape[1]}x{image['img_res'].shape[0]}, "
f"scale {image['scale_x']:.1f}x{image['scale_y']:.1f}, "
f"{image['dpi']} DPI, {num_col_classifier} columns")
self.logger.info(f"Enhancement complete ({time.time() - t0:.1f}s)") self.logger.info(f"Enhancement complete ({time.time() - t0:.1f}s)")
# Image Extraction Mode # Image Extraction Mode
self.logger.info("Step 2/5: Image Extraction Mode") self.logger.info("Step 2/5: Image Extraction Mode")
t1 = time.time()
page_cont, image_page, _ = self.extract_page(image)
page = Region(page_cont)
_, _, _, polygons_of_images, \ _, _, images_cont = self.get_early_layout(
image_page, page_coord, cont_page = \ image['img_res'], num_col_classifier)
self.get_regions_light_v_extract_only_images(img_res, num_col_classifier) self.logger.debug("Found %d images", len(images_cont))
pcgts = self.writer.build_pagexml_no_full_layout( # FIXME: post-hoc cropping (remove when models support it, and replace image['img_res'] with image_page)
found_polygons_text_region=[], page_box = cv2.boundingRect(page.contour)
page_coord=page_coord, images_cont = [cont - [page_box[:2]]
order_of_texts=[], for cont in images_cont]
all_found_textline_polygons=[],
all_box_coord=[],
found_polygons_text_region_img=polygons_of_images,
found_polygons_marginals_left=[],
found_polygons_marginals_right=[],
all_found_textline_polygons_marginals_left=[],
all_found_textline_polygons_marginals_right=[],
all_box_coord_marginals_left=[],
all_box_coord_marginals_right=[],
slopes=[],
slopes_marginals_left=[],
slopes_marginals_right=[],
cont_page=cont_page,
polygons_seplines=[],
found_polygons_tables=[],
)
if self.plotter: if self.plotter:
self.plotter.write_images_into_directory(polygons_of_images, image_page) self.plotter.write_images_into_directory(images_cont, image_page,
name=image['name'])
self.logger.info("Image extraction complete") self.logger.info("Image extraction complete")
return pcgts
images = [Region(cont) for cont in images_cont]
# can be empty if above page frame
images = [image for image in images if image.area]
pcgts = writer.build_pagexml(
page=page,
img_bin=self.imread(image, binary=True) if self.input_binary else None,
num_col=num_col_classifier,
images=images,
)
writer.write_pagexml(pcgts)
self.logger.info("Job done in %.1fs", time.time() - t0)

File diff suppressed because it is too large Load diff

View file

@ -3,9 +3,9 @@ Image enhancer. The output can be written as same scale of input or in new predi
""" """
import logging import logging
import time
import os import os
from typing import Optional from typing import Optional
from pathlib import Path
import cv2 import cv2
@ -48,6 +48,8 @@ class Enhancer(Eynollah):
dir_out: Optional[str] = None, dir_out: Optional[str] = None,
overwrite: bool = False, overwrite: bool = False,
) -> None: ) -> None:
t0 = time.time()
self.logger.info(img_filename)
image = self.cache_images(image_filename=img_filename, image_pil=img_pil) image = self.cache_images(image_filename=img_filename, image_pil=img_pil)
output_filename = os.path.join(dir_out or "", image['name'] + '.png') output_filename = os.path.join(dir_out or "", image['name'] + '.png')
@ -67,6 +69,7 @@ class Enhancer(Eynollah):
cv2.imwrite(output_filename, img_res) cv2.imwrite(output_filename, img_res)
self.logger.info("output filename: '%s'", output_filename) self.logger.info("output filename: '%s'", output_filename)
self.logger.info("Job done in %.1fs", time.time() - t0)
def run(self, def run(self,
overwrite: bool = False, overwrite: bool = False,
@ -78,6 +81,7 @@ class Enhancer(Eynollah):
Enlarge and enhance the scanned images Enlarge and enhance the scanned images
""" """
if dir_in: if dir_in:
t0_tot = time.time()
ls_imgs = [os.path.join(dir_in, image_filename) ls_imgs = [os.path.join(dir_in, image_filename)
for image_filename in filter(is_image_filename, for image_filename in filter(is_image_filename,
os.listdir(dir_in))] os.listdir(dir_in))]
@ -87,8 +91,8 @@ class Enhancer(Eynollah):
raise ValueError("run requires either a single image filename or a directory") raise ValueError("run requires either a single image filename or a directory")
for img_filename in ls_imgs: for img_filename in ls_imgs:
self.logger.info(img_filename)
self.run_single(img_filename, self.run_single(img_filename,
dir_out=dir_out, dir_out=dir_out,
overwrite=overwrite) overwrite=overwrite)
if dir_in:
self.logger.info("All jobs done in %.1fs", time.time() - t0_tot)

View file

@ -1,806 +0,0 @@
"""
Machine learning based reading order detection
"""
# pyright: reportCallIssue=false
# pyright: reportUnboundVariable=false
# pyright: reportArgumentType=false
import logging
import os
import time
from typing import Optional
from pathlib import Path
import xml.etree.ElementTree as ET
import cv2
import numpy as np
import statistics
from .eynollah import Eynollah
from .model_zoo import EynollahModelZoo
from .utils.resize import resize_image
from .utils.contour import (
find_new_features_of_contours,
return_contours_of_image,
return_parent_contours,
)
from .utils import is_xml_filename
DPI_THRESHOLD = 298
KERNEL = np.ones((5, 5), np.uint8)
class Reorder(Eynollah):
def __init__(
self,
*,
model_zoo: EynollahModelZoo,
logger : Optional[logging.Logger] = None,
device: str = '',
):
self.logger = logger or logging.getLogger('eynollah.mbreorder')
self.model_zoo = model_zoo
self.setup_models(device=device)
def setup_models(self, device=''):
loadable = ['reading_order']
self.model_zoo.load_models(*loadable, device=device)
for model in loadable:
self.logger.debug("model %s has input shape %s", model,
self.model_zoo.get(model).input_shape)
def read_xml(self, xml_file):
tree1 = ET.parse(xml_file, parser = ET.XMLParser(encoding='utf-8'))
root1=tree1.getroot()
alltags=[elem.tag for elem in root1.iter()]
link=alltags[0].split('}')[0]+'}'
index_tot_regions = []
tot_region_ref = []
y_len, x_len = 0, 0
for jj in root1.iter(link+'Page'):
y_len=int(jj.attrib['imageHeight'])
x_len=int(jj.attrib['imageWidth'])
for jj in root1.iter(link+'RegionRefIndexed'):
index_tot_regions.append(jj.attrib['index'])
tot_region_ref.append(jj.attrib['regionRef'])
if (link+'PrintSpace' in alltags) or (link+'Border' in alltags):
co_printspace = []
if link+'PrintSpace' in alltags:
region_tags_printspace = np.unique([x for x in alltags if x.endswith('PrintSpace')])
else:
region_tags_printspace = np.unique([x for x in alltags if x.endswith('Border')])
for tag in region_tags_printspace:
if link+'PrintSpace' in alltags:
tag_endings_printspace = ['}PrintSpace','}printspace']
else:
tag_endings_printspace = ['}Border','}border']
if tag.endswith(tag_endings_printspace[0]) or tag.endswith(tag_endings_printspace[1]):
for nn in root1.iter(tag):
c_t_in = []
sumi = 0
for vv in nn.iter():
# check the format of coords
if vv.tag == link + 'Coords':
coords = bool(vv.attrib)
if coords:
p_h = vv.attrib['points'].split(' ')
c_t_in.append(
np.array([[int(x.split(',')[0]), int(x.split(',')[1])] for x in p_h]))
break
else:
pass
if vv.tag == link + 'Point':
c_t_in.append([int(float(vv.attrib['x'])), int(float(vv.attrib['y']))])
sumi += 1
elif vv.tag != link + 'Point' and sumi >= 1:
break
co_printspace.append(np.array(c_t_in))
img_printspace = np.zeros( (y_len,x_len,3) )
img_printspace=cv2.fillPoly(img_printspace, pts =co_printspace, color=(1,1,1))
img_printspace = img_printspace.astype(np.uint8)
imgray = cv2.cvtColor(img_printspace, cv2.COLOR_BGR2GRAY)
_, thresh = cv2.threshold(imgray, 0, 255, 0)
contours, _ = cv2.findContours(thresh, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE)
cnt_size = np.array([cv2.contourArea(contours[j]) for j in range(len(contours))])
cnt = contours[np.argmax(cnt_size)]
x, y, w, h = cv2.boundingRect(cnt)
bb_coord_printspace = [x, y, w, h]
else:
bb_coord_printspace = None
region_tags=np.unique([x for x in alltags if x.endswith('Region')])
co_text_paragraph=[]
co_text_drop=[]
co_text_heading=[]
co_text_header=[]
co_text_marginalia=[]
co_text_catch=[]
co_text_page_number=[]
co_text_signature_mark=[]
co_sep=[]
co_img=[]
co_table=[]
co_graphic=[]
co_graphic_text_annotation=[]
co_graphic_decoration=[]
co_noise=[]
co_text_paragraph_text=[]
co_text_drop_text=[]
co_text_heading_text=[]
co_text_header_text=[]
co_text_marginalia_text=[]
co_text_catch_text=[]
co_text_page_number_text=[]
co_text_signature_mark_text=[]
co_sep_text=[]
co_img_text=[]
co_table_text=[]
co_graphic_text=[]
co_graphic_text_annotation_text=[]
co_graphic_decoration_text=[]
co_noise_text=[]
id_paragraph = []
id_header = []
id_heading = []
id_marginalia = []
for tag in region_tags:
if tag.endswith('}TextRegion') or tag.endswith('}Textregion'):
for nn in root1.iter(tag):
for child2 in nn:
tag2 = child2.tag
if tag2.endswith('}TextEquiv') or tag2.endswith('}TextEquiv'):
for childtext2 in child2:
if childtext2.tag.endswith('}Unicode') or childtext2.tag.endswith('}Unicode'):
if "type" in nn.attrib and nn.attrib['type']=='drop-capital':
co_text_drop_text.append(childtext2.text)
elif "type" in nn.attrib and nn.attrib['type']=='heading':
co_text_heading_text.append(childtext2.text)
elif "type" in nn.attrib and nn.attrib['type']=='signature-mark':
co_text_signature_mark_text.append(childtext2.text)
elif "type" in nn.attrib and nn.attrib['type']=='header':
co_text_header_text.append(childtext2.text)
###elif "type" in nn.attrib and nn.attrib['type']=='catch-word':
###co_text_catch_text.append(childtext2.text)
###elif "type" in nn.attrib and nn.attrib['type']=='page-number':
###co_text_page_number_text.append(childtext2.text)
elif "type" in nn.attrib and nn.attrib['type']=='marginalia':
co_text_marginalia_text.append(childtext2.text)
else:
co_text_paragraph_text.append(childtext2.text)
c_t_in_drop=[]
c_t_in_paragraph=[]
c_t_in_heading=[]
c_t_in_header=[]
c_t_in_page_number=[]
c_t_in_signature_mark=[]
c_t_in_catch=[]
c_t_in_marginalia=[]
sumi=0
for vv in nn.iter():
# check the format of coords
if vv.tag==link+'Coords':
coords=bool(vv.attrib)
if coords:
#print('birda1')
p_h=vv.attrib['points'].split(' ')
if "type" in nn.attrib and nn.attrib['type']=='drop-capital':
c_t_in_drop.append( np.array( [ [ int(x.split(',')[0]) , int(x.split(',')[1]) ] for x in p_h] ) )
elif "type" in nn.attrib and nn.attrib['type']=='heading':
##id_heading.append(nn.attrib['id'])
c_t_in_heading.append( np.array( [ [ int(x.split(',')[0]) , int(x.split(',')[1]) ] for x in p_h] ) )
elif "type" in nn.attrib and nn.attrib['type']=='signature-mark':
c_t_in_signature_mark.append( np.array( [ [ int(x.split(',')[0]) , int(x.split(',')[1]) ] for x in p_h] ) )
#print(c_t_in_paragraph)
elif "type" in nn.attrib and nn.attrib['type']=='header':
#id_header.append(nn.attrib['id'])
c_t_in_header.append( np.array( [ [ int(x.split(',')[0]) , int(x.split(',')[1]) ] for x in p_h] ) )
###elif "type" in nn.attrib and nn.attrib['type']=='catch-word':
###c_t_in_catch.append( np.array( [ [ int(x.split(',')[0]) , int(x.split(',')[1]) ] for x in p_h] ) )
###elif "type" in nn.attrib and nn.attrib['type']=='page-number':
###c_t_in_page_number.append( np.array( [ [ int(x.split(',')[0]) , int(x.split(',')[1]) ] for x in p_h] ) )
elif "type" in nn.attrib and nn.attrib['type']=='marginalia':
#id_marginalia.append(nn.attrib['id'])
c_t_in_marginalia.append( np.array( [ [ int(x.split(',')[0]) , int(x.split(',')[1]) ] for x in p_h] ) )
else:
#id_paragraph.append(nn.attrib['id'])
c_t_in_paragraph.append( np.array( [ [ int(x.split(',')[0]) , int(x.split(',')[1]) ] for x in p_h] ) )
break
else:
pass
if vv.tag==link+'Point':
if "type" in nn.attrib and nn.attrib['type']=='drop-capital':
c_t_in_drop.append([ int(float(vv.attrib['x'])) , int(float(vv.attrib['y'])) ])
sumi+=1
elif "type" in nn.attrib and nn.attrib['type']=='heading':
#id_heading.append(nn.attrib['id'])
c_t_in_heading.append([ int(float(vv.attrib['x'])) , int(float(vv.attrib['y'])) ])
sumi+=1
elif "type" in nn.attrib and nn.attrib['type']=='signature-mark':
c_t_in_signature_mark.append([ int(float(vv.attrib['x'])) , int(float(vv.attrib['y'])) ])
sumi+=1
elif "type" in nn.attrib and nn.attrib['type']=='header':
#id_header.append(nn.attrib['id'])
c_t_in_header.append([ int(float(vv.attrib['x'])) , int(float(vv.attrib['y'])) ])
sumi+=1
###elif "type" in nn.attrib and nn.attrib['type']=='catch-word':
###c_t_in_catch.append([ int(float(vv.attrib['x'])) , int(float(vv.attrib['y'])) ])
###sumi+=1
###elif "type" in nn.attrib and nn.attrib['type']=='page-number':
###c_t_in_page_number.append([ int(float(vv.attrib['x'])) , int(float(vv.attrib['y'])) ])
###sumi+=1
elif "type" in nn.attrib and nn.attrib['type']=='marginalia':
#id_marginalia.append(nn.attrib['id'])
c_t_in_marginalia.append([ int(float(vv.attrib['x'])) , int(float(vv.attrib['y'])) ])
sumi+=1
else:
#id_paragraph.append(nn.attrib['id'])
c_t_in_paragraph.append([ int(float(vv.attrib['x'])) , int(float(vv.attrib['y'])) ])
sumi+=1
elif vv.tag!=link+'Point' and sumi>=1:
break
if len(c_t_in_drop)>0:
co_text_drop.append(np.array(c_t_in_drop))
if len(c_t_in_paragraph)>0:
co_text_paragraph.append(np.array(c_t_in_paragraph))
id_paragraph.append(nn.attrib['id'])
if len(c_t_in_heading)>0:
co_text_heading.append(np.array(c_t_in_heading))
id_heading.append(nn.attrib['id'])
if len(c_t_in_header)>0:
co_text_header.append(np.array(c_t_in_header))
id_header.append(nn.attrib['id'])
if len(c_t_in_page_number)>0:
co_text_page_number.append(np.array(c_t_in_page_number))
if len(c_t_in_catch)>0:
co_text_catch.append(np.array(c_t_in_catch))
if len(c_t_in_signature_mark)>0:
co_text_signature_mark.append(np.array(c_t_in_signature_mark))
if len(c_t_in_marginalia)>0:
co_text_marginalia.append(np.array(c_t_in_marginalia))
id_marginalia.append(nn.attrib['id'])
elif tag.endswith('}GraphicRegion') or tag.endswith('}graphicregion'):
for nn in root1.iter(tag):
c_t_in=[]
c_t_in_text_annotation=[]
c_t_in_decoration=[]
sumi=0
for vv in nn.iter():
# check the format of coords
if vv.tag==link+'Coords':
coords=bool(vv.attrib)
if coords:
p_h=vv.attrib['points'].split(' ')
if "type" in nn.attrib and nn.attrib['type']=='handwritten-annotation':
c_t_in_text_annotation.append( np.array( [ [ int(x.split(',')[0]) , int(x.split(',')[1]) ] for x in p_h] ) )
elif "type" in nn.attrib and nn.attrib['type']=='decoration':
c_t_in_decoration.append( np.array( [ [ int(x.split(',')[0]) , int(x.split(',')[1]) ] for x in p_h] ) )
else:
c_t_in.append( np.array( [ [ int(x.split(',')[0]) , int(x.split(',')[1]) ] for x in p_h] ) )
break
else:
pass
if vv.tag==link+'Point':
if "type" in nn.attrib and nn.attrib['type']=='handwritten-annotation':
c_t_in_text_annotation.append([ int(float(vv.attrib['x'])) , int(float(vv.attrib['y'])) ])
sumi+=1
elif "type" in nn.attrib and nn.attrib['type']=='decoration':
c_t_in_decoration.append([ int(float(vv.attrib['x'])) , int(float(vv.attrib['y'])) ])
sumi+=1
else:
c_t_in.append([ int(float(vv.attrib['x'])) , int(float(vv.attrib['y'])) ])
sumi+=1
if len(c_t_in_text_annotation)>0:
co_graphic_text_annotation.append(np.array(c_t_in_text_annotation))
if len(c_t_in_decoration)>0:
co_graphic_decoration.append(np.array(c_t_in_decoration))
if len(c_t_in)>0:
co_graphic.append(np.array(c_t_in))
elif tag.endswith('}ImageRegion') or tag.endswith('}imageregion'):
for nn in root1.iter(tag):
c_t_in=[]
sumi=0
for vv in nn.iter():
# check the format of coords
if vv.tag==link+'Coords':
coords=bool(vv.attrib)
if coords:
p_h=vv.attrib['points'].split(' ')
c_t_in.append( np.array( [ [ int(x.split(',')[0]) , int(x.split(',')[1]) ] for x in p_h] ) )
break
else:
pass
if vv.tag==link+'Point':
c_t_in.append([ int(float(vv.attrib['x'])) , int(float(vv.attrib['y'])) ])
sumi+=1
elif vv.tag!=link+'Point' and sumi>=1:
break
co_img.append(np.array(c_t_in))
co_img_text.append(' ')
elif tag.endswith('}SeparatorRegion') or tag.endswith('}separatorregion'):
for nn in root1.iter(tag):
c_t_in=[]
sumi=0
for vv in nn.iter():
# check the format of coords
if vv.tag==link+'Coords':
coords=bool(vv.attrib)
if coords:
p_h=vv.attrib['points'].split(' ')
c_t_in.append( np.array( [ [ int(x.split(',')[0]) , int(x.split(',')[1]) ] for x in p_h] ) )
break
else:
pass
if vv.tag==link+'Point':
c_t_in.append([ int(float(vv.attrib['x'])) , int(float(vv.attrib['y'])) ])
sumi+=1
elif vv.tag!=link+'Point' and sumi>=1:
break
co_sep.append(np.array(c_t_in))
elif tag.endswith('}TableRegion') or tag.endswith('}tableregion'):
for nn in root1.iter(tag):
c_t_in=[]
sumi=0
for vv in nn.iter():
# check the format of coords
if vv.tag==link+'Coords':
coords=bool(vv.attrib)
if coords:
p_h=vv.attrib['points'].split(' ')
c_t_in.append( np.array( [ [ int(x.split(',')[0]) , int(x.split(',')[1]) ] for x in p_h] ) )
break
else:
pass
if vv.tag==link+'Point':
c_t_in.append([ int(float(vv.attrib['x'])) , int(float(vv.attrib['y'])) ])
sumi+=1
elif vv.tag!=link+'Point' and sumi>=1:
break
co_table.append(np.array(c_t_in))
co_table_text.append(' ')
elif tag.endswith('}NoiseRegion') or tag.endswith('}noiseregion'):
for nn in root1.iter(tag):
c_t_in=[]
sumi=0
for vv in nn.iter():
# check the format of coords
if vv.tag==link+'Coords':
coords=bool(vv.attrib)
if coords:
p_h=vv.attrib['points'].split(' ')
c_t_in.append( np.array( [ [ int(x.split(',')[0]) , int(x.split(',')[1]) ] for x in p_h] ) )
break
else:
pass
if vv.tag==link+'Point':
c_t_in.append([ int(float(vv.attrib['x'])) , int(float(vv.attrib['y'])) ])
sumi+=1
elif vv.tag!=link+'Point' and sumi>=1:
break
co_noise.append(np.array(c_t_in))
co_noise_text.append(' ')
img = np.zeros( (y_len,x_len,3) )
img_poly=cv2.fillPoly(img, pts =co_text_paragraph, color=(1,1,1))
img_poly=cv2.fillPoly(img, pts =co_text_heading, color=(2,2,2))
img_poly=cv2.fillPoly(img, pts =co_text_header, color=(2,2,2))
img_poly=cv2.fillPoly(img, pts =co_text_marginalia, color=(3,3,3))
img_poly=cv2.fillPoly(img, pts =co_img, color=(4,4,4))
img_poly=cv2.fillPoly(img, pts =co_sep, color=(5,5,5))
return tree1, root1, bb_coord_printspace, id_paragraph, id_header+id_heading, co_text_paragraph, co_text_header+co_text_heading,\
tot_region_ref,x_len, y_len,index_tot_regions, img_poly
def return_indexes_of_contours_loctaed_inside_another_list_of_contours(self, contours, contours_loc, cx_main_loc, cy_main_loc, indexes_loc):
indexes_of_located_cont = []
center_x_coordinates_of_located = []
center_y_coordinates_of_located = []
#M_main_tot = [cv2.moments(contours_loc[j])
#for j in range(len(contours_loc))]
#cx_main_loc = [(M_main_tot[j]["m10"] / (M_main_tot[j]["m00"] + 1e-32)) for j in range(len(M_main_tot))]
#cy_main_loc = [(M_main_tot[j]["m01"] / (M_main_tot[j]["m00"] + 1e-32)) for j in range(len(M_main_tot))]
for ij in range(len(contours)):
results = [cv2.pointPolygonTest(contours[ij], (cx_main_loc[ind], cy_main_loc[ind]), False)
for ind in range(len(cy_main_loc)) ]
results = np.array(results)
indexes_in = np.where((results == 0) | (results == 1))
indexes = indexes_loc[indexes_in]# [(results == 0) | (results == 1)]#np.where((results == 0) | (results == 1))
indexes_of_located_cont.append(indexes)
center_x_coordinates_of_located.append(np.array(cx_main_loc)[indexes_in] )
center_y_coordinates_of_located.append(np.array(cy_main_loc)[indexes_in] )
return indexes_of_located_cont, center_x_coordinates_of_located, center_y_coordinates_of_located
def do_order_of_regions_with_model(self, contours_only_text_parent, contours_only_text_parent_h, text_regions_p):
height1 =672#448
width1 = 448#224
height2 =672#448
width2= 448#224
height3 =672#448
width3 = 448#224
inference_bs = 3
ver_kernel = np.ones((5, 1), dtype=np.uint8)
hor_kernel = np.ones((1, 5), dtype=np.uint8)
min_cont_size_to_be_dilated = 10
if len(contours_only_text_parent)>min_cont_size_to_be_dilated:
cx_conts, cy_conts, x_min_conts, x_max_conts, y_min_conts, y_max_conts, _ = find_new_features_of_contours(contours_only_text_parent)
args_cont_located = np.array(range(len(contours_only_text_parent)))
diff_y_conts = np.abs(y_max_conts[:]-y_min_conts)
diff_x_conts = np.abs(x_max_conts[:]-x_min_conts)
mean_x = statistics.mean(diff_x_conts)
median_x = statistics.median(diff_x_conts)
diff_x_ratio= diff_x_conts/mean_x
args_cont_located_excluded = args_cont_located[diff_x_ratio>=1.3]
args_cont_located_included = args_cont_located[diff_x_ratio<1.3]
contours_only_text_parent_excluded = [contours_only_text_parent[ind] for ind in range(len(contours_only_text_parent)) if diff_x_ratio[ind]>=1.3]#contours_only_text_parent[diff_x_ratio>=1.3]
contours_only_text_parent_included = [contours_only_text_parent[ind] for ind in range(len(contours_only_text_parent)) if diff_x_ratio[ind]<1.3]#contours_only_text_parent[diff_x_ratio<1.3]
cx_conts_excluded = [cx_conts[ind] for ind in range(len(cx_conts)) if diff_x_ratio[ind]>=1.3]#cx_conts[diff_x_ratio>=1.3]
cx_conts_included = [cx_conts[ind] for ind in range(len(cx_conts)) if diff_x_ratio[ind]<1.3]#cx_conts[diff_x_ratio<1.3]
cy_conts_excluded = [cy_conts[ind] for ind in range(len(cy_conts)) if diff_x_ratio[ind]>=1.3]#cy_conts[diff_x_ratio>=1.3]
cy_conts_included = [cy_conts[ind] for ind in range(len(cy_conts)) if diff_x_ratio[ind]<1.3]#cy_conts[diff_x_ratio<1.3]
#print(diff_x_ratio, 'ratio')
text_regions_p = text_regions_p.astype('uint8')
if len(contours_only_text_parent_excluded)>0:
textregion_par = np.zeros((text_regions_p.shape[0], text_regions_p.shape[1])).astype('uint8')
textregion_par = cv2.fillPoly(textregion_par, pts=contours_only_text_parent_included, color=(1,1))
else:
textregion_par = (text_regions_p[:,:]==1)*1
textregion_par = textregion_par.astype('uint8')
text_regions_p_textregions_dilated = cv2.erode(textregion_par , hor_kernel, iterations=2)
text_regions_p_textregions_dilated = cv2.dilate(text_regions_p_textregions_dilated , ver_kernel, iterations=4)
text_regions_p_textregions_dilated = cv2.erode(text_regions_p_textregions_dilated , hor_kernel, iterations=1)
text_regions_p_textregions_dilated = cv2.dilate(text_regions_p_textregions_dilated , ver_kernel, iterations=5)
text_regions_p_textregions_dilated[text_regions_p[:,:]>1] = 0
contours_only_dilated, hir_on_text_dilated = return_contours_of_image(text_regions_p_textregions_dilated)
contours_only_dilated = return_parent_contours(contours_only_dilated, hir_on_text_dilated)
indexes_of_located_cont, center_x_coordinates_of_located, center_y_coordinates_of_located = self.return_indexes_of_contours_loctaed_inside_another_list_of_contours(contours_only_dilated, contours_only_text_parent_included, cx_conts_included, cy_conts_included, args_cont_located_included)
if len(args_cont_located_excluded)>0:
for ind in args_cont_located_excluded:
indexes_of_located_cont.append(np.array([ind]))
contours_only_dilated.append(contours_only_text_parent[ind])
center_y_coordinates_of_located.append(0)
array_list = [np.array([elem]) if isinstance(elem, int) else elem for elem in indexes_of_located_cont]
flattened_array = np.concatenate([arr.ravel() for arr in array_list])
#print(len( np.unique(flattened_array)), 'indexes_of_located_cont uniques')
missing_textregions = list( set(np.array(range(len(contours_only_text_parent))) ) - set(np.unique(flattened_array)) )
#print(missing_textregions, 'missing_textregions')
for ind in missing_textregions:
indexes_of_located_cont.append(np.array([ind]))
contours_only_dilated.append(contours_only_text_parent[ind])
center_y_coordinates_of_located.append(0)
if contours_only_text_parent_h:
for vi in range(len(contours_only_text_parent_h)):
indexes_of_located_cont.append(int(vi+len(contours_only_text_parent)))
array_list = [np.array([elem]) if isinstance(elem, int) else elem for elem in indexes_of_located_cont]
flattened_array = np.concatenate([arr.ravel() for arr in array_list])
y_len = text_regions_p.shape[0]
x_len = text_regions_p.shape[1]
img_poly = np.zeros((y_len,x_len), dtype='uint8')
###img_poly[text_regions_p[:,:]==1] = 1
###img_poly[text_regions_p[:,:]==2] = 2
###img_poly[text_regions_p[:,:]==3] = 4
###img_poly[text_regions_p[:,:]==6] = 5
##img_poly[text_regions_p[:,:]==1] = 1
##img_poly[text_regions_p[:,:]==2] = 2
##img_poly[text_regions_p[:,:]==3] = 3
##img_poly[text_regions_p[:,:]==4] = 4
##img_poly[text_regions_p[:,:]==5] = 5
img_poly = np.copy(text_regions_p)
img_header_and_sep = np.zeros((y_len,x_len), dtype='uint8')
if contours_only_text_parent_h:
_, cy_main, x_min_main, x_max_main, y_min_main, y_max_main, _ = find_new_features_of_contours(
contours_only_text_parent_h)
for j in range(len(cy_main)):
img_header_and_sep[int(y_max_main[j]):int(y_max_main[j])+12,
int(x_min_main[j]):int(x_max_main[j])] = 1
co_text_all_org = contours_only_text_parent + contours_only_text_parent_h
if len(contours_only_text_parent)>min_cont_size_to_be_dilated:
co_text_all = contours_only_dilated + contours_only_text_parent_h
else:
co_text_all = contours_only_text_parent + contours_only_text_parent_h
else:
co_text_all_org = contours_only_text_parent
if len(contours_only_text_parent)>min_cont_size_to_be_dilated:
co_text_all = contours_only_dilated
else:
co_text_all = contours_only_text_parent
if not len(co_text_all):
return [], []
labels_con = np.zeros((int(y_len /6.), int(x_len/6.), len(co_text_all)), dtype=bool)
co_text_all = [(i/6).astype(int) for i in co_text_all]
for i in range(len(co_text_all)):
img = labels_con[:,:,i].astype(np.uint8)
#img = cv2.resize(img, (int(img.shape[1]/6), int(img.shape[0]/6)), interpolation=cv2.INTER_NEAREST)
cv2.fillPoly(img, pts=[co_text_all[i]], color=(1,))
labels_con[:,:,i] = img
labels_con = resize_image(labels_con.astype(np.uint8), height1, width1).astype(bool)
img_header_and_sep = resize_image(img_header_and_sep, height1, width1)
img_poly = resize_image(img_poly, height3, width3)
input_1 = np.zeros((inference_bs, height1, width1, 3))
ordered = [list(range(len(co_text_all)))]
index_update = 0
#print(labels_con.shape[2],"number of regions for reading order")
while index_update>=0:
ij_list = ordered.pop(index_update)
i = ij_list.pop(0)
ante_list = []
post_list = []
tot_counter = 0
batch = []
for j in ij_list:
img1 = labels_con[:,:,i].astype(float)
img2 = labels_con[:,:,j].astype(float)
img1[img_poly==5] = 2
img2[img_poly==5] = 2
img1[img_header_and_sep==1] = 3
img2[img_header_and_sep==1] = 3
input_1[len(batch), :, :, 0] = img1 / 3.
input_1[len(batch), :, :, 2] = img2 / 3.
input_1[len(batch), :, :, 1] = img_poly / 5.
tot_counter += 1
batch.append(j)
if tot_counter % inference_bs == 0 or tot_counter == len(ij_list):
y_pr = self.model_zoo.get('reading_order').predict(input_1, verbose=0)
for jb, j in enumerate(batch):
if y_pr[jb][0]>=0.5:
post_list.append(j)
else:
ante_list.append(j)
batch = []
if len(ante_list):
ordered.insert(index_update, ante_list)
index_update += 1
ordered.insert(index_update, [i])
if len(post_list):
ordered.insert(index_update + 1, post_list)
index_update = -1
for index_next, ij_list in enumerate(ordered):
if len(ij_list) > 1:
index_update = index_next
break
ordered = [i[0] for i in ordered]
##id_all_text = np.array(id_all_text)[index_sort]
if len(contours_only_text_parent)>min_cont_size_to_be_dilated:
org_contours_indexes = []
for ind in range(len(ordered)):
region_with_curr_order = ordered[ind]
if region_with_curr_order < len(contours_only_dilated):
if np.isscalar(indexes_of_located_cont[region_with_curr_order]):
org_contours_indexes = org_contours_indexes + [indexes_of_located_cont[region_with_curr_order]]
else:
arg_sort_located_cont = np.argsort(center_y_coordinates_of_located[region_with_curr_order])
org_contours_indexes = org_contours_indexes + list(np.array(indexes_of_located_cont[region_with_curr_order])[arg_sort_located_cont]) ##org_contours_indexes + list (
else:
org_contours_indexes = org_contours_indexes + [indexes_of_located_cont[region_with_curr_order]]
region_ids = ['region_%04d' % i for i in range(len(co_text_all_org))]
return org_contours_indexes, region_ids
else:
region_ids = ['region_%04d' % i for i in range(len(co_text_all_org))]
return ordered, region_ids
def run(self,
overwrite: bool = False,
xml_filename: Optional[str] = None,
dir_in: Optional[str] = None,
dir_out: Optional[str] = None,
):
"""
Get image and scales, then extract the page of scanned image
"""
self.logger.debug("enter run")
t0_tot = time.time()
if dir_in:
ls_xmls = [os.path.join(dir_in, xml_filename)
for xml_filename in filter(is_xml_filename,
os.listdir(dir_in))]
elif xml_filename:
ls_xmls = [xml_filename]
else:
raise ValueError("run requires either a single image filename or a directory")
for xml_filename in ls_xmls:
self.logger.info(xml_filename)
t0 = time.time()
file_name = Path(xml_filename).stem
(tree_xml, root_xml, bb_coord_printspace, id_paragraph, id_header,
co_text_paragraph, co_text_header, tot_region_ref,
x_len, y_len, index_tot_regions, img_poly) = self.read_xml(xml_filename)
id_all_text = id_paragraph + id_header
order_text_new, id_of_texts_tot = self.do_order_of_regions_with_model(co_text_paragraph, co_text_header, img_poly[:,:,0])
id_all_text = np.array(id_all_text)[order_text_new]
alltags=[elem.tag for elem in root_xml.iter()]
link=alltags[0].split('}')[0]+'}'
name_space = alltags[0].split('}')[0]
name_space = name_space.split('{')[1]
page_element = root_xml.find(link+'Page')
old_ro = root_xml.find(".//{*}ReadingOrder")
if old_ro is not None:
page_element.remove(old_ro)
#print(old_ro, 'old_ro')
ro_subelement = ET.Element('ReadingOrder')
ro_subelement2 = ET.SubElement(ro_subelement, 'OrderedGroup')
ro_subelement2.set('id', "ro357564684568544579089")
for index, id_text in enumerate(id_all_text):
new_element_2 = ET.SubElement(ro_subelement2, 'RegionRefIndexed')
new_element_2.set('regionRef', id_all_text[index])
new_element_2.set('index', str(index))
if (link+'PrintSpace' in alltags) or (link+'Border' in alltags):
page_element.insert(1, ro_subelement)
else:
page_element.insert(0, ro_subelement)
alltags=[elem.tag for elem in root_xml.iter()]
ET.register_namespace("",name_space)
assert dir_out
tree_xml.write(os.path.join(dir_out, file_name+'.xml'),
xml_declaration=True,
method='xml',
encoding="utf-8",
default_namespace=None)
#sys.exit()

View file

@ -25,6 +25,7 @@ MODEL_VRAM_LIMITS = {
"table": 1818, "table": 1818,
"reading_order": 632, "reading_order": 632,
"ocr": 2600, # 850 for bs 8 "ocr": 2600, # 850 for bs 8
"extract_images": 954,
} }
class EynollahModelZoo: class EynollahModelZoo:
@ -48,7 +49,7 @@ class EynollahModelZoo:
self._overrides = [] self._overrides = []
if model_overrides: if model_overrides:
self.override_models(*model_overrides) self.override_models(*model_overrides)
self._loaded: Dict[str, Union[Predictor, AnyModel]] = {} self._loaded: Dict[str, Predictor] = {}
@property @property
def model_overrides(self): def model_overrides(self):
@ -162,7 +163,7 @@ class EynollahModelZoo:
model._name = model_category model._name = model_category
return model return model
def get(self, model_category: str) -> Union[Predictor, AnyModel]: def get(self, model_category: str) -> Predictor:
if model_category not in self._loaded: if model_category not in self._loaded:
raise ValueError(f'Model "{model_category}" not previously loaded with "load_model(..)"') raise ValueError(f'Model "{model_category}" not previously loaded with "load_model(..)"')
return self._loaded[model_category] return self._loaded[model_category]
@ -245,7 +246,7 @@ class EynollahModelZoo:
self.logger.warning("no GPU device available") self.logger.warning("no GPU device available")
return device0 return device0
def _load_keras_model(self, model_category, model_path, device=''): def _load_keras_model(self, model_category, model_path, device='') -> AnyModel:
os.environ['TF_USE_LEGACY_KERAS'] = '1' # avoid Keras 3 after TF 2.15 os.environ['TF_USE_LEGACY_KERAS'] = '1' # avoid Keras 3 after TF 2.15
from ocrd_utils import tf_disable_interactive_logs from ocrd_utils import tf_disable_interactive_logs
tf_disable_interactive_logs() tf_disable_interactive_logs()
@ -279,7 +280,7 @@ class EynollahModelZoo:
return model return model
def _load_serving_model(self, model_category, model_path, device=''): def _load_serving_model(self, model_category, model_path, device='') -> AnyModel:
from ocrd_utils import tf_disable_interactive_logs from ocrd_utils import tf_disable_interactive_logs
tf_disable_interactive_logs() tf_disable_interactive_logs()
import tensorflow as tf import tensorflow as tf
@ -303,7 +304,7 @@ class EynollahModelZoo:
return model return model
def _load_onnx_model(self, model_category, model_path, device=''): def _load_onnx_model(self, model_category, model_path, device='') -> AnyModel:
import onnxruntime as ort import onnxruntime as ort
import numpy as np import numpy as np
from ocrd_utils import config from ocrd_utils import config

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

@ -1,11 +1,12 @@
from functools import cached_property from functools import cached_property
from typing import Optional from typing import Optional
from PIL import Image
from ocrd_models import OcrdPage from ocrd_models import OcrdPage
from ocrd import OcrdPageResultImage, Processor, OcrdPageResult from ocrd import OcrdPageResultImage, Processor, OcrdPageResult
from eynollah.model_zoo.model_zoo import EynollahModelZoo from eynollah.model_zoo.model_zoo import EynollahModelZoo
from .eynollah import Eynollah, EynollahXmlWriter from .eynollah import Eynollah
class EynollahProcessor(Processor): class EynollahProcessor(Processor):
@cached_property @cached_property
@ -95,4 +96,9 @@ class EynollahProcessor(Processor):
img_pil=page_image, pcgts=pcgts, img_pil=page_image, pcgts=pcgts,
# ocrd.Processor will handle OCRD_EXISTING_OUTPUT more flexibly # ocrd.Processor will handle OCRD_EXISTING_OUTPUT more flexibly
overwrite=True) overwrite=True)
if self.parameter['binarize'] and (img_alt := next(
(img for img in pcgts.Page.AlternativeImage
if img.comments == "binarized"), None)):
result.images.append(OcrdPageResultImage(
Image.fromarray(img_alt.filename), '.IMG-BIN', img_alt))
return result return result

359
src/eynollah/reorder.py Normal file
View file

@ -0,0 +1,359 @@
"""
Machine learning based reading order detection
"""
# pyright: reportCallIssue=false
# pyright: reportUnboundVariable=false
# pyright: reportArgumentType=false
import logging
import os
import time
from typing import Optional
from pathlib import Path
import xml.etree.ElementTree as ET
import cv2
import numpy as np
import statistics
from ocrd_utils import (
polygon_from_points,
xywh_from_points,
)
from .eynollah import Eynollah
from .model_zoo import EynollahModelZoo
from .utils.resize import resize_image
from .utils.contour import (
find_new_features_of_contours,
return_contours_of_image,
return_parent_contours,
)
from .utils import is_xml_filename
DPI_THRESHOLD = 298
KERNEL = np.ones((5, 5), np.uint8)
class Reorder(Eynollah):
def __init__(
self,
*,
model_zoo: EynollahModelZoo,
logger : Optional[logging.Logger] = None,
device: str = '',
model_based: bool = True,
# also expose these on CLI?
ignore_page_extraction: bool = False,
right2left : bool = False,
input_binary: bool = False,
num_col_upper: int = 0,
num_col_lower: int = 0,
):
self.logger = logger or logging.getLogger('eynollah.reorder')
self.model_based = model_based
self.ignore_page_extraction = ignore_page_extraction
self.tables = True # for find_num_col
self.right2left = right2left
self.input_binary = input_binary
self.num_col_upper = num_col_upper
self.num_col_lower = num_col_lower
self.model_zoo = model_zoo
self.setup_models(device=device)
def setup_models(self, device=''):
if self.model_based:
loadable = ['reading_order']
else:
loadable = ['page', 'col_classifier']
if self.input_binary:
loadable.append('binarization')
self.model_zoo.load_models(*loadable, device=device)
for model in loadable:
self.logger.debug("model %s has input shape %s", model,
self.model_zoo.get(model).input_shape)
def read_xml(self,
xml_file: str,
label_text=1,
label_head=2,
label_imgs=5,
label_seps=6,
label_marg=8,
label_drop=4,
label_tabs=10,
):
tree1 = ET.parse(xml_file, parser=ET.XMLParser(encoding='utf-8'))
root1 = tree1.getroot()
alltags=[elem.tag for elem in root1.iter()]
link=alltags[0].split('}')[0]+'}'
index_tot_regions = []
tot_region_ref = []
page = root1.find(link+'Page')
height = int(page.get('imageHeight', 0))
width = int(page.get('imageWidth', 0))
skew = -float(page.get('orientation', 0))
img_filename = page.get('imageFilename', '')
for jj in root1.iter(link+'RegionRefIndexed'):
index_tot_regions.append(jj.attrib['index'])
tot_region_ref.append(jj.attrib['regionRef'])
bb_coord_printspace = None
if (link+'PrintSpace' in alltags or
link+'Border' in alltags):
tag_printspace = next(x for x in alltags
if x.endswith(('Border', 'PrintSpace')))
nn = page.find(tag_printspace)
coords = nn.find(link + 'Coords')
if points := coords.attrib.get('points'):
xywh = xywh_from_points(points)
bb_coord_printspace = [xywh['x'],
xywh['y'],
xywh['w'],
xywh['h']]
seps_cont = []
imgs_cont = []
tabs_cont = []
text_para_cont = []
text_para_ids = []
text_drop_cont = []
text_drop_ids = []
text_head_cont = []
text_head_ids = []
text_marg_cont = []
text_marg_ids = []
for nn in root1.iter():
if not nn.tag.endswith('Region'):
continue
if (coords := nn.find(link + 'Coords')) is None:
continue
if (points := coords.attrib.get('points')) is None:
continue
if (id_ := nn.get('id')) is None:
continue
poly = polygon_from_points(points)
cont = np.array(poly, dtype=int)[:, np.newaxis]
if nn.tag.endswith('}TextRegion'):
type_ = nn.get('type', '')
if type_ == 'drop-capital':
text_drop_cont.append(cont)
text_drop_ids.append(id_)
elif type_ == 'heading':
text_head_cont.append(cont)
text_head_ids.append(id_)
elif type_ == 'header':
# FIXME: do not keep that mapping
text_head_cont.append(cont)
text_head_ids.append(id_)
elif type_ == 'marginalia':
text_marg_cont.append(cont)
text_marg_ids.append(id_)
else:
text_para_cont.append(cont)
text_para_ids.append(id_)
elif nn.tag.endswith('}TableRegion'):
tabs_cont.append(cont)
elif nn.tag.endswith('}GraphicRegion'):
imgs_cont.append(cont)
elif nn.tag.endswith('}ImageRegion'):
imgs_cont.append(cont)
elif nn.tag.endswith('}SeparatorRegion'):
seps_cont.append(cont)
img = np.zeros((height, width), dtype=np.uint8)
img = cv2.fillPoly(img, pts=text_para_cont, color=label_text)
img = cv2.fillPoly(img, pts=text_head_cont, color=label_head)
img = cv2.fillPoly(img, pts=text_marg_cont, color=label_marg)
img = cv2.fillPoly(img, pts=text_drop_cont, color=label_drop)
img = cv2.fillPoly(img, pts=tabs_cont, color=label_tabs)
img = cv2.fillPoly(img, pts=imgs_cont, color=label_imgs)
img = cv2.fillPoly(img, pts=seps_cont, color=label_seps)
return (tree1, root1,
bb_coord_printspace,
text_para_ids, text_head_ids, text_drop_ids,
text_para_cont, text_head_cont, text_drop_cont,
tot_region_ref,
width, height, skew, img_filename,
index_tot_regions,
img)
def run(self,
overwrite: bool = False,
xml_filename: Optional[str] = None,
dir_in: Optional[str] = None,
dir_imgs: Optional[str] = None,
dir_out: Optional[str] = None,
):
"""
Get image and scales, then extract the page of scanned image
"""
self.logger.debug("enter run")
if dir_in:
t0_tot = time.time()
ls_xmls = [os.path.join(dir_in, xml_filename)
for xml_filename in filter(is_xml_filename,
os.listdir(dir_in))]
elif xml_filename:
ls_xmls = [xml_filename]
else:
raise ValueError("run requires either a single image filename or a directory")
for xml_filename in ls_xmls:
self.run_single(xml_filename,
dir_out=dir_out,
dir_imgs=dir_imgs,
overwrite=overwrite)
if dir_in:
self.logger.info("All jobs done in %.1fs", time.time() - t0_tot)
def run_single(self,
xml_filename: str,
dir_imgs: Optional[str] = None,
dir_out: Optional[str] = None,
overwrite: bool = False,
label_imgs=5,
label_seps=6,
) -> None:
self.logger.info(xml_filename)
t0 = time.time()
file_name = Path(xml_filename).stem
(tree_xml, root_xml,
_, # FIXME: crop img_poly and contours (bb_coord_printspace)
para_ids, head_ids, drop_ids,
para_cont, head_cont, drop_cont,
_, # FIXME: do not ignore existing RO (tot_region_ref)
width, height, skew, img_filename,
_, # FIXME: do not ignore existing RO (index_tot_regions)
region_labels) = self.read_xml(xml_filename)
all_text_ids = np.array(para_ids + head_ids + drop_ids)
self.logger.debug("ordering %d paragraphs, %d headings and %d drop-capitals",
len(para_ids), len(head_ids), len(drop_ids))
if self.model_based:
order_text = self.do_order_of_regions_with_model(
para_cont,
head_cont,
drop_cont,
region_labels)
else:
if img_filename and (
os.path.exists(img_path := img_filename) or
os.path.exists(img_path := os.path.join('..', img_filename)) or
dir_imgs and
os.path.exists(img_path := os.path.join(dir_imgs, img_filename)) or
dir_imgs and
os.path.exists(img_path := os.path.join(dir_imgs, os.path.basename(img_filename)))):
img_filename = img_path
else:
xml_basename = Path(xml_filename).with_suffix('')
def try_suffixes(basename, suffixes):
for suf in suffixes:
if (filename := basename.with_suffix(suf)).exists():
yield filename
elif dir_imgs and (filename := (dir_imgs / basename).with_suffix(suf)).exists():
yield filename
img_filename = next(try_suffixes(xml_basename,
['.tif', '.TIF',
'.jpg', '.JPG',
'.png', '.PNG',
'.jpeg', '.gif']))
# load and analyse image
image = self.cache_images(image_filename=img_filename)
_, num_col, _ = self.resize_and_enhance_image_with_column_classifier(image)
if image['img_res'].shape[:2] != region_labels.shape:
# image was resized by col-classifier
# so bring label map to same size
# (order rules have similar expectations)
region_labels = resize_image(region_labels, *image['img_res'].shape[:2])
scale_factor = np.array([[image['scale_x'], image['scale_y']]])
para_cont = [(cont * scale_factor).astype(int) for cont in para_cont]
head_cont = [(cont * scale_factor).astype(int) for cont in head_cont]
drop_cont = [(cont * scale_factor).astype(int) for cont in drop_cont]
# in Eynollah: regions_without_separators
nonsep_labels = np.copy(region_labels)
nonsep_labels[label_seps] = 0
nonsep_labels[label_imgs] = 0
# deskew
if np.abs(skew) >= 0.13: # in Eynollah: SLOPE_THRESHOLD
_, region_labels, nonsep_labels = self.get_deskewed_masks(
skew, np.zeros((1, 1)), region_labels, nonsep_labels)
# also deskew contours
# (directly instead of match_deskewed_contours)
# rotate_image() does not enlarge canvas,
# so our calculation must compensate
h_o, w_o = image['img_res'].shape[:2]
M = cv2.getRotationMatrix2D((0.5 * w_o, 0.5 * h_o), -skew, 1.0)[:2, :2]
cos = np.abs(M[0, 0])
sin = np.abs(M[0, 1])
off = np.array([[0.5 * (w_o * cos + h_o * sin - w_o),
0.5 * (w_o * sin + h_o * cos - h_o)]],
dtype=int)
if skew > 0:
off[0, 1] = -off[0, 1]
else:
off[0, 0] = -off[0, 0]
para_cont = [np.dot(cont, M).astype(int) - off for cont in para_cont]
head_cont = [np.dot(cont, M).astype(int) - off for cont in head_cont]
drop_cont = [np.dot(cont, M).astype(int) - off for cont in drop_cont]
order_text = self.do_order_of_regions_heuristic(
para_cont,
head_cont,
drop_cont,
region_labels,
nonsep_labels,
num_col,
False) # in Eynollah: erosion_hurts
all_text_ids = all_text_ids[order_text]
alltags=[elem.tag for elem in root_xml.iter()]
link=alltags[0].split('}')[0]+'}'
ET.register_namespace("", link[1:-1])
page = root_xml.find(link+'Page')
ro_old = page.find(link+'ReadingOrder')
if ro_old is not None:
page.remove(ro_old)
ro_new = ET.Element('ReadingOrder')
ro_group = ET.SubElement(ro_new, 'OrderedGroup')
ro_group.set('id', "ro357564684568544579089")
for index, id_text in enumerate(all_text_ids):
ro_ref = ET.SubElement(ro_group, 'RegionRefIndexed')
ro_ref.set('regionRef', id_text)
ro_ref.set('index', str(index))
pos = len(page.findall(link+'AlternativeImage') +
page.findall(link+'Border') +
page.findall(link+'PrintSpace'))
page.insert(pos, ro_new)
output_filename = os.path.join(dir_out or "", file_name + '.xml')
self.logger.info("output filename: '%s'", output_filename)
tree_xml.write(output_filename,
xml_declaration=True,
method='xml',
encoding="utf-8",
default_namespace=None)
self.logger.info("Job done in %.1fs", time.time() - t0)

View file

@ -22,6 +22,7 @@ CURRENT_MODELS += model_eynollah_ocr_cnnrnn_20250930
CURRENT_MODELS += eynollah-binarization_20210425 CURRENT_MODELS += eynollah-binarization_20210425
CURRENT_MODELS += eynollah-column-classifier_20210425 CURRENT_MODELS += eynollah-column-classifier_20210425
CURRENT_MODELS += eynollah-enhancement_20210425 CURRENT_MODELS += eynollah-enhancement_20210425
CURRENT_MODELS += eynollah-main-regions_20231127_672_org_ens_11_13_16_17_18
# tf (SavedModel format) for training # tf (SavedModel format) for training
# onnx conversion for fast inference # onnx conversion for fast inference

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, prepared, ops
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 = [prepared.prep(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.context).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
@ -1288,7 +1219,7 @@ def find_number_of_columns_in_document(
separator_mask: np.ndarray, separator_mask: np.ndarray,
num_col_classifier: int, num_col_classifier: int,
tables: bool, tables: bool,
contours_h: List[np.ndarray] = None, contours_h: List[np.ndarray] = [],
logger=None logger=None
) -> Tuple[int, List[int], np.ndarray, List[int], np.ndarray]: ) -> Tuple[int, List[int], np.ndarray, List[int], np.ndarray]:
""" """
@ -1299,7 +1230,7 @@ def find_number_of_columns_in_document(
* separator_mask: mask of (separator-only) region labels * separator_mask: mask of (separator-only) region labels
* num_col_classifier: predicted (expected) number of columns of the page * num_col_classifier: predicted (expected) number of columns of the page
* tables: whether tables may be present * tables: whether tables may be present
* contours_h: polygons of potential headings (serving as additional horizontal separators) * contours_h: contours of potential headings (serving as additional horizontal separators)
* logger * logger
Returns: a tuple of Returns: a tuple of
@ -1422,7 +1353,7 @@ def find_number_of_columns_in_document(
matrix_of_seps_ch[len(cy_seps_hor):,8]=dist_y_ver matrix_of_seps_ch[len(cy_seps_hor):,8]=dist_y_ver
matrix_of_seps_ch[len(cy_seps_hor):,9]=1 matrix_of_seps_ch[len(cy_seps_hor):,9]=1
if contours_h is not None: if len(contours_h):
_, dist_x_head, x_min_head, x_max_head, cy_head, _, y_min_head, y_max_head, _ = \ _, dist_x_head, x_min_head, x_max_head, cy_head, _, y_min_head, y_max_head, _ = \
find_features_of_lines(contours_h) find_features_of_lines(contours_h)
matrix_l_n = np.zeros((len(cy_head), matrix_of_seps_ch.shape[1]), dtype=int) matrix_l_n = np.zeros((len(cy_head), matrix_of_seps_ch.shape[1]), dtype=int)
@ -1451,7 +1382,7 @@ def find_number_of_columns_in_document(
(x_max_seps_hor >= .84 * width)] (x_max_seps_hor >= .84 * width)]
cy_seps_splitters = np.append(cy_seps_splitters, special_separators) cy_seps_splitters = np.append(cy_seps_splitters, special_separators)
if contours_h is not None: if len(contours_h):
y_min_splitters_head = y_min_head[(x_min_head <= .16 * width) & y_min_splitters_head = y_min_head[(x_min_head <= .16 * width) &
(x_max_head >= .84 * width)] (x_max_head >= .84 * width)]
y_max_splitters_head = y_max_head[(x_min_head <= .16 * width) & y_max_splitters_head = y_max_head[(x_min_head <= .16 * width) &

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

@ -1585,6 +1585,7 @@ def do_work_of_slopes_new_curved(
mask_line = np.zeros_like(mask_parent) mask_line = np.zeros_like(mask_parent)
mask_line = cv2.fillPoly(mask_line, pts=[contour], color=1) mask_line = cv2.fillPoly(mask_line, pts=[contour], color=1)
mask_line = cv2.dilate(mask_line, KERNEL, iterations=5 if num_col == 0 else 4) mask_line = cv2.dilate(mask_line, KERNEL, iterations=5 if num_col == 0 else 4)
mask_line *= mask_parent
# plt.subplot(1, 2, 1, title="parent mask") # plt.subplot(1, 2, 1, title="parent mask")
# plt.imshow(mask_parent) # plt.imshow(mask_parent)
# plt.subplot(1, 2, 2, title="single textline") # plt.subplot(1, 2, 2, title="single textline")

View file

@ -3,36 +3,47 @@
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
import cv2
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, AlternativeImageType,
CoordsType, BorderType,
TextLineType, CoordsType,
TextEquivType, TextLineType,
TextRegionType, TextEquivType,
ImageRegionType, TextRegionType,
TableRegionType, ImageRegionType,
SeparatorRegionType, TableRegionType,
to_xml SeparatorRegionType,
) 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,254 +51,159 @@ 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)
if img_alt := next(
(img for img in pcgts.Page.AlternativeImage
if img.comments == "binarized"
and isinstance(img.filename, np.ndarray)), None):
img_alt_filename = self.output_filename[:-4] + '.bin.png'
cv2.imwrite(img_alt_filename, img_alt.filename)
img_alt.filename = os.path.basename(img_alt_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, page: Region,
found_polygons_text_region, img_bin: Optional[np.ndarray] = None,
page_coord, num_col: int = 1,
page_slope, order_of_texts: List[int] = [],
order_of_texts, textregions: List[TextRegion] = [],
all_found_textline_polygons, textregions_h: List[TextRegion] = [],
found_polygons_images, images: List[Region] = [],
found_polygons_tables, tables: List[Region] = [],
found_polygons_marginals_left, drop_caps: List[Region] = [],
found_polygons_marginals_right, marginals_left: List[TextRegion] = [],
all_found_textline_polygons_marginals_left, marginals_right: List[TextRegion] = [],
all_found_textline_polygons_marginals_right, seplines: List[Region] = [],
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,
): ):
self.logger.debug('enter build_pagexml') self.logger.debug('enter build_pagexml')
# create the file structure # create the file structure
pcgts = self.pcgts if self.pcgts else create_page_xml( pcgts = self.pcgts if self.pcgts else create_page_xml(
self.image_filename, self.image_height, self.image_width) self.image_filename, self.image_height, self.image_width)
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) if img_bin is not None:
page.set_orientation(-page_slope) img_alt = AlternativeImageType(filename=img_bin, # will be replaced later
if len(cont_page): comments="binarized")
page.set_Border(BorderType(Coords=CoordsType(points=self.calculate_points(cont_page[0])))) pcgts.Page.add_AlternativeImage(img_alt)
pcgts.Page.set_custom('layout {num_col:%d;} ' % num_col)
offset = [page_coord[2], page_coord[0]] pcgts.Page.set_orientation(-page.skew)
pcgts.Page.set_Border(BorderType(Coords=CoordsType(
points=self.calculate_points(page.contour))))
x, y, w, h = cv2.boundingRect(page.contour)
offset = [x, y]
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(pcgts.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: pcgts.Page.add_TextRegion(textregion)
textregion.Coords.set_conf(conf_textregions[mm])
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: pcgts.Page.add_TextRegion(textregion)
textregion.Coords.set_conf(conf_textregions_h[mm])
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]) pcgts.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]) pcgts.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]) pcgts.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]) pcgts.Page.add_ImageRegion(image)
page.add_ImageRegion(image)
for region_contour in polygons_seplines: for region in seplines:
page.add_SeparatorRegion( pcgts.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]) pcgts.Page.add_TableRegion(table)
page.add_TableRegion(table)
return pcgts return pcgts

View file

@ -1,21 +1,30 @@
import pytest
from ocrd_modelfactory import page_from_file from ocrd_modelfactory import page_from_file
from ocrd_models.constants import NAMESPACES as NS from ocrd_models.constants import NAMESPACES as NS
@pytest.mark.parametrize(
"options",
[
[], # defaults
["--model_based"],
], ids=str)
def test_run_eynollah_mbreorder_filename( def test_run_eynollah_mbreorder_filename(
tmp_path, tmp_path,
resources_dir, resources_dir,
run_eynollah_ok_and_check_logs, run_eynollah_ok_and_check_logs,
options,
): ):
infile = resources_dir / '2files/kant_aufklaerung_1784_0020.xml' infile = resources_dir / '2files/kant_aufklaerung_1784_0020.xml'
outfile = tmp_path /'kant_aufklaerung_1784_0020.xml' outfile = tmp_path /'kant_aufklaerung_1784_0020.xml'
run_eynollah_ok_and_check_logs( run_eynollah_ok_and_check_logs(
'machine-based-reading-order', 'reorder',
[ [
'-i', str(infile), '-i', str(infile),
'-dim', str(resources_dir / '2files'),
'-o', str(outfile.parent), '-o', str(outfile.parent),
], ] + options,
[ [
# FIXME: mbreorder has no logging! str(infile)
] ]
) )
assert outfile.exists() assert outfile.exists()
@ -34,13 +43,15 @@ def test_run_eynollah_mbreorder_directory(
): ):
outdir = tmp_path outdir = tmp_path
run_eynollah_ok_and_check_logs( run_eynollah_ok_and_check_logs(
'machine-based-reading-order', 'reorder',
[ [
'-di', str(resources_dir / '2files'), '-di', str(resources_dir / '2files'),
'-dim', str(resources_dir / '2files'),
'-o', str(outdir), '-o', str(outdir),
], ],
[ [
# FIXME: mbreorder has no logging! 'Job done in',
'All jobs done in',
] ]
) )
assert len(list(outdir.iterdir())) == 2 assert len(list(outdir.iterdir())) == 2

View file

@ -31,7 +31,7 @@ def eynollah_subcommands():
'layout', 'layout',
'ocr', 'ocr',
'enhancement', 'enhancement',
'machine-based-reading-order', 'reorder',
'models', 'models',
] ]