vkit.mechanism.distortion.geometric.affine

  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, Sequence, List, Tuple, Union, Iterable, TypeVar, Type
 15import math
 16
 17import attrs
 18import numpy as np
 19from numpy.random import Generator as RandomGenerator
 20import cv2 as cv
 21
 22from vkit.element import (
 23    Image,
 24    ScoreMap,
 25    Mask,
 26    Point,
 27    PointList,
 28    PointTuple,
 29    Polygon,
 30)
 31from ..interface import (
 32    DistortionConfig,
 33    DistortionState,
 34    Distortion,
 35)
 36
 37
 38def affine_mat(trans_mat: np.ndarray, dsize: Tuple[int, int], mat: np.ndarray) -> np.ndarray:
 39    if trans_mat.shape[0] == 2:
 40        return cv.warpAffine(mat, trans_mat, dsize)
 41    else:
 42        assert trans_mat.shape[0] == 3
 43        return cv.warpPerspective(mat, trans_mat, dsize)
 44
 45
 46def affine_np_points(trans_mat: np.ndarray, np_points: np.ndarray) -> np.ndarray:
 47    # (2, *)
 48    np_points = np_points.transpose()
 49    # (3, *)
 50    np_points = np.concatenate((
 51        np_points,
 52        np.ones((1, np_points.shape[1]), dtype=np.float32),
 53    ))
 54
 55    new_np_points = np.matmul(trans_mat, np_points)
 56    if trans_mat.shape[0] == 2:
 57        # new_np_points.shape is (2, *), do nothing.
 58        pass
 59
 60    else:
 61        assert trans_mat.shape[0] == 3
 62        new_np_points = new_np_points[:2, :] / new_np_points[2, :]
 63
 64    return new_np_points.transpose()
 65
 66
 67def affine_points(trans_mat: np.ndarray, points: PointTuple):
 68    new_np_points = affine_np_points(trans_mat, points.to_smooth_np_array())
 69    return PointTuple.from_np_array(new_np_points)
 70
 71
 72def affine_polygons(trans_mat: np.ndarray, polygons: Sequence[Polygon]) -> Sequence[Polygon]:
 73    points_ranges: List[Tuple[int, int]] = []
 74    points = PointList()
 75    for polygon in polygons:
 76        points_ranges.append((len(points), len(points) + polygon.num_points))
 77        points.extend(polygon.points)
 78
 79    new_np_points = affine_np_points(trans_mat, points.to_smooth_np_array())
 80    new_polygons = []
 81    for begin, end in points_ranges:
 82        new_polygons.append(Polygon.from_np_array(new_np_points[begin:end]))
 83
 84    return new_polygons
 85
 86
 87def convert_dsize_to_result_shape(dsize: Optional[Tuple[int, int]]):
 88    if dsize:
 89        return dsize[1], dsize[0]
 90
 91
 92@attrs.define
 93class ShearHoriConfig(DistortionConfig):
 94    # angle: int, (-90, 90), positive value for rightward direction.
 95    angle: int
 96
 97    @property
 98    def is_nop(self):
 99        return self.angle == 0
