vkit.pipeline.text_detection.page_assembler

  1# Copyright 2022 vkit-x Administrator. All Rights Reserved.
  2#
  3# This project (vkit-x/vkit) is dual-licensed under commercial and SSPL licenses.
  4#
  5# The commercial license gives you the full rights to create and distribute software
  6# on your own terms without any SSPL license obligations. For more information,
  7# please see the "LICENSE_COMMERCIAL.txt" file.
  8#
  9# This project is also available under Server Side Public License (SSPL).
 10# The SSPL licensing is ideal for use cases such as open source projects with
 11# SSPL distribution, student/academic purposes, hobby projects, internal research
 12# projects without external distribution, or other projects where all SSPL
 13# obligations can be met. For more information, please see the "LICENSE_SSPL.txt" file.
 14from typing import Sequence, List
 15
 16import attrs
 17from numpy.random import Generator as RandomGenerator
 18
 19from vkit.element import Shapable, Box, Polygon, Image
 20from vkit.engine.seal_impression import fill_text_line_to_seal_impression
 21from vkit.mechanism.distortion import rotate
 22from ..interface import PipelineStep, PipelineStepFactory
 23from .page_layout import (
 24    PageLayoutStepOutput,
 25    DisconnectedTextRegion,
 26    NonTextRegion,
 27)
 28from .page_background import PageBackgroundStepOutput
 29from .page_image import PageImageStepOutput, PageImageCollection
 30from .page_barcode import PageBarcodeStepOutput
 31from .page_text_line import (
 32    PageTextLineStepOutput,
 33    PageTextLineCollection,
 34    PageSealImpressionTextLineCollection,
 35)
 36from .page_non_text_symbol import PageNonTextSymbolStepOutput
 37from .page_text_line_label import (
 38    PageTextLineLabelStepOutput,
 39    PageCharPolygonCollection,
 40    PageTextLinePolygonCollection,
 41)
 42from .page_text_line_bounding_box import PageTextLineBoundingBoxStepOutput
 43
 44
 45@attrs.define
 46class PageAssemblerStepConfig:
 47    pass
 48
 49
 50@attrs.define
 51class PageAssemblerStepInput:
 52    page_layout_step_output: PageLayoutStepOutput
 53    page_background_step_output: PageBackgroundStepOutput
 54    page_image_step_output: PageImageStepOutput
 55    page_barcode_step_output: PageBarcodeStepOutput
 56    page_text_line_step_output: PageTextLineStepOutput
 57    page_non_text_symbol_step_output: PageNonTextSymbolStepOutput
 58    page_text_line_bounding_box_step_output: PageTextLineBoundingBoxStepOutput
 59    page_text_line_label_step_output: PageTextLineLabelStepOutput
 60
 61
 62@attrs.define
 63class PageDisconnectedTextRegionCollection:
 64    disconnected_text_regions: Sequence[DisconnectedTextRegion]
 65
 66    def to_polygons(self):
 67        for disconnected_text_region in self.disconnected_text_regions:
 68            yield disconnected_text_region.polygon
 69
 70
 71@attrs.define
 72class PageNonTextRegionCollection:
 73    non_text_regions: Sequence[NonTextRegion]
 74
 75    def to_polygons(self):
 76        for non_text_region in self.non_text_regions:
 77            yield non_text_region.polygon
 78
 79
 80@attrs.define
 81class PageSealImpressionCharPolygonCollection:
 82    char_polygons: Sequence[Polygon]
 83
 84
 85@attrs.define
 86class Page(Shapable):
 87    image: Image
 88    page_image_collection: PageImageCollection
 89    page_bottom_layer_image: Image
 90    page_text_line_collection: PageTextLineCollection
 91    page_seal_impression_text_line_collection: PageSealImpressionTextLineCollection
 92    page_char_polygon_collection: PageCharPolygonCollection
 93    page_text_line_polygon_collection: PageTextLinePolygonCollection
 94    page_disconnected_text_region_collection: PageDisconnectedTextRegionCollection
 95    page_non_text_region_collection: PageNonTextRegionCollection
 96    page_seal_impression_char_polygon_collection: PageSealImpressionCharPolygonCollection
 97
 98    @property
 99    def height(self):
