vkit.mechanism.distortion.photometric.blur

  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, Tuple, Mapping, Any
 15
 16import attrs
 17import numpy as np
 18from numpy.random import Generator as RandomGenerator
 19import cv2 as cv
 20
 21from vkit.element import Image
 22from ..interface import DistortionConfig, DistortionNopState, Distortion
 23from .opt import to_rgb_image, to_original_image, clip_mat_back_to_uint8
 24
 25
 26def _estimate_gaussian_kernel_size(sigma: float):
 27    kernel_size = max(3, round(3 * sigma) + 1)
 28    if kernel_size % 2 == 0:
 29        kernel_size += 1
 30    return kernel_size
 31
 32
 33def _get_anti_aliasing_kernel_size_and_padding(anti_aliasing_sigma: float):
 34    kernel_size = _estimate_gaussian_kernel_size(anti_aliasing_sigma)
 35
 36    anti_aliasing_ksize = (kernel_size, kernel_size)
 37    anti_aliasing_kernel_padding = kernel_size // 2 * 2
 38    return anti_aliasing_ksize, anti_aliasing_kernel_padding
 39
 40
 41def _apply_anti_aliasing_to_kernel(
 42    kernel: np.ndarray,
 43    anti_aliasing_ksize: Tuple[int, int],
 44    anti_aliasing_sigma: float,
 45):
 46    return cv.GaussianBlur(kernel, anti_aliasing_ksize, anti_aliasing_sigma)
 47
 48
 49@attrs.define
 50class GaussianBlurConfig(DistortionConfig):
 51    sigma: float
 52
 53
 54def gaussian_blur_image(
 55    config: GaussianBlurConfig,
 56    state: Optional[DistortionNopState[GaussianBlurConfig]],
 57    image: Image,
 58    rng: Optional[RandomGenerator],
 59):
 60    mode = image.mode
 61    image = to_rgb_image(image, mode)
 62
 63    kernel_size = _estimate_gaussian_kernel_size(config.sigma)
 64    ksize = (kernel_size, kernel_size)
 65    mat = cv.GaussianBlur(image.mat, ksize, config.sigma)
 66    image = attrs.evolve(image, mat=mat)
 67
 68    image = to_original_image(image, mode)
 69    return image
 70
 71
 72gaussian_blur = Distortion(
 73    config_cls=GaussianBlurConfig,
 74    state_cls=DistortionNopState[GaussianBlurConfig],
 75    func_image=gaussian_blur_image,
 76)
 77
 78
 79@attrs.define
 80class DefocusBlurConfig(DistortionConfig):
 81    radius: int
 82    anti_aliasing_sigma: float = 0.5
 83
 84
 85def defocus_blur_image(
 86    config: DefocusBlurConfig,
 87    state: Optional[DistortionNopState[DefocusBlurConfig]],
 88    image: Image,
 89    rng: Optional[RandomGenerator],
 90):
 91    # Generate blurring kernel.
 92    assert 0 < config.radius
 93    kernel_size = 2 * config.radius + 1
 94
 95    (
 96        anti_aliasing_ksize,
 97        anti_aliasing_kernel_padding,
 98    ) = _get_anti_aliasing_kernel_size_and_padding(config.anti_aliasing_sigma)
 99    kernel_size += anti_aliasing_kernel_padding
