layout/writer/extract-images: also refactor page as dataclass…

use `Region` for page (Border) coordinates and skew angle
(as for the other segment types), pass as mandatory arg
to writer
This commit is contained in:
Robert Sachunsky 2026-07-19 19:05:46 +02:00
parent 147a86ba42
commit f1505f5ae6
3 changed files with 45 additions and 67 deletions

View file

@ -231,15 +231,16 @@ class EynollahImageExtractor(Eynollah):
# Image Extraction Mode # Image Extraction Mode
self.logger.info("Step 2/5: Image Extraction Mode") self.logger.info("Step 2/5: Image Extraction Mode")
t1 = time.time() t1 = time.time()
page_coord, cont_page, image_page, mask_page = self.extract_page(image) page_cont, image_page, _ = self.extract_page(image)
page = Region(page_cont)
_, _, images_cont = self.get_early_layout( _, _, images_cont = self.get_early_layout(
image['img_res'], num_col_classifier) image['img_res'], num_col_classifier)
self.logger.debug("Found %d images", len(images_cont)) self.logger.debug("Found %d images", len(images_cont))
# FIXME: post-hoc cropping (remove when models support it, and replace image['img_res'] with image_page) # FIXME: post-hoc cropping (remove when models support it, and replace image['img_res'] with image_page)
page_coord = np.array(page_coord) page_box = cv2.boundingRect(page.contour)
images_cont = [cont - page_coord[::2][::-1][np.newaxis, np.newaxis] images_cont = [cont - [page_box[:2]]
for cont in images_cont] for cont in images_cont]
if self.plotter: if self.plotter:
self.plotter.write_images_into_directory(images_cont, image_page, self.plotter.write_images_into_directory(images_cont, image_page,
@ -250,9 +251,8 @@ class EynollahImageExtractor(Eynollah):
# can be empty if above page frame # can be empty if above page frame
images = [image for image in images if image.area] images = [image for image in images if image.area]
pcgts = writer.build_pagexml( pcgts = writer.build_pagexml(
page=page,
num_col=num_col_classifier, num_col=num_col_classifier,
page_coord=page_coord,
page_contour=cont_page[0],
images=images, images=images,
) )
writer.write_pagexml(pcgts) writer.write_pagexml(pcgts)

View file

@ -74,7 +74,7 @@ from .utils import (
is_image_filename, is_image_filename,
isNaN, isNaN,
crop_image_inside_box, crop_image_inside_box,
box2rect, box2slice,
find_num_col, find_num_col,
otsu_copy_binary, otsu_copy_binary,
seg_mask_label, seg_mask_label,
@ -806,14 +806,13 @@ class Eynollah:
return segmentation, confidence return segmentation, confidence
def extract_page(self, image): def extract_page(self, image):
cropped_page = img = image['img_res'] page_cropped = img = image['img_res']
h, w = img.shape[:2] h, w = img.shape[:2]
page_coord = [0, h, 0, w] page_cont = np.array([[[0, 0]],
cont_page = [np.array([[[0, 0]],
[[w, 0]], [[w, 0]],
[[w, h]], [[w, h]],
[[0, h]]])] [[0, h]]])
mask_page = np.ones((h, w), dtype=np.uint8) page_mask = np.ones((h, w), dtype=np.uint8)
if not self.ignore_page_extraction: if not self.ignore_page_extraction:
self.logger.debug("enter extract_page") self.logger.debug("enter extract_page")
#cv2.GaussianBlur(img, (5, 5), 0) #cv2.GaussianBlur(img, (5, 5), 0)
@ -821,26 +820,13 @@ class Eynollah:
contours, _ = cv2.findContours(prediction, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) contours, _ = cv2.findContours(prediction, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
if len(contours): if len(contours):
areas = np.array(list(map(cv2.contourArea, contours))) areas = np.array(list(map(cv2.contourArea, contours)))
cnt = contours[np.argmax(areas)] page_cont = contours[np.argmax(areas)]
cont_page = [cnt] box = (x, y, w, h) = cv2.boundingRect(page_cont)
x, y, w, h = cv2.boundingRect(cnt) page_cropped = img[box2slice(box)]
#if x <= 30: page_mask = np.zeros((h, w), dtype=np.uint8)
#w += x page_mask = cv2.fillPoly(page_mask, pts=[page_cont - [x, y]], color=1)
#x = 0
#if (self.image.shape[1] - (x + w)) <= 30:
#w = w + (self.image.shape[1] - (x + w))
#if y <= 30:
#h = h + y
#y = 0
#if (self.image.shape[0] - (y + h)) <= 30:
#h = h + (self.image.shape[0] - (y + h))
box = [x, y, w, h]
cropped_page, page_coord = crop_image_inside_box(box, img)
mask_page = np.zeros((h, w), dtype=np.uint8)
mask_page = cv2.fillPoly(mask_page, pts=[cnt - [x, y]], color=1)
self.logger.debug("exit extract_page") self.logger.debug("exit extract_page")
return page_coord, cont_page, cropped_page, mask_page return page_cont, page_cropped, page_mask
def early_page_for_num_of_column_classification(self, img): def early_page_for_num_of_column_classification(self, img):
if not self.ignore_page_extraction: if not self.ignore_page_extraction:
@ -2075,7 +2061,8 @@ class Eynollah:
self.logger.info(f"Enhancement complete ({time.time() - t0:.1f}s)") self.logger.info(f"Enhancement complete ({time.time() - t0:.1f}s)")
t1 = time.time() t1 = time.time()
page_coord, cont_page, image_page, mask_page = self.extract_page(image) page_cont, image_page, mask_page = self.extract_page(image)
page = Region(page_cont)
if not self.ignore_page_extraction: if not self.ignore_page_extraction:
self.logger.debug("Cropped page is %dx%d", image_page.shape[1], image_page.shape[0]) self.logger.debug("Cropped page is %dx%d", image_page.shape[1], image_page.shape[0])
self.logger.info("Cropping took %.1fs", time.time() - t1) self.logger.info("Cropping took %.1fs", time.time() - t1)
@ -2102,7 +2089,7 @@ class Eynollah:
textlines_cont, textlines_conf, textlines_cont, textlines_conf,
textlines_cx, textlines_cy, textlines_w_h) textlines_cx, textlines_cy, textlines_w_h)
textregions = [ textregions = [
TextRegion(cont_page, lines=[ TextRegion(page.contour, lines=[
Region(cont, conf=conf) Region(cont, conf=conf)
for cont, conf in zip(textlines_cont, textlines_conf)]) for cont, conf in zip(textlines_cont, textlines_conf)])
] ]
@ -2110,9 +2097,8 @@ class Eynollah:
self.logger.info("Basic processing complete") self.logger.info("Basic processing complete")
pcgts = writer.build_pagexml( pcgts = writer.build_pagexml(
page=page,
num_col=num_col_classifier, num_col=num_col_classifier,
page_coord=page_coord,
page_contour=cont_page[0],
order_of_texts=[0], order_of_texts=[0],
textregions=textregions, textregions=textregions,
) )
@ -2153,19 +2139,19 @@ class Eynollah:
if (abs(slope_deskew) > 45 and if (abs(slope_deskew) > 45 and
((text_regions_p == label_text).sum()) <= 0.3 * image_page.size): ((text_regions_p == label_text).sum()) <= 0.3 * image_page.size):
slope_deskew = 0 slope_deskew = 0
page.skew = slope_deskew
if self.plotter: if self.plotter:
self.plotter.save_deskewed_image(slope_deskew, image['img'], image['name']) self.plotter.save_deskewed_image(slope_deskew, image['img'], image['name'])
t3 = time.time() t3 = time.time()
self.logger.info("Deskewing took %.1fs", t3 - t2) self.logger.info("Deskewing took %.1fs", t3 - t2)
# FIXME: post-hoc cropping (remove when models support it, and replace image['img_res'] with image_page) # FIXME: post-hoc cropping (remove when models support it, and replace image['img_res'] with image_page)
page_coord = np.array(page_coord) page_box = cv2.boundingRect(page.contour)
page_box = (slice(*page_coord[:2]),
slice(*page_coord[2:]))
seplines_conf = get_region_confidences(seplines_cont, regions_confidence) seplines_conf = get_region_confidences(seplines_cont, regions_confidence)
seplines = [Region(cont - page_coord[::2][::-1][np.newaxis, np.newaxis], seplines = [Region(cont - [page_box[:2]],
conf=conf) conf=conf)
for cont, conf in zip(seplines_cont, seplines_conf)] for cont, conf in zip(seplines_cont, seplines_conf)]
page_box = box2slice(page_box)
regions_without_separators = regions_without_separators[page_box] * mask_page regions_without_separators = regions_without_separators[page_box] * mask_page
text_regions_p = text_regions_p[page_box] * mask_page text_regions_p = text_regions_p[page_box] * mask_page
textline_mask_tot_ea = textline_mask_tot_ea[page_box] * mask_page textline_mask_tot_ea = textline_mask_tot_ea[page_box] * mask_page
@ -2183,10 +2169,8 @@ class Eynollah:
self.logger.info("No columns detected - generating empty PAGE-XML") self.logger.info("No columns detected - generating empty PAGE-XML")
pcgts = writer.build_pagexml( pcgts = writer.build_pagexml(
page=page,
num_col=0, num_col=0,
page_coord=page_coord,
page_contour=cont_page[0],
page_skew=slope_deskew,
) )
if writer.pcgts is None: if writer.pcgts is None:
writer.write_pagexml(pcgts) writer.write_pagexml(pcgts)
@ -2385,10 +2369,8 @@ class Eynollah:
self.logger.info("Step 5/5: Output Generation") self.logger.info("Step 5/5: Output Generation")
pcgts = writer.build_pagexml( pcgts = writer.build_pagexml(
page=page,
num_col=num_col_classifier, num_col=num_col_classifier,
page_coord=page_coord,
page_contour=cont_page[0],
page_skew=slope_deskew,
order_of_texts=order_text, order_of_texts=order_text,
textregions=textregions, textregions=textregions,
textregions_h=textregions_h, textregions_h=textregions_h,

View file

@ -5,6 +5,7 @@ import os.path
import logging import logging
from typing import Optional, List, Tuple from typing import Optional, List, Tuple
import numpy as np import numpy as np
import cv2
from shapely import affinity, clip_by_rect from shapely import affinity, clip_by_rect
from ocrd_utils import points_from_polygon from ocrd_utils import points_from_polygon
@ -85,10 +86,8 @@ class EynollahXmlWriter:
def build_pagexml( def build_pagexml(
self, self,
*, *,
page: Region,
num_col=1, num_col=1,
page_coord: Optional[List[int]] = None,
page_contour: Optional[np.ndarray] = None,
page_skew: float = 0.,
order_of_texts: List[int] = [], order_of_texts: List[int] = [],
textregions: List[TextRegion] = [], textregions: List[TextRegion] = [],
textregions_h: List[TextRegion] = [], textregions_h: List[TextRegion] = [],
@ -104,16 +103,13 @@ class EynollahXmlWriter:
# create the file structure # create the file structure
pcgts = self.pcgts if self.pcgts else create_page_xml( pcgts = self.pcgts if self.pcgts else create_page_xml(
self.image_filename, self.image_height, self.image_width) self.image_filename, self.image_height, self.image_width)
page = pcgts.get_Page()
pcgts.Metadata.Comments = "num_col %d" % num_col pcgts.Metadata.Comments = "num_col %d" % num_col
page.set_custom('layout {num_col:%d;} ' % num_col) pcgts.Page.set_custom('layout {num_col:%d;} ' % num_col)
page.set_orientation(-page_skew) pcgts.Page.set_orientation(-page.skew)
if page_contour is not None: pcgts.Page.set_Border(BorderType(Coords=CoordsType(
page.set_Border(BorderType(Coords=CoordsType(points=self.calculate_points(page_contour)))) points=self.calculate_points(page.contour))))
if page_coord is None: x, y, w, h = cv2.boundingRect(page.contour)
offset = [0, 0] offset = [x, y]
else:
offset = [page_coord[2], page_coord[0]]
counter = EynollahIdCounter() counter = EynollahIdCounter()
if len(order_of_texts): if len(order_of_texts):
_counter_marginals = EynollahIdCounter(region_idx=len(order_of_texts)) _counter_marginals = EynollahIdCounter(region_idx=len(order_of_texts))
@ -121,7 +117,7 @@ class EynollahXmlWriter:
for _ in marginals_left] for _ in marginals_left]
id_of_marginalia_right = [_counter_marginals.next_region_id id_of_marginalia_right = [_counter_marginals.next_region_id
for _ in marginals_right] for _ in marginals_right]
xml_reading_order(page, order_of_texts, id_of_marginalia_left, id_of_marginalia_right) xml_reading_order(pcgts.Page, order_of_texts, id_of_marginalia_left, id_of_marginalia_right)
for region in textregions: for region in textregions:
textregion = TextRegionType( textregion = TextRegionType(
@ -131,7 +127,7 @@ class EynollahXmlWriter:
orientation=-region.skew orientation=-region.skew
) )
self.serialize_lines_in_region(textregion, offset, counter, region.lines) self.serialize_lines_in_region(textregion, offset, counter, region.lines)
page.add_TextRegion(textregion) pcgts.Page.add_TextRegion(textregion)
self.logger.debug('len(textregions_h) %s', len(textregions_h)) self.logger.debug('len(textregions_h) %s', len(textregions_h))
for region in textregions_h: for region in textregions_h:
@ -142,7 +138,7 @@ class EynollahXmlWriter:
orientation=-region.skew orientation=-region.skew
) )
self.serialize_lines_in_region(textregion, offset, counter, region.lines) self.serialize_lines_in_region(textregion, offset, counter, region.lines)
page.add_TextRegion(textregion) pcgts.Page.add_TextRegion(textregion)
for region in drop_caps: for region in drop_caps:
textregion = TextRegionType( textregion = TextRegionType(
@ -152,7 +148,7 @@ class EynollahXmlWriter:
orientation=-region.skew orientation=-region.skew
) )
self.serialize_lines_in_region(textregion, offset, counter, [region]) self.serialize_lines_in_region(textregion, offset, counter, [region])
page.add_TextRegion(textregion) pcgts.Page.add_TextRegion(textregion)
for region in marginals_left: for region in marginals_left:
textregion = TextRegionType( textregion = TextRegionType(
@ -162,7 +158,7 @@ class EynollahXmlWriter:
orientation=-region.skew orientation=-region.skew
) )
self.serialize_lines_in_region(textregion, offset, counter, region.lines) self.serialize_lines_in_region(textregion, offset, counter, region.lines)
page.add_TextRegion(textregion) pcgts.Page.add_TextRegion(textregion)
for region in marginals_right: for region in marginals_right:
textregion = TextRegionType( textregion = TextRegionType(
@ -172,17 +168,17 @@ class EynollahXmlWriter:
orientation=-region.skew orientation=-region.skew
) )
self.serialize_lines_in_region(textregion, offset, counter, region.lines) self.serialize_lines_in_region(textregion, offset, counter, region.lines)
page.add_TextRegion(textregion) pcgts.Page.add_TextRegion(textregion)
for region in images: for region in images:
image = ImageRegionType( image = ImageRegionType(
id=counter.next_region_id, id=counter.next_region_id,
Coords=CoordsType(points=self.calculate_points(region.contour, offset, 2), Coords=CoordsType(points=self.calculate_points(region.contour, offset, 2),
conf=region.conf)) conf=region.conf))
page.add_ImageRegion(image) pcgts.Page.add_ImageRegion(image)
for region in seplines: for region in seplines:
page.add_SeparatorRegion( pcgts.Page.add_SeparatorRegion(
SeparatorRegionType( SeparatorRegionType(
id=counter.next_region_id, id=counter.next_region_id,
Coords=CoordsType(points=self.calculate_points(region.contour, offset, 2), Coords=CoordsType(points=self.calculate_points(region.contour, offset, 2),
@ -193,7 +189,7 @@ class EynollahXmlWriter:
id=counter.next_region_id, id=counter.next_region_id,
Coords=CoordsType(points=self.calculate_points(region.contour, offset, 6), Coords=CoordsType(points=self.calculate_points(region.contour, offset, 6),
conf=region.conf)) conf=region.conf))
page.add_TableRegion(table) pcgts.Page.add_TableRegion(table)
return pcgts return pcgts