100
101
102class ShearHoriState(DistortionState[ShearHoriConfig]):
103
104    def __init__(
105        self,
106        config: ShearHoriConfig,
107        shape: Tuple[int, int],
108        rng: Optional[RandomGenerator],
109    ):
110        tan_phi = math.tan(math.radians(config.angle))
111
112        height, width = shape
113        shift_x = abs(height * tan_phi)
114        self.dsize = (math.ceil(width + shift_x), height)
115
116        if config.angle < 0:
117            # Shear left & the negative part.
118            self.trans_mat = np.asarray(
119                [
120                    (1, -tan_phi, 0),
121                    (0, 1, 0),
122                ],
123                dtype=np.float32,
124            )
125
126        elif config.angle > 0:
127            # Shear right.
128            self.trans_mat = np.asarray(
129                [
130                    (1, -tan_phi, shift_x),
131                    (0, 1, 0),
132                ],
133                dtype=np.float32,
134            )
135
136        else:
137            # No need to transform.
138            self.trans_mat = None
139            self.dsize = None
140
141    @property
142    def result_shape(self):
143        return convert_dsize_to_result_shape(self.dsize)
144
145
146@attrs.define
147class ShearVertConfig(DistortionConfig):
148    # angle: int, (-90, 90), positive value for downward direction.
149    angle: int
150
151    @property
152    def is_nop(self):
153        return self.angle == 0
154
155
156class ShearVertState(DistortionState[ShearVertConfig]):
157
158    def __init__(
159        self,
160        config: ShearVertConfig,
161        shape: Tuple[int, int],
162        rng: Optional[RandomGenerator],
163    ):
164        tan_abs_phi = math.tan(math.radians(abs(config.angle)))
165
166        height, width = shape
167        shift_y = width * tan_abs_phi
168        self.dsize = (width, math.ceil(height + shift_y))
169
170        if config.angle < 0:
171            # Shear up.
172            self.trans_mat = np.asarray(
173                [
174                    (1, 0, 0),
175                    (-tan_abs_phi, 1, shift_y),
176                ],
177                dtype=np.float32,
178            )
179        elif config.angle > 0:
180            # Shear down & the negative part.
181            self.trans_mat = np.asarray(
182                [
183                    (1, 0, 0),
184                    (tan_abs_phi, 1, 0),
185                ],
186                dtype=np.float32,
187            )
188        else:
189            # No need to transform.
190            self.trans_mat = None
191            self.dsize = None
192
193    @property
194    def result_shape(self):
195        return convert_dsize_to_result_shape(self.dsize)
196
197
198@attrs.define
199class RotateConfig(DistortionConfig):
200    # angle: int, [0, 360], clockwise angle.
201    angle: int
202
203    @property
204    def is_nop(self):
205        return self.angle == 0
206
207
208class RotateState(DistortionState[RotateConfig]):
209
210    def __init__(
211        self,
212        config: RotateConfig,
213        shape: Tuple[int, int],
214        rng: Optional[RandomGenerator],
215    ):
216        height, width = shape
217
218        angle = config.angle % 360
219        rad = math.radians(angle)
220
221        shift_x = 0
222        shift_y = 0
223
224        if rad <= math.pi / 2:
225            # In 3-4 quadrant.
226            shift_x = height * math.sin(rad)
227
228            dst_width = height * math.sin(rad) + width * math.cos(rad)
229            dst_height = height * math.cos(rad) + width * math.sin(rad)
230
231        elif rad <= math.pi:
232            # In 2-3 quadrant.
233            shift_rad = rad - math.pi / 2
234
235            shift_x = width * math.sin(shift_rad) + height * math.cos(shift_rad)
236            shift_y = height * math.sin(shift_rad)
237
238            dst_width = shift_x
239            dst_height = shift_y + width * math.cos(shift_rad)
240
241        elif rad < math.pi * 3 / 2:
242            # In 1-2 quadrant.
243            shift_rad = rad - math.pi
244
245            shift_x = width * math.cos(shift_rad)
246            shift_y = width * math.sin(shift_rad) + height * math.cos(shift_rad)
247
248            dst_width = shift_x + height * math.sin(shift_rad)
249            dst_height = shift_y
250
251        else:
252            # In 1-4 quadrant.
253            shift_rad = rad - math.pi * 3 / 2
254
255            shift_y = width * math.cos(shift_rad)
256
257            dst_width = width * math.sin(shift_rad) + height * math.cos(shift_rad)
258            dst_height = shift_y + height * math.sin(shift_rad)
259
260        shift_x = math.ceil(shift_x)
261        shift_y = math.ceil(shift_y)
262
263        self.trans_mat = np.asarray(
264            [
265                (math.cos(rad), -math.sin(rad), shift_x),
266                (math.sin(rad), math.cos(rad), shift_y),
267            ],
268            dtype=np.float32,
269        )
270
271        self.dsize = (math.ceil(dst_width), math.ceil(dst_height))
272
273    @property
274    def result_shape(self):
275        return convert_dsize_to_result_shape(self.dsize)
276
277
278@attrs.define
279class SkewHoriConfig(DistortionConfig):
280    # (-1.0, 0.0], shrink the left side.
281    # [0.0, 1.0), shrink the right side.
282    # The larger abs(ratio), the more to shrink.
283    ratio: float
284
285    @property
286    def is_nop(self):
287        return self.ratio == 0
288
289
290class SkewHoriState(DistortionState[SkewHoriConfig]):
291
292    def __init__(
293        self,
294        config: SkewHoriConfig,
295        shape: Tuple[int, int],
296        rng: Optional[RandomGenerator],
297    ):
298        height, width = shape
299
300        src_xy_pairs = [
301            (0, 0),
302            (width - 1, 0),
303            (width - 1, height - 1),
304            (0, height - 1),
305        ]
306
307        shrink_size = round(height * abs(config.ratio))
308        shrink_up = shrink_size // 2
309        shrink_down = shrink_size - shrink_up
310
311        if config.ratio < 0:
312            dst_xy_pairs = [
313                (0, shrink_up),
314                (width - 1, 0),
315                (width - 1, height - 1),
316                (0, height - shrink_down - 1),
317            ]
318        else:
319            dst_xy_pairs = [
320                (0, 0),
321                (width - 1, shrink_up),
322                (width - 1, height - shrink_down - 1),
323                (0, height - 1),
324            ]
325
326        self.trans_mat = cv.getPerspectiveTransform(
327            np.asarray(src_xy_pairs, dtype=np.float32),
328            np.asarray(dst_xy_pairs, dtype=np.float32),
329            cv.DECOMP_SVD,
330        )
331        self.dsize = (width, height)
332
333    @property
334    def result_shape(self):
335        return convert_dsize_to_result_shape(self.dsize)
336
337
338@attrs.define
339class SkewVertConfig(DistortionConfig):
340    # (-1.0, 0.0], shrink the up side.
341    # [0.0, 1.0), shrink the down side.
342    # The larger abs(ratio), the more to shrink.
343    ratio: float
344
345    @property
346    def is_nop(self):
347        return self.ratio == 0
348
349
350class SkewVertState(DistortionState[SkewVertConfig]):
351
352    def __init__(
353        self,
354        config: SkewVertConfig,
355        shape: Tuple[int, int],
356        rng: Optional[RandomGenerator],
357    ):
358        height, width = shape
359
360        src_xy_pairs = [
361            (0, 0),
362            (width - 1, 0),
363            (width - 1, height - 1),
364            (0, height - 1),
365        ]
366
367        shrink_size = round(width * abs(config.ratio))
368        shrink_left = shrink_size // 2
369        shrink_right = shrink_size - shrink_left
370
371        if config.ratio < 0:
372            dst_xy_pairs = [
373                (shrink_left, 0),
374                (width - shrink_right - 1, 0),
375                (width - 1, height - 1),
376                (0, height - 1),
377            ]
378        else:
379            dst_xy_pairs = [
380                (0, 0),
381                (width - 1, 0),
382                (width - shrink_right - 1, height - 1),
383                (shrink_right, height - 1),
384            ]
385
386        self.trans_mat = cv.getPerspectiveTransform(
387            np.asarray(src_xy_pairs, dtype=np.float32),
388            np.asarray(dst_xy_pairs, dtype=np.float32),
389            cv.DECOMP_SVD,
390        )
391        self.dsize = (width, height)
392
393    @property
394    def result_shape(self):
395        return convert_dsize_to_result_shape(self.dsize)
396
397
398_T_AFFINE_CONFIG = TypeVar(
399    '_T_AFFINE_CONFIG',
400    ShearHoriConfig,
401    ShearVertConfig,
402    RotateConfig,
403    SkewHoriConfig,
404    SkewVertConfig,
405)
406_T_AFFINE_STATE = TypeVar(
407    '_T_AFFINE_STATE',
408    ShearHoriState,
409    ShearVertState,
410    RotateState,
411    SkewHoriState,
412    SkewVertState,
413)
414
415
416def affine_trait_func_mat(
417    config: _T_AFFINE_CONFIG,
418    state: Optional[_T_AFFINE_STATE],
419    mat: np.ndarray,
420):
421    assert state
422    if config.is_nop:
423        return mat
424    else:
425        assert state.trans_mat is not None
426        assert state.dsize is not None
427        return affine_mat(state.trans_mat, state.dsize, mat)
428
429
430def affine_trait_func_image(
431    config: _T_AFFINE_CONFIG,
432    state: Optional[_T_AFFINE_STATE],
433    image: Image,
434    rng: Optional[RandomGenerator],
435):
436    return Image(mat=affine_trait_func_mat(config, state, image.mat))
437
438
439def affine_trait_func_score_map(
440    config: _T_AFFINE_CONFIG,
441    state: Optional[_T_AFFINE_STATE],
442    score_map: ScoreMap,
443    rng: Optional[RandomGenerator],
444):
445    assert state
446    return ScoreMap(mat=affine_trait_func_mat(config, state, score_map.mat))
447
448
449def affine_trait_func_mask(
450    config: _T_AFFINE_CONFIG,
451    state: Optional[_T_AFFINE_STATE],
452    mask: Mask,
453    rng: Optional[RandomGenerator],
454):
455    assert state
456    return Mask(mat=affine_trait_func_mat(config, state, mask.mat))
457
458
459def affine_trait_func_points(
460    config: _T_AFFINE_CONFIG,
461    state: Optional[_T_AFFINE_STATE],
462    shape: Tuple[int, int],
463    points: Union[PointList, PointTuple, Iterable[Point]],
464    rng: Optional[RandomGenerator],
465):
466    assert state
467    points = PointTuple(points)
468    if config.is_nop:
469        return points
470    else:
471        assert state.trans_mat is not None
472        return affine_points(state.trans_mat, points)
473
474
475def affine_trait_func_polygons(
476    config: _T_AFFINE_CONFIG,
477    state: Optional[_T_AFFINE_STATE],
478    shape: Tuple[int, int],
479    polygons: Iterable[Polygon],
480    rng: Optional[RandomGenerator],
481):
482    assert state
483    polygons = tuple(polygons)
484    if config.is_nop:
485        return polygons
486    else:
487        assert state.trans_mat is not None
488        return affine_polygons(state.trans_mat, polygons)
489
490
491class DistortionAffine(Distortion[_T_AFFINE_CONFIG, _T_AFFINE_STATE]):
492
493    def __init__(
494        self,
495        config_cls: Type[_T_AFFINE_CONFIG],
496        state_cls: Type[_T_AFFINE_STATE],
497    ):
498        super().__init__(
499            config_cls=config_cls,
500            state_cls=state_cls,
501            func_image=affine_trait_func_image,
502            func_mask=affine_trait_func_mask,
503            func_score_map=affine_trait_func_score_map,
504            func_points=affine_trait_func_points,
505            func_polygons=affine_trait_func_polygons,
506        )
507
508
509shear_hori = DistortionAffine(
510    config_cls=ShearHoriConfig,
511    state_cls=ShearHoriState,
512)
513
514shear_vert = DistortionAffine(
515    config_cls=ShearVertConfig,
516    state_cls=ShearVertState,
517)
518
519rotate = DistortionAffine(
520    config_cls=RotateConfig,
521    state_cls=RotateState,
522)
523
524skew_hori = DistortionAffine(
525    config_cls=SkewHoriConfig,
526    state_cls=SkewHoriState,
527)
528
529skew_vert = DistortionAffine(
530    config_cls=SkewVertConfig,
531    state_cls=SkewVertState,
532)
def affine_mat( trans_mat: numpy.ndarray, dsize: Tuple[int, int], mat: numpy.ndarray) -> numpy.ndarray:
39def affine_mat(trans_mat: np.ndarray, dsize: Tuple[int, int], mat: np.ndarray) -> np.ndarray:
40    if trans_mat.shape[0] == 2:
41        return cv.warpAffine(mat, trans_mat, dsize)
42    else:
43        assert trans_mat.shape[0] == 3
44        return cv.warpPerspective(mat, trans_mat, dsize)
def affine_np_points(trans_mat: numpy.ndarray, np_points: numpy.ndarray) -> numpy.ndarray:
47def affine_np_points(trans_mat: np.ndarray, np_points: np.ndarray) -> np.ndarray:
48    # (2, *)
49    np_points = np_points.transpose()
50    # (3, *)
51    np_points = np.concatenate((
52        np_points,
53        np.ones((1, np_points.shape[1]), dtype=np.float32),
54    ))
55
56    new_np_points = np.matmul(trans_mat, np_points)
57    if trans_mat.shape[0] == 2:
58        # new_np_points.shape is (2, *), do nothing.
59        pass
60
61    else:
62        assert trans_mat.shape[0] == 3
63        new_np_points = new_np_points[:2, :] / new_np_points[2, :]
64
65    return new_np_points.transpose()
def affine_points(trans_mat: numpy.ndarray, points: vkit.element.point.PointTuple):
68def affine_points(trans_mat: np.ndarray, points: PointTuple):
69    new_np_points = affine_np_points(trans_mat, points.to_smooth_np_array())
70    return PointTuple.from_np_array(new_np_points)
def affine_polygons( trans_mat: numpy.ndarray, polygons: Sequence[vkit.element.polygon.Polygon]) -> Sequence[vkit.element.polygon.Polygon]:
73def affine_polygons(trans_mat: np.ndarray, polygons: Sequence[Polygon]) -> Sequence[Polygon]:
74    points_ranges: List[Tuple[int, int]] = []
75    points = PointList()
76    for polygon in polygons:
77        points_ranges.append((len(points), len(points) + polygon.num_points))
78        points.extend(polygon.points)
79
80    new_np_points = affine_np_points(trans_mat, points.to_smooth_np_array())
81    new_polygons = []
82    for begin, end in points_ranges:
83        new_polygons.append(Polygon.from_np_array(new_np_points[begin:end]))
84
85    return new_polygons
def convert_dsize_to_result_shape(dsize: Union[Tuple[int, int], NoneType]):
88def convert_dsize_to_result_shape(dsize: Optional[Tuple[int, int]]):
89    if dsize:
90        return dsize[1], dsize[0]
class ShearHoriConfig(vkit.mechanism.distortion.interface.DistortionConfig):
 94class ShearHoriConfig(DistortionConfig):
 95    # angle: int, (-90, 90), positive value for rightward direction.
 96    angle: int
 97
 98    @property
 99    def is_nop(self):
