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)
Inherited Members
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
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.
Inherited Members
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
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.
Inherited Members
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
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.
Inherited Members
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
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.
Inherited Members
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