From f1505f5ae620765c32b537c687811b97ef8b90a5 Mon Sep 17 00:00:00 2001 From: Robert Sachunsky Date: Sun, 19 Jul 2026 19:05:46 +0200 Subject: [PATCH] =?UTF-8?q?layout/writer/extract-images:=20also=20refactor?= =?UTF-8?q?=20page=20as=20dataclass=E2=80=A6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit use `Region` for page (Border) coordinates and skew angle (as for the other segment types), pass as mandatory arg to writer --- src/eynollah/extract_images.py | 10 +++--- src/eynollah/eynollah.py | 64 ++++++++++++---------------------- src/eynollah/writer.py | 38 +++++++++----------- 3 files changed, 45 insertions(+), 67 deletions(-) diff --git a/src/eynollah/extract_images.py b/src/eynollah/extract_images.py index 29ec6b4..9365e1a 100644 --- a/src/eynollah/extract_images.py +++ b/src/eynollah/extract_images.py @@ -231,15 +231,16 @@ class EynollahImageExtractor(Eynollah): # Image Extraction Mode self.logger.info("Step 2/5: Image Extraction Mode") 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( image['img_res'], num_col_classifier) 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) - page_coord = np.array(page_coord) - images_cont = [cont - page_coord[::2][::-1][np.newaxis, np.newaxis] + 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(images_cont, image_page, @@ -250,9 +251,8 @@ class EynollahImageExtractor(Eynollah): # can be empty if above page frame images = [image for image in images if image.area] pcgts = writer.build_pagexml( + page=page, num_col=num_col_classifier, - page_coord=page_coord, - page_contour=cont_page[0], images=images, ) writer.write_pagexml(pcgts) diff --git a/src/eynollah/eynollah.py b/src/eynollah/eynollah.py index 452a2fc..d6bd434 100644 --- a/src/eynollah/eynollah.py +++ b/src/eynollah/eynollah.py @@ -74,7 +74,7 @@ from .utils import ( is_image_filename, isNaN, crop_image_inside_box, - box2rect, + box2slice, find_num_col, otsu_copy_binary, seg_mask_label, @@ -806,14 +806,13 @@ class Eynollah: return segmentation, confidence def extract_page(self, image): - cropped_page = img = image['img_res'] + page_cropped = img = image['img_res'] h, w = img.shape[:2] - page_coord = [0, h, 0, w] - cont_page = [np.array([[[0, 0]], - [[w, 0]], - [[w, h]], - [[0, h]]])] - mask_page = np.ones((h, w), dtype=np.uint8) + page_cont = np.array([[[0, 0]], + [[w, 0]], + [[w, h]], + [[0, h]]]) + page_mask = np.ones((h, w), dtype=np.uint8) if not self.ignore_page_extraction: self.logger.debug("enter extract_page") #cv2.GaussianBlur(img, (5, 5), 0) @@ -821,26 +820,13 @@ class Eynollah: contours, _ = cv2.findContours(prediction, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) if len(contours): areas = np.array(list(map(cv2.contourArea, contours))) - cnt = contours[np.argmax(areas)] - cont_page = [cnt] - x, y, w, h = cv2.boundingRect(cnt) - #if x <= 30: - #w += x - #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) - + page_cont = contours[np.argmax(areas)] + box = (x, y, w, h) = cv2.boundingRect(page_cont) + page_cropped = img[box2slice(box)] + page_mask = np.zeros((h, w), dtype=np.uint8) + page_mask = cv2.fillPoly(page_mask, pts=[page_cont - [x, y]], color=1) 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): if not self.ignore_page_extraction: @@ -2075,7 +2061,8 @@ class Eynollah: self.logger.info(f"Enhancement complete ({time.time() - t0:.1f}s)") 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: 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) @@ -2102,7 +2089,7 @@ class Eynollah: textlines_cont, textlines_conf, textlines_cx, textlines_cy, textlines_w_h) textregions = [ - TextRegion(cont_page, lines=[ + TextRegion(page.contour, lines=[ Region(cont, conf=conf) for cont, conf in zip(textlines_cont, textlines_conf)]) ] @@ -2110,9 +2097,8 @@ class Eynollah: self.logger.info("Basic processing complete") pcgts = writer.build_pagexml( + page=page, num_col=num_col_classifier, - page_coord=page_coord, - page_contour=cont_page[0], order_of_texts=[0], textregions=textregions, ) @@ -2153,19 +2139,19 @@ class Eynollah: if (abs(slope_deskew) > 45 and ((text_regions_p == label_text).sum()) <= 0.3 * image_page.size): slope_deskew = 0 + page.skew = slope_deskew if self.plotter: self.plotter.save_deskewed_image(slope_deskew, image['img'], image['name']) t3 = time.time() 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) - page_coord = np.array(page_coord) - page_box = (slice(*page_coord[:2]), - slice(*page_coord[2:])) + page_box = cv2.boundingRect(page.contour) 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) for cont, conf in zip(seplines_cont, seplines_conf)] + page_box = box2slice(page_box) regions_without_separators = regions_without_separators[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 @@ -2183,10 +2169,8 @@ class Eynollah: self.logger.info("No columns detected - generating empty PAGE-XML") pcgts = writer.build_pagexml( + page=page, num_col=0, - page_coord=page_coord, - page_contour=cont_page[0], - page_skew=slope_deskew, ) if writer.pcgts is None: writer.write_pagexml(pcgts) @@ -2385,10 +2369,8 @@ class Eynollah: self.logger.info("Step 5/5: Output Generation") pcgts = writer.build_pagexml( + page=page, num_col=num_col_classifier, - page_coord=page_coord, - page_contour=cont_page[0], - page_skew=slope_deskew, order_of_texts=order_text, textregions=textregions, textregions_h=textregions_h, diff --git a/src/eynollah/writer.py b/src/eynollah/writer.py index 7c295f6..68f909e 100644 --- a/src/eynollah/writer.py +++ b/src/eynollah/writer.py @@ -5,6 +5,7 @@ import os.path import logging 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 @@ -85,10 +86,8 @@ class EynollahXmlWriter: def build_pagexml( self, *, + page: Region, num_col=1, - page_coord: Optional[List[int]] = None, - page_contour: Optional[np.ndarray] = None, - page_skew: float = 0., order_of_texts: List[int] = [], textregions: List[TextRegion] = [], textregions_h: List[TextRegion] = [], @@ -104,16 +103,13 @@ class EynollahXmlWriter: # 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_skew) - if page_contour is not None: - page.set_Border(BorderType(Coords=CoordsType(points=self.calculate_points(page_contour)))) - if page_coord is None: - offset = [0, 0] - else: - offset = [page_coord[2], page_coord[0]] + 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)) @@ -121,7 +117,7 @@ class EynollahXmlWriter: for _ in marginals_left] id_of_marginalia_right = [_counter_marginals.next_region_id 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: textregion = TextRegionType( @@ -131,7 +127,7 @@ class EynollahXmlWriter: orientation=-region.skew ) 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)) for region in textregions_h: @@ -142,7 +138,7 @@ class EynollahXmlWriter: orientation=-region.skew ) self.serialize_lines_in_region(textregion, offset, counter, region.lines) - page.add_TextRegion(textregion) + pcgts.Page.add_TextRegion(textregion) for region in drop_caps: textregion = TextRegionType( @@ -152,7 +148,7 @@ class EynollahXmlWriter: orientation=-region.skew ) self.serialize_lines_in_region(textregion, offset, counter, [region]) - page.add_TextRegion(textregion) + pcgts.Page.add_TextRegion(textregion) for region in marginals_left: textregion = TextRegionType( @@ -162,7 +158,7 @@ class EynollahXmlWriter: orientation=-region.skew ) self.serialize_lines_in_region(textregion, offset, counter, region.lines) - page.add_TextRegion(textregion) + pcgts.Page.add_TextRegion(textregion) for region in marginals_right: textregion = TextRegionType( @@ -172,17 +168,17 @@ class EynollahXmlWriter: orientation=-region.skew ) self.serialize_lines_in_region(textregion, offset, counter, region.lines) - page.add_TextRegion(textregion) + pcgts.Page.add_TextRegion(textregion) for region in images: image = ImageRegionType( id=counter.next_region_id, Coords=CoordsType(points=self.calculate_points(region.contour, offset, 2), conf=region.conf)) - page.add_ImageRegion(image) + pcgts.Page.add_ImageRegion(image) for region in seplines: - page.add_SeparatorRegion( + pcgts.Page.add_SeparatorRegion( SeparatorRegionType( id=counter.next_region_id, Coords=CoordsType(points=self.calculate_points(region.contour, offset, 2), @@ -193,7 +189,7 @@ class EynollahXmlWriter: id=counter.next_region_id, Coords=CoordsType(points=self.calculate_points(region.contour, offset, 6), conf=region.conf)) - page.add_TableRegion(table) + pcgts.Page.add_TableRegion(table) return pcgts