100        return self.angle == 0
ShearHoriConfig(angle: int)
2def __init__(self, angle):
3    self.angle = angle

Method generated by attrs for class ShearHoriConfig.

103class ShearHoriState(DistortionState[ShearHoriConfig]):
104
105    def __init__(
106        self,
107        config: ShearHoriConfig,
108        shape: Tuple[int, int],
109        rng: Optional[RandomGenerator],
110    ):
111        tan_phi = math.tan(math.radians(config.angle))
112
113        height, width = shape
114        shift_x = abs(height * tan_phi)
115        self.dsize = (math.ceil(width + shift_x), height)
116
117        if config.angle < 0:
118            # Shear left & the negative part.
119            self.trans_mat = np.asarray(
120                [
121                    (1, -tan_phi, 0),
122                    (0, 1, 0),
123                ],
124                dtype=np.float32,
125            )
126
127        elif config.angle > 0:
128            # Shear right.
129            self.trans_mat = np.asarray(
130                [
131                    (1, -tan_phi, shift_x),
132                    (0, 1, 0),
133                ],
134                dtype=np.float32,
135            )
136
137        else:
138            # No need to transform.
139            self.trans_mat = None
140            self.dsize = None
141
142    @property
143    def result_shape(self):
144        return convert_dsize_to_result_shape(self.dsize)

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