100        return self.image.height
101
102    @property
103    def width(self):
104        return self.image.width
105
106
107@attrs.define
108class PageAssemblerStepOutput:
109    page: Page
110
111
112class PageAssemblerStep(
113    PipelineStep[
114        PageAssemblerStepConfig,
115        PageAssemblerStepInput,
116        PageAssemblerStepOutput,
117    ]
118):  # yapf: disable
119
120    def run(self, input: PageAssemblerStepInput, rng: RandomGenerator):
121        page_layout_step_output = input.page_layout_step_output
122        page_layout = page_layout_step_output.page_layout
123
124        page_background_step_output = input.page_background_step_output
125        background_image = page_background_step_output.background_image
126
127        page_image_step_output = input.page_image_step_output
128        page_image_collection = page_image_step_output.page_image_collection
129        page_bottom_layer_image = page_image_step_output.page_bottom_layer_image
130
131        page_barcode_step_output = input.page_barcode_step_output
132
133        page_text_line_step_output = input.page_text_line_step_output
134        page_text_line_collection = page_text_line_step_output.page_text_line_collection
135        page_seal_impression_text_line_collection = \
136            page_text_line_step_output.page_seal_impression_text_line_collection
137
138        page_non_text_symbol_step_output = input.page_non_text_symbol_step_output
139
140        page_text_line_bounding_box_step_output = input.page_text_line_bounding_box_step_output
141        text_line_bounding_box_score_maps = page_text_line_bounding_box_step_output.score_maps
142        text_line_bounding_box_colors = page_text_line_bounding_box_step_output.colors
143
144        page_text_line_label_step_output = input.page_text_line_label_step_output
145        page_char_polygon_collection = \
146            page_text_line_label_step_output.page_char_polygon_collection
147        page_text_line_polygon_collection = \
148            page_text_line_label_step_output.page_text_line_polygon_collection
149
150        # Page background.
151        assert background_image.mat.shape == (page_layout.height, page_layout.width, 3)
152        assembled_image = background_image.copy()
153
154        # Page images.
155        for page_image in page_image_collection.page_images:
156            page_image.box.fill_image(
157                assembled_image,
158                page_image.image,
159                alpha=page_image.alpha,
160            )
161
162        # Page barcodes.
163        for barcode_qr_score_map in page_barcode_step_output.barcode_qr_score_maps:
164            assembled_image[barcode_qr_score_map] = (0, 0, 0)
165        for barcode_code39_score_map in page_barcode_step_output.barcode_code39_score_maps:
166            assembled_image[barcode_code39_score_map] = (0, 0, 0)
167
168        # Page text line bounding boxes.
169        for text_line_bounding_box_score_map, text_line_bounding_box_color in zip(
170            text_line_bounding_box_score_maps, text_line_bounding_box_colors
171        ):
172            assembled_image[text_line_bounding_box_score_map] = text_line_bounding_box_color
173
174        # Page text lines.
175        for text_line in page_text_line_collection.text_lines:
176            if text_line.score_map:
177                text_line.score_map.fill_image(assembled_image, text_line.glyph_color)
178            else:
179                text_line.mask.fill_image(assembled_image, text_line.image)
180
181        # Page non-text symbols.
182        for image, box, alpha in zip(
183            page_non_text_symbol_step_output.images,
184            page_non_text_symbol_step_output.boxes,
185            page_non_text_symbol_step_output.alphas,
186        ):
187            box.fill_image(assembled_image, value=image, alpha=alpha)
188
189        # Page seal impressions.
190        page_seal_impression_char_polygons: List[Polygon] = []
191        for seal_impression, seal_impression_resource in zip(
192            page_seal_impression_text_line_collection.seal_impressions,
193            page_seal_impression_text_line_collection.seal_impression_resources,
194        ):
195            alpha = seal_impression.alpha
196            color = seal_impression.color
197
198            # Prepare foreground (text) and background.
199            background_mask = seal_impression.background_mask
200            text_line_filled_score_map, char_polygons = fill_text_line_to_seal_impression(
201                seal_impression,
202                seal_impression_resource.text_line_slot_indices,
203                seal_impression_resource.text_lines,
204                seal_impression_resource.internal_text_line,
205            )
206
207            # Rotate, shift, and trim.
208            rotated_result = rotate.distort(
209                {'angle': seal_impression_resource.angle},
210                mask=background_mask,
211                score_map=text_line_filled_score_map,
212                polygons=char_polygons,
213            )
214            assert rotated_result.mask
215            background_mask = rotated_result.mask
216            assert rotated_result.score_map
217            text_line_filled_score_map = rotated_result.score_map
218            assert background_mask.shape == text_line_filled_score_map.shape
219            assert rotated_result.polygons
220
221            # Placement box.
222            box_center_point = seal_impression_resource.box.get_center_point()
223            up = box_center_point.y - background_mask.height // 2
224            down = up + background_mask.height - 1
225            left = box_center_point.x - background_mask.width // 2
226            right = left + background_mask.width - 1
227
228            if up < 0 or down >= assembled_image.height \
229                    or left < 0 or right >= assembled_image.width:
230                # Simply ignore this seal impression.
231                continue
232
233            # Rendering.
234            box = Box(up=up, down=down, left=left, right=right)
235            box.fill_image(assembled_image, value=color, image_mask=background_mask, alpha=alpha)
236            box.fill_image(assembled_image, value=color, alpha=text_line_filled_score_map)
237
238            # Shift and keep char polyghons.
239            char_polygons = [
240                char_polygon.to_shifted_polygon(
241                    offset_y=up,
242                    offset_x=left,
243                ) for char_polygon in rotated_result.polygons
244            ]
245            page_seal_impression_char_polygons.extend(char_polygons)
246
247        # For char-level polygon regression.
248        page_disconnected_text_region_collection = PageDisconnectedTextRegionCollection(
249            page_layout.disconnected_text_regions
250        )
251
252        # For sampling negative text region area.
253        page_non_text_region_collection = PageNonTextRegionCollection(page_layout.non_text_regions)
254
255        # For generating char-level seal impression labeling.
256        page_seal_impression_char_polygon_collection = PageSealImpressionCharPolygonCollection(
257            char_polygons=page_seal_impression_char_polygons,
258        )
259
260        page = Page(
261            image=assembled_image,
262            page_image_collection=page_image_collection,
263            page_bottom_layer_image=page_bottom_layer_image,
264            page_text_line_collection=page_text_line_collection,
265            page_seal_impression_text_line_collection=page_seal_impression_text_line_collection,
266            page_char_polygon_collection=page_char_polygon_collection,
267            page_text_line_polygon_collection=page_text_line_polygon_collection,
268            page_disconnected_text_region_collection=page_disconnected_text_region_collection,
269            page_non_text_region_collection=page_non_text_region_collection,
270            page_seal_impression_char_polygon_collection=(
271                page_seal_impression_char_polygon_collection
272            ),
273        )
274        return PageAssemblerStepOutput(page=page)
275
276
277page_assembler_step_factory = PipelineStepFactory(PageAssemblerStep)
class PageAssemblerStepConfig:
47class PageAssemblerStepConfig:
48    pass
PageAssemblerStepConfig()
2def __init__(self, ):
3    pass

