vkit.engine.char_heatmap.default
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 Optional 15 16import attrs 17from numpy.random import Generator as RandomGenerator 18import numpy as np 19import cv2 as cv 20 21from vkit.element import Mask, ScoreMap, ElementSetOperationMode 22from ..interface import ( 23 Engine, 24 EngineExecutorFactory, 25 NoneTypeEngineInitResource, 26) 27from .type import CharHeatmapEngineRunConfig, CharHeatmap 28 29 30def build_np_distance(radius: int): 31 # Make it symmetric. 32 side_length = radius * 2 + 1 33 34 # Build distances to the center point. 35 np_offset = np.abs(np.arange(side_length, dtype=np.float32) - radius) 36 np_vert_offset = np.repeat(np_offset[:, None], side_length, axis=1) 37 np_hori_offset = np.repeat(np_offset[None, :], side_length, axis=0) 38 np_distance = np.sqrt(np.square(np_vert_offset) + np.square(np_hori_offset)) 39 40 return np_distance 41 42 43@attrs.define 44class CharHeatmapDefaultEngineInitConfig: 45 # Adjust the std. The std gets smaller as the distance factor gets larger. 46 # The activated area shrinks to the center as the std gets smaller. 47 # https://colab.research.google.com/drive/1TQ1-BTisMYZHIRVVNpVwDFPviXYMhT7A 48 gaussian_map_distance_factor: float = 2.25 49 gaussian_map_char_radius: int = 25 50 gaussian_map_preserving_score_min: float = 0.9 51 weight_neutralized_score_map: float = 0.4 52 53 54@attrs.define 55class CharHeatmapDefaultDebug: 56 score_map_max: ScoreMap 57 score_map_min: ScoreMap 58 char_overlapped_mask: Mask 59 char_neutralized_score_map: ScoreMap 60 neutralized_mask: Mask 61 neutralized_score_map: ScoreMap 62 63 64class CharHeatmapDefaultEngine( 65 Engine[ 66 CharHeatmapDefaultEngineInitConfig, 67 NoneTypeEngineInitResource, 68 CharHeatmapEngineRunConfig, 69 CharHeatmap, 70 ] 71): # yapf: disable 72 73 @classmethod 74 def get_type_name(cls) -> str: 75 return 'default' 76 77 def generate_np_gaussian_map(self): 78 char_radius = self.init_config.gaussian_map_char_radius 79 np_distance = build_np_distance(char_radius) 80 side_length = np_distance.shape[0] 81 82 np_norm_distance = np_distance / char_radius 83 np_gaussian_map = np.exp( 84 -0.5 * np.square(self.init_config.gaussian_map_distance_factor * np_norm_distance) 85 ) 86 87 # For perspective transformation. 88 char_begin = 0 89 char_end = side_length - 1 90 np_char_src_points = np.asarray( 91 [ 92 (char_begin, char_begin), 93 (char_end, char_begin), 94 (char_end, char_end), 95 (char_begin, char_end), 96 ], 97 dtype=np.float32, 98 ) 99 100 return np_gaussian_map, np_char_src_points 101 102 def __init__( 103 self, 104 init_config: CharHeatmapDefaultEngineInitConfig, 105 init_resource: Optional[NoneTypeEngineInitResource] = None, 106 ): 107 super().__init__(init_config, init_resource) 108 109 self.np_gaussian_map, self.np_char_points = self.generate_np_gaussian_map() 110 111 def run( 112 self, 113 run_config: CharHeatmapEngineRunConfig, 114 rng: Optional[RandomGenerator] = None, 115 ) -> CharHeatmap: 116 height = run_config.height 117 width = run_config.width 118 char_polygons = run_config.char_polygons 119 120 shape = (height, width) 121 122 # Intermediate score maps. 123 score_map_max = ScoreMap.from_shape(shape) 124 score_map_min = ScoreMap.from_shape(shape, value=1.0) 125 126 for char_polygon in char_polygons: 127 # Get transformation matrix. 128 np_trans_mat = cv.getPerspectiveTransform( 129 self.np_char_points, 130 char_polygon.internals.np_self_relative_points, 131 cv.DECOMP_SVD, 132 ) 133 134 # Transform gaussian map. 135 char_bounding_box = char_polygon.bounding_box 136 137 np_gaussian_map = cv.warpPerspective( 138 self.np_gaussian_map, 139 np_trans_mat, 140 (char_bounding_box.width, char_bounding_box.height), 141 ) 142 score_map = ScoreMap(mat=np_gaussian_map, box=char_bounding_box) 143 144 # Fill score maps. 145 char_polygon.fill_score_map(score_map_max, score_map, keep_max_value=True) 146 char_polygon.fill_score_map(score_map_min, score_map, keep_min_value=True) 147 148 # Set the char overlapped area while preserving score gte threshold. 149 char_overlapped_mask = Mask.from_polygons( 150 shape, 151 char_polygons, 152 ElementSetOperationMode.INTERSECT, 153 ) 154 155 preserving_score_min = self.init_config.gaussian_map_preserving_score_min 156 preserving_mask = Mask(mat=(score_map_max.mat >= preserving_score_min).astype(np.uint8)) 157 158 neutralized_mask = Mask.from_masks( 159 shape, 160 [ 161 char_overlapped_mask, 162 preserving_mask.to_inverted_mask(), 163 ], 164 ElementSetOperationMode.INTERSECT, 165 ) 166 167 # Estimate the neutralized score. 168 np_delta: np.ndarray = score_map_max.mat - score_map_min.mat # type: ignore 169 np_delta = np.clip(np_delta, 0.0, 1.0) 170 char_neutralized_score_map = ScoreMap(mat=np_delta) 171 172 neutralized_score_map = score_map_max.copy() 173 neutralized_mask.fill_score_map(neutralized_score_map, char_neutralized_score_map) 174 175 weight = self.init_config.weight_neutralized_score_map 176 score_map = ScoreMap( 177 mat=((1 - weight) * score_map_max.mat + weight * neutralized_score_map.mat) 178 ) 179 180 debug = None 181 if run_config.enable_debug: 182 debug = CharHeatmapDefaultDebug( 183 score_map_max=score_map_max, 184 score_map_min=score_map_min, 185 char_overlapped_mask=char_overlapped_mask, 186 char_neutralized_score_map=char_neutralized_score_map, 187 neutralized_mask=neutralized_mask, 188 neutralized_score_map=neutralized_score_map, 189 ) 190 191 return CharHeatmap(score_map=score_map, debug=debug) 192 193 194char_heatmap_default_engine_executor_factory = EngineExecutorFactory(CharHeatmapDefaultEngine)
def
build_np_distance(radius: int):
31def build_np_distance(radius: int): 32 # Make it symmetric. 33 side_length = radius * 2 + 1 34 35 # Build distances to the center point. 36 np_offset = np.abs(np.arange(side_length, dtype=np.float32) - radius) 37 np_vert_offset = np.repeat(np_offset[:, None], side_length, axis=1) 38 np_hori_offset = np.repeat(np_offset[None, :], side_length, axis=0) 39 np_distance = np.sqrt(np.square(np_vert_offset) + np.square(np_hori_offset)) 40 41 return np_distance
class
CharHeatmapDefaultEngineInitConfig:
45class CharHeatmapDefaultEngineInitConfig: 46 # Adjust the std. The std gets smaller as the distance factor gets larger. 47 # The activated area shrinks to the center as the std gets smaller. 48 # https://colab.research.google.com/drive/1TQ1-BTisMYZHIRVVNpVwDFPviXYMhT7A 49 gaussian_map_distance_factor: float = 2.25 50 gaussian_map_char_radius: int = 25 51 gaussian_map_preserving_score_min: float = 0.9 52 weight_neutralized_score_map: float = 0.4
CharHeatmapDefaultEngineInitConfig( gaussian_map_distance_factor: float = 2.25, gaussian_map_char_radius: int = 25, gaussian_map_preserving_score_min: float = 0.9, weight_neutralized_score_map: float = 0.4)
2def __init__(self, gaussian_map_distance_factor=attr_dict['gaussian_map_distance_factor'].default, gaussian_map_char_radius=attr_dict['gaussian_map_char_radius'].default, gaussian_map_preserving_score_min=attr_dict['gaussian_map_preserving_score_min'].default, weight_neutralized_score_map=attr_dict['weight_neutralized_score_map'].default): 3 self.gaussian_map_distance_factor = gaussian_map_distance_factor 4 self.gaussian_map_char_radius = gaussian_map_char_radius 5 self.gaussian_map_preserving_score_min = gaussian_map_preserving_score_min 6 self.weight_neutralized_score_map = weight_neutralized_score_map
Method generated by attrs for class CharHeatmapDefaultEngineInitConfig.
class
CharHeatmapDefaultDebug:
56class CharHeatmapDefaultDebug: 57 score_map_max: ScoreMap 58 score_map_min: ScoreMap 59 char_overlapped_mask: Mask 60 char_neutralized_score_map: ScoreMap 61 neutralized_mask: Mask 62 neutralized_score_map: ScoreMap
CharHeatmapDefaultDebug( score_map_max: vkit.element.score_map.ScoreMap, score_map_min: vkit.element.score_map.ScoreMap, char_overlapped_mask: vkit.element.mask.Mask, char_neutralized_score_map: vkit.element.score_map.ScoreMap, neutralized_mask: vkit.element.mask.Mask, neutralized_score_map: vkit.element.score_map.ScoreMap)
2def __init__(self, score_map_max, score_map_min, char_overlapped_mask, char_neutralized_score_map, neutralized_mask, neutralized_score_map): 3 self.score_map_max = score_map_max 4 self.score_map_min = score_map_min 5 self.char_overlapped_mask = char_overlapped_mask 6 self.char_neutralized_score_map = char_neutralized_score_map 7 self.neutralized_mask = neutralized_mask 8 self.neutralized_score_map = neutralized_score_map
Method generated by attrs for class CharHeatmapDefaultDebug.
class
CharHeatmapDefaultEngine(vkit.engine.interface.Engine[vkit.engine.char_heatmap.default.CharHeatmapDefaultEngineInitConfig, vkit.engine.interface.NoneTypeEngineInitResource, vkit.engine.char_heatmap.type.CharHeatmapEngineRunConfig, vkit.engine.char_heatmap.type.CharHeatmap]):
65class CharHeatmapDefaultEngine( 66 Engine[ 67 CharHeatmapDefaultEngineInitConfig, 68 NoneTypeEngineInitResource, 69 CharHeatmapEngineRunConfig, 70 CharHeatmap, 71 ] 72): # yapf: disable 73 74 @classmethod 75 def get_type_name(cls) -> str: 76 return 'default' 77 78 def generate_np_gaussian_map(self): 79 char_radius = self.init_config.gaussian_map_char_radius 80 np_distance = build_np_distance(char_radius) 81 side_length = np_distance.shape[0] 82 83 np_norm_distance = np_distance / char_radius 84 np_gaussian_map = np.exp( 85 -0.5 * np.square(self.init_config.gaussian_map_distance_factor * np_norm_distance) 86 ) 87 88 # For perspective transformation. 89 char_begin = 0 90 char_end = side_length - 1 91 np_char_src_points = np.asarray( 92 [ 93 (char_begin, char_begin), 94 (char_end, char_begin), 95 (char_end, char_end), 96 (char_begin, char_end), 97 ], 98 dtype=np.float32, 99 ) 100 101 return np_gaussian_map, np_char_src_points 102 103 def __init__( 104 self, 105 init_config: CharHeatmapDefaultEngineInitConfig, 106 init_resource: Optional[NoneTypeEngineInitResource] = None, 107 ): 108 super().__init__(init_config, init_resource) 109 110 self.np_gaussian_map, self.np_char_points = self.generate_np_gaussian_map() 111 112 def run( 113 self, 114 run_config: CharHeatmapEngineRunConfig, 115 rng: Optional[RandomGenerator] = None, 116 ) -> CharHeatmap: 117 height = run_config.height 118 width = run_config.width 119 char_polygons = run_config.char_polygons 120 121 shape = (height, width) 122 123 # Intermediate score maps. 124 score_map_max = ScoreMap.from_shape(shape) 125 score_map_min = ScoreMap.from_shape(shape, value=1.0) 126 127 for char_polygon in char_polygons: 128 # Get transformation matrix. 129 np_trans_mat = cv.getPerspectiveTransform( 130 self.np_char_points, 131 char_polygon.internals.np_self_relative_points, 132 cv.DECOMP_SVD, 133 ) 134 135 # Transform gaussian map. 136 char_bounding_box = char_polygon.bounding_box 137 138 np_gaussian_map = cv.warpPerspective( 139 self.np_gaussian_map, 140 np_trans_mat, 141 (char_bounding_box.width, char_bounding_box.height), 142 ) 143 score_map = ScoreMap(mat=np_gaussian_map, box=char_bounding_box) 144 145 # Fill score maps. 146 char_polygon.fill_score_map(score_map_max, score_map, keep_max_value=True) 147 char_polygon.fill_score_map(score_map_min, score_map, keep_min_value=True) 148 149 # Set the char overlapped area while preserving score gte threshold. 150 char_overlapped_mask = Mask.from_polygons( 151 shape, 152 char_polygons, 153 ElementSetOperationMode.INTERSECT, 154 ) 155 156 preserving_score_min = self.init_config.gaussian_map_preserving_score_min 157 preserving_mask = Mask(mat=(score_map_max.mat >= preserving_score_min).astype(np.uint8)) 158 159 neutralized_mask = Mask.from_masks( 160 shape, 161 [ 162 char_overlapped_mask, 163 preserving_mask.to_inverted_mask(), 164 ], 165 ElementSetOperationMode.INTERSECT, 166 ) 167 168 # Estimate the neutralized score. 169 np_delta: np.ndarray = score_map_max.mat - score_map_min.mat # type: ignore 170 np_delta = np.clip(np_delta, 0.0, 1.0) 171 char_neutralized_score_map = ScoreMap(mat=np_delta) 172 173 neutralized_score_map = score_map_max.copy() 174 neutralized_mask.fill_score_map(neutralized_score_map, char_neutralized_score_map) 175 176 weight = self.init_config.weight_neutralized_score_map 177 score_map = ScoreMap( 178 mat=((1 - weight) * score_map_max.mat + weight * neutralized_score_map.mat) 179 ) 180 181 debug = None 182 if run_config.enable_debug: 183 debug = CharHeatmapDefaultDebug( 184 score_map_max=score_map_max, 185 score_map_min=score_map_min, 186 char_overlapped_mask=char_overlapped_mask, 187 char_neutralized_score_map=char_neutralized_score_map, 188 neutralized_mask=neutralized_mask, 189 neutralized_score_map=neutralized_score_map, 190 ) 191 192 return CharHeatmap(score_map=score_map, debug=debug)
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
CharHeatmapDefaultEngine( init_config: vkit.engine.char_heatmap.default.CharHeatmapDefaultEngineInitConfig, init_resource: Union[vkit.engine.interface.NoneTypeEngineInitResource, NoneType] = None)
def
generate_np_gaussian_map(self):
78 def generate_np_gaussian_map(self): 79 char_radius = self.init_config.gaussian_map_char_radius 80 np_distance = build_np_distance(char_radius) 81 side_length = np_distance.shape[0] 82 83 np_norm_distance = np_distance / char_radius 84 np_gaussian_map = np.exp( 85 -0.5 * np.square(self.init_config.gaussian_map_distance_factor * np_norm_distance) 86 ) 87 88 # For perspective transformation. 89 char_begin = 0 90 char_end = side_length - 1 91 np_char_src_points = np.asarray( 92 [ 93 (char_begin, char_begin), 94 (char_end, char_begin), 95 (char_end, char_end), 96 (char_begin, char_end), 97 ], 98 dtype=np.float32, 99 ) 100 101 return np_gaussian_map, np_char_src_points
def
run( self, run_config: vkit.engine.char_heatmap.type.CharHeatmapEngineRunConfig, rng: Union[numpy.random._generator.Generator, NoneType] = None) -> vkit.engine.char_heatmap.type.CharHeatmap:
112 def run( 113 self, 114 run_config: CharHeatmapEngineRunConfig, 115 rng: Optional[RandomGenerator] = None, 116 ) -> CharHeatmap: 117 height = run_config.height 118 width = run_config.width 119 char_polygons = run_config.char_polygons 120 121 shape = (height, width) 122 123 # Intermediate score maps. 124 score_map_max = ScoreMap.from_shape(shape) 125 score_map_min = ScoreMap.from_shape(shape, value=1.0) 126 127 for char_polygon in char_polygons: 128 # Get transformation matrix. 129 np_trans_mat = cv.getPerspectiveTransform( 130 self.np_char_points, 131 char_polygon.internals.np_self_relative_points, 132 cv.DECOMP_SVD, 133 ) 134 135 # Transform gaussian map. 136 char_bounding_box = char_polygon.bounding_box 137 138 np_gaussian_map = cv.warpPerspective( 139 self.np_gaussian_map, 140 np_trans_mat, 141 (char_bounding_box.width, char_bounding_box.height), 142 ) 143 score_map = ScoreMap(mat=np_gaussian_map, box=char_bounding_box) 144 145 # Fill score maps. 146 char_polygon.fill_score_map(score_map_max, score_map, keep_max_value=True) 147 char_polygon.fill_score_map(score_map_min, score_map, keep_min_value=True) 148 149 # Set the char overlapped area while preserving score gte threshold. 150 char_overlapped_mask = Mask.from_polygons( 151 shape, 152 char_polygons, 153 ElementSetOperationMode.INTERSECT, 154 ) 155 156 preserving_score_min = self.init_config.gaussian_map_preserving_score_min 157 preserving_mask = Mask(mat=(score_map_max.mat >= preserving_score_min).astype(np.uint8)) 158 159 neutralized_mask = Mask.from_masks( 160 shape, 161 [ 162 char_overlapped_mask, 163 preserving_mask.to_inverted_mask(), 164 ], 165 ElementSetOperationMode.INTERSECT, 166 ) 167 168 # Estimate the neutralized score. 169 np_delta: np.ndarray = score_map_max.mat - score_map_min.mat # type: ignore 170 np_delta = np.clip(np_delta, 0.0, 1.0) 171 char_neutralized_score_map = ScoreMap(mat=np_delta) 172 173 neutralized_score_map = score_map_max.copy() 174 neutralized_mask.fill_score_map(neutralized_score_map, char_neutralized_score_map) 175 176 weight = self.init_config.weight_neutralized_score_map 177 score_map = ScoreMap( 178 mat=((1 - weight) * score_map_max.mat + weight * neutralized_score_map.mat) 179 ) 180 181 debug = None 182 if run_config.enable_debug: 183 debug = CharHeatmapDefaultDebug( 184 score_map_max=score_map_max, 185 score_map_min=score_map_min, 186 char_overlapped_mask=char_overlapped_mask, 187 char_neutralized_score_map=char_neutralized_score_map, 188 neutralized_mask=neutralized_mask, 189 neutralized_score_map=neutralized_score_map, 190 ) 191 192 return CharHeatmap(score_map=score_map, debug=debug)