ShearHoriState( config: vkit.mechanism.distortion.geometric.affine.ShearHoriConfig, shape: Tuple[int, int], rng: Union[numpy.random._generator.Generator, NoneType])
105    def __init__(
106        self,
107        config: ShearHoriConfig,
108        shape: Tuple[int, int],
109        rng: Optional[RandomGenerator],
110    ):
111        tan_phi = math.tan(math.radians(config.angle))
112
113        height, width = shape
114        shift_x = abs(height * tan_phi)
115        self.dsize = (math.ceil(width + shift_x), height)
116
117        if config.angle < 0:
118            # Shear left & the negative part.
119            self.trans_mat = np.asarray(
120                [
121                    (1, -tan_phi, 0),
122                    (0, 1, 0),
123                ],
124                dtype=np.float32,
125            )
126
127        elif config.angle > 0:
128            # Shear right.
129            self.trans_mat = np.asarray(
130                [
131                    (1, -tan_phi, shift_x),
132                    (0, 1, 0),
133                ],
134                dtype=np.float32,
135            )
136
137        else:
138            # No need to transform.
139            self.trans_mat = None
140            self.dsize = None
class ShearVertConfig(vkit.mechanism.distortion.interface.DistortionConfig):
148class ShearVertConfig(DistortionConfig):
149    # angle: int, (-90, 90), positive value for downward direction.
150    angle: int
151
152    @property
153    def is_nop(self):
154        return self.angle == 0
ShearVertConfig(angle: int)
2def __init__(self, angle):
3    self.angle = angle