Method generated by attrs for class PageAssemblerStepConfig.

class PageAssemblerStepInput:
52class PageAssemblerStepInput:
53    page_layout_step_output: PageLayoutStepOutput
54    page_background_step_output: PageBackgroundStepOutput
55    page_image_step_output: PageImageStepOutput
56    page_barcode_step_output: PageBarcodeStepOutput
57    page_text_line_step_output: PageTextLineStepOutput
58    page_non_text_symbol_step_output: PageNonTextSymbolStepOutput
59    page_text_line_bounding_box_step_output: PageTextLineBoundingBoxStepOutput
60    page_text_line_label_step_output: PageTextLineLabelStepOutput
 2def __init__(self, page_layout_step_output, page_background_step_output, page_image_step_output, page_barcode_step_output, page_text_line_step_output, page_non_text_symbol_step_output, page_text_line_bounding_box_step_output, page_text_line_label_step_output):
 3    self.page_layout_step_output = page_layout_step_output
 4    self.page_background_step_output = page_background_step_output
 5    self.page_image_step_output = page_image_step_output
 6    self.page_barcode_step_output = page_barcode_step_output
 7    self.page_text_line_step_output = page_text_line_step_output
 8    self.page_non_text_symbol_step_output = page_non_text_symbol_step_output
 9    self.page_text_line_bounding_box_step_output = page_text_line_bounding_box_step_output
