add extract-page CLI for cropping only

This commit is contained in:
Robert Sachunsky 2026-07-30 15:30:41 +02:00
parent 628e76cae7
commit fa21cff3a9
3 changed files with 238 additions and 0 deletions

View file

@ -1,6 +1,7 @@
from .cli import main from .cli import main
from .cli_binarize import binarize_cli from .cli_binarize import binarize_cli
from .cli_enhance import enhance_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_extract_images import extract_images_cli
from .cli_layout import layout_cli from .cli_layout import layout_cli
from .cli_models import models_cli from .cli_models import models_cli
@ -13,4 +14,5 @@ main.add_command(layout_cli, 'layout')
main.add_command(readingorder_cli, 'reorder') main.add_command(readingorder_cli, 'reorder')
main.add_command(models_cli, 'models') main.add_command(models_cli, 'models')
main.add_command(ocr_cli, 'ocr') main.add_command(ocr_cli, 'ocr')
main.add_command(extract_page_cli, 'extract-page')
main.add_command(extract_images_cli, 'extract-images') main.add_command(extract_images_cli, 'extract-images')

View 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,
)

View 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)