Method generated by attrs for class ShearVertConfig.

157class ShearVertState(DistortionState[ShearVertConfig]):
158
159    def __init__(
160        self,
161        config: ShearVertConfig,
162        shape: Tuple[int, int],
163        rng: Optional[RandomGenerator],
164    ):
165        tan_abs_phi = math.tan(math.radians(abs(config.angle)))
166
167        height, width = shape
168        shift_y = width * tan_abs_phi
169        self.dsize = (width, math.ceil(height + shift_y))
170
171        if config.angle < 0:
172            # Shear up.
173            self.trans_mat = np.asarray(
174                [
175                    (1, 0, 0),
176                    (-tan_abs_phi, 1, shift_y),
177                ],
178                dtype=np.float32,
179            )
180        elif config.angle > 0:
181            # Shear down & the negative part.
182            self.trans_mat = np.asarray(
183                [
184                    (1, 0, 0),
185                    (tan_abs_phi, 1, 0),
186                ],
187                dtype=np.float32,
188            )
189        else:
190            # No need to transform.
191            self.trans_mat = None
192            self.dsize = None
193
194    @property
195    def result_shape(self):
196        return convert_dsize_to_result_shape(self.dsize)

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

ShearVertState( config: vkit.mechanism.distortion.geometric.affine.ShearVertConfig, shape: Tuple[int, int], rng: Union[numpy.random._generator.Generator, NoneType])
159    def __init__(
160        self,
161        config: ShearVertConfig,
162        shape: Tuple[int, int],
163        rng: Optional[RandomGenerator],
164    ):
165        tan_abs_phi = math.tan(math.radians(abs(config.angle)))
166
167        height, width = shape
168        shift_y = width * tan_abs_phi
169        self.dsize = (width, math.ceil(height + shift_y))
170
171        if config.angle < 0:
172            # Shear up.
173            self.trans_mat = np.asarray(
174                [
175                    (1, 0, 0),
176                    (-tan_abs_phi, 1, shift_y),
177                ],
178                dtype=np.float32,
179            )
180        elif config.angle > 0:
181            # Shear down & the negative part.
182            self.trans_mat = np.asarray(
183                [
184                    (1, 0, 0),
185                    (tan_abs_phi, 1, 0),
186                ],
187                dtype=np.float32,
188            )
189        else:
190            # No need to transform.
191            self.trans_mat = None
192            self.dsize = None
class RotateConfig(vkit.mechanism.distortion.interface.DistortionConfig):
200class RotateConfig(DistortionConfig):
201    # angle: int, [0, 360], clockwise angle.
202    angle: int
203
204    @property
205    def is_nop(self):
206        return self.angle == 0
RotateConfig(angle: int)
2def __init__(self, angle):
3    self.angle = angle

Method generated by attrs for class RotateConfig.