10    self.page_text_line_label_step_output = page_text_line_label_step_output

Method generated by attrs for class PageAssemblerStepInput.

class PageDisconnectedTextRegionCollection:
64class PageDisconnectedTextRegionCollection:
65    disconnected_text_regions: Sequence[DisconnectedTextRegion]
66
67    def to_polygons(self):
68        for disconnected_text_region in self.disconnected_text_regions:
69            yield disconnected_text_region.polygon
PageDisconnectedTextRegionCollection( disconnected_text_regions: Sequence[vkit.pipeline.text_detection.page_layout.DisconnectedTextRegion])
2def __init__(self, disconnected_text_regions):
3    self.disconnected_text_regions = disconnected_text_regions

Method generated by attrs for class PageDisconnectedTextRegionCollection.

def to_polygons(self):
67    def to_polygons(self):
68        for disconnected_text_region in self.disconnected_text_regions:
69            yield disconnected_text_region.polygon
class PageNonTextRegionCollection:
73class PageNonTextRegionCollection:
74    non_text_regions: Sequence[NonTextRegion]
75
76    def to_polygons(self):
77        for non_text_region in self.non_text_regions:
78            yield non_text_region.polygon
PageNonTextRegionCollection( non_text_regions: Sequence[vkit.pipeline.text_detection.page_layout.NonTextRegion])
2def __init__(self, non_text_regions):
3    self.non_text_regions = non_text_regions

Method generated by attrs for class PageNonTextRegionCollection.

def to_polygons(self):
76    def to_polygons(self):
77        for non_text_region in self.non_text_regions:
78            yield non_text_region.polygon
class PageSealImpressionCharPolygonCollection:
82class PageSealImpressionCharPolygonCollection:
83    char_polygons: Sequence[Polygon]
PageSealImpressionCharPolygonCollection(char_polygons: Sequence[vkit.element.polygon.Polygon])
2def __init__(self, char_polygons):
3    self.char_polygons = char_polygons

Method generated by attrs for class PageSealImpressionCharPolygonCollection.

class Page(vkit.element.type.Shapable):
 87class Page(Shapable):
 88    image: Image
 89    page_image_collection: PageImageCollection
 90    page_bottom_layer_image: Image
 91    page_text_line_collection: PageTextLineCollection
 92    page_seal_impression_text_line_collection: PageSealImpressionTextLineCollection
 93    page_char_polygon_collection: PageCharPolygonCollection
 94    page_text_line_polygon_collection: PageTextLinePolygonCollection
 95    page_disconnected_text_region_collection: PageDisconnectedTextRegionCollection
 96    page_non_text_region_collection: PageNonTextRegionCollection
 97    page_seal_impression_char_polygon_collection: PageSealImpressionCharPolygonCollection
 98
 99    @property