100
101    begin = -(kernel_size // 2)
102    end = begin + kernel_size
103    coords = np.arange(begin, end)
104    x, y = np.meshgrid(coords, coords)
105    kernel: np.ndarray = ((x**2 + y**2) <= config.radius**2).astype(np.float32)
106    kernel /= kernel.sum()
107
108    kernel = _apply_anti_aliasing_to_kernel(
109        kernel,
110        anti_aliasing_ksize,
111        config.anti_aliasing_sigma,
112    )
113
114    # Convolution.
115    mode = image.mode
116    image = to_rgb_image(image, mode)
117
118    mat = cv.filter2D(image.mat, -1, kernel)
119    image = attrs.evolve(image, mat=mat)
120
121    image = to_original_image(image, mode)
122    return image
123
124
125defocus_blur = Distortion(
126    config_cls=DefocusBlurConfig,
127    state_cls=DistortionNopState[DefocusBlurConfig],
128    func_image=defocus_blur_image,
129)
130
131
132@attrs.define
133class MotionBlurConfig(DistortionConfig):
134    radius: int
135    angle: int
136    anti_aliasing_sigma: float = 0.5
137
138
139def motion_blur_image(
140    config: MotionBlurConfig,
141    state: Optional[DistortionNopState[MotionBlurConfig]],
142    image: Image,
143    rng: Optional[RandomGenerator],
144):
145    # Generate blurring kernel.
146    kernel_size = 2 * config.radius + 1
147
148    (
149        anti_aliasing_ksize,
150        anti_aliasing_kernel_padding,
151    ) = _get_anti_aliasing_kernel_size_and_padding(config.anti_aliasing_sigma)
152    anti_aliasing_kernel_padding_half = anti_aliasing_kernel_padding // 2
153    center = config.radius + anti_aliasing_kernel_padding_half
154    left = anti_aliasing_kernel_padding_half
155    right = left + kernel_size - 1
156    kernel_size += anti_aliasing_kernel_padding
157
158    kernel = np.zeros((kernel_size, kernel_size), dtype=np.float32)
159    kernel[center, left:right + 1] = 1.0
160
161    trans_mat = cv.getRotationMatrix2D(
162        (center, center),
163        # 1. to [0, 359].
164        # 2. getRotationMatrix2D accepts counter-clockwise angle, hence need to subtract.
165        360 - (config.angle % 360),
166        1.0,
167    )
168    kernel = cv.warpAffine(kernel, trans_mat, kernel.shape)
169    kernel /= kernel.sum()
170
171    kernel = _apply_anti_aliasing_to_kernel(
172        kernel,
173        anti_aliasing_ksize,
174        config.anti_aliasing_sigma,
175    )
176
177    # Convolution.
178    mode = image.mode
179    image = to_rgb_image(image, mode)
180
181    mat = cv.filter2D(image.mat, -1, kernel)
182    image = attrs.evolve(image, mat=mat)
183
184    image = to_original_image(image, mode)
185    return image
186
187
188motion_blur = Distortion(
189    config_cls=MotionBlurConfig,
190    state_cls=DistortionNopState[MotionBlurConfig],
191    func_image=motion_blur_image,
192)
193
194
195@attrs.define
196class GlassBlurConfig(DistortionConfig):
197    sigma: float
198    delta: int = 1
199    loop: int = 5
200
201    _rng_state: Optional[Mapping[str, Any]] = None
202
203    @property
204    def supports_rng_state(self) -> bool:
205        return True
206
207    @property
208    def rng_state(self) -> Optional[Mapping[str, Any]]:
209        return self._rng_state
210
211    @rng_state.setter
212    def rng_state(self, val: Mapping[str, Any]):
213        self._rng_state = val
214
215
216def glass_blur_image(
217    config: GlassBlurConfig,
218    state: Optional[DistortionNopState[GlassBlurConfig]],
219    image: Image,
220    rng: Optional[RandomGenerator],
221):
222    mode = image.mode
223    image = to_rgb_image(image, mode)
224
225    # Gaussian blur.
226    kernel_size = _estimate_gaussian_kernel_size(config.sigma)
227    ksize = (kernel_size, kernel_size)
228    mat = cv.GaussianBlur(image.mat, ksize, config.sigma)
229
230    # Random pixel swap.
231    assert rng is not None
232
233    coords_y = np.arange(image.height)
234    coords_x = np.arange(image.width)
235    pos_x, pos_y = np.meshgrid(coords_x, coords_y)
236
237    for _ in range(config.loop):
238        offset_y = rng.integers(0, 2 * config.delta + 1)
239        center_y = np.arange(offset_y, image.height - config.delta, 2 * config.delta + 1)
240        num_center_y = center_y.shape[0]
241        center_y = center_y.reshape(-1, 1)
242
243        offset_x = rng.integers(0, 2 * config.delta + 1)
244        center_x = np.arange(offset_x, image.width - config.delta, 2 * config.delta + 1)
245        num_center_x = center_x.shape[0]
246        center_x = center_x.reshape(1, -1)
247
248        delta_shape = (num_center_y, num_center_x)
249        delta_y = rng.integers(-config.delta, config.delta + 1, delta_shape)
250        delta_x = rng.integers(-config.delta, config.delta + 1, delta_shape)
251
252        deformed_y: np.ndarray = pos_y[center_y, center_x] + delta_y
253        deformed_y = np.clip(deformed_y, 0, image.height - 1)
254        deformed_x: np.ndarray = pos_x[center_y, center_x] + delta_x
255        deformed_x = np.clip(deformed_x, 0, image.width - 1)
256
257        # Swap.
258        pos_y[center_y, center_x], pos_y[deformed_y, deformed_x] = \
259            pos_y[deformed_y, deformed_x], pos_y[center_y, center_x]
260
261        pos_x[center_y, center_x], pos_x[deformed_y, deformed_x] = \
262            pos_x[deformed_y, deformed_x], pos_x[center_y, center_x]
263
264    mat = mat[pos_y, pos_x]
265    image = attrs.evolve(image, mat=mat)
266
267    image = to_original_image(image, mode)
268    return image
269
270
271glass_blur = Distortion(
272    config_cls=GlassBlurConfig,
273    state_cls=DistortionNopState[GlassBlurConfig],
274    func_image=glass_blur_image,
275)
276
277
278@attrs.define
279class ZoomInBlurConfig(DistortionConfig):
280    ratio: float = 0.1
281    step: float = 0.01
282    alpha: float = 0.5
283
284
285def zoom_in_blur_image(
286    config: ZoomInBlurConfig,
287    state: Optional[DistortionNopState[ZoomInBlurConfig]],
288    image: Image,
289    rng: Optional[RandomGenerator],
290):
291    mode = image.mode
292    image = to_rgb_image(image, mode)
293
294    mat = image.mat.astype(np.uint16)
295    count = 1
296    for ratio in np.arange(
297        1 + config.step,
298        1 + config.ratio + config.step,
299        config.step,
300    ):
301        resized_height = round(image.height * ratio)
302        resized_width = round(image.width * ratio)
303        resized_image = image.to_resized_image(resized_height, resized_width)
304
305        pad_y = resized_height - image.height
306        up = pad_y // 2
307        down = up + image.height - 1
308
309        pad_x = resized_width - image.width
310        left = pad_x // 2
311        right = left + image.width - 1
312
313        mat += resized_image.mat[up:down + 1, left:right + 1]
314        count += 1
315
316    # NOTE: Label confusion could be significant if alpha is large.
317    mat: np.ndarray = (1 - config.alpha) * image.mat + config.alpha * np.round(mat / count)
318    mat = clip_mat_back_to_uint8(mat)
319
320    image = attrs.evolve(image, mat=mat)
321
322    image = to_original_image(image, mode)
323    return image
324
325
326zoom_in_blur = Distortion(
327    config_cls=ZoomInBlurConfig,
328    state_cls=DistortionNopState[ZoomInBlurConfig],
329    func_image=zoom_in_blur_image,
330)
class GaussianBlurConfig(vkit.mechanism.distortion.interface.DistortionConfig):
51class GaussianBlurConfig(DistortionConfig):
52    sigma: float
GaussianBlurConfig(sigma: float)
2def __init__(self, sigma):
3    self.sigma = sigma

Method generated by attrs for class GaussianBlurConfig.

def gaussian_blur_image( config: vkit.mechanism.distortion.photometric.blur.GaussianBlurConfig, state: Union[vkit.mechanism.distortion.interface.DistortionNopState[vkit.mechanism.distortion.photometric.blur.GaussianBlurConfig], NoneType], image: vkit.element.image.Image, rng: Union[numpy.random._generator.Generator, NoneType]):
55def gaussian_blur_image(
56    config: GaussianBlurConfig,
57    state: Optional[DistortionNopState[GaussianBlurConfig]],
58    image: Image,
59    rng: Optional[RandomGenerator],
60):
61    mode = image.mode
62    image = to_rgb_image(image, mode)
63
64    kernel_size = _estimate_gaussian_kernel_size(config.sigma)
65    ksize = (kernel_size, kernel_size)
66    mat = cv.GaussianBlur(image.mat, ksize, config.sigma)
67    image = attrs.evolve(image, mat=mat)
68
69    image = to_original_image(image, mode)
70    return image
class DefocusBlurConfig(vkit.mechanism.distortion.interface.DistortionConfig):
81class DefocusBlurConfig(DistortionConfig):
82    radius: int
83    anti_aliasing_sigma: float = 0.5
DefocusBlurConfig(radius: int, anti_aliasing_sigma: float = 0.5)
2def __init__(self, radius, anti_aliasing_sigma=attr_dict['anti_aliasing_sigma'].default):
3    self.radius = radius
4    self.anti_aliasing_sigma = anti_aliasing_sigma

Method generated by attrs for class DefocusBlurConfig.

def defocus_blur_image( config: vkit.mechanism.distortion.photometric.blur.DefocusBlurConfig, state: Union[vkit.mechanism.distortion.interface.DistortionNopState[vkit.mechanism.distortion.photometric.blur.DefocusBlurConfig], NoneType], image: vkit.element.image.Image, rng: Union[numpy.random._generator.Generator, NoneType]):
 86def defocus_blur_image(
 87    config: DefocusBlurConfig,
 88    state: Optional[DistortionNopState[DefocusBlurConfig]],
 89    image: Image,
 90    rng: Optional[RandomGenerator],
 91):
 92    # Generate blurring kernel.
 93    assert 0 < config.radius
 94    kernel_size = 2 * config.radius + 1
 95
 96    (
 97        anti_aliasing_ksize,
 98        anti_aliasing_kernel_padding,
 99    ) = _get_anti_aliasing_kernel_size_and_padding(config.anti_aliasing_sigma)
100    kernel_size += anti_aliasing_kernel_padding
101
102    begin = -(kernel_size // 2)
103    end = begin + kernel_size
104    coords = np.arange(begin, end)
105    x, y = np.meshgrid(coords, coords)
106    kernel: np.ndarray = ((x**2 + y**2) <= config.radius**2).astype(np.float32)
107    kernel /= kernel.sum()
108
109    kernel = _apply_anti_aliasing_to_kernel(
110        kernel,
111        anti_aliasing_ksize,
112        config.anti_aliasing_sigma,
113    )
114
115    # Convolution.
116    mode = image.mode
117    image = to_rgb_image(image, mode)
118
119    mat = cv.filter2D(image.mat, -1, kernel)
120    image = attrs.evolve(image, mat=mat)
121
122    image = to_original_image(image, mode)
123    return image
class MotionBlurConfig(vkit.mechanism.distortion.interface.DistortionConfig):
134class MotionBlurConfig(DistortionConfig):
135    radius: int
136    angle: int
137    anti_aliasing_sigma: float = 0.5
MotionBlurConfig(radius: int, angle: int, anti_aliasing_sigma: float = 0.5)
2def __init__(self, radius, angle, anti_aliasing_sigma=attr_dict['anti_aliasing_sigma'].default):
3    self.radius = radius
4    self.angle = angle
5    self.anti_aliasing_sigma = anti_aliasing_sigma

Method generated by attrs for class MotionBlurConfig.

def motion_blur_image( config: vkit.mechanism.distortion.photometric.blur.MotionBlurConfig, state: Union[vkit.mechanism.distortion.interface.DistortionNopState[vkit.mechanism.distortion.photometric.blur.MotionBlurConfig], NoneType], image: vkit.element.image.Image, rng: Union[numpy.random._generator.Generator, NoneType]):
140def motion_blur_image(
141    config: MotionBlurConfig,
142    state: Optional[DistortionNopState[MotionBlurConfig]],
143    image: Image,
144    rng: Optional[RandomGenerator],
145):
146    # Generate blurring kernel.
147    kernel_size = 2 * config.radius + 1
148
149    (
150        anti_aliasing_ksize,
151        anti_aliasing_kernel_padding,
152    ) = _get_anti_aliasing_kernel_size_and_padding(config.anti_aliasing_sigma)
153    anti_aliasing_kernel_padding_half = anti_aliasing_kernel_padding // 2
154    center = config.radius + anti_aliasing_kernel_padding_half
155    left = anti_aliasing_kernel_padding_half
156    right = left + kernel_size - 1
157    kernel_size += anti_aliasing_kernel_padding
158
159    kernel = np.zeros((kernel_size, kernel_size), dtype=np.float32)
160    kernel[center, left:right + 1] = 1.0
161
162    trans_mat = cv.getRotationMatrix2D(
163        (center, center),
164        # 1. to [0, 359].
165        # 2. getRotationMatrix2D accepts counter-clockwise angle, hence need to subtract.
166        360 - (config.angle % 360),
167        1.0,
168    )
169    kernel = cv.warpAffine(kernel, trans_mat, kernel.shape)
170    kernel /= kernel.sum()
171
172    kernel = _apply_anti_aliasing_to_kernel(
173        kernel,
174        anti_aliasing_ksize,
175        config.anti_aliasing_sigma,
176    )
177
178    # Convolution.
179    mode = image.mode
180    image = to_rgb_image(image, mode)
181
182    mat = cv.filter2D(image.mat, -1, kernel)
183    image = attrs.evolve(image, mat=mat)
184
185    image = to_original_image(image, mode)
186    return image
class GlassBlurConfig(vkit.mechanism.distortion.interface.DistortionConfig):
197class GlassBlurConfig(DistortionConfig):
198    sigma: float
199    delta: int = 1
200    loop: int = 5
201
202    _rng_state: Optional[Mapping[str, Any]] = None
203
204    @property
205    def supports_rng_state(self) -> bool:
206        return True
207
208    @property
209    def rng_state(self) -> Optional[Mapping[str, Any]]:
210        return self._rng_state
211
212    @rng_state.setter
213    def rng_state(self, val: Mapping[str, Any]):
214        self._rng_state = val
GlassBlurConfig( sigma: float, delta: int = 1, loop: int = 5, rng_state: Union[Mapping[str, Any], NoneType] = None)
2def __init__(self, sigma, delta=attr_dict['delta'].default, loop=attr_dict['loop'].default, rng_state=attr_dict['_rng_state'].default):
3    self.sigma = sigma
4    self.delta = delta
5    self.loop = loop
6    self._rng_state = rng_state

Method generated by attrs for class GlassBlurConfig.

def glass_blur_image( config: vkit.mechanism.distortion.photometric.blur.GlassBlurConfig, state: Union[vkit.mechanism.distortion.interface.DistortionNopState[vkit.mechanism.distortion.photometric.blur.GlassBlurConfig], NoneType], image: vkit.element.image.Image, rng: Union[numpy.random._generator.Generator, NoneType]):
217def glass_blur_image(
218    config: GlassBlurConfig,
219    state: Optional[DistortionNopState[GlassBlurConfig]],
220    image: Image,
221    rng: Optional[RandomGenerator],
222):
223    mode = image.mode
224    image = to_rgb_image(image, mode)
225
226    # Gaussian blur.
227    kernel_size = _estimate_gaussian_kernel_size(config.sigma)
228    ksize = (kernel_size, kernel_size)
229    mat = cv.GaussianBlur(image.mat, ksize, config.sigma)
230
231    # Random pixel swap.
232    assert rng is not None
233
234    coords_y = np.arange(image.height)
235    coords_x = np.arange(image.width)
236    pos_x, pos_y = np.meshgrid(coords_x, coords_y)
237
238    for _ in range(config.loop):
239        offset_y = rng.integers(0, 2 * config.delta + 1)
240        center_y = np.arange(offset_y, image.height - config.delta, 2 * config.delta + 1)
241        num_center_y = center_y.shape[0]
242        center_y = center_y.reshape(-1, 1)
243
244        offset_x = rng.integers(0, 2 * config.delta + 1)
245        center_x = np.arange(offset_x, image.width - config.delta, 2 * config.delta + 1)
246        num_center_x = center_x.shape[0]
247        center_x = center_x.reshape(1, -1)
248
249        delta_shape = (num_center_y, num_center_x)
250        delta_y = rng.integers(-config.delta, config.delta + 1, delta_shape)
251        delta_x = rng.integers(-config.delta, config.delta + 1, delta_shape)
252
253        deformed_y: np.ndarray = pos_y[center_y, center_x] + delta_y
254        deformed_y = np.clip(deformed_y, 0, image.height - 1)
255        deformed_x: np.ndarray = pos_x[center_y, center_x] + delta_x
256        deformed_x = np.clip(deformed_x, 0, image.width - 1)
257
258        # Swap.
259        pos_y[center_y, center_x], pos_y[deformed_y, deformed_x] = \
260            pos_y[deformed_y, deformed_x], pos_y[center_y, center_x]
261
262        pos_x[center_y, center_x], pos_x[deformed_y, deformed_x] = \
263            pos_x[deformed_y, deformed_x], pos_x[center_y, center_x]
264
265    mat = mat[pos_y, pos_x]
266    image = attrs.evolve(image, mat=mat)
267
268    image = to_original_image(image, mode)
269    return image
class ZoomInBlurConfig(vkit.mechanism.distortion.interface.DistortionConfig):
280class ZoomInBlurConfig(DistortionConfig):
281    ratio: float = 0.1
282    step: float = 0.01
283    alpha: float = 0.5
ZoomInBlurConfig(ratio: float = 0.1, step: float = 0.01, alpha: float = 0.5)
2def __init__(self, ratio=attr_dict['ratio'].default, step=attr_dict['step'].default, alpha=attr_dict['alpha'].default):
3    self.ratio = ratio
4    self.step = step
5    self.alpha = alpha

Method generated by attrs for class ZoomInBlurConfig.

def zoom_in_blur_image( config: vkit.mechanism.distortion.photometric.blur.ZoomInBlurConfig, state: Union[vkit.mechanism.distortion.interface.DistortionNopState[vkit.mechanism.distortion.photometric.blur.ZoomInBlurConfig], NoneType], image: vkit.element.image.Image, rng: Union[numpy.random._generator.Generator, NoneType]):
286def zoom_in_blur_image(
287    config: ZoomInBlurConfig,
288    state: Optional[DistortionNopState[ZoomInBlurConfig]],
289    image: Image,
290    rng: Optional[RandomGenerator],
291):
292    mode = image.mode
293    image = to_rgb_image(image, mode)
294
295    mat = image.mat.astype(np.uint16)
296    count = 1
297    for ratio in np.arange(
298        1 + config.step,
299        1 + config.ratio + config.step,
300        config.step,
301    ):
302        resized_height = round(image.height * ratio)
303        resized_width = round(image.width * ratio)
304        resized_image = image.to_resized_image(resized_height, resized_width)
305
306        pad_y = resized_height - image.height
307        up = pad_y // 2
308        down = up + image.height - 1
309
310        pad_x = resized_width - image.width
311        left = pad_x // 2
312        right = left + image.width - 1
313
314        mat += resized_image.mat[up:down + 1, left:right + 1]
315        count += 1
316
317    # NOTE: Label confusion could be significant if alpha is large.
318    mat: np.ndarray = (1 - config.alpha) * image.mat + config.alpha * np.round(mat / count)
319    mat = clip_mat_back_to_uint8(mat)
320
321    image = attrs.evolve(image, mat=mat)
322
323    image = to_original_image(image, mode)
324    return image