209class RotateState(DistortionState[RotateConfig]):
210
211    def __init__(
212        self,
213        config: RotateConfig,
214        shape: Tuple[int, int],
215        rng: Optional[RandomGenerator],
216    ):
217        height, width = shape
218
219        angle = config.angle % 360
220        rad = math.radians(angle)
221
222        shift_x = 0
223        shift_y = 0
224
225        if rad <= math.pi / 2:
226            # In 3-4 quadrant.
227            shift_x = height * math.sin(rad)
228
229            dst_width = height * math.sin(rad) + width * math.cos(rad)
230            dst_height = height * math.cos(rad) + width * math.sin(rad)
231
232        elif rad <= math.pi:
233            # In 2-3 quadrant.
234            shift_rad = rad - math.pi / 2
235
236            shift_x = width * math.sin(shift_rad) + height * math.cos(shift_rad)
237            shift_y = height * math.sin(shift_rad)
238
239            dst_width = shift_x
240            dst_height = shift_y + width * math.cos(shift_rad)
241
242        elif rad < math.pi * 3 / 2:
243            # In 1-2 quadrant.
244            shift_rad = rad - math.pi
245
246            shift_x = width * math.cos(shift_rad)
247            shift_y = width * math.sin(shift_rad) + height * math.cos(shift_rad)
248
249            dst_width = shift_x + height * math.sin(shift_rad)
250            dst_height = shift_y
251
252        else:
253            # In 1-4 quadrant.
254            shift_rad = rad - math.pi * 3 / 2
255
256            shift_y = width * math.cos(shift_rad)
257
258            dst_width = width * math.sin(shift_rad) + height * math.cos(shift_rad)
259            dst_height = shift_y + height * math.sin(shift_rad)
260
261        shift_x = math.ceil(shift_x)
262        shift_y = math.ceil(shift_y)
263
264        self.trans_mat = np.asarray(
265            [
266                (math.cos(rad), -math.sin(rad), shift_x),
267                (math.sin(rad), math.cos(rad), shift_y),
268            ],
269            dtype=np.float32,
270        )
271
272        self.dsize = (math.ceil(dst_width), math.ceil(dst_height))
273
274    @property
275    def result_shape(self):
276        return convert_dsize_to_result_shape(self.dsize)

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

RotateState( config: vkit.mechanism.distortion.geometric.affine.RotateConfig, shape: Tuple[int, int], rng: Union[numpy.random._generator.Generator, NoneType])
211    def __init__(
212        self,
213        config: RotateConfig,
214        shape: Tuple[int, int],
215        rng: Optional[RandomGenerator],
216    ):
217        height, width = shape
218
219        angle = config.angle % 360
220        rad = math.radians(angle)
221
222        shift_x = 0
223        shift_y = 0
224
225        if rad <= math.pi / 2:
226            # In 3-4 quadrant.
227            shift_x = height * math.sin(rad)
228
229            dst_width = height * math.sin(rad) + width * math.cos(rad)
230            dst_height = height * math.cos(rad) + width * math.sin(rad)
231
232        elif rad <= math.pi:
233            # In 2-3 quadrant.
234            shift_rad = rad - math.pi / 2
235
236            shift_x = width * math.sin(shift_rad) + height * math.cos(shift_rad)
237            shift_y = height * math.sin(shift_rad)
238
239            dst_width = shift_x
240            dst_height = shift_y + width * math.cos(shift_rad)
241
242        elif rad < math.pi * 3 / 2:
243            # In 1-2 quadrant.
244            shift_rad = rad - math.pi
245
246            shift_x = width * math.cos(shift_rad)
247            shift_y = width * math.sin(shift_rad) + height * math.cos(shift_rad)
248
249            dst_width = shift_x + height * math.sin(shift_rad)
250            dst_height = shift_y
251
252        else:
253            # In 1-4 quadrant.
254            shift_rad = rad - math.pi * 3 / 2
255
256            shift_y = width * math.cos(shift_rad)
257
258            dst_width = width * math.sin(shift_rad) + height * math.cos(shift_rad)
259            dst_height = shift_y + height * math.sin(shift_rad)
260
261        shift_x = math.ceil(shift_x)
262        shift_y = math.ceil(shift_y)
263
264        self.trans_mat = np.asarray(
265            [
266                (math.cos(rad), -math.sin(rad), shift_x),
267                (math.sin(rad), math.cos(rad), shift_y),
268            ],
269            dtype=np.float32,
270        )
271
272        self.dsize = (math.ceil(dst_width), math.ceil(dst_height))
class SkewHoriConfig(vkit.mechanism.distortion.interface.DistortionConfig):
280class SkewHoriConfig(DistortionConfig):
281    # (-1.0, 0.0], shrink the left side.
282    # [0.0, 1.0), shrink the right side.
283    # The larger abs(ratio), the more to shrink.
284    ratio: float
285
286    @property
287    def is_nop(self):
288        return self.ratio == 0
SkewHoriConfig(ratio: float)
2def __init__(self, ratio):
3    self.ratio = ratio

Method generated by attrs for class SkewHoriConfig.

291class SkewHoriState(DistortionState[SkewHoriConfig]):
292
293    def __init__(
294        self,
295        config: SkewHoriConfig,
296        shape: Tuple[int, int],
297        rng: Optional[RandomGenerator],
298    ):
299        height, width = shape
300
301        src_xy_pairs = [
302            (0, 0),
303            (width - 1, 0),
304            (width - 1, height - 1),
305            (0, height - 1),
306        ]
307
308        shrink_size = round(height * abs(config.ratio))
309        shrink_up = shrink_size // 2
310        shrink_down = shrink_size - shrink_up
311
312        if config.ratio < 0:
313            dst_xy_pairs = [
314                (0, shrink_up),
315                (width - 1, 0),
316                (width - 1, height - 1),
317                (0, height - shrink_down - 1),
318            ]
319        else:
320            dst_xy_pairs = [
321                (0, 0),
322                (width - 1, shrink_up),
323                (width - 1, height - shrink_down - 1),
324                (0, height - 1),
325            ]
326
327        self.trans_mat = cv.getPerspectiveTransform(
328            np.asarray(src_xy_pairs, dtype=np.float32),
329            np.asarray(dst_xy_pairs, dtype=np.float32),
330            cv.DECOMP_SVD,
331        )
332        self.dsize = (width, height)
333
334    @property
335    def result_shape(self):
336        return convert_dsize_to_result_shape(self.dsize)

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