100    def height(self):
101        return self.image.height
102
103    @property
104    def width(self):
105        return self.image.width
 2def __init__(self, image, page_image_collection, page_bottom_layer_image, page_text_line_collection, page_seal_impression_text_line_collection, page_char_polygon_collection, page_text_line_polygon_collection, page_disconnected_text_region_collection, page_non_text_region_collection, page_seal_impression_char_polygon_collection):
 3    self.image = image
 4    self.page_image_collection = page_image_collection
 5    self.page_bottom_layer_image = page_bottom_layer_image
 6    self.page_text_line_collection = page_text_line_collection
 7    self.page_seal_impression_text_line_collection = page_seal_impression_text_line_collection
 8    self.page_char_polygon_collection = page_char_polygon_collection
 9    self.page_text_line_polygon_collection = page_text_line_polygon_collection
10    self.page_disconnected_text_region_collection = page_disconnected_text_region_collection
11    self.page_non_text_region_collection = page_non_text_region_collection
12    self.page_seal_impression_char_polygon_collection = page_seal_impression_char_polygon_collection

Method generated by attrs for class Page.

class PageAssemblerStepOutput:
109class PageAssemblerStepOutput:
110    page: Page
PageAssemblerStepOutput(page: vkit.pipeline.text_detection.page_assembler.Page)
2def __init__(self, page):
3    self.page = page

Method generated by attrs for class PageAssemblerStepOutput.

