vkit.engine.seal_impression.text_line_slot_filler
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, Optional 15import logging 16 17import numpy as np 18import attrs 19 20from vkit.element import Point, Box, Polygon, ScoreMap 21from vkit.engine.font import TextLine 22from vkit.mechanism.distortion import rotate 23from .type import SealImpression 24 25logger = logging.getLogger(__name__) 26 27 28def fill_text_line_to_seal_impression( 29 seal_impression: SealImpression, 30 text_line_slot_indices: Sequence[int], 31 text_lines: Sequence[TextLine], 32 internal_text_line: Optional[TextLine], 33): 34 score_map = ScoreMap.from_shape(seal_impression.shape) 35 char_polygons: List[Polygon] = [] 36 37 assert len(text_line_slot_indices) == len(text_lines) 38 39 for text_line_slot_idx, text_line in zip(text_line_slot_indices, text_lines): 40 if text_line_slot_idx >= len(seal_impression.text_line_slots): 41 logger.error('fill_text_line_to_seal_impression: something wrong.') 42 break 43 44 assert text_line.is_hori 45 assert not text_line.shifted 46 47 # Get the text line slot to be filled. 48 text_line_slot = seal_impression.text_line_slots[text_line_slot_idx] 49 50 # Get the reference char height of text line, for adjusting aspect ratio. 51 text_line_ref_char_height = 0 52 text_line_ref_char_width = 0 53 for char_glyph in text_line.char_glyphs: 54 if char_glyph.ref_char_height > text_line_ref_char_height: 55 text_line_ref_char_height = char_glyph.ref_char_height 56 text_line_ref_char_width = char_glyph.ref_char_width 57 assert text_line_ref_char_height > 0 and text_line_ref_char_width > 0 58 text_line_aspect_ratio = text_line_ref_char_width / text_line_ref_char_height 59 60 # For resizing. 61 resized_char_width_factor = text_line_slot.char_aspect_ratio / text_line_aspect_ratio 62 63 # Fill each char to the char slot. 64 for char_slot_idx, (char_box, char_glyph) \ 65 in enumerate(zip(text_line.char_boxes, text_line.char_glyphs)): 66 # Get the char slot to be filled. 67 if char_slot_idx >= len(text_line_slot.char_slots): 68 logger.error('fill_text_line_to_seal_impression: something wrong.') 69 break 70 71 char_slot = text_line_slot.char_slots[char_slot_idx] 72 73 # Convert char glyph to score map and adjust aspect ratio. 74 # NOTE: Only the width of char could be resized. 75 resized_width = max(1, round(resized_char_width_factor * char_glyph.width)) 76 resized_box = attrs.evolve(char_box.box, left=0, right=resized_width - 1) 77 # NOTE: Since the height of char is fixed, we simply preserve the text line height. 78 char_score_map = ScoreMap.from_shape((text_line.box.height, resized_width)) 79 80 if char_glyph.score_map: 81 char_glyph_score_map = char_glyph.score_map 82 if char_glyph_score_map.shape != resized_box.shape: 83 char_glyph_score_map = char_glyph_score_map.to_resized_score_map( 84 resized_height=resized_box.height, 85 resized_width=resized_box.width, 86 cv_resize_interpolation=text_line.cv_resize_interpolation, 87 ) 88 resized_box.fill_score_map(char_score_map, char_glyph_score_map) 89 90 else: 91 # LCD, fallback to mask. 92 char_glyph_mask = char_glyph.get_glyph_mask( 93 box=char_box.box, 94 cv_resize_interpolation=text_line.cv_resize_interpolation, 95 ) 96 if char_glyph_mask.shape != resized_box.shape: 97 char_glyph_mask = char_glyph_mask.to_resized_mask( 98 resized_height=resized_box.height, 99 resized_width=resized_box.width, 100 cv_resize_interpolation=text_line.cv_resize_interpolation, 101 ) 102 resized_box.fill_score_map(char_score_map, char_glyph_mask.mat.astype(np.float32)) 103 104 # To match char_slot.point_up. 105 point_up = Point.create(y=0, x=char_score_map.width / 2) 106 107 # Generate char polygon. 108 up = resized_box.up 109 down = resized_box.down 110 ref_char_height = char_glyph.ref_char_height 111 if resized_box.height < ref_char_height: 112 inc = ref_char_height - resized_box.height 113 half_inc = inc / 2 114 up = up - half_inc 115 down = down + half_inc 116 117 left = resized_box.left 118 right = resized_box.right 119 # NOTE: Resize factor is applied. 120 ref_char_width = resized_char_width_factor * char_glyph.ref_char_width 121 if resized_box.width < ref_char_width: 122 inc = ref_char_width - resized_box.width 123 half_inc = inc / 2 124 left = left - half_inc 125 right = right + half_inc 126 127 char_polygon = Polygon.from_xy_pairs([ 128 (left, up), 129 (right, up), 130 (right, down), 131 (left, down), 132 ]) 133 134 # Rotate. 135 rotated_result = rotate.distort( 136 # Horizontal text line has angle 270. 137 {'angle': char_slot.angle - 270}, 138 score_map=char_score_map, 139 point=point_up, 140 polygon=char_polygon, 141 # The char polygon could be out-of-bound and should not be clipped. 142 disable_clip_result_elements=True, 143 ) 144 rotated_char_score_map = rotated_result.score_map 145 assert rotated_char_score_map 146 rotated_point_up = rotated_result.point 147 assert rotated_point_up 148 rotated_char_polygon = rotated_result.polygon 149 assert rotated_char_polygon 150 151 # Calculate the bounding box based on point_up. 152 # NOTE: rotated_point_up.y represents the vertical offset here. 153 dst_up = char_slot.point_up.y - rotated_point_up.y 154 dst_down = dst_up + rotated_char_score_map.height - 1 155 # NOTE: rotated_point_up.x represents the horizontal offset here. 156 dst_left = char_slot.point_up.x - rotated_point_up.x 157 dst_right = dst_left + rotated_char_score_map.width - 1 158 159 if dst_up < 0 \ 160 or dst_down >= score_map.height \ 161 or dst_left < 0 \ 162 or dst_right >= score_map.width: 163 logger.error('fill_text_line_to_seal_impression: out-of-bound.') 164 continue 165 166 # Fill. 167 dst_box = Box(up=dst_up, down=dst_down, left=dst_left, right=dst_right) 168 dst_box.fill_score_map( 169 score_map, 170 rotated_char_score_map, 171 keep_max_value=True, 172 ) 173 174 # Shift and keep rotated char polygon. 175 rotated_char_polygon = rotated_char_polygon.to_shifted_polygon( 176 offset_y=dst_up, 177 offset_x=dst_left, 178 ) 179 char_polygons.append(rotated_char_polygon) 180 181 if internal_text_line: 182 internal_text_line_box = seal_impression.internal_text_line_box 183 assert internal_text_line_box 184 185 internal_text_line = internal_text_line.to_shifted_text_line( 186 offset_y=internal_text_line_box.up, 187 offset_x=internal_text_line_box.left, 188 ) 189 if internal_text_line.score_map: 190 internal_text_line.box.fill_score_map(score_map, internal_text_line.score_map) 191 else: 192 internal_text_line.box.fill_score_map(score_map, internal_text_line.mask.mat) 193 194 char_polygons.extend( 195 internal_text_line.to_char_polygons( 196 page_height=score_map.height, 197 page_width=score_map.width, 198 ) 199 ) 200 201 # Adjust alpha. 202 score_map_max = score_map.mat.max() 203 score_map.assign_mat(score_map.mat * seal_impression.alpha / score_map_max) 204 205 return score_map, char_polygons
def
fill_text_line_to_seal_impression( seal_impression: vkit.engine.seal_impression.type.SealImpression, text_line_slot_indices: Sequence[int], text_lines: Sequence[vkit.engine.font.type.TextLine], internal_text_line: Union[vkit.engine.font.type.TextLine, NoneType]):
29def fill_text_line_to_seal_impression( 30 seal_impression: SealImpression, 31 text_line_slot_indices: Sequence[int], 32 text_lines: Sequence[TextLine], 33 internal_text_line: Optional[TextLine], 34): 35 score_map = ScoreMap.from_shape(seal_impression.shape) 36 char_polygons: List[Polygon] = [] 37 38 assert len(text_line_slot_indices) == len(text_lines) 39 40 for text_line_slot_idx, text_line in zip(text_line_slot_indices, text_lines): 41 if text_line_slot_idx >= len(seal_impression.text_line_slots): 42 logger.error('fill_text_line_to_seal_impression: something wrong.') 43 break 44 45 assert text_line.is_hori 46 assert not text_line.shifted 47 48 # Get the text line slot to be filled. 49 text_line_slot = seal_impression.text_line_slots[text_line_slot_idx] 50 51 # Get the reference char height of text line, for adjusting aspect ratio. 52 text_line_ref_char_height = 0 53 text_line_ref_char_width = 0 54 for char_glyph in text_line.char_glyphs: 55 if char_glyph.ref_char_height > text_line_ref_char_height: 56 text_line_ref_char_height = char_glyph.ref_char_height 57 text_line_ref_char_width = char_glyph.ref_char_width 58 assert text_line_ref_char_height > 0 and text_line_ref_char_width > 0 59 text_line_aspect_ratio = text_line_ref_char_width / text_line_ref_char_height 60 61 # For resizing. 62 resized_char_width_factor = text_line_slot.char_aspect_ratio / text_line_aspect_ratio 63 64 # Fill each char to the char slot. 65 for char_slot_idx, (char_box, char_glyph) \ 66 in enumerate(zip(text_line.char_boxes, text_line.char_glyphs)): 67 # Get the char slot to be filled. 68 if char_slot_idx >= len(text_line_slot.char_slots): 69 logger.error('fill_text_line_to_seal_impression: something wrong.') 70 break 71 72 char_slot = text_line_slot.char_slots[char_slot_idx] 73 74 # Convert char glyph to score map and adjust aspect ratio. 75 # NOTE: Only the width of char could be resized. 76 resized_width = max(1, round(resized_char_width_factor * char_glyph.width)) 77 resized_box = attrs.evolve(char_box.box, left=0, right=resized_width - 1) 78 # NOTE: Since the height of char is fixed, we simply preserve the text line height. 79 char_score_map = ScoreMap.from_shape((text_line.box.height, resized_width)) 80 81 if char_glyph.score_map: 82 char_glyph_score_map = char_glyph.score_map 83 if char_glyph_score_map.shape != resized_box.shape: 84 char_glyph_score_map = char_glyph_score_map.to_resized_score_map( 85 resized_height=resized_box.height, 86 resized_width=resized_box.width, 87 cv_resize_interpolation=text_line.cv_resize_interpolation, 88 ) 89 resized_box.fill_score_map(char_score_map, char_glyph_score_map) 90 91 else: 92 # LCD, fallback to mask. 93 char_glyph_mask = char_glyph.get_glyph_mask( 94 box=char_box.box, 95 cv_resize_interpolation=text_line.cv_resize_interpolation, 96 ) 97 if char_glyph_mask.shape != resized_box.shape: 98 char_glyph_mask = char_glyph_mask.to_resized_mask( 99 resized_height=resized_box.height, 100 resized_width=resized_box.width, 101 cv_resize_interpolation=text_line.cv_resize_interpolation, 102 ) 103 resized_box.fill_score_map(char_score_map, char_glyph_mask.mat.astype(np.float32)) 104 105 # To match char_slot.point_up. 106 point_up = Point.create(y=0, x=char_score_map.width / 2) 107 108 # Generate char polygon. 109 up = resized_box.up 110 down = resized_box.down 111 ref_char_height = char_glyph.ref_char_height 112 if resized_box.height < ref_char_height: 113 inc = ref_char_height - resized_box.height 114 half_inc = inc / 2 115 up = up - half_inc 116 down = down + half_inc 117 118 left = resized_box.left 119 right = resized_box.right 120 # NOTE: Resize factor is applied. 121 ref_char_width = resized_char_width_factor * char_glyph.ref_char_width 122 if resized_box.width < ref_char_width: 123 inc = ref_char_width - resized_box.width 124 half_inc = inc / 2 125 left = left - half_inc 126 right = right + half_inc 127 128 char_polygon = Polygon.from_xy_pairs([ 129 (left, up), 130 (right, up), 131 (right, down), 132 (left, down), 133 ]) 134 135 # Rotate. 136 rotated_result = rotate.distort( 137 # Horizontal text line has angle 270. 138 {'angle': char_slot.angle - 270}, 139 score_map=char_score_map, 140 point=point_up, 141 polygon=char_polygon, 142 # The char polygon could be out-of-bound and should not be clipped. 143 disable_clip_result_elements=True, 144 ) 145 rotated_char_score_map = rotated_result.score_map 146 assert rotated_char_score_map 147 rotated_point_up = rotated_result.point 148 assert rotated_point_up 149 rotated_char_polygon = rotated_result.polygon 150 assert rotated_char_polygon 151 152 # Calculate the bounding box based on point_up. 153 # NOTE: rotated_point_up.y represents the vertical offset here. 154 dst_up = char_slot.point_up.y - rotated_point_up.y 155 dst_down = dst_up + rotated_char_score_map.height - 1 156 # NOTE: rotated_point_up.x represents the horizontal offset here. 157 dst_left = char_slot.point_up.x - rotated_point_up.x 158 dst_right = dst_left + rotated_char_score_map.width - 1 159 160 if dst_up < 0 \ 161 or dst_down >= score_map.height \ 162 or dst_left < 0 \ 163 or dst_right >= score_map.width: 164 logger.error('fill_text_line_to_seal_impression: out-of-bound.') 165 continue 166 167 # Fill. 168 dst_box = Box(up=dst_up, down=dst_down, left=dst_left, right=dst_right) 169 dst_box.fill_score_map( 170 score_map, 171 rotated_char_score_map, 172 keep_max_value=True, 173 ) 174 175 # Shift and keep rotated char polygon. 176 rotated_char_polygon = rotated_char_polygon.to_shifted_polygon( 177 offset_y=dst_up, 178 offset_x=dst_left, 179 ) 180 char_polygons.append(rotated_char_polygon) 181 182 if internal_text_line: 183 internal_text_line_box = seal_impression.internal_text_line_box 184 assert internal_text_line_box 185 186 internal_text_line = internal_text_line.to_shifted_text_line( 187 offset_y=internal_text_line_box.up, 188 offset_x=internal_text_line_box.left, 189 ) 190 if internal_text_line.score_map: 191 internal_text_line.box.fill_score_map(score_map, internal_text_line.score_map) 192 else: 193 internal_text_line.box.fill_score_map(score_map, internal_text_line.mask.mat) 194 195 char_polygons.extend( 196 internal_text_line.to_char_polygons( 197 page_height=score_map.height, 198 page_width=score_map.width, 199 ) 200 ) 201 202 # Adjust alpha. 203 score_map_max = score_map.mat.max() 204 score_map.assign_mat(score_map.mat * seal_impression.alpha / score_map_max) 205 206 return score_map, char_polygons