SkewHoriState( config: vkit.mechanism.distortion.geometric.affine.SkewHoriConfig, shape: Tuple[int, int], rng: Union[numpy.random._generator.Generator, NoneType])
293    def __init__(
294        self,
295        config: SkewHoriConfig,
296        shape: Tuple[int, int],
297        rng: Optional[RandomGenerator],
298    ):
299        height, width = shape
300
301        src_xy_pairs = [
302            (0, 0),
303            (width - 1, 0),
304            (width - 1, height - 1),
305            (0, height - 1),
306        ]
307
308        shrink_size = round(height * abs(config.ratio))
309        shrink_up = shrink_size // 2
310        shrink_down = shrink_size - shrink_up
311
312        if config.ratio < 0:
313            dst_xy_pairs = [
314                (0, shrink_up),
315                (width - 1, 0),
316                (width - 1, height - 1),
317                (0, height - shrink_down - 1),
318            ]
319        else:
320            dst_xy_pairs = [
321                (0, 0),
322                (width - 1, shrink_up),
323                (width - 1, height - shrink_down - 1),
324                (0, height - 1),
325            ]
326
327        self.trans_mat = cv.getPerspectiveTransform(
328            np.asarray(src_xy_pairs, dtype=np.float32),
329            np.asarray(dst_xy_pairs, dtype=np.float32),
330            cv.DECOMP_SVD,
331        )
332        self.dsize = (width, height)
class SkewVertConfig(vkit.mechanism.distortion.interface.DistortionConfig):
340class SkewVertConfig(DistortionConfig):
341    # (-1.0, 0.0], shrink the up side.
342    # [0.0, 1.0), shrink the down side.
343    # The larger abs(ratio), the more to shrink.
344    ratio: float
345
346    @property
347    def is_nop(self):
348        return self.ratio == 0
SkewVertConfig(ratio: float)
2def __init__(self, ratio):
3    self.ratio = ratio

Method generated by attrs for class SkewVertConfig.

351class SkewVertState(DistortionState[SkewVertConfig]):
352
353    def __init__(
354        self,
355        config: SkewVertConfig,
356        shape: Tuple[int, int],
357        rng: Optional[RandomGenerator],
358    ):
359        height, width = shape
360
361        src_xy_pairs = [
362            (0, 0),
363            (width - 1, 0),
364            (width - 1, height - 1),
365            (0, height - 1),
366        ]
367
368        shrink_size = round(width * abs(config.ratio))
369        shrink_left = shrink_size // 2
370        shrink_right = shrink_size - shrink_left
371
372        if config.ratio < 0:
373            dst_xy_pairs = [
374                (shrink_left, 0),
375                (width - shrink_right - 1, 0),
376                (width - 1, height - 1),
377                (0, height - 1),
378            ]
379        else:
380            dst_xy_pairs = [
381                (0, 0),
382                (width - 1, 0),
383                (width - shrink_right - 1, height - 1),
384                (shrink_right, height - 1),
385            ]
386
387        self.trans_mat = cv.getPerspectiveTransform(
388            np.asarray(src_xy_pairs, dtype=np.float32),
389            np.asarray(dst_xy_pairs, dtype=np.float32),
390            cv.DECOMP_SVD,
391        )
392        self.dsize = (width, height)
393
394    @property
395    def result_shape(self):
396        return convert_dsize_to_result_shape(self.dsize)

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