113class PageAssemblerStep(
114    PipelineStep[
115        PageAssemblerStepConfig,
116        PageAssemblerStepInput,
117        PageAssemblerStepOutput,
118    ]
119):  # yapf: disable
120
121    def run(self, input: PageAssemblerStepInput, rng: RandomGenerator):
122        page_layout_step_output = input.page_layout_step_output
123        page_layout = page_layout_step_output.page_layout
124
125        page_background_step_output = input.page_background_step_output
126        background_image = page_background_step_output.background_image
127
128        page_image_step_output = input.page_image_step_output
129        page_image_collection = page_image_step_output.page_image_collection
130        page_bottom_layer_image = page_image_step_output.page_bottom_layer_image
131
132        page_barcode_step_output = input.page_barcode_step_output
133
134        page_text_line_step_output = input.page_text_line_step_output
135        page_text_line_collection = page_text_line_step_output.page_text_line_collection
136        page_seal_impression_text_line_collection = \
137            page_text_line_step_output.page_seal_impression_text_line_collection
138
139        page_non_text_symbol_step_output = input.page_non_text_symbol_step_output
140
141        page_text_line_bounding_box_step_output = input.page_text_line_bounding_box_step_output
142        text_line_bounding_box_score_maps = page_text_line_bounding_box_step_output.score_maps
143        text_line_bounding_box_colors = page_text_line_bounding_box_step_output.colors
144
145        page_text_line_label_step_output = input.page_text_line_label_step_output
146        page_char_polygon_collection = \
147            page_text_line_label_step_output.page_char_polygon_collection
148        page_text_line_polygon_collection = \
149            page_text_line_label_step_output.page_text_line_polygon_collection
150
151        # Page background.
152        assert background_image.mat.shape == (page_layout.height, page_layout.width, 3)
153        assembled_image = background_image.copy()
154
155        # Page images.
156        for page_image in page_image_collection.page_images:
157            page_image.box.fill_image(
158                assembled_image,
159                page_image.image,
160                alpha=page_image.alpha,
161            )
162
163        # Page barcodes.
164        for barcode_qr_score_map in page_barcode_step_output.barcode_qr_score_maps:
165            assembled_image[barcode_qr_score_map] = (0, 0, 0)
166        for barcode_code39_score_map in page_barcode_step_output.barcode_code39_score_maps:
167            assembled_image[barcode_code39_score_map] = (0, 0, 0)
168
169        # Page text line bounding boxes.
170        for text_line_bounding_box_score_map, text_line_bounding_box_color in zip(
171            text_line_bounding_box_score_maps, text_line_bounding_box_colors
172        ):
173            assembled_image[text_line_bounding_box_score_map] = text_line_bounding_box_color
174
175        # Page text lines.
176        for text_line in page_text_line_collection.text_lines:
177            if text_line.score_map:
178                text_line.score_map.fill_image(assembled_image, text_line.glyph_color)
179            else:
180                text_line.mask.fill_image(assembled_image, text_line.image)
181
182        # Page non-text symbols.
183        for image, box, alpha in zip(
184            page_non_text_symbol_step_output.images,
185            page_non_text_symbol_step_output.boxes,
186            page_non_text_symbol_step_output.alphas,
187        ):
188            box.fill_image(assembled_image, value=image, alpha=alpha)
189
190        # Page seal impressions.
191        page_seal_impression_char_polygons: List[Polygon] = []
192        for seal_impression, seal_impression_resource in zip(
193            page_seal_impression_text_line_collection.seal_impressions,
194            page_seal_impression_text_line_collection.seal_impression_resources,
195        ):
196            alpha = seal_impression.alpha
197            color = seal_impression.color
198
199            # Prepare foreground (text) and background.
200            background_mask = seal_impression.background_mask
201            text_line_filled_score_map, char_polygons = fill_text_line_to_seal_impression(
202                seal_impression,
203                seal_impression_resource.text_line_slot_indices,
204                seal_impression_resource.text_lines,
205                seal_impression_resource.internal_text_line,
206            )
207
208            # Rotate, shift, and trim.
209            rotated_result = rotate.distort(
210                {'angle': seal_impression_resource.angle},
211                mask=background_mask,
212                score_map=text_line_filled_score_map,
213                polygons=char_polygons,
214            )
215            assert rotated_result.mask
216            background_mask = rotated_result.mask
217            assert rotated_result.score_map
218            text_line_filled_score_map = rotated_result.score_map
219            assert background_mask.shape == text_line_filled_score_map.shape
220            assert rotated_result.polygons
221
222            # Placement box.
223            box_center_point = seal_impression_resource.box.get_center_point()
224            up = box_center_point.y - background_mask.height // 2
225            down = up + background_mask.height - 1
226            left = box_center_point.x - background_mask.width // 2
227            right = left + background_mask.width - 1
228
229            if up < 0 or down >= assembled_image.height \
230                    or left < 0 or right >= assembled_image.width:
231                # Simply ignore this seal impression.
232                continue
233
234            # Rendering.
235            box = Box(up=up, down=down, left=left, right=right)
236            box.fill_image(assembled_image, value=color, image_mask=background_mask, alpha=alpha)
237            box.fill_image(assembled_image, value=color, alpha=text_line_filled_score_map)
238
239            # Shift and keep char polyghons.
240            char_polygons = [
241                char_polygon.to_shifted_polygon(
242                    offset_y=up,
243                    offset_x=left,
244                ) for char_polygon in rotated_result.polygons
245            ]
246            page_seal_impression_char_polygons.extend(char_polygons)
247
248        # For char-level polygon regression.
249        page_disconnected_text_region_collection = PageDisconnectedTextRegionCollection(
250            page_layout.disconnected_text_regions
251        )
252
253        # For sampling negative text region area.
254        page_non_text_region_collection = PageNonTextRegionCollection(page_layout.non_text_regions)
255
256        # For generating char-level seal impression labeling.
257        page_seal_impression_char_polygon_collection = PageSealImpressionCharPolygonCollection(
258            char_polygons=page_seal_impression_char_polygons,
259        )
260
261        page = Page(
262            image=assembled_image,
263            page_image_collection=page_image_collection,
264            page_bottom_layer_image=page_bottom_layer_image,
265            page_text_line_collection=page_text_line_collection,
266            page_seal_impression_text_line_collection=page_seal_impression_text_line_collection,
267            page_char_polygon_collection=page_char_polygon_collection,
268            page_text_line_polygon_collection=page_text_line_polygon_collection,
269            page_disconnected_text_region_collection=page_disconnected_text_region_collection,
270            page_non_text_region_collection=page_non_text_region_collection,
271            page_seal_impression_char_polygon_collection=(
272                page_seal_impression_char_polygon_collection
273            ),
274        )
275        return PageAssemblerStepOutput(page=page)

