mirror of
https://github.com/qurator-spk/eynollah.git
synced 2026-08-09 04:12:42 +02:00
Merge 642363c7d0 into 15ddb7750e
This commit is contained in:
commit
7052939db2
26 changed files with 1954 additions and 2290 deletions
2
Makefile
2
Makefile
|
|
@ -83,7 +83,7 @@ smoke-test: tests/resources/2files/kant_aufklaerung_1784_0020.tif
|
|||
eynollah -m $(CURDIR) layout -di $(<D) -o $(TMPDIR)
|
||||
test -s $(TMPDIR)/euler_rechenkunst01_1738_0025.xml
|
||||
# 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 -c -e RegionRefIndexed $(TMPDIR)/$(basename $(<F)).xml
|
||||
# binarize:
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
from .cli import main
|
||||
from .cli_binarize import binarize_cli
|
||||
from .cli_enhance import enhance_cli
|
||||
from .cli_extract_page import extract_page_cli
|
||||
from .cli_extract_images import extract_images_cli
|
||||
from .cli_layout import layout_cli
|
||||
from .cli_models import models_cli
|
||||
|
|
@ -10,7 +11,8 @@ from .cli_readingorder import readingorder_cli
|
|||
main.add_command(binarize_cli, 'binarization')
|
||||
main.add_command(enhance_cli, 'enhancement')
|
||||
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(ocr_cli, 'ocr')
|
||||
main.add_command(extract_page_cli, 'extract-page')
|
||||
main.add_command(extract_images_cli, 'extract-images')
|
||||
|
|
|
|||
|
|
@ -82,7 +82,7 @@ def extract_images_cli(
|
|||
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 not enable_plotting or save_images, "Plotting with -ep also requires -si"
|
||||
|
|
|
|||
80
src/eynollah/cli/cli_extract_page.py
Normal file
80
src/eynollah/cli/cli_extract_page.py
Normal file
|
|
@ -0,0 +1,80 @@
|
|||
import click
|
||||
|
||||
@click.command(context_settings=dict(
|
||||
help_option_names=['-h', '--help'],
|
||||
show_default=True))
|
||||
@click.option(
|
||||
"--image",
|
||||
"-i",
|
||||
help="input image filename",
|
||||
type=click.Path(exists=True, dir_okay=False),
|
||||
)
|
||||
|
||||
@click.option(
|
||||
"--out",
|
||||
"-o",
|
||||
help="directory for output PAGE-XML files",
|
||||
type=click.Path(exists=True, file_okay=False),
|
||||
required=True,
|
||||
)
|
||||
@click.option(
|
||||
"--overwrite",
|
||||
"-O",
|
||||
help="overwrite (instead of skipping) if output xml exists",
|
||||
is_flag=True,
|
||||
)
|
||||
@click.option(
|
||||
"--dir_in",
|
||||
"-di",
|
||||
help="directory of input images (instead of --image)",
|
||||
type=click.Path(exists=True, file_okay=False),
|
||||
)
|
||||
@click.option(
|
||||
"--input_binary",
|
||||
"-ib",
|
||||
is_flag=True,
|
||||
help="In general, eynollah uses RGB as input, but if the input document is very dark, very bright or for any other reason you can turn on internal binarization here. When set, eynollah will binarize the RGB input document first.",
|
||||
)
|
||||
@click.option(
|
||||
"--num_col_upper",
|
||||
"-ncu",
|
||||
default=0,
|
||||
type=click.IntRange(min=0),
|
||||
help="lower limit of columns in document image; 0 means autodetected from model",
|
||||
)
|
||||
@click.option(
|
||||
"--num_col_lower",
|
||||
"-ncl",
|
||||
default=0,
|
||||
type=click.IntRange(min=0),
|
||||
help="upper limit of columns in document image; 0 means autodetected from model",
|
||||
)
|
||||
@click.pass_context
|
||||
def extract_page_cli(
|
||||
ctx,
|
||||
image,
|
||||
out,
|
||||
overwrite,
|
||||
dir_in,
|
||||
input_binary,
|
||||
num_col_upper,
|
||||
num_col_lower,
|
||||
):
|
||||
"""
|
||||
Detect image regions only
|
||||
"""
|
||||
assert bool(image) != bool(dir_in), "Either -i (single input) or -di (directory) must be provided, but not both."
|
||||
|
||||
from ..extract_page import EynollahPageExtractor
|
||||
extractor = EynollahPageExtractor(
|
||||
model_zoo=ctx.obj.model_zoo,
|
||||
input_binary=input_binary,
|
||||
num_col_upper=num_col_upper,
|
||||
num_col_lower=num_col_lower,
|
||||
)
|
||||
extractor.run(overwrite=overwrite,
|
||||
image_filename=image,
|
||||
dir_in=dir_in,
|
||||
dir_out=out,
|
||||
)
|
||||
|
||||
|
|
@ -3,6 +3,12 @@ import click
|
|||
@click.command(context_settings=dict(
|
||||
help_option_names=['-h', '--help'],
|
||||
show_default=True))
|
||||
@click.option(
|
||||
"--model_based",
|
||||
"-mb",
|
||||
help="use machine-learning model instead of heuristic rules",
|
||||
is_flag=True,
|
||||
)
|
||||
@click.option(
|
||||
"--input",
|
||||
"-i",
|
||||
|
|
@ -15,6 +21,12 @@ import click
|
|||
help="directory of PAGE-XML input files (instead of --input)",
|
||||
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(
|
||||
"--out",
|
||||
"-o",
|
||||
|
|
@ -22,17 +34,27 @@ import click
|
|||
type=click.Path(exists=True, file_okay=False),
|
||||
required=True,
|
||||
)
|
||||
@click.option(
|
||||
"--overwrite",
|
||||
"-O",
|
||||
help="overwrite (instead of skipping) if output xml exists",
|
||||
is_flag=True,
|
||||
)
|
||||
@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(model_based) or bool(dir_imgs), "For heuristic reading order, -dim must be provided, too."
|
||||
orderer = Reorder(model_zoo=ctx.obj.model_zoo,
|
||||
device=ctx.obj.device)
|
||||
orderer.run(xml_filename=input,
|
||||
device=ctx.obj.device,
|
||||
model_based=model_based)
|
||||
orderer.run(overwrite=overwrite,
|
||||
xml_filename=input,
|
||||
dir_in=dir_in,
|
||||
dir_imgs=dir_imgs,
|
||||
dir_out=out,
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
"""
|
||||
extract images?
|
||||
extract image regions only
|
||||
"""
|
||||
|
||||
from concurrent.futures import ProcessPoolExecutor
|
||||
|
|
@ -16,9 +16,12 @@ from eynollah.utils.contour import filter_contours_area_of_image, return_contour
|
|||
from eynollah.utils.resize import resize_image
|
||||
|
||||
from .model_zoo.model_zoo import EynollahModelZoo
|
||||
from .writer import EynollahXmlWriter
|
||||
from .eynollah import Eynollah
|
||||
from .utils import box2rect, is_image_filename
|
||||
from .utils.tiling import do_prediction_new_concept
|
||||
from .plot import EynollahPlotter
|
||||
from .utils import Region
|
||||
|
||||
class EynollahImageExtractor(Eynollah):
|
||||
|
||||
|
|
@ -67,22 +70,29 @@ class EynollahImageExtractor(Eynollah):
|
|||
self.setup_models()
|
||||
self.logger.info(f"Model initialization complete ({time.time() - t_start:.1f}s)")
|
||||
|
||||
def setup_models(self):
|
||||
def setup_models(self, device=''):
|
||||
|
||||
loadable = [
|
||||
"col_classifier",
|
||||
"binarization",
|
||||
"page",
|
||||
"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")
|
||||
erosion_hurts = False
|
||||
img_org = np.copy(img)
|
||||
img_height_h = img_org.shape[0]
|
||||
img_width_h = img_org.shape[1]
|
||||
# already cropped
|
||||
img_height_h, img_width_h = img.shape[:2]
|
||||
|
||||
if num_col_classifier == 1:
|
||||
img_w_new = 700
|
||||
|
|
@ -98,60 +108,48 @@ class EynollahImageExtractor(Eynollah):
|
|||
img_w_new = 2500
|
||||
else:
|
||||
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_resized = resize_image(img,img_h_new, 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)
|
||||
|
||||
prediction_regions_org, _ = self.do_prediction_new_concept(True, img_resized, self.model_zoo.get("extract_images"))
|
||||
prediction_regions, _ = do_prediction_new_concept(
|
||||
img_resized, self.model_zoo.get("extract_images"),
|
||||
patches=True, logger=self.logger)
|
||||
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 )
|
||||
image_page, page_coord, cont_page = self.extract_page()
|
||||
mask_texts_only = (prediction_regions == label_text).astype(np.uint8)
|
||||
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]]
|
||||
prediction_regions_org=prediction_regions_org[:,:,0]
|
||||
texts_only_cont = return_contours_of_interested_region(mask_texts_only,1,0.00001)
|
||||
seps_only_cont = return_contours_of_interested_region(mask_seps_only,1,0.00001)
|
||||
|
||||
mask_seps_only = (prediction_regions_org[:,:] ==3)*1
|
||||
mask_texts_only = (prediction_regions_org[:,:] ==1)*1
|
||||
mask_images_only=(prediction_regions_org[:,:] ==2)*1
|
||||
text_regions_p = np.zeros_like(prediction_regions)
|
||||
text_regions_p = cv2.fillPoly(text_regions_p, pts=seps_only_cont, color=label_seps)
|
||||
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)
|
||||
polygons_seplines = filter_contours_area_of_image(
|
||||
mask_seps_only, polygons_seplines, hir_seplines, max_area=1, min_area=0.00001, dilate=1)
|
||||
# rs: why?
|
||||
text_regions_p[-15:] = 0
|
||||
text_regions_p[:, -15:] = 0
|
||||
|
||||
polygons_of_only_texts = return_contours_of_interested_region(mask_texts_only,1,0.00001)
|
||||
polygons_of_only_seps = return_contours_of_interested_region(mask_seps_only,1,0.00001)
|
||||
images_cont = return_contours_of_interested_region(text_regions_p, label_imgs, 0.001)
|
||||
|
||||
text_regions_p_true = np.zeros(prediction_regions_org.shape)
|
||||
text_regions_p_true = cv2.fillPoly(text_regions_p_true, pts = polygons_of_only_seps, color=(3,3,3))
|
||||
|
||||
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)
|
||||
images_cont_fin = []
|
||||
for cont in images_cont:
|
||||
_, _, w, h = box = cv2.boundingRect(cont)
|
||||
if h < 150 or w < 150:
|
||||
pass
|
||||
else:
|
||||
page_coord_img = box2rect(box) # type: ignore
|
||||
polygons_of_images_fin.append(np.array([[page_coord_img[2], page_coord_img[0]],
|
||||
[page_coord_img[3], page_coord_img[0]],
|
||||
[page_coord_img[3], page_coord_img[1]],
|
||||
[page_coord_img[2], page_coord_img[1]]]))
|
||||
y1, y2, x1, x2 = box2rect(box) # type: ignore
|
||||
images_cont_fin.append(np.array([[[x1, y1]],
|
||||
[[x2, y1]],
|
||||
[[x2, y2]],
|
||||
[[x1, y2]]]))
|
||||
|
||||
self.logger.debug("exit get_regions_extract_images_only")
|
||||
return (text_regions_p_true,
|
||||
return (text_regions_p,
|
||||
erosion_hurts,
|
||||
polygons_seplines,
|
||||
polygons_of_images_fin,
|
||||
image_page,
|
||||
page_coord,
|
||||
cont_page)
|
||||
images_cont_fin)
|
||||
|
||||
def run(self,
|
||||
overwrite: bool = False,
|
||||
|
|
@ -159,34 +157,24 @@ class EynollahImageExtractor(Eynollah):
|
|||
dir_in: Optional[str] = None,
|
||||
dir_out: Optional[str] = None,
|
||||
dir_of_cropped_images: Optional[str] = None,
|
||||
dir_of_layout: Optional[str] = None,
|
||||
dir_of_deskewed: Optional[str] = None,
|
||||
dir_of_all: Optional[str] = None,
|
||||
dir_save_page: Optional[str] = None,
|
||||
**kwargs
|
||||
):
|
||||
"""
|
||||
Get image and scales, then extract the page of scanned image
|
||||
Get scanned image and scales, then crop, and detect image regions
|
||||
"""
|
||||
self.logger.debug("enter run")
|
||||
t0_tot = time.time()
|
||||
# Log enabled features directly
|
||||
enabled_modes = []
|
||||
if self.full_layout:
|
||||
enabled_modes.append("Full layout analysis")
|
||||
if self.tables:
|
||||
enabled_modes.append("Table detection")
|
||||
if enabled_modes:
|
||||
self.logger.info("Enabled modes: " + ", ".join(enabled_modes))
|
||||
if self.enable_plotting:
|
||||
self.logger.info("Saving debug plots")
|
||||
if dir_of_cropped_images:
|
||||
self.logger.info(f"Saving cropped images to: {dir_of_cropped_images}")
|
||||
if dir_of_layout:
|
||||
self.logger.info(f"Saving layout plots to: {dir_of_layout}")
|
||||
if dir_of_deskewed:
|
||||
self.logger.info(f"Saving deskewed images to: {dir_of_deskewed}")
|
||||
|
||||
self.plotter = EynollahPlotter(
|
||||
dir_out=dir_out,
|
||||
dir_of_cropped_images=dir_of_cropped_images,
|
||||
)
|
||||
if dir_in:
|
||||
t0_tot = time.time()
|
||||
ls_imgs = [os.path.join(dir_in, image_filename)
|
||||
for image_filename in filter(is_image_filename,
|
||||
os.listdir(dir_in))]
|
||||
|
|
@ -196,79 +184,73 @@ class EynollahImageExtractor(Eynollah):
|
|||
raise ValueError("run requires either a single image filename or a directory")
|
||||
|
||||
for img_filename in ls_imgs:
|
||||
self.logger.info(img_filename)
|
||||
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)
|
||||
self.run_single(img_filename, dir_out=dir_out, overwrite=overwrite)
|
||||
|
||||
if dir_in:
|
||||
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()
|
||||
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")
|
||||
|
||||
img_res, is_image_enhanced, num_col_classifier, _ = \
|
||||
self.run_enhancement()
|
||||
|
||||
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")
|
||||
num_col_classifier, _ = self.run_enhancement(image)
|
||||
writer.scale_x = image['scale_x']
|
||||
writer.scale_y = image['scale_y']
|
||||
|
||||
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)")
|
||||
|
||||
|
||||
# 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, \
|
||||
image_page, page_coord, cont_page = \
|
||||
self.get_regions_light_v_extract_only_images(img_res, num_col_classifier)
|
||||
_, _, images_cont = self.get_early_layout(
|
||||
image['img_res'], num_col_classifier)
|
||||
self.logger.debug("Found %d images", len(images_cont))
|
||||
|
||||
pcgts = self.writer.build_pagexml_no_full_layout(
|
||||
found_polygons_text_region=[],
|
||||
page_coord=page_coord,
|
||||
order_of_texts=[],
|
||||
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=[],
|
||||
)
|
||||
# FIXME: post-hoc cropping (remove when models support it, and replace image['img_res'] with image_page)
|
||||
page_box = cv2.boundingRect(page.contour)
|
||||
images_cont = [cont - [page_box[:2]]
|
||||
for cont in images_cont]
|
||||
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")
|
||||
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)
|
||||
|
|
|
|||
156
src/eynollah/extract_page.py
Normal file
156
src/eynollah/extract_page.py
Normal file
|
|
@ -0,0 +1,156 @@
|
|||
"""
|
||||
extract page border (i.e. crop)
|
||||
"""
|
||||
|
||||
from concurrent.futures import ProcessPoolExecutor
|
||||
import logging
|
||||
from multiprocessing import cpu_count
|
||||
import os
|
||||
import time
|
||||
from typing import Optional
|
||||
from pathlib import Path
|
||||
import numpy as np
|
||||
import cv2
|
||||
|
||||
from eynollah.utils.contour import filter_contours_area_of_image, return_contours_of_image, return_contours_of_interested_region
|
||||
from eynollah.utils.resize import resize_image
|
||||
|
||||
from .model_zoo.model_zoo import EynollahModelZoo
|
||||
from .writer import EynollahXmlWriter
|
||||
from .eynollah import Eynollah
|
||||
from .utils import box2rect, is_image_filename
|
||||
from .plot import EynollahPlotter
|
||||
from .utils import Region
|
||||
|
||||
class EynollahPageExtractor(Eynollah):
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
*,
|
||||
model_zoo: EynollahModelZoo,
|
||||
enable_plotting : bool = False,
|
||||
input_binary : bool = False,
|
||||
ignore_page_extraction : bool = False,
|
||||
num_col_upper : Optional[int] = None,
|
||||
num_col_lower : Optional[int] = None,
|
||||
full_layout : bool = False,
|
||||
tables : bool = False,
|
||||
curved_line : bool = False,
|
||||
allow_enhancement : bool = False,
|
||||
|
||||
):
|
||||
self.logger = logging.getLogger('eynollah.extract_page')
|
||||
self.model_zoo = model_zoo
|
||||
self.plotter = None
|
||||
self.tables = tables
|
||||
self.curved_line = curved_line
|
||||
self.allow_enhancement = allow_enhancement
|
||||
|
||||
self.enable_plotting = enable_plotting
|
||||
# --input-binary sensible if image is very dark, if layout is not working.
|
||||
self.input_binary = input_binary
|
||||
self.full_layout = full_layout
|
||||
self.ignore_page_extraction = ignore_page_extraction
|
||||
if num_col_upper:
|
||||
self.num_col_upper = int(num_col_upper)
|
||||
else:
|
||||
self.num_col_upper = num_col_upper
|
||||
if num_col_lower:
|
||||
self.num_col_lower = int(num_col_lower)
|
||||
else:
|
||||
self.num_col_lower = num_col_lower
|
||||
|
||||
# for parallelization of CPU-intensive tasks:
|
||||
self.executor = ProcessPoolExecutor(max_workers=cpu_count())
|
||||
|
||||
t_start = time.time()
|
||||
|
||||
self.logger.info("Loading models...")
|
||||
self.setup_models()
|
||||
self.logger.info(f"Model initialization complete ({time.time() - t_start:.1f}s)")
|
||||
|
||||
def setup_models(self, device=''):
|
||||
|
||||
loadable = [
|
||||
"col_classifier",
|
||||
"page",
|
||||
]
|
||||
if self.input_binary:
|
||||
loadable.append("binarization")
|
||||
self.model_zoo.load_models(*loadable, device=device)
|
||||
|
||||
def run(self,
|
||||
overwrite: bool = False,
|
||||
image_filename: Optional[str] = None,
|
||||
dir_in: Optional[str] = None,
|
||||
dir_out: Optional[str] = None,
|
||||
**kwargs
|
||||
):
|
||||
"""
|
||||
Get scanned image and scales, then detect the page border
|
||||
"""
|
||||
self.logger.debug("enter run")
|
||||
if dir_in:
|
||||
t0_tot = time.time()
|
||||
ls_imgs = [os.path.join(dir_in, image_filename)
|
||||
for image_filename in filter(is_image_filename,
|
||||
os.listdir(dir_in))]
|
||||
elif image_filename:
|
||||
ls_imgs = [image_filename]
|
||||
else:
|
||||
raise ValueError("run requires either a single image filename or a directory")
|
||||
|
||||
for img_filename in ls_imgs:
|
||||
self.run_single(img_filename, dir_out=dir_out, overwrite=overwrite)
|
||||
|
||||
if dir_in:
|
||||
self.logger.info("All jobs done in %.1fs", time.time() - t0_tot)
|
||||
|
||||
def run_single(self,
|
||||
img_filename: str,
|
||||
dir_out: Optional[str] = None,
|
||||
overwrite: bool = False
|
||||
) -> None:
|
||||
t0 = time.time()
|
||||
self.logger.info(img_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")
|
||||
|
||||
num_col_classifier, _ = self.run_enhancement(image)
|
||||
writer.scale_x = image['scale_x']
|
||||
writer.scale_y = image['scale_y']
|
||||
|
||||
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)")
|
||||
|
||||
# Image Extraction Mode
|
||||
self.logger.info("Step 2/5: Image Extraction Mode")
|
||||
t1 = time.time()
|
||||
page_cont, _, _ = self.extract_page(image)
|
||||
page = Region(page_cont)
|
||||
|
||||
pcgts = writer.build_pagexml(
|
||||
page=page,
|
||||
img_bin=self.imread(image, binary=True) if self.input_binary else None,
|
||||
num_col=num_col_classifier,
|
||||
)
|
||||
writer.write_pagexml(pcgts)
|
||||
self.logger.info("Job done in %.1fs", time.time() - t0)
|
||||
File diff suppressed because it is too large
Load diff
|
|
@ -32,6 +32,7 @@ from .utils import (
|
|||
from .utils.font import get_font
|
||||
from .utils.xml import etree_namespace_for_element_tag
|
||||
from .utils.resize import resize_image
|
||||
from .utils.tiling import do_prediction
|
||||
from .utils.utils_ocr import (
|
||||
break_curved_line_into_small_pieces_and_then_merge,
|
||||
fit_text_single_line,
|
||||
|
|
@ -200,7 +201,9 @@ class Eynollah_ocr(Eynollah):
|
|||
if img_bin is None:
|
||||
# run ad-hoc binarization
|
||||
self.logger.info("running binarization for ensemble input")
|
||||
img_bin = self.do_prediction(True, img, self.model_zoo.get("binarization"),
|
||||
img_bin = do_prediction(img, self.model_zoo.get("binarization"),
|
||||
patches=True,
|
||||
logger=self.logger,
|
||||
n_batch_inference=5)
|
||||
img_bin = np.repeat(img_bin[:, :, np.newaxis], 3, axis=2)
|
||||
img_bin = 255 * (img_bin == 0).astype(np.uint8)
|
||||
|
|
|
|||
|
|
@ -3,9 +3,9 @@ Image enhancer. The output can be written as same scale of input or in new predi
|
|||
"""
|
||||
|
||||
import logging
|
||||
import time
|
||||
import os
|
||||
from typing import Optional
|
||||
from pathlib import Path
|
||||
|
||||
import cv2
|
||||
|
||||
|
|
@ -48,6 +48,8 @@ class Enhancer(Eynollah):
|
|||
dir_out: Optional[str] = None,
|
||||
overwrite: bool = False,
|
||||
) -> None:
|
||||
t0 = time.time()
|
||||
self.logger.info(img_filename)
|
||||
|
||||
image = self.cache_images(image_filename=img_filename, image_pil=img_pil)
|
||||
output_filename = os.path.join(dir_out or "", image['name'] + '.png')
|
||||
|
|
@ -67,6 +69,7 @@ class Enhancer(Eynollah):
|
|||
|
||||
cv2.imwrite(output_filename, img_res)
|
||||
self.logger.info("output filename: '%s'", output_filename)
|
||||
self.logger.info("Job done in %.1fs", time.time() - t0)
|
||||
|
||||
def run(self,
|
||||
overwrite: bool = False,
|
||||
|
|
@ -78,6 +81,7 @@ class Enhancer(Eynollah):
|
|||
Enlarge and enhance the scanned images
|
||||
"""
|
||||
if dir_in:
|
||||
t0_tot = time.time()
|
||||
ls_imgs = [os.path.join(dir_in, image_filename)
|
||||
for image_filename in filter(is_image_filename,
|
||||
os.listdir(dir_in))]
|
||||
|
|
@ -87,8 +91,8 @@ class Enhancer(Eynollah):
|
|||
raise ValueError("run requires either a single image filename or a directory")
|
||||
|
||||
for img_filename in ls_imgs:
|
||||
self.logger.info(img_filename)
|
||||
|
||||
self.run_single(img_filename,
|
||||
dir_out=dir_out,
|
||||
overwrite=overwrite)
|
||||
if dir_in:
|
||||
self.logger.info("All jobs done in %.1fs", time.time() - t0_tot)
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
||||
|
|
@ -25,6 +25,7 @@ MODEL_VRAM_LIMITS = {
|
|||
"table": 1818,
|
||||
"reading_order": 632,
|
||||
"ocr": 2600, # 850 for bs 8
|
||||
"extract_images": 954,
|
||||
}
|
||||
|
||||
class EynollahModelZoo:
|
||||
|
|
@ -48,7 +49,7 @@ class EynollahModelZoo:
|
|||
self._overrides = []
|
||||
if model_overrides:
|
||||
self.override_models(*model_overrides)
|
||||
self._loaded: Dict[str, Union[Predictor, AnyModel]] = {}
|
||||
self._loaded: Dict[str, Predictor] = {}
|
||||
|
||||
@property
|
||||
def model_overrides(self):
|
||||
|
|
@ -162,7 +163,7 @@ class EynollahModelZoo:
|
|||
model._name = model_category
|
||||
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:
|
||||
raise ValueError(f'Model "{model_category}" not previously loaded with "load_model(..)"')
|
||||
return self._loaded[model_category]
|
||||
|
|
@ -245,7 +246,7 @@ class EynollahModelZoo:
|
|||
self.logger.warning("no GPU device available")
|
||||
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
|
||||
from ocrd_utils import tf_disable_interactive_logs
|
||||
tf_disable_interactive_logs()
|
||||
|
|
@ -279,7 +280,7 @@ class EynollahModelZoo:
|
|||
|
||||
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
|
||||
tf_disable_interactive_logs()
|
||||
import tensorflow as tf
|
||||
|
|
@ -303,7 +304,7 @@ class EynollahModelZoo:
|
|||
|
||||
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 numpy as np
|
||||
from ocrd_utils import config
|
||||
|
|
|
|||
|
|
@ -1,10 +1,11 @@
|
|||
from typing import Optional
|
||||
import os.path
|
||||
try:
|
||||
import matplotlib.pyplot as plt
|
||||
import matplotlib.patches as mpatches
|
||||
except ImportError:
|
||||
plt = mpatches = None
|
||||
import numpy as np
|
||||
import os.path
|
||||
import cv2
|
||||
from scipy.ndimage import gaussian_filter1d
|
||||
|
||||
|
|
@ -21,11 +22,11 @@ class EynollahPlotter:
|
|||
self,
|
||||
*,
|
||||
dir_out,
|
||||
dir_of_all,
|
||||
dir_save_page,
|
||||
dir_of_deskewed,
|
||||
dir_of_layout,
|
||||
dir_of_cropped_images,
|
||||
dir_of_all: Optional[str] = None,
|
||||
dir_save_page: Optional[str] = None,
|
||||
dir_of_deskewed: Optional[str] = None,
|
||||
dir_of_layout: Optional[str] = None,
|
||||
dir_of_cropped_images: Optional[str] = None,
|
||||
):
|
||||
self.dir_out = dir_out
|
||||
self.dir_of_all = dir_of_all
|
||||
|
|
@ -172,6 +173,8 @@ class EynollahPlotter:
|
|||
x, y, w, h = cv2.boundingRect(cont_ind)
|
||||
box = [x, y, w, h]
|
||||
image, _ = crop_image_inside_box(box, image_page)
|
||||
if not image.size:
|
||||
continue
|
||||
image = resize_image(image,
|
||||
int(image.shape[0] / scale_y),
|
||||
int(image.shape[1] / scale_x))
|
||||
|
|
|
|||
|
|
@ -1,11 +1,12 @@
|
|||
from functools import cached_property
|
||||
from typing import Optional
|
||||
from PIL import Image
|
||||
from ocrd_models import OcrdPage
|
||||
from ocrd import OcrdPageResultImage, Processor, OcrdPageResult
|
||||
|
||||
from eynollah.model_zoo.model_zoo import EynollahModelZoo
|
||||
|
||||
from .eynollah import Eynollah, EynollahXmlWriter
|
||||
from .eynollah import Eynollah
|
||||
|
||||
class EynollahProcessor(Processor):
|
||||
@cached_property
|
||||
|
|
@ -95,4 +96,9 @@ class EynollahProcessor(Processor):
|
|||
img_pil=page_image, pcgts=pcgts,
|
||||
# ocrd.Processor will handle OCRD_EXISTING_OUTPUT more flexibly
|
||||
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
|
||||
|
|
|
|||
359
src/eynollah/reorder.py
Normal file
359
src/eynollah/reorder.py
Normal 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)
|
||||
|
||||
|
|
@ -18,6 +18,7 @@ import cv2
|
|||
from .eynollah import Eynollah
|
||||
from .model_zoo import EynollahModelZoo
|
||||
from .utils.resize import resize_image
|
||||
from .utils.tiling import do_prediction
|
||||
from .utils import is_image_filename
|
||||
|
||||
class SbbBinarizer(Eynollah):
|
||||
|
|
@ -84,7 +85,9 @@ class SbbBinarizer(Eynollah):
|
|||
):
|
||||
image = self.cache_images(image_filename=img_filename, image_pil=img_pil)
|
||||
img = self.imread(image)
|
||||
img_bin = self.do_prediction(use_patches, img, self.model_zoo.get("binarization"),
|
||||
img_bin = do_prediction(img, self.model_zoo.get("binarization"),
|
||||
patches=use_patches,
|
||||
logger=self.logger,
|
||||
n_batch_inference=5)
|
||||
img_bin = 255 * (img_bin == 0).astype(np.uint8)
|
||||
#img_bin = np.repeat(img_bin[:, :, np.newaxis], 3, axis=2).astype(np.uint8)
|
||||
|
|
|
|||
|
|
@ -22,6 +22,7 @@ CURRENT_MODELS += model_eynollah_ocr_cnnrnn_20250930
|
|||
CURRENT_MODELS += eynollah-binarization_20210425
|
||||
CURRENT_MODELS += eynollah-column-classifier_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
|
||||
# onnx conversion for fast inference
|
||||
|
|
|
|||
|
|
@ -2,7 +2,8 @@ from typing import Iterable, List, Tuple
|
|||
from logging import getLogger
|
||||
import time
|
||||
import math
|
||||
from itertools import islice
|
||||
from dataclasses import dataclass, field
|
||||
from itertools import islice, compress
|
||||
|
||||
try:
|
||||
import matplotlib.pyplot as plt
|
||||
|
|
@ -10,16 +11,18 @@ try:
|
|||
except ImportError:
|
||||
plt = None
|
||||
import numpy as np
|
||||
from shapely import geometry
|
||||
from shapely import geometry, prepared, ops
|
||||
import cv2
|
||||
from scipy.signal import find_peaks
|
||||
from scipy.ndimage import gaussian_filter1d
|
||||
from skimage import morphology
|
||||
|
||||
from .is_nan import isNaN
|
||||
from .contour import (contours_in_same_horizon,
|
||||
from .contour import (contour2polygon,
|
||||
contours_in_same_horizon,
|
||||
find_center_of_contours,
|
||||
find_new_features_of_contours,
|
||||
polygon2contour,
|
||||
return_contours_of_image,
|
||||
return_parent_contours)
|
||||
|
||||
|
|
@ -39,6 +42,32 @@ def batched(iterable, n):
|
|||
while batch := tuple(islice(iterator, n)):
|
||||
yield batch
|
||||
|
||||
def itemgetter(seq):
|
||||
# replacement for operator.itemgetter
|
||||
# (which is ambiguous about its return type:
|
||||
# single-item if 1 argument, tuple otherwise)
|
||||
def fun(obj):
|
||||
return list(obj[idx] for idx in seq)
|
||||
return fun
|
||||
|
||||
@dataclass
|
||||
class Region:
|
||||
contour: np.ndarray
|
||||
area: int = 0
|
||||
cx: float = 0
|
||||
cy: float = 0
|
||||
skew: float = 0
|
||||
conf: float = 0
|
||||
def __post_init__(self):
|
||||
self.area = cv2.contourArea(self.contour)
|
||||
moments = cv2.moments(self.contour)
|
||||
self.cx = moments['m10'] / (moments['m00'] or 1e-32)
|
||||
self.cy = moments['m01'] / (moments['m00'] or 1e-32)
|
||||
|
||||
@dataclass
|
||||
class TextRegion(Region):
|
||||
lines: List[Region] = field(default_factory=list)
|
||||
|
||||
def return_multicol_separators_x_start_end(
|
||||
regions_without_separators, peak_points, top, bot,
|
||||
x_min_hor_some, x_max_hor_some, cy_hor_some, y_min_hor_some, y_max_hor_some):
|
||||
|
|
@ -800,93 +829,16 @@ def fill_bb_of_drop_capitals(
|
|||
|
||||
return full_prediction == label_drop_fl_model
|
||||
|
||||
def check_any_text_region_in_model_one_is_main_or_header(
|
||||
regions_model_1, regions_model_full,
|
||||
contours_only_text_parent,
|
||||
all_box_coord, all_found_textline_polygons,
|
||||
slopes,
|
||||
contours_only_text_parent_d_ordered, conf_contours):
|
||||
|
||||
cx_main, cy_main, x_min_main, x_max_main, y_min_main, y_max_main, y_corr_x_min_from_argmin = \
|
||||
find_new_features_of_contours(contours_only_text_parent)
|
||||
|
||||
length_con=x_max_main-x_min_main
|
||||
height_con=y_max_main-y_min_main
|
||||
|
||||
all_found_textline_polygons_main=[]
|
||||
all_found_textline_polygons_head=[]
|
||||
|
||||
all_box_coord_main=[]
|
||||
all_box_coord_head=[]
|
||||
|
||||
slopes_main=[]
|
||||
slopes_head=[]
|
||||
|
||||
contours_only_text_parent_main=[]
|
||||
contours_only_text_parent_head=[]
|
||||
|
||||
conf_contours_main=[]
|
||||
conf_contours_head=[]
|
||||
|
||||
contours_only_text_parent_main_d=[]
|
||||
contours_only_text_parent_head_d=[]
|
||||
|
||||
for ii, con in enumerate(contours_only_text_parent):
|
||||
img = np.zeros(regions_model_1.shape[:2])
|
||||
img = cv2.fillPoly(img, pts=[con], color=255)
|
||||
|
||||
all_pixels=((img == 255)*1).sum()
|
||||
pixels_header=( ( (img == 255) & (regions_model_full[:,:,0]==2) )*1 ).sum()
|
||||
pixels_main=all_pixels-pixels_header
|
||||
|
||||
if (pixels_header>=pixels_main) and ( (length_con[ii]/float(height_con[ii]) )>=1.3 ):
|
||||
regions_model_1[:,:][(regions_model_1[:,:]==1) & (img == 255) ]=2
|
||||
contours_only_text_parent_head.append(con)
|
||||
if len(contours_only_text_parent_d_ordered):
|
||||
contours_only_text_parent_head_d.append(contours_only_text_parent_d_ordered[ii])
|
||||
all_box_coord_head.append(all_box_coord[ii])
|
||||
slopes_head.append(slopes[ii])
|
||||
all_found_textline_polygons_head.append(all_found_textline_polygons[ii])
|
||||
conf_contours_head.append(None)
|
||||
else:
|
||||
regions_model_1[:,:][(regions_model_1[:,:]==1) & (img == 255) ]=1
|
||||
contours_only_text_parent_main.append(con)
|
||||
conf_contours_main.append(conf_contours[ii])
|
||||
if len(contours_only_text_parent_d_ordered):
|
||||
contours_only_text_parent_main_d.append(contours_only_text_parent_d_ordered[ii])
|
||||
all_box_coord_main.append(all_box_coord[ii])
|
||||
slopes_main.append(slopes[ii])
|
||||
all_found_textline_polygons_main.append(all_found_textline_polygons[ii])
|
||||
|
||||
#print(all_pixels,pixels_main,pixels_header)
|
||||
|
||||
return (regions_model_1,
|
||||
contours_only_text_parent_main,
|
||||
contours_only_text_parent_head,
|
||||
all_box_coord_main,
|
||||
all_box_coord_head,
|
||||
all_found_textline_polygons_main,
|
||||
all_found_textline_polygons_head,
|
||||
slopes_main,
|
||||
slopes_head,
|
||||
contours_only_text_parent_main_d,
|
||||
contours_only_text_parent_head_d,
|
||||
conf_contours_main,
|
||||
conf_contours_head)
|
||||
|
||||
def split_textregion_main_vs_head(
|
||||
regions_model_1,
|
||||
regions_model_full,
|
||||
polygons_of_textregions,
|
||||
polygons_of_textregions_d,
|
||||
all_found_textline_polygons,
|
||||
slopes,
|
||||
conf_textregions,
|
||||
regions_model_1: np.ndarray,
|
||||
regions_model_full: np.ndarray,
|
||||
textregions: List[Region],
|
||||
textregions_d: List[Region],
|
||||
label_text=1,
|
||||
label_head_full=2,
|
||||
label_head_final=2,
|
||||
label_main_final=1,
|
||||
):
|
||||
) -> Tuple[np.ndarray, List[Region], List[Region], List[Region], List[Region]]:
|
||||
|
||||
### to make it faster
|
||||
h_o = regions_model_1.shape[0]
|
||||
|
|
@ -900,19 +852,12 @@ def split_textregion_main_vs_head(
|
|||
(regions_model_full.shape[1] // zoom,
|
||||
regions_model_full.shape[0] // zoom),
|
||||
interpolation=cv2.INTER_NEAREST)
|
||||
contours_z = [contour // zoom
|
||||
for contour in polygons_of_textregions]
|
||||
contours_z = [textregion.contour // zoom
|
||||
for textregion in textregions]
|
||||
|
||||
###
|
||||
_, _, x_min_main, x_max_main, y_min_main, y_max_main, _ = \
|
||||
find_new_features_of_contours(contours_z)
|
||||
|
||||
length_con=x_max_main-x_min_main
|
||||
height_con=y_max_main-y_min_main
|
||||
|
||||
main = []
|
||||
head = []
|
||||
main = np.ones(len(contours_z), dtype=bool)
|
||||
for ii, con in enumerate(contours_z):
|
||||
width, height = cv2.boundingRect(con)[2:]
|
||||
parent = np.zeros_like(regions_model_1)
|
||||
parent = cv2.fillPoly(parent, pts=[con], color=1)
|
||||
|
||||
|
|
@ -920,16 +865,14 @@ def split_textregion_main_vs_head(
|
|||
pixels_main = parent.sum() - pixels_head
|
||||
|
||||
if (( pixels_head >= 0.6 * pixels_main and
|
||||
length_con[ii] >= 1.3 * height_con[ii] and
|
||||
length_con[ii] <= 3 * height_con[ii] ) or
|
||||
width >= 1.3 * height and
|
||||
width <= 3 * height ) or
|
||||
( pixels_head >= 0.3 * pixels_main and
|
||||
length_con[ii] >= 3 * height_con[ii] )):
|
||||
width >= 3 * height )):
|
||||
|
||||
head.append(ii)
|
||||
main[ii] = False
|
||||
label = label_head_final
|
||||
|
||||
else:
|
||||
main.append(ii)
|
||||
label = label_main_final
|
||||
|
||||
regions_model_1[(regions_model_1 == label_text) & (parent > 0)] = label
|
||||
|
|
@ -941,35 +884,26 @@ def split_textregion_main_vs_head(
|
|||
# interpolation=cv2.INTER_NEAREST)
|
||||
###
|
||||
|
||||
def select(lis, indexes):
|
||||
if not len(lis):
|
||||
return []
|
||||
return [lis[ind] for ind in indexes]
|
||||
|
||||
return (regions_model_1,
|
||||
select(polygons_of_textregions, main),
|
||||
select(polygons_of_textregions, head),
|
||||
select(polygons_of_textregions_d, main),
|
||||
select(polygons_of_textregions_d, head),
|
||||
select(all_found_textline_polygons, main),
|
||||
select(all_found_textline_polygons, head),
|
||||
select(slopes, main),
|
||||
select(slopes, head),
|
||||
select(conf_textregions, main),
|
||||
select(conf_textregions, head),
|
||||
list(compress(textregions, main)),
|
||||
list(compress(textregions, ~main)),
|
||||
list(compress(textregions_d, main)),
|
||||
list(compress(textregions_d, ~main)),
|
||||
)
|
||||
|
||||
def small_textlines_to_parent_adherence2(textlines_con, textline_mask, num_col):
|
||||
def small_textlines_to_parent_adherence2(
|
||||
textregions: List[TextRegion],
|
||||
area_factor: float,
|
||||
num_col: int,
|
||||
) -> None:
|
||||
"""
|
||||
for each region, split up textlines into small and large areas;
|
||||
for each region, split up textlines into small and large;
|
||||
keep only the ones with large area, but expanded (by merging
|
||||
contours) by all intersecting lines with small area
|
||||
"""
|
||||
textlines_con_new = []
|
||||
for region in textlines_con:
|
||||
areas_cnt_text = np.array(list(map(cv2.contourArea, region)))
|
||||
areas_cnt_text = areas_cnt_text / float(textline_mask.size)
|
||||
indexes_textlines = np.arange(len(region))
|
||||
for textregion in textregions:
|
||||
areas = np.array([line.area for line in textregion.lines]) * area_factor
|
||||
|
||||
if num_col == 0:
|
||||
min_area = 0.0004
|
||||
|
|
@ -977,52 +911,43 @@ def small_textlines_to_parent_adherence2(textlines_con, textline_mask, num_col):
|
|||
min_area = 0.0003
|
||||
else:
|
||||
min_area = 0.0001
|
||||
indexes_textlines_small = indexes_textlines[areas_cnt_text < min_area]
|
||||
indexes_textlines_large = indexes_textlines[areas_cnt_text >= min_area]
|
||||
textlines_small = list(compress(textregion.lines, areas < min_area))
|
||||
textlines_large = list(compress(textregion.lines, areas >= min_area))
|
||||
textlines_small_poly = [contour2polygon(line.contour) for line in textlines_small]
|
||||
textlines_large_poly = [contour2polygon(line.contour) for line in textlines_large]
|
||||
textregion.lines = textlines_large
|
||||
|
||||
textlines_small = [region[i] for i in indexes_textlines_small]
|
||||
textlines_large = [region[i] for i in indexes_textlines_large]
|
||||
|
||||
img_small = np.zeros_like(textline_mask)
|
||||
img_small = cv2.fillPoly(img_small, pts=textlines_small, color=1)
|
||||
img_large = np.zeros_like(textline_mask)
|
||||
img_large = cv2.fillPoly(img_large, pts=textlines_large, color=1)
|
||||
img_inter = img_small + img_large == 2
|
||||
if np.any(img_inter):
|
||||
indexes_textlines_inter = []
|
||||
for contour_small in textlines_small:
|
||||
intersections = []
|
||||
for contour_large in textlines_large:
|
||||
img0_small = np.zeros_like(textline_mask)
|
||||
img0_small = cv2.fillPoly(img0_small, pts=[contour_small], color=1)
|
||||
img0_large = np.zeros_like(textline_mask)
|
||||
img0_large = cv2.fillPoly(img0_large, pts=[contour_large], color=1)
|
||||
img0_inter = img0_small + img0_large == 2
|
||||
intersections.append(np.count_nonzero(img0_inter))
|
||||
if geometry.MultiPolygon(textlines_small_poly).intersects(
|
||||
geometry.MultiPolygon(textlines_large_poly)):
|
||||
# FIXME: also consider confidence (less certain lines replaced by better ones)...
|
||||
textlines_large_prep = [prepared.prep(poly) for poly in textlines_large_poly]
|
||||
textlines_small_indexes_interlarge = []
|
||||
for small_poly in textlines_small_poly:
|
||||
intersections = [small_poly.intersection(prep.context).area
|
||||
if prep.intersects(small_poly) else 0
|
||||
for prep in textlines_large_prep]
|
||||
idx_large = np.argmax(intersections)
|
||||
if intersections[idx_large] <= 0:
|
||||
if intersections[idx_large] == 0:
|
||||
idx_large = -1
|
||||
indexes_textlines_inter.append(idx_large)
|
||||
|
||||
indexes_textlines_inter = np.array(indexes_textlines_inter)
|
||||
for idx_large in set(indexes_textlines_inter):
|
||||
textlines_small_indexes_interlarge.append(idx_large)
|
||||
textlines_small_indexes_interlarge = np.array(textlines_small_indexes_interlarge)
|
||||
for idx_large in set(textlines_small_indexes_interlarge):
|
||||
if idx_large < 0:
|
||||
continue
|
||||
img0_union = np.zeros_like(textline_mask)
|
||||
img0_union = cv2.fillPoly(img0_union, pts=[textlines_large[idx_large]], color=255)
|
||||
indexes_inter_small = np.flatnonzero(indexes_textlines_inter == idx_large)
|
||||
for idx_small in indexes_inter_small:
|
||||
img0_union = cv2.fillPoly(img0_union, pts=[textlines_small[idx_small]], color=255)
|
||||
|
||||
_, thresh = cv2.threshold(img0_union, 0, 255, 0)
|
||||
contours_union, _ = cv2.findContours(thresh.astype(np.uint8),
|
||||
cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
|
||||
areas_union = np.array(list(map(cv2.contourArea, contours_union)))
|
||||
contour_union = contours_union[np.argmax(areas_union)] #contours_union[0]
|
||||
textlines_large[idx_large] = contour_union
|
||||
|
||||
textlines_con_new.append(textlines_large)
|
||||
return textlines_con_new
|
||||
large_poly = textlines_large_poly[idx_large]
|
||||
indexes_small = np.flatnonzero(textlines_small_indexes_interlarge == idx_large)
|
||||
for idx_small in indexes_small:
|
||||
large_poly = large_poly.union(textlines_small_poly[idx_small])
|
||||
if large_poly.geom_type == 'GeometryCollection':
|
||||
# hetergeneous: filter lines and points
|
||||
large_poly = ops.unary_union([geom for geom in large_poly.geoms
|
||||
if geom.area > 0])
|
||||
if large_poly.geom_type == 'MultiPolygon':
|
||||
# disjoint: pick largest part
|
||||
idx_large = np.argmax([geom.area for geom in large_poly.geoms])
|
||||
large_poly = large_poly.geoms[idx_large]
|
||||
# replace original
|
||||
textregion.lines[idx_large].contour = polygon2contour(large_poly)
|
||||
|
||||
def order_of_regions(textline_mask, contours_main, contours_head, contours_drop, y_ref, x_ref):
|
||||
"""
|
||||
|
|
@ -1051,7 +976,7 @@ def order_of_regions(textline_mask, contours_main, contours_head, contours_drop,
|
|||
total = len(contours_main) + len(contours_head) + len(contours_drop)
|
||||
assert total == 0 or np.any(textline_mask)
|
||||
|
||||
# ax1 = plt.subplot(2, 1, 1, title="order_of_regions textline_mask")
|
||||
# ax1 = plt.subplot(1, 2, 1, title="order_of_regions textline_mask")
|
||||
# plt.imshow(textline_mask, aspect='auto')
|
||||
y = textline_mask.sum(axis=1) # horizontal projection profile
|
||||
y_padded = np.zeros(len(y) + 40)
|
||||
|
|
@ -1061,15 +986,21 @@ def order_of_regions(textline_mask, contours_main, contours_head, contours_drop,
|
|||
#z = gaussian_filter1d(y_padded, sigma_gaus)
|
||||
#peaks, _ = find_peaks(z, height=0)
|
||||
#peaks = peaks - 20
|
||||
# ax2 = plt.subplot(2, 1, 2, title="smoothed horizontal projection", sharex=ax1)
|
||||
# plt.plot(y)
|
||||
# ax2 = plt.subplot(1, 2, 2, title="smoothed horizontal projection", sharey=ax1)
|
||||
# ax2plot = plt.plot(y)[0]
|
||||
# xdata = ax2plot.get_xdata()
|
||||
# ydata = ax2plot.get_ydata()
|
||||
# ax2plot.set_xdata(ydata)
|
||||
# ax2plot.set_ydata(xdata)
|
||||
# ax2.set_xlim(*ax1.get_xlim())
|
||||
# ax2.set_ylim(*ax1.get_ylim())
|
||||
zneg_rev = np.max(y_padded) - y_padded
|
||||
zneg = np.zeros(len(zneg_rev) + 40)
|
||||
zneg[20 : len(zneg_rev) + 20] = zneg_rev
|
||||
zneg = gaussian_filter1d(zneg, sigma_gaus)
|
||||
|
||||
peaks_neg, _ = find_peaks(zneg, height=0)
|
||||
# plt.vlines(peaks_neg - 40, 0, None, label="peaks")
|
||||
# plt.hlines(peaks_neg - 40, 0, textline_mask.shape[1], label="peaks")
|
||||
# plt.show()
|
||||
peaks_neg = peaks_neg - 20 - 20
|
||||
|
||||
|
|
@ -1288,7 +1219,7 @@ def find_number_of_columns_in_document(
|
|||
separator_mask: np.ndarray,
|
||||
num_col_classifier: int,
|
||||
tables: bool,
|
||||
contours_h: List[np.ndarray] = None,
|
||||
contours_h: List[np.ndarray] = [],
|
||||
logger=None
|
||||
) -> 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
|
||||
* num_col_classifier: predicted (expected) number of columns of the page
|
||||
* 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
|
||||
|
||||
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):,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, _ = \
|
||||
find_features_of_lines(contours_h)
|
||||
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)]
|
||||
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) &
|
||||
(x_max_head >= .84 * width)]
|
||||
y_max_splitters_head = y_max_head[(x_min_head <= .16 * width) &
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
from typing import Sequence, Union
|
||||
from typing import List, Sequence, Union, Tuple
|
||||
from numbers import Number
|
||||
from functools import partial
|
||||
import itertools
|
||||
|
|
@ -167,26 +167,34 @@ def dilate_textregion_contours(all_found_textregion_polygons):
|
|||
[polygon2contour(contour2polygon(contour, dilate=6))
|
||||
for contour in all_found_textregion_polygons])
|
||||
|
||||
def match_deskewed_contours(slope_deskew, contours_o, contours_d, shape_o, shape_d):
|
||||
def match_deskewed_contours(
|
||||
slope_deskew: float,
|
||||
regions_o, #: List[Region], # (cyclic import)
|
||||
regions_d, #: List[Region], # (cyclic import)
|
||||
shape_o: Tuple[int, int],
|
||||
shape_d: Tuple[int, int],
|
||||
) -> np.ndarray:
|
||||
from . import ensure_array
|
||||
|
||||
cntareas_o = np.array([cv2.contourArea(contour) for contour in contours_o])
|
||||
cntareas_d = np.array([cv2.contourArea(contour) for contour in contours_d])
|
||||
centers_o = np.array([[region.cx, region.cy] for region in regions_o]) # [N, 2]
|
||||
centers_d = np.array([[region.cx, region.cy] for region in regions_d]) # [N, 2]
|
||||
cntareas_o = np.array([region.area for region in regions_o])
|
||||
cntareas_d = np.array([region.area for region in regions_d])
|
||||
cntareas_o = cntareas_o / float(np.prod(shape_o[:2]))
|
||||
cntareas_d = cntareas_d / float(np.prod(shape_d[:2]))
|
||||
|
||||
contours_o = ensure_array(contours_o)
|
||||
contours_d = ensure_array(contours_d)
|
||||
contours_o = ensure_array([region.contour for region in regions_o])
|
||||
contours_d = ensure_array([region.contour for region in regions_d])
|
||||
|
||||
sort_o = np.argsort(cntareas_o)
|
||||
sort_d = np.argsort(cntareas_d)
|
||||
centers_o = centers_o[sort_o].T # [2, N]
|
||||
centers_d = centers_d[sort_d].T # [2, N]
|
||||
contours_o = contours_o[sort_o]
|
||||
contours_d = contours_d[sort_d]
|
||||
cntareas_o = cntareas_o[sort_o]
|
||||
cntareas_d = cntareas_d[sort_d]
|
||||
|
||||
centers_o = np.stack(find_center_of_contours(contours_o)) # [2, N]
|
||||
centers_d = np.stack(find_center_of_contours(contours_d)) # [2, N]
|
||||
center0_o = centers_o[:, -1:] # [2, 1]
|
||||
center0_d = centers_d[:, -1:] # [2, 1]
|
||||
|
||||
|
|
|
|||
|
|
@ -10,6 +10,9 @@ def cv2pil(img):
|
|||
return Image.fromarray(np.array(cvtColor(img, COLOR_BGR2RGB)))
|
||||
|
||||
def pil2cv(img):
|
||||
if img.mode.endswith(('a', 'A')):
|
||||
# remove transparency (RGBA, LA, PA, and pre-multiplied variants)
|
||||
img = img.convert(img.mode[:-1])
|
||||
# from ocrd/workspace.py
|
||||
color_conversion = COLOR_GRAY2BGR if img.mode in ('1', 'L') else COLOR_RGB2BGR
|
||||
pil_as_np_array = np.array(img).astype('uint8') if img.mode == '1' else np.array(img)
|
||||
|
|
|
|||
|
|
@ -1585,6 +1585,7 @@ def do_work_of_slopes_new_curved(
|
|||
mask_line = np.zeros_like(mask_parent)
|
||||
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 *= mask_parent
|
||||
# plt.subplot(1, 2, 1, title="parent mask")
|
||||
# plt.imshow(mask_parent)
|
||||
# plt.subplot(1, 2, 2, title="single textline")
|
||||
|
|
|
|||
402
src/eynollah/utils/tiling.py
Normal file
402
src/eynollah/utils/tiling.py
Normal file
|
|
@ -0,0 +1,402 @@
|
|||
from logging import getLogger
|
||||
import math
|
||||
import gc
|
||||
|
||||
import numpy as np
|
||||
|
||||
from . import seg_mask_label
|
||||
from .resize import resize_image
|
||||
from ..predictor import Predictor
|
||||
|
||||
def do_prediction(
|
||||
img: np.ndarray,
|
||||
model: Predictor,
|
||||
logger=None,
|
||||
patches=False,
|
||||
n_batch_inference=1,
|
||||
marginal_of_patch_percent=0.1,
|
||||
thresholding_for_some_classes=False,
|
||||
thresholding_for_heading=False,
|
||||
heading_class=2,
|
||||
thresholding_for_artificial_class=False,
|
||||
threshold_art_class=0.1,
|
||||
artificial_class=2,
|
||||
is_enhancement=False,
|
||||
) -> np.ndarray:
|
||||
if logger is None:
|
||||
logger = getLogger('eynollah')
|
||||
|
||||
logger.debug("enter do_prediction (patches=%d)", patches)
|
||||
_, img_height_model, img_width_model, _ = model.input_shape
|
||||
img_h_page = img.shape[0]
|
||||
img_w_page = img.shape[1]
|
||||
|
||||
img = img / 255.
|
||||
img = img.astype(np.float16)
|
||||
|
||||
if not patches:
|
||||
img = resize_image(img, img_height_model, img_width_model)
|
||||
|
||||
label_p_pred = model.predict(img[np.newaxis], verbose=0)[0]
|
||||
if is_enhancement:
|
||||
seg = np.round(label_p_pred * 255).astype(np.uint8)
|
||||
else:
|
||||
seg = np.argmax(label_p_pred, axis=2).astype(np.uint8)
|
||||
|
||||
if thresholding_for_artificial_class:
|
||||
seg_mask_label(
|
||||
seg, label_p_pred[:, :, artificial_class] >= threshold_art_class,
|
||||
label=artificial_class,
|
||||
skeletonize=True)
|
||||
|
||||
if thresholding_for_heading:
|
||||
seg_mask_label(
|
||||
seg, label_p_pred[:, :, heading_class] >= 0.2,
|
||||
label=heading_class)
|
||||
|
||||
return resize_image(seg, img_h_page, img_w_page)
|
||||
|
||||
if img_h_page < img_height_model:
|
||||
img = resize_image(img, img_height_model, img.shape[1])
|
||||
if img_w_page < img_width_model:
|
||||
img = resize_image(img, img.shape[0], img_width_model)
|
||||
|
||||
logger.debug("Patch size: %sx%s", img_height_model, img_width_model)
|
||||
margin = int(marginal_of_patch_percent * img_height_model)
|
||||
if 2 * margin > 0.5 * img_width_model:
|
||||
margin = img_width_model // 4
|
||||
if 2 * margin > 0.5 * img_height_model:
|
||||
margin = img_height_model // 4
|
||||
window = 1 / (1 + np.exp(5.0 - 5 * np.arange(2 * margin) / margin))
|
||||
width_mid = img_width_model - 2 * margin
|
||||
height_mid = img_height_model - 2 * margin
|
||||
img_h = img.shape[0]
|
||||
img_w = img.shape[1]
|
||||
prediction: np.ndarray = None # type: ignore
|
||||
nxf = math.ceil((img_w - 2.0 * margin) / width_mid)
|
||||
nyf = math.ceil((img_h - 2.0 * margin) / height_mid)
|
||||
|
||||
batch_i = []
|
||||
batch_j = []
|
||||
batch_x_u = []
|
||||
batch_x_d = []
|
||||
batch_x_s = []
|
||||
batch_y_u = []
|
||||
batch_y_d = []
|
||||
batch_y_s = []
|
||||
|
||||
batch = 0
|
||||
img_patch = np.zeros((n_batch_inference,
|
||||
img_height_model,
|
||||
img_width_model,
|
||||
3), dtype=np.float16)
|
||||
for i in range(nxf):
|
||||
for j in range(nyf):
|
||||
index_x_d = i * width_mid
|
||||
index_x_u = index_x_d + img_width_model
|
||||
if index_x_u > img_w:
|
||||
index_x_s = index_x_u - img_w
|
||||
index_x_u = img_w
|
||||
index_x_d = img_w - img_width_model
|
||||
else:
|
||||
index_x_s = 0
|
||||
index_y_d = j * height_mid
|
||||
index_y_u = index_y_d + img_height_model
|
||||
if index_y_u > img_h:
|
||||
index_y_s = index_y_u - img_h
|
||||
index_y_u = img_h
|
||||
index_y_d = img_h - img_height_model
|
||||
else:
|
||||
index_y_s = 0
|
||||
|
||||
batch_i.append(i)
|
||||
batch_j.append(j)
|
||||
batch_x_u.append(index_x_u)
|
||||
batch_x_d.append(index_x_d)
|
||||
batch_x_s.append(index_x_s)
|
||||
batch_y_d.append(index_y_d)
|
||||
batch_y_u.append(index_y_u)
|
||||
batch_y_s.append(index_y_s)
|
||||
|
||||
img_patch[batch] = img[index_y_d: index_y_u,
|
||||
index_x_d: index_x_u]
|
||||
batch += 1
|
||||
if (batch == n_batch_inference or
|
||||
# last batch
|
||||
i == nxf - 1 and j == nyf - 1):
|
||||
logger.debug("predicting patches on %s", str(img_patch.shape))
|
||||
label_p_pred = model.predict(img_patch, verbose=0)
|
||||
if prediction is None:
|
||||
# now we know the number of classes
|
||||
prediction = np.zeros((img_h, img_w, label_p_pred.shape[-1]), dtype=float)
|
||||
|
||||
for batch in range(batch):
|
||||
where = np.index_exp[batch_y_d[batch]: batch_y_u[batch],
|
||||
batch_x_d[batch]: batch_x_u[batch]]
|
||||
# shorter window on last tile
|
||||
part = np.index_exp[batch_y_s[batch]:,
|
||||
batch_x_s[batch]:]
|
||||
# normalize probability (where windows overlap)
|
||||
attenuation_y = np.ones(img_height_model - batch_y_s[batch])
|
||||
attenuation_x = np.ones(img_width_model - batch_x_s[batch])
|
||||
if margin and batch_j[batch] > 0:
|
||||
attenuation_y[:2 * margin] = window
|
||||
if margin and batch_j[batch] < nyf - 1:
|
||||
attenuation_y[-2 * margin:] = 1 - window
|
||||
if margin and batch_i[batch] > 0:
|
||||
attenuation_x[:2 * margin] = window
|
||||
if margin and batch_i[batch] < nxf - 1:
|
||||
attenuation_x[-2 * margin:] = 1 - window
|
||||
label_p_pred[batch][part] *= attenuation_y[:, np.newaxis, np.newaxis]
|
||||
label_p_pred[batch][part] *= attenuation_x[np.newaxis, :, np.newaxis]
|
||||
prediction[where][part] += label_p_pred[batch][part]
|
||||
|
||||
batch_i = []
|
||||
batch_j = []
|
||||
batch_x_u = []
|
||||
batch_x_d = []
|
||||
batch_x_s = []
|
||||
batch_y_u = []
|
||||
batch_y_d = []
|
||||
batch_y_s = []
|
||||
batch = 0
|
||||
img_patch[:] = 0
|
||||
|
||||
if is_enhancement:
|
||||
seg = np.round(prediction * 255).astype(np.uint8)
|
||||
else:
|
||||
seg = np.argmax(prediction, axis=2).astype(np.uint8)
|
||||
if thresholding_for_some_classes:
|
||||
seg_mask_label(
|
||||
seg, prediction[:, :, 4] > 0.03,
|
||||
label=4) #
|
||||
seg_mask_label(
|
||||
seg, prediction[:, :, 0] > 0.25,
|
||||
label=0) # bg
|
||||
seg_mask_label(
|
||||
seg, prediction[:, :, 3] > 0.10 & seg == 0,
|
||||
label=3) # line
|
||||
if thresholding_for_artificial_class:
|
||||
seg_art = prediction[:, :, artificial_class] >= threshold_art_class
|
||||
seg_mask_label(seg, seg_art,
|
||||
label=artificial_class,
|
||||
only=True,
|
||||
skeletonize=True,
|
||||
dilate=3)
|
||||
|
||||
if img_h != img_h_page or img_w != img_w_page:
|
||||
seg = resize_image(seg, img_h_page, img_w_page)
|
||||
|
||||
gc.collect()
|
||||
return seg
|
||||
|
||||
def do_prediction_new_concept(
|
||||
img: np.ndarray,
|
||||
model: Predictor,
|
||||
logger=None,
|
||||
patches=False,
|
||||
n_batch_inference=1,
|
||||
marginal_of_patch_percent=0.1,
|
||||
thresholding_for_heading=False,
|
||||
heading_class=2,
|
||||
thresholding_for_artificial_class=False,
|
||||
threshold_art_class=0.1,
|
||||
artificial_class=4,
|
||||
separator_class=0,
|
||||
) -> np.ndarray:
|
||||
if logger is None:
|
||||
logger = getLogger('eynollah')
|
||||
|
||||
logger.debug("enter do_prediction_new_concept (patches=%d)", patches)
|
||||
_, img_height_model, img_width_model, _ = model.input_shape
|
||||
|
||||
img = img / 255.0
|
||||
img = img.astype(np.float16)
|
||||
|
||||
if not patches:
|
||||
img_h_page = img.shape[0]
|
||||
img_w_page = img.shape[1]
|
||||
img = resize_image(img, img_height_model, img_width_model)
|
||||
|
||||
label_p_pred = model.predict(img[np.newaxis], verbose=0)[0]
|
||||
seg = np.argmax(label_p_pred, axis=2).astype(np.uint8)
|
||||
|
||||
prediction = resize_image(seg, img_h_page, img_w_page)
|
||||
|
||||
if thresholding_for_artificial_class:
|
||||
mask = resize_image(label_p_pred[:, :, artificial_class],
|
||||
img_h_page, img_w_page) >= threshold_art_class
|
||||
seg_mask_label(prediction, mask,
|
||||
label=artificial_class,
|
||||
only=True,
|
||||
skeletonize=True,
|
||||
dilate=3,
|
||||
keep=separator_class)
|
||||
if thresholding_for_heading:
|
||||
mask = resize_image(label_p_pred[:, :, heading_class],
|
||||
img_h_page, img_w_page) >= 0.2
|
||||
seg_mask_label(prediction, mask,
|
||||
label=heading_class)
|
||||
|
||||
conf = label_p_pred[tuple(np.indices(seg.shape)) + (seg,)]
|
||||
conf = resize_image(conf, img_h_page, img_w_page)
|
||||
return prediction, conf
|
||||
|
||||
if img.shape[0] < img_height_model:
|
||||
img = resize_image(img, img_height_model, img.shape[1])
|
||||
if img.shape[1] < img_width_model:
|
||||
img = resize_image(img, img.shape[0], img_width_model)
|
||||
|
||||
logger.debug("Patch size: %sx%s", img_height_model, img_width_model)
|
||||
margin = int(marginal_of_patch_percent * img_height_model)
|
||||
if 2 * margin > 0.5 * img_width_model:
|
||||
margin = img_width_model // 4
|
||||
if 2 * margin > 0.5 * img_height_model:
|
||||
margin = img_height_model // 4
|
||||
window = 1 / (1 + np.exp(5.0 - 5 * np.arange(2 * margin) / margin))
|
||||
width_mid = img_width_model - 2 * margin
|
||||
height_mid = img_height_model - 2 * margin
|
||||
img_h = img.shape[0]
|
||||
img_w = img.shape[1]
|
||||
prediction = None
|
||||
nxf = math.ceil((img_w - 2.0 * margin) / width_mid)
|
||||
nyf = math.ceil((img_h - 2.0 * margin) / height_mid)
|
||||
|
||||
batch_i = []
|
||||
batch_j = []
|
||||
batch_x_u = []
|
||||
batch_x_d = []
|
||||
batch_x_s = []
|
||||
batch_y_u = []
|
||||
batch_y_d = []
|
||||
batch_y_s = []
|
||||
batch = 0
|
||||
img_patch = np.zeros((n_batch_inference,
|
||||
img_height_model,
|
||||
img_width_model,
|
||||
3), dtype=np.float16)
|
||||
for i in range(nxf):
|
||||
for j in range(nyf):
|
||||
index_x_d = i * width_mid
|
||||
index_x_u = index_x_d + img_width_model
|
||||
if index_x_u > img_w:
|
||||
index_x_s = index_x_u - img_w
|
||||
index_x_u = img_w
|
||||
index_x_d = img_w - img_width_model
|
||||
else:
|
||||
index_x_s = 0
|
||||
index_y_d = j * height_mid
|
||||
index_y_u = index_y_d + img_height_model
|
||||
if index_y_u > img_h:
|
||||
index_y_s = index_y_u - img_h
|
||||
index_y_u = img_h
|
||||
index_y_d = img_h - img_height_model
|
||||
else:
|
||||
index_y_s = 0
|
||||
|
||||
batch_i.append(i)
|
||||
batch_j.append(j)
|
||||
batch_x_u.append(index_x_u)
|
||||
batch_x_d.append(index_x_d)
|
||||
batch_x_s.append(index_x_s)
|
||||
batch_y_d.append(index_y_d)
|
||||
batch_y_u.append(index_y_u)
|
||||
batch_y_s.append(index_y_s)
|
||||
|
||||
img_patch[batch] = img[index_y_d: index_y_u,
|
||||
index_x_d: index_x_u]
|
||||
batch += 1
|
||||
if (batch == n_batch_inference or
|
||||
# last batch
|
||||
i == nxf - 1 and j == nyf - 1):
|
||||
logger.debug("predicting patches on %s", str(img_patch.shape))
|
||||
label_p_pred = model.predict(img_patch, verbose=0)
|
||||
if prediction is None:
|
||||
# now we know the number of classes
|
||||
prediction = np.zeros((img_h, img_w, label_p_pred.shape[-1]), dtype=float)
|
||||
|
||||
for batch in range(batch):
|
||||
where = np.index_exp[batch_y_d[batch]: batch_y_u[batch],
|
||||
batch_x_d[batch]: batch_x_u[batch]]
|
||||
# shorter window on last tile
|
||||
part = np.index_exp[batch_y_s[batch]:,
|
||||
batch_x_s[batch]:]
|
||||
# normalize probability (where windows overlap)
|
||||
attenuation_y = np.ones(img_height_model - batch_y_s[batch])
|
||||
attenuation_x = np.ones(img_width_model - batch_x_s[batch])
|
||||
if margin and batch_j[batch] > 0:
|
||||
attenuation_y[:2 * margin] = window
|
||||
if margin and batch_j[batch] < nyf - 1:
|
||||
attenuation_y[-2 * margin:] = 1 - window
|
||||
if margin and batch_i[batch] > 0:
|
||||
attenuation_x[:2 * margin] = window
|
||||
if margin and batch_i[batch] < nxf - 1:
|
||||
attenuation_x[-2 * margin:] = 1 - window
|
||||
label_p_pred[batch][part] *= attenuation_y[:, np.newaxis, np.newaxis]
|
||||
label_p_pred[batch][part] *= attenuation_x[np.newaxis, :, np.newaxis]
|
||||
prediction[where][part] += label_p_pred[batch][part]
|
||||
|
||||
batch_i = []
|
||||
batch_j = []
|
||||
batch_x_u = []
|
||||
batch_x_d = []
|
||||
batch_x_s = []
|
||||
batch_y_u = []
|
||||
batch_y_d = []
|
||||
batch_y_s = []
|
||||
batch = 0
|
||||
img_patch[:] = 0
|
||||
|
||||
# decode
|
||||
seg = np.argmax(prediction, axis=2).astype(np.uint8)
|
||||
conf = prediction[tuple(np.indices(seg.shape)) + (seg,)]
|
||||
if thresholding_for_artificial_class:
|
||||
seg_art = prediction[:, :, artificial_class] >= threshold_art_class
|
||||
seg_mask_label(seg, seg_art,
|
||||
label=artificial_class,
|
||||
only=True,
|
||||
skeletonize=True,
|
||||
dilate=3,
|
||||
keep=separator_class)
|
||||
gc.collect()
|
||||
return seg, conf
|
||||
|
||||
# variant of do_prediction_new_concept with no need
|
||||
# for resizing or tiling into patches - done on model
|
||||
# (Tensorflow/CUDA) side
|
||||
# (after loading wrapped resized or patched model)
|
||||
def do_prediction_new_concept_autosize(
|
||||
img: np.ndarray,
|
||||
model: Predictor,
|
||||
logger=None,
|
||||
n_batch_inference=None,
|
||||
thresholding_for_heading=False,
|
||||
thresholding_for_artificial_class=False,
|
||||
threshold_art_class=0.1,
|
||||
artificial_class=4,
|
||||
) -> np.ndarray:
|
||||
if logger is None:
|
||||
logger = getLogger('eynollah')
|
||||
|
||||
logger.debug("enter do_prediction_new_concept (%s)", model.name)
|
||||
img = img / 255.0
|
||||
img = img.astype(np.float16)
|
||||
|
||||
prediction = model.predict(img[np.newaxis])[0]
|
||||
confidence = prediction[:, :, 1]
|
||||
segmentation = np.argmax(prediction, axis=2).astype(np.uint8)
|
||||
|
||||
if thresholding_for_artificial_class:
|
||||
seg_mask_label(segmentation,
|
||||
prediction[:, :, artificial_class] >= threshold_art_class,
|
||||
label=artificial_class,
|
||||
only=True,
|
||||
skeletonize=True,
|
||||
dilate=3)
|
||||
if thresholding_for_heading:
|
||||
seg_mask_label(segmentation,
|
||||
prediction[:, :, 2] >= 0.2,
|
||||
label=2)
|
||||
gc.collect()
|
||||
return segmentation, confidence
|
||||
|
||||
|
|
@ -3,12 +3,15 @@
|
|||
from pathlib import Path
|
||||
import os.path
|
||||
import logging
|
||||
from typing import Optional
|
||||
from typing import Optional, List, Tuple
|
||||
|
||||
import numpy as np
|
||||
import cv2
|
||||
from shapely import affinity, clip_by_rect
|
||||
|
||||
from ocrd_utils import points_from_polygon
|
||||
from ocrd_models.ocrd_page import (
|
||||
AlternativeImageType,
|
||||
BorderType,
|
||||
CoordsType,
|
||||
TextLineType,
|
||||
|
|
@ -17,22 +20,30 @@ from ocrd_models.ocrd_page import (
|
|||
ImageRegionType,
|
||||
TableRegionType,
|
||||
SeparatorRegionType,
|
||||
PcGtsType,
|
||||
to_xml
|
||||
)
|
||||
)
|
||||
|
||||
from .utils import Region, TextRegion
|
||||
from .utils.xml import create_page_xml, xml_reading_order
|
||||
from .utils.counter import EynollahIdCounter
|
||||
from .utils.contour import contour2polygon, make_valid
|
||||
|
||||
class EynollahXmlWriter:
|
||||
|
||||
def __init__(self, *, dir_out, image_filename, image_width, image_height, curved_line, pcgts=None):
|
||||
def __init__(
|
||||
self, *,
|
||||
dir_out: Optional[str],
|
||||
image_filename: str,
|
||||
image_width: int,
|
||||
image_height: int,
|
||||
pcgts: Optional[PcGtsType] = None,
|
||||
):
|
||||
self.logger = logging.getLogger('eynollah.writer')
|
||||
self.counter = EynollahIdCounter()
|
||||
self.dir_out = dir_out
|
||||
self.image_filename = image_filename
|
||||
self.output_filename = os.path.join(self.dir_out or "", self.image_filename_stem) + ".xml"
|
||||
self.curved_line = curved_line
|
||||
self.pcgts = pcgts
|
||||
self.image_height = image_height
|
||||
self.image_width = image_width
|
||||
|
|
@ -40,254 +51,159 @@ class EynollahXmlWriter:
|
|||
self.scale_y = 1.0
|
||||
|
||||
@property
|
||||
def image_filename_stem(self):
|
||||
def image_filename_stem(self) -> str:
|
||||
return Path(Path(self.image_filename).name).stem
|
||||
|
||||
def calculate_points(self, contour, offset=None):
|
||||
poly = contour2polygon(contour)
|
||||
def calculate_points(
|
||||
self, contour: np.ndarray,
|
||||
offset: Optional[List[int]] = None,
|
||||
dilate: int = 0,
|
||||
) -> str:
|
||||
poly = contour2polygon(contour, dilate=dilate)
|
||||
if offset is not None:
|
||||
poly = affinity.translate(poly, *offset)
|
||||
poly = affinity.scale(poly, xfact=1 / self.scale_x, yfact=1 / self.scale_y, origin=(0, 0))
|
||||
poly = make_valid(clip_by_rect(poly, 0, 0, self.image_width, self.image_height))
|
||||
return points_from_polygon(poly.exterior.coords[:-1])
|
||||
|
||||
def serialize_lines_in_region(self, text_region, all_found_textline_polygons, region_idx, page_coord, slopes, counter, ocr_all_textlines_textregion):
|
||||
for j, polygon_textline in enumerate(all_found_textline_polygons[region_idx]):
|
||||
coords = CoordsType()
|
||||
textline = TextLineType(id=counter.next_line_id, Coords=coords)
|
||||
if ocr_all_textlines_textregion:
|
||||
# FIXME: add OCR confidence
|
||||
textline.set_TextEquiv([TextEquivType(Unicode=ocr_all_textlines_textregion[j])])
|
||||
def serialize_lines_in_region(
|
||||
self, text_region: TextRegionType,
|
||||
offset: List[int],
|
||||
counter: EynollahIdCounter,
|
||||
lines: List[Region],
|
||||
) -> None:
|
||||
for line in lines:
|
||||
textline = TextLineType(
|
||||
id=counter.next_line_id,
|
||||
Coords=CoordsType(points=self.calculate_points(line.contour, offset, 5),
|
||||
conf=line.conf)
|
||||
)
|
||||
text_region.add_TextLine(textline)
|
||||
text_region.set_orientation(-slopes[region_idx])
|
||||
offset = [page_coord[2], page_coord[0]]
|
||||
coords.set_points(self.calculate_points(polygon_textline, offset))
|
||||
|
||||
def write_pagexml(self, pcgts):
|
||||
self.logger.info("output filename: '%s'", self.output_filename)
|
||||
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:
|
||||
f.write(to_xml(pcgts))
|
||||
|
||||
def build_pagexml_no_full_layout(
|
||||
def build_pagexml(
|
||||
self,
|
||||
*,
|
||||
num_col,
|
||||
found_polygons_text_region,
|
||||
page_coord,
|
||||
page_slope,
|
||||
order_of_texts,
|
||||
all_found_textline_polygons,
|
||||
found_polygons_images,
|
||||
found_polygons_tables,
|
||||
found_polygons_marginals_left,
|
||||
found_polygons_marginals_right,
|
||||
all_found_textline_polygons_marginals_left,
|
||||
all_found_textline_polygons_marginals_right,
|
||||
slopes,
|
||||
slopes_marginals_left,
|
||||
slopes_marginals_right,
|
||||
cont_page,
|
||||
polygons_seplines,
|
||||
ocr_all_textlines=None,
|
||||
ocr_all_textlines_marginals_left=None,
|
||||
ocr_all_textlines_marginals_right=None,
|
||||
ocr_all_textlines_drop=None,
|
||||
conf_textregions=None,
|
||||
conf_marginals_left=None,
|
||||
conf_marginals_right=None,
|
||||
conf_images=None,
|
||||
conf_tables=None,
|
||||
):
|
||||
return self.build_pagexml_full_layout(
|
||||
num_col=num_col,
|
||||
found_polygons_text_region=found_polygons_text_region,
|
||||
found_polygons_text_region_h=[],
|
||||
page_coord=page_coord,
|
||||
page_slope=page_slope,
|
||||
order_of_texts=order_of_texts,
|
||||
all_found_textline_polygons=all_found_textline_polygons,
|
||||
all_found_textline_polygons_h=[],
|
||||
found_polygons_images=found_polygons_images,
|
||||
found_polygons_tables=found_polygons_tables,
|
||||
found_polygons_drop_capitals=[],
|
||||
found_polygons_marginals_left=found_polygons_marginals_left,
|
||||
found_polygons_marginals_right=found_polygons_marginals_right,
|
||||
all_found_textline_polygons_marginals_left=all_found_textline_polygons_marginals_left,
|
||||
all_found_textline_polygons_marginals_right=all_found_textline_polygons_marginals_right,
|
||||
slopes=slopes,
|
||||
slopes_h=[],
|
||||
slopes_marginals_left=slopes_marginals_left,
|
||||
slopes_marginals_right=slopes_marginals_right,
|
||||
cont_page=cont_page,
|
||||
polygons_seplines=polygons_seplines,
|
||||
ocr_all_textlines=ocr_all_textlines,
|
||||
ocr_all_textlines_marginals_left=ocr_all_textlines_marginals_left,
|
||||
ocr_all_textlines_marginals_right=ocr_all_textlines_marginals_right,
|
||||
conf_textregions=conf_textregions,
|
||||
conf_marginals_left=conf_marginals_left,
|
||||
conf_marginals_right=conf_marginals_right,
|
||||
conf_images=conf_images,
|
||||
conf_tables=conf_tables,
|
||||
)
|
||||
|
||||
def build_pagexml_full_layout(
|
||||
self,
|
||||
*,
|
||||
num_col,
|
||||
found_polygons_text_region,
|
||||
found_polygons_text_region_h,
|
||||
page_coord,
|
||||
page_slope,
|
||||
order_of_texts,
|
||||
all_found_textline_polygons,
|
||||
all_found_textline_polygons_h,
|
||||
found_polygons_images,
|
||||
found_polygons_tables,
|
||||
found_polygons_drop_capitals,
|
||||
found_polygons_marginals_left,
|
||||
found_polygons_marginals_right,
|
||||
all_found_textline_polygons_marginals_left,
|
||||
all_found_textline_polygons_marginals_right,
|
||||
slopes,
|
||||
slopes_h,
|
||||
slopes_marginals_left,
|
||||
slopes_marginals_right,
|
||||
cont_page,
|
||||
polygons_seplines,
|
||||
ocr_all_textlines=None,
|
||||
ocr_all_textlines_h=None,
|
||||
ocr_all_textlines_marginals_left=None,
|
||||
ocr_all_textlines_marginals_right=None,
|
||||
ocr_all_textlines_drop=None,
|
||||
conf_textregions=None,
|
||||
conf_textregions_h=None,
|
||||
conf_marginals_left=None,
|
||||
conf_marginals_right=None,
|
||||
conf_images=None,
|
||||
conf_tables=None,
|
||||
conf_drops=None,
|
||||
page: Region,
|
||||
img_bin: Optional[np.ndarray] = None,
|
||||
num_col: int = 1,
|
||||
order_of_texts: List[int] = [],
|
||||
textregions: List[TextRegion] = [],
|
||||
textregions_h: List[TextRegion] = [],
|
||||
images: List[Region] = [],
|
||||
tables: List[Region] = [],
|
||||
drop_caps: List[Region] = [],
|
||||
marginals_left: List[TextRegion] = [],
|
||||
marginals_right: List[TextRegion] = [],
|
||||
seplines: List[Region] = [],
|
||||
):
|
||||
self.logger.debug('enter build_pagexml')
|
||||
|
||||
# create the file structure
|
||||
pcgts = self.pcgts if self.pcgts else create_page_xml(
|
||||
self.image_filename, self.image_height, self.image_width)
|
||||
page = pcgts.get_Page()
|
||||
pcgts.Metadata.Comments = "num_col %d" % num_col
|
||||
page.set_custom('layout {num_col:%d;} ' % num_col)
|
||||
page.set_orientation(-page_slope)
|
||||
if len(cont_page):
|
||||
page.set_Border(BorderType(Coords=CoordsType(points=self.calculate_points(cont_page[0]))))
|
||||
|
||||
offset = [page_coord[2], page_coord[0]]
|
||||
if img_bin is not None:
|
||||
img_alt = AlternativeImageType(filename=img_bin, # will be replaced later
|
||||
comments="binarized")
|
||||
pcgts.Page.add_AlternativeImage(img_alt)
|
||||
pcgts.Page.set_custom('layout {num_col:%d;} ' % num_col)
|
||||
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()
|
||||
if len(order_of_texts):
|
||||
_counter_marginals = EynollahIdCounter(region_idx=len(order_of_texts))
|
||||
id_of_marginalia_left = [_counter_marginals.next_region_id
|
||||
for _ in found_polygons_marginals_left]
|
||||
for _ in marginals_left]
|
||||
id_of_marginalia_right = [_counter_marginals.next_region_id
|
||||
for _ in found_polygons_marginals_right]
|
||||
xml_reading_order(page, order_of_texts, id_of_marginalia_left, id_of_marginalia_right)
|
||||
for _ in marginals_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(
|
||||
id=counter.next_region_id, type_='paragraph',
|
||||
Coords=CoordsType(points=self.calculate_points(region_contour, offset))
|
||||
Coords=CoordsType(points=self.calculate_points(region.contour, offset, 6),
|
||||
conf=region.conf),
|
||||
orientation=-region.skew
|
||||
)
|
||||
assert textregion.Coords
|
||||
if conf_textregions:
|
||||
textregion.Coords.set_conf(conf_textregions[mm])
|
||||
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.serialize_lines_in_region(textregion, offset, counter, region.lines)
|
||||
pcgts.Page.add_TextRegion(textregion)
|
||||
|
||||
self.logger.debug('len(found_polygons_text_region_h) %s', len(found_polygons_text_region_h))
|
||||
for mm, region_contour in enumerate(found_polygons_text_region_h):
|
||||
self.logger.debug('len(textregions_h) %s', len(textregions_h))
|
||||
for region in textregions_h:
|
||||
textregion = TextRegionType(
|
||||
id=counter.next_region_id, type_='heading',
|
||||
Coords=CoordsType(points=self.calculate_points(region_contour, offset))
|
||||
Coords=CoordsType(points=self.calculate_points(region.contour, offset, 6),
|
||||
conf=region.conf),
|
||||
orientation=-region.skew
|
||||
)
|
||||
assert textregion.Coords
|
||||
if conf_textregions_h:
|
||||
textregion.Coords.set_conf(conf_textregions_h[mm])
|
||||
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)
|
||||
self.serialize_lines_in_region(textregion, offset, counter, region.lines)
|
||||
pcgts.Page.add_TextRegion(textregion)
|
||||
|
||||
for mm, region_contour in enumerate(found_polygons_drop_capitals):
|
||||
dropcapital = TextRegionType(
|
||||
for region in drop_caps:
|
||||
textregion = TextRegionType(
|
||||
id=counter.next_region_id, type_='drop-capital',
|
||||
Coords=CoordsType(points=self.calculate_points(region_contour, offset))
|
||||
Coords=CoordsType(points=self.calculate_points(region.contour, offset, 6),
|
||||
conf=region.conf),
|
||||
orientation=-region.skew
|
||||
)
|
||||
if conf_drops:
|
||||
dropcapital.Coords.set_conf(conf_drops[mm])
|
||||
page.add_TextRegion(dropcapital)
|
||||
slopes_drop = [0]
|
||||
if ocr_all_textlines_drop:
|
||||
ocr_textlines = ocr_all_textlines_drop[mm]
|
||||
else:
|
||||
ocr_textlines = None
|
||||
self.serialize_lines_in_region(dropcapital, [[found_polygons_drop_capitals[mm]]], 0, page_coord,
|
||||
slopes_drop, counter, ocr_textlines)
|
||||
self.serialize_lines_in_region(textregion, offset, counter, [region])
|
||||
pcgts.Page.add_TextRegion(textregion)
|
||||
|
||||
for mm, region_contour in enumerate(found_polygons_marginals_left):
|
||||
marginal = TextRegionType(
|
||||
for region in marginals_left:
|
||||
textregion = TextRegionType(
|
||||
id=counter.next_region_id, type_='marginalia',
|
||||
Coords=CoordsType(points=self.calculate_points(region_contour, offset))
|
||||
Coords=CoordsType(points=self.calculate_points(region.contour, offset, 6),
|
||||
conf=region.conf),
|
||||
orientation=-region.skew
|
||||
)
|
||||
if conf_marginals_left:
|
||||
marginal.Coords.set_conf(conf_marginals_left[mm])
|
||||
page.add_TextRegion(marginal)
|
||||
if ocr_all_textlines_marginals_left:
|
||||
ocr_textlines = ocr_all_textlines_marginals_left[mm]
|
||||
else:
|
||||
ocr_textlines = None
|
||||
self.serialize_lines_in_region(marginal, all_found_textline_polygons_marginals_left, mm, page_coord,
|
||||
slopes_marginals_left, counter, ocr_textlines)
|
||||
self.serialize_lines_in_region(textregion, offset, counter, region.lines)
|
||||
pcgts.Page.add_TextRegion(textregion)
|
||||
|
||||
for mm, region_contour in enumerate(found_polygons_marginals_right):
|
||||
marginal = TextRegionType(
|
||||
for region in marginals_right:
|
||||
textregion = TextRegionType(
|
||||
id=counter.next_region_id, type_='marginalia',
|
||||
Coords=CoordsType(points=self.calculate_points(region_contour, offset))
|
||||
Coords=CoordsType(points=self.calculate_points(region.contour, offset, 6),
|
||||
conf=region.conf),
|
||||
orientation=-region.skew
|
||||
)
|
||||
if conf_marginals_right:
|
||||
marginal.Coords.set_conf(conf_marginals_right[mm])
|
||||
page.add_TextRegion(marginal)
|
||||
if ocr_all_textlines_marginals_right:
|
||||
ocr_textlines = ocr_all_textlines_marginals_right[mm]
|
||||
else:
|
||||
ocr_textlines = None
|
||||
self.serialize_lines_in_region(marginal, all_found_textline_polygons_marginals_right, mm, page_coord,
|
||||
slopes_marginals_right, counter, ocr_textlines)
|
||||
self.serialize_lines_in_region(textregion, offset, counter, region.lines)
|
||||
pcgts.Page.add_TextRegion(textregion)
|
||||
|
||||
for mm, region_contour in enumerate(found_polygons_images):
|
||||
for region in images:
|
||||
image = ImageRegionType(
|
||||
id=counter.next_region_id,
|
||||
Coords=CoordsType(points=self.calculate_points(region_contour, offset)))
|
||||
if conf_images:
|
||||
image.Coords.set_conf(conf_images[mm])
|
||||
page.add_ImageRegion(image)
|
||||
Coords=CoordsType(points=self.calculate_points(region.contour, offset, 2),
|
||||
conf=region.conf))
|
||||
pcgts.Page.add_ImageRegion(image)
|
||||
|
||||
for region_contour in polygons_seplines:
|
||||
page.add_SeparatorRegion(
|
||||
SeparatorRegionType(id=counter.next_region_id,
|
||||
Coords=CoordsType(points=self.calculate_points(region_contour, offset))))
|
||||
for region in seplines:
|
||||
pcgts.Page.add_SeparatorRegion(
|
||||
SeparatorRegionType(
|
||||
id=counter.next_region_id,
|
||||
Coords=CoordsType(points=self.calculate_points(region.contour, offset, 2),
|
||||
conf=region.conf)))
|
||||
|
||||
for mm, region_contour in enumerate(found_polygons_tables):
|
||||
for region in tables:
|
||||
table = TableRegionType(
|
||||
id=counter.next_region_id,
|
||||
Coords=CoordsType(points=self.calculate_points(region_contour, offset)))
|
||||
if conf_tables:
|
||||
table.Coords.set_conf(conf_tables[mm])
|
||||
page.add_TableRegion(table)
|
||||
Coords=CoordsType(points=self.calculate_points(region.contour, offset, 6),
|
||||
conf=region.conf))
|
||||
pcgts.Page.add_TableRegion(table)
|
||||
|
||||
return pcgts
|
||||
|
||||
|
|
|
|||
|
|
@ -1,21 +1,30 @@
|
|||
import pytest
|
||||
from ocrd_modelfactory import page_from_file
|
||||
from ocrd_models.constants import NAMESPACES as NS
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"options",
|
||||
[
|
||||
[], # defaults
|
||||
["--model_based"],
|
||||
], ids=str)
|
||||
def test_run_eynollah_mbreorder_filename(
|
||||
tmp_path,
|
||||
resources_dir,
|
||||
run_eynollah_ok_and_check_logs,
|
||||
options,
|
||||
):
|
||||
infile = resources_dir / '2files/kant_aufklaerung_1784_0020.xml'
|
||||
outfile = tmp_path /'kant_aufklaerung_1784_0020.xml'
|
||||
run_eynollah_ok_and_check_logs(
|
||||
'machine-based-reading-order',
|
||||
'reorder',
|
||||
[
|
||||
'-i', str(infile),
|
||||
'-dim', str(resources_dir / '2files'),
|
||||
'-o', str(outfile.parent),
|
||||
],
|
||||
] + options,
|
||||
[
|
||||
# FIXME: mbreorder has no logging!
|
||||
str(infile)
|
||||
]
|
||||
)
|
||||
assert outfile.exists()
|
||||
|
|
@ -34,13 +43,15 @@ def test_run_eynollah_mbreorder_directory(
|
|||
):
|
||||
outdir = tmp_path
|
||||
run_eynollah_ok_and_check_logs(
|
||||
'machine-based-reading-order',
|
||||
'reorder',
|
||||
[
|
||||
'-di', str(resources_dir / '2files'),
|
||||
'-dim', str(resources_dir / '2files'),
|
||||
'-o', str(outdir),
|
||||
],
|
||||
[
|
||||
# FIXME: mbreorder has no logging!
|
||||
'Job done in',
|
||||
'All jobs done in',
|
||||
]
|
||||
)
|
||||
assert len(list(outdir.iterdir())) == 2
|
||||
|
|
|
|||
|
|
@ -31,7 +31,7 @@ def eynollah_subcommands():
|
|||
'layout',
|
||||
'ocr',
|
||||
'enhancement',
|
||||
'machine-based-reading-order',
|
||||
'reorder',
|
||||
'models',
|
||||
]
|
||||
|
||||
|
|
|
|||
108
tests/test_tiling.py
Normal file
108
tests/test_tiling.py
Normal file
|
|
@ -0,0 +1,108 @@
|
|||
import pytest
|
||||
import cv2
|
||||
import numpy as np
|
||||
from matplotlib import pyplot as plt
|
||||
|
||||
from eynollah.utils.tiling import do_prediction, do_prediction_new_concept
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"height,width",
|
||||
[
|
||||
(448, 448),
|
||||
(672, 672),
|
||||
(1088, 832),
|
||||
(1152, 896),
|
||||
])
|
||||
def test_tiling_idem(image_resources, height, width):
|
||||
infile = image_resources[0]
|
||||
class PseudoModel:
|
||||
def predict(self, images, **kwargs):
|
||||
return images
|
||||
@property
|
||||
def input_shape(self):
|
||||
return None, height, width, None
|
||||
model = PseudoModel()
|
||||
in_img = cv2.imread(infile)
|
||||
outimg = do_prediction(in_img, model, patches=True, is_enhancement=True,
|
||||
marginal_of_patch_percent=0)
|
||||
assert in_img.shape == outimg.shape
|
||||
assert np.all(in_img == outimg)
|
||||
outimg = do_prediction(in_img, model, patches=True, is_enhancement=True,
|
||||
marginal_of_patch_percent=0.1)
|
||||
assert in_img.shape == outimg.shape
|
||||
assert np.all(in_img == outimg)
|
||||
outimg = do_prediction(in_img, model, patches=True, is_enhancement=True,
|
||||
marginal_of_patch_percent=0.2)
|
||||
assert in_img.shape == outimg.shape
|
||||
assert in_img.dtype == outimg.dtype
|
||||
assert np.all(in_img == outimg)
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"height,width",
|
||||
[
|
||||
(448, 448),
|
||||
(672, 672),
|
||||
(1088, 832),
|
||||
(1152, 896),
|
||||
])
|
||||
def test_tiling_min(image_resources, height, width):
|
||||
infile = image_resources[0]
|
||||
class PseudoModel:
|
||||
def predict(self, images, **kwargs):
|
||||
M = images.min(axis=(1, 2, 3))
|
||||
return 1. * (images == M)
|
||||
@property
|
||||
def input_shape(self):
|
||||
return None, height, width, None
|
||||
model = PseudoModel()
|
||||
in_img = cv2.imread(infile)
|
||||
outimg = do_prediction(in_img, model, patches=True,
|
||||
marginal_of_patch_percent=0)
|
||||
assert in_img.shape[:2] == outimg.shape
|
||||
assert np.any(outimg)
|
||||
outimg = do_prediction(in_img, model, patches=True,
|
||||
marginal_of_patch_percent=0.1)
|
||||
assert in_img.shape[:2] == outimg.shape
|
||||
assert np.any(outimg)
|
||||
outimg = do_prediction(in_img, model, patches=True,
|
||||
marginal_of_patch_percent=0.2)
|
||||
assert in_img.shape[:2] == outimg.shape
|
||||
assert np.any(outimg)
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"height,width",
|
||||
[
|
||||
(448, 448),
|
||||
(672, 672),
|
||||
(1088, 832),
|
||||
(1152, 896),
|
||||
])
|
||||
def test_tiling_min_conf(image_resources, height, width):
|
||||
infile = image_resources[0]
|
||||
class PseudoModel:
|
||||
def predict(self, images, **kwargs):
|
||||
M = images.min(axis=(1, 2, 3))
|
||||
return 1. * (images == M)
|
||||
@property
|
||||
def input_shape(self):
|
||||
return None, height, width, None
|
||||
model = PseudoModel()
|
||||
in_img = cv2.imread(infile)
|
||||
outimg, conf = do_prediction_new_concept(
|
||||
in_img, model, patches=True,
|
||||
marginal_of_patch_percent=0)
|
||||
assert in_img.shape[:2] == outimg.shape
|
||||
assert np.any(outimg)
|
||||
assert np.sum(conf) < conf.size
|
||||
outimg, conf = do_prediction_new_concept(
|
||||
in_img, model, patches=True,
|
||||
marginal_of_patch_percent=0.1)
|
||||
assert in_img.shape[:2] == outimg.shape
|
||||
assert np.any(outimg)
|
||||
assert np.sum(conf) < conf.size
|
||||
outimg, conf = do_prediction_new_concept(
|
||||
in_img, model, patches=True,
|
||||
marginal_of_patch_percent=0.2)
|
||||
assert in_img.shape[:2] == outimg.shape
|
||||
assert np.any(outimg)
|
||||
assert np.sum(conf) < conf.size
|
||||
Loading…
Add table
Add a link
Reference in a new issue