SkewVertState( config: vkit.mechanism.distortion.geometric.affine.SkewVertConfig, shape: Tuple[int, int], rng: Union[numpy.random._generator.Generator, NoneType])
353    def __init__(
354        self,
355        config: SkewVertConfig,
356        shape: Tuple[int, int],
357        rng: Optional[RandomGenerator],
358    ):
359        height, width = shape
360
361        src_xy_pairs = [
362            (0, 0),
363            (width - 1, 0),
364            (width - 1, height - 1),
365            (0, height - 1),
366        ]
367
368        shrink_size = round(width * abs(config.ratio))
369        shrink_left = shrink_size // 2
370        shrink_right = shrink_size - shrink_left
371
372        if config.ratio < 0:
373            dst_xy_pairs = [
374                (shrink_left, 0),
375                (width - shrink_right - 1, 0),
376                (width - 1, height - 1),
377                (0, height - 1),
378            ]
379        else:
380            dst_xy_pairs = [
381                (0, 0),
382                (width - 1, 0),
383                (width - shrink_right - 1, height - 1),
384                (shrink_right, height - 1),
385            ]
386
387        self.trans_mat = cv.getPerspectiveTransform(
388            np.asarray(src_xy_pairs, dtype=np.float32),
389            np.asarray(dst_xy_pairs, dtype=np.float32),
390            cv.DECOMP_SVD,
391        )
392        self.dsize = (width, height)
def affine_trait_func_mat( config: ~_T_AFFINE_CONFIG, state: Union[~_T_AFFINE_STATE, NoneType], mat: numpy.ndarray):
417def affine_trait_func_mat(
418    config: _T_AFFINE_CONFIG,
419    state: Optional[_T_AFFINE_STATE],
420    mat: np.ndarray,
421):
422    assert state
423    if config.is_nop:
424        return mat
425    else:
426        assert state.trans_mat is not None
427        assert state.dsize is not None
428        return affine_mat(state.trans_mat, state.dsize, mat)
def affine_trait_func_image( config: ~_T_AFFINE_CONFIG, state: Union[~_T_AFFINE_STATE, NoneType], image: vkit.element.image.Image, rng: Union[numpy.random._generator.Generator, NoneType]):
431def affine_trait_func_image(
432    config: _T_AFFINE_CONFIG,
433    state: Optional[_T_AFFINE_STATE],
434    image: Image,
435    rng: Optional[RandomGenerator],
436):
437    return Image(mat=affine_trait_func_mat(config, state, image.mat))
def affine_trait_func_score_map( config: ~_T_AFFINE_CONFIG, state: Union[~_T_AFFINE_STATE, NoneType], score_map: vkit.element.score_map.ScoreMap, rng: Union[numpy.random._generator.Generator, NoneType]):
440def affine_trait_func_score_map(
441    config: _T_AFFINE_CONFIG,
442    state: Optional[_T_AFFINE_STATE],
443    score_map: ScoreMap,
444    rng: Optional[RandomGenerator],
445):
446    assert state
447    return ScoreMap(mat=affine_trait_func_mat(config, state, score_map.mat))
def affine_trait_func_mask( config: ~_T_AFFINE_CONFIG, state: Union[~_T_AFFINE_STATE, NoneType], mask: vkit.element.mask.Mask, rng: Union[numpy.random._generator.Generator, NoneType]):
450def affine_trait_func_mask(
451    config: _T_AFFINE_CONFIG,
452    state: Optional[_T_AFFINE_STATE],
453    mask: Mask,
454    rng: Optional[RandomGenerator],
455):
456    assert state
457    return Mask(mat=affine_trait_func_mat(config, state, mask.mat))
def affine_trait_func_points( config: ~_T_AFFINE_CONFIG, state: Union[~_T_AFFINE_STATE, NoneType], shape: Tuple[int, int], points: Union[vkit.element.point.PointList, vkit.element.point.PointTuple, Iterable[vkit.element.point.Point]], rng: Union[numpy.random._generator.Generator, NoneType]):
460def affine_trait_func_points(
461    config: _T_AFFINE_CONFIG,
462    state: Optional[_T_AFFINE_STATE],
463    shape: Tuple[int, int],
464    points: Union[PointList, PointTuple, Iterable[Point]],
465    rng: Optional[RandomGenerator],
466):
467    assert state
468    points = PointTuple(points)
469    if config.is_nop:
470        return points
471    else:
472        assert state.trans_mat is not None
473        return affine_points(state.trans_mat, points)
def affine_trait_func_polygons( config: ~_T_AFFINE_CONFIG, state: Union[~_T_AFFINE_STATE, NoneType], shape: Tuple[int, int], polygons: Iterable[vkit.element.polygon.Polygon], rng: Union[numpy.random._generator.Generator, NoneType]):
476def affine_trait_func_polygons(
477    config: _T_AFFINE_CONFIG,
478    state: Optional[_T_AFFINE_STATE],
479    shape: Tuple[int, int],
480    polygons: Iterable[Polygon],
481    rng: Optional[RandomGenerator],
482):
483    assert state
484    polygons = tuple(polygons)
485    if config.is_nop:
486        return polygons
487    else:
488        assert state.trans_mat is not None
489        return affine_polygons(state.trans_mat, polygons)
492class DistortionAffine(Distortion[_T_AFFINE_CONFIG, _T_AFFINE_STATE]):
493
494    def __init__(
495        self,
496        config_cls: Type[_T_AFFINE_CONFIG],
497        state_cls: Type[_T_AFFINE_STATE],
498    ):
499        super().__init__(
500            config_cls=config_cls,
501            state_cls=state_cls,
502            func_image=affine_trait_func_image,
503            func_mask=affine_trait_func_mask,
504            func_score_map=affine_trait_func_score_map,
505            func_points=affine_trait_func_points,
506            func_polygons=affine_trait_func_polygons,
507        )

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

DistortionAffine( config_cls: Type[~_T_AFFINE_CONFIG], state_cls: Type[~_T_AFFINE_STATE])
494    def __init__(
495        self,
496        config_cls: Type[_T_AFFINE_CONFIG],
497        state_cls: Type[_T_AFFINE_STATE],
498    ):
499        super().__init__(
500            config_cls=config_cls,
501            state_cls=state_cls,
502            func_image=affine_trait_func_image,
503            func_mask=affine_trait_func_mask,
504            func_score_map=affine_trait_func_score_map,
505            func_points=affine_trait_func_points,
506            func_polygons=affine_trait_func_polygons,
507        )