Abstract base class for generic types.

A generic type is typically declared by inheriting from this class parameterized with one or more type variables. For example, a generic mapping type might be defined as::

class Mapping(Generic[KT, VT]): def __getitem__(self, key: KT) -> VT: ... # Etc.

This class can then be used as follows::

def lookup_name(mapping: Mapping[KT, VT], key: KT, default: VT) -> VT: try: return mapping[key] except KeyError: return default

def run( self, input: vkit.pipeline.text_detection.page_assembler.PageAssemblerStepInput, rng: numpy.random._generator.Generator):
121    def run(self, input: PageAssemblerStepInput, rng: RandomGenerator):
122        page_layout_step_output = input.page_layout_step_output
123        page_layout = page_layout_step_output.page_layout
124
125        page_background_step_output = input.page_background_step_output
126        background_image = page_background_step_output.background_image
127
128        page_image_step_output = input.page_image_step_output
129        page_image_collection = page_image_step_output.page_image_collection
130        page_bottom_layer_image = page_image_step_output.page_bottom_layer_image
131
132        page_barcode_step_output = input.page_barcode_step_output
133
134        page_text_line_step_output = input.page_text_line_step_output
135        page_text_line_collection = page_text_line_step_output.page_text_line_collection
136        page_seal_impression_text_line_collection = \
137            page_text_line_step_output.page_seal_impression_text_line_collection
138
139        page_non_text_symbol_step_output = input.page_non_text_symbol_step_output
140
141        page_text_line_bounding_box_step_output = input.page_text_line_bounding_box_step_output
142        text_line_bounding_box_score_maps = page_text_line_bounding_box_step_output.score_maps
143        text_line_bounding_box_colors = page_text_line_bounding_box_step_output.colors
144
145        page_text_line_label_step_output = input.page_text_line_label_step_output
146        page_char_polygon_collection = \
147            page_text_line_label_step_output.page_char_polygon_collection
148        page_text_line_polygon_collection = \
149            page_text_line_label_step_output.page_text_line_polygon_collection
150
151        # Page background.
152        assert background_image.mat.shape == (page_layout.height, page_layout.width, 3)
153        assembled_image = background_image.copy()
154
155        # Page images.
156        for page_image in page_image_collection.page_images:
157            page_image.box.fill_image(
158                assembled_image,
159                page_image.image,
160                alpha=page_image.alpha,
161            )
162
163        # Page barcodes.
164        for barcode_qr_score_map in page_barcode_step_output.barcode_qr_score_maps:
165            assembled_image[barcode_qr_score_map] = (0, 0, 0)
166        for barcode_code39_score_map in page_barcode_step_output.barcode_code39_score_maps:
167            assembled_image[barcode_code39_score_map] = (0, 0, 0)
168
169        # Page text line bounding boxes.
170        for text_line_bounding_box_score_map, text_line_bounding_box_color in zip(
171            text_line_bounding_box_score_maps, text_line_bounding_box_colors
172        ):
173            assembled_image[text_line_bounding_box_score_map] = text_line_bounding_box_color
174
175        # Page text lines.
176        for text_line in page_text_line_collection.text_lines:
177            if text_line.score_map:
178                text_line.score_map.fill_image(assembled_image, text_line.glyph_color)
179            else:
180                text_line.mask.fill_image(assembled_image, text_line.image)
181
182        # Page non-text symbols.
183        for image, box, alpha in zip(
184            page_non_text_symbol_step_output.images,
185            page_non_text_symbol_step_output.boxes,
186            page_non_text_symbol_step_output.alphas,
187        ):
188            box.fill_image(assembled_image, value=image, alpha=alpha)
189
190        # Page seal impressions.
191        page_seal_impression_char_polygons: List[Polygon] = []
192        for seal_impression, seal_impression_resource in zip(
193            page_seal_impression_text_line_collection.seal_impressions,
194            page_seal_impression_text_line_collection.seal_impression_resources,
195        ):
196            alpha = seal_impression.alpha
197            color = seal_impression.color
198
199            # Prepare foreground (text) and background.
200            background_mask = seal_impression.background_mask
201            text_line_filled_score_map, char_polygons = fill_text_line_to_seal_impression(
202                seal_impression,
203                seal_impression_resource.text_line_slot_indices,
204                seal_impression_resource.text_lines,
205                seal_impression_resource.internal_text_line,
206            )
207
208            # Rotate, shift, and trim.
209            rotated_result = rotate.distort(
210                {'angle': seal_impression_resource.angle},
211                mask=background_mask,
212                score_map=text_line_filled_score_map,
213                polygons=char_polygons,
214            )
215            assert rotated_result.mask
216            background_mask = rotated_result.mask
217            assert rotated_result.score_map
218            text_line_filled_score_map = rotated_result.score_map
219            assert background_mask.shape == text_line_filled_score_map.shape
220            assert rotated_result.polygons
221
222            # Placement box.
223            box_center_point = seal_impression_resource.box.get_center_point()
224            up = box_center_point.y - background_mask.height // 2
225            down = up + background_mask.height - 1
226            left = box_center_point.x - background_mask.width // 2
227            right = left + background_mask.width - 1
228
229            if up < 0 or down >= assembled_image.height \
230                    or left < 0 or right >= assembled_image.width:
231                # Simply ignore this seal impression.
232                continue
233
234            # Rendering.
235            box = Box(up=up, down=down, left=left, right=right)
236            box.fill_image(assembled_image, value=color, image_mask=background_mask, alpha=alpha)
237            box.fill_image(assembled_image, value=color, alpha=text_line_filled_score_map)
238
239            # Shift and keep char polyghons.
240            char_polygons = [
241                char_polygon.to_shifted_polygon(
242                    offset_y=up,
243                    offset_x=left,
244                ) for char_polygon in rotated_result.polygons
245            ]
246            page_seal_impression_char_polygons.extend(char_polygons)
247
248        # For char-level polygon regression.
249        page_disconnected_text_region_collection = PageDisconnectedTextRegionCollection(
250            page_layout.disconnected_text_regions
251        )
252
253        # For sampling negative text region area.
254        page_non_text_region_collection = PageNonTextRegionCollection(page_layout.non_text_regions)
255
256        # For generating char-level seal impression labeling.
257        page_seal_impression_char_polygon_collection = PageSealImpressionCharPolygonCollection(
258            char_polygons=page_seal_impression_char_polygons,
259        )
260
261        page = Page(
262            image=assembled_image,
263            page_image_collection=page_image_collection,
264            page_bottom_layer_image=page_bottom_layer_image,
265            page_text_line_collection=page_text_line_collection,
266            page_seal_impression_text_line_collection=page_seal_impression_text_line_collection,
267            page_char_polygon_collection=page_char_polygon_collection,
268            page_text_line_polygon_collection=page_text_line_polygon_collection,
269            page_disconnected_text_region_collection=page_disconnected_text_region_collection,
270            page_non_text_region_collection=page_non_text_region_collection,
271            page_seal_impression_char_polygon_collection=(
272                page_seal_impression_char_polygon_collection
273            ),
274        )
275        return PageAssemblerStepOutput(page=page)