buteo.array.convolution_kernels

Convolutions kernels.

  1"""### Convolutions kernels. ###"""
  2
  3# Standard library
  4from typing import Tuple
  5
  6# External
  7import numpy as np
  8from numba import jit
  9
 10
 11
 12@jit(nopython=True, nogil=True, cache=True, fastmath=True, inline='always')
 13def _simple_blur_kernel_2d_3x3() -> Tuple[np.ndarray, np.ndarray]:
 14    """Create a 2D blur kernel.
 15
 16    The kernel has the following form:
 17    ```python
 18    >>> weights = [
 19    ...     0.08422299, 0.12822174, 0.08422299,
 20    ...     0.12822174, 0.15022110, 0.12822174,
 21    ...     0.08422299, 0.12822174, 0.08422299,
 22    ... ]
 23    ```
 24    Returns
 25    -------
 26    Tuple[np.ndarray, np.ndarray]
 27        A tuple of two numpy arrays. The first array contains the (x, y) offsets
 28        of the kernel values. The second array contains the corresponding weights
 29        of each kernel value.
 30    """
 31    offsets = np.array([
 32        [ 1, -1], [ 1, 0], [ 1, 1],
 33        [ 0, -1], [ 0, 0], [ 0, 1],
 34        [-1, -1], [-1, 0], [-1, 1],
 35    ])
 36
 37    weights = np.array([
 38        0.08422299, 0.12822174, 0.08422299,
 39        0.12822174, 0.1502211 , 0.12822174,
 40        0.08422299, 0.12822174, 0.08422299,
 41    ], dtype=np.float32)
 42
 43    return offsets, weights
 44
 45
 46@jit(nopython=True, nogil=True, cache=True, fastmath=True, inline='always')
 47def _simple_unsharp_kernel_2d_3x3() -> Tuple[np.ndarray, np.ndarray]:
 48    """Create a 2D unsharp kernel.
 49
 50    Returns
 51    -------
 52    Tuple[np.ndarray, np.ndarray]
 53        A tuple of two numpy arrays. The first array contains the (x, y) offsets
 54        of the kernel values. The second array contains the corresponding weights
 55        of each kernel value.
 56    """
 57    offsets = np.array([
 58        [ 1, -1], [ 1, 0], [ 1, 1],
 59        [ 0, -1], [ 0, 0], [ 0, 1],
 60        [-1, -1], [-1, 0], [-1, 1],
 61    ])
 62
 63    weights = np.array([
 64        -0.09911165, -0.15088834, -0.09911165,
 65        -0.15088834,  2.        , -0.15088834,
 66        -0.09911165, -0.15088834, -0.09911165,
 67    ], dtype=np.float32)
 68
 69    return offsets, weights
 70
 71
 72@jit(nopython=True, nogil=True, cache=True, fastmath=True, inline='always')
 73def _simple_shift_kernel_2d(
 74    x_offset: float,
 75    y_offset: float,
 76) -> Tuple[np.ndarray, np.ndarray]:
 77    """Create a 2D shift kernel. For augmentations."""
 78    return kernel_shift(x_offset, y_offset)
 79
 80
 81@jit(nopython=True, nogil=True, cache=True, fastmath=True, inline="always")
 82def _distance_2D(p1: np.ndarray, p2: np.ndarray) -> float:
 83    """Returns the distance between two points. (2D)"""
 84    d1 = (p1[0] - p2[0]) ** 2
 85    d2 = (p1[1] - p2[1]) ** 2
 86
 87    return np.sqrt(d1 + d2)
 88
 89
 90@jit(nopython=True, nogil=True, cache=True, fastmath=True, parallel=True)
 91def _area_covered(square, radius):
 92    """Calculates the area covered by a circle within a square.
 93    Monte-carlo(ish) method. Can be parallelized.
 94    """
 95    n_points = 100
 96    min_y = square[:, 0].min()
 97    max_y = square[:, 0].max()
 98    min_x = square[:, 1].min()
 99    max_x = square[:, 1].max()
100
101    steps = int(np.rint(np.sqrt(n_points)))
102    range_y = np.linspace(min_x, max_x, steps)
103    range_x = np.linspace(min_y, max_y, steps)
104
105    center = np.array([0.0, 0.0], dtype=np.float32)
106    adjusted_radius = radius + 0.5
107
108    points_within = 0
109    for y in range_y:
110        for x in range_x:
111            point = np.array([y, x], dtype=np.float32)
112            if _distance_2D(center, point) <= adjusted_radius:
113                points_within += 1
114
115    area = points_within / (steps ** 2)
116
117    return area
118
119
120@jit(nopython=True, nogil=True, cache=True, fastmath=True, parallel=True)
121def _circular_kernel_2D(radius):
122    """Creates a circular 2D kernel. Supports fractional radii."""
123    size = np.int64(np.ceil(radius) * 2 + 1)
124    kernel = np.zeros((size, size), dtype=np.float32)
125
126    center = np.array([0.0, 0.0], dtype=np.float32)
127
128    step = size // 2
129    for idx_i, col in enumerate(range(-step, step + 1)):
130        for idx_j, row in enumerate(range(-step, step + 1)):
131            square = np.zeros((4, 2), dtype=np.float32)
132            square[0] = np.array([col - 0.5, row - 0.5], dtype=np.float32)
133            square[1] = np.array([col + 0.5, row - 0.5], dtype=np.float32)
134            square[2] = np.array([col + 0.5, row + 0.5], dtype=np.float32)
135            square[3] = np.array([col - 0.5, row + 0.5], dtype=np.float32)
136
137            within = np.zeros(4, dtype=np.uint8)
138            for i in range(4):
139                within[i] = _distance_2D(center, square[i]) <= radius + 0.5
140
141            # Case 1: completely inside
142            if within.sum() == 4:
143                kernel[idx_i][idx_j] = 1.0
144
145            # Case 2: completely outside
146            elif within.sum() == 0:
147                kernel[idx_i][idx_j] = 0.0
148
149            # Case 3: partially inside
150            else:
151                kernel[idx_i][idx_j] = _area_covered(square, radius)
152
153    return kernel
154
155
156@jit(nopython=True, nogil=True, cache=True, fastmath=True, parallel=True)
157def _distance_weighted_kernel_2D(radius, method, decay=0.2, sigma=2.0):
158    """Creates a distance weighted kernel.
159
160    Parameters
161    ----------
162
163    radius : float
164        Radius of the kernel.
165
166    method : int
167        Method to use for weighting.
168        0. linear
169        1. sqrt
170        2. power
171        3. log
172        4. gaussian
173        5. constant
174    """
175    size = np.int64(np.ceil(radius) * 2 + 1)
176    kernel = np.zeros((size, size), dtype=np.float32)
177
178    center = np.array([0.0, 0.0], dtype=np.float32)
179
180    step = size // 2
181    for idx_i, col in enumerate(range(-step, step + 1)):
182        for idx_j, row in enumerate(range(-step, step + 1)):
183            point = np.array([col, row], dtype=np.float32)
184            distance = _distance_2D(center, point)
185
186            # Linear
187            if method == 0:
188                kernel[idx_i, idx_j] = np.power((1 - decay), distance)
189
190            # Sqrt
191            elif method == 1:
192                kernel[idx_i, idx_j] = np.power(np.sqrt((1 - decay)), distance)
193
194            # Power
195            elif method == 2:
196                kernel[idx_i, idx_j] = np.power(np.power((1 - decay), 2), distance)
197
198            # Gaussian
199            elif method == 3:
200                kernel[idx_i, idx_j] = np.exp(-(np.power(distance, 2)) / (2 * np.power(sigma, 2)))
201
202            # Constant
203            else:
204                kernel[idx_i, idx_j] = 1.0
205
206    return kernel
207
208
209def kernel_base(
210    radius: float,
211    circular: bool = False,
212    distance_weighted: bool = False,
213    normalised: bool = True,
214    hole: bool = False,
215    method: int = 0,
216    decay: float = 0.2,
217    sigma: float = 2.0,
218) -> np.ndarray:
219    """Creates a 2D kernel for convolution.
220
221    Parameters
222    ----------
223    radius : float
224        Radius of the kernel.
225
226    circular : bool
227        Whether to use a circular kernel.
228
229    distance_weighted : bool
230        Whether to use a distance weighted kernel.
231
232    normalised : bool
233        Whether to normalise the kernel.
234
235    hole : bool
236        Whether to create a hole in the center of the kernel.
237
238    method : int
239        Method to use for weighting.
240        0. linear
241        1. sqrt
242        2. power
243        3. gaussian
244        4. constant
245
246    decay : float
247        Decay rate for distance weighted kernels. Only used if `distance_weighted` is True.
248
249    sigma : float
250        Sigma for gaussian distance weighted kernels. Only used if `distance_weighted` is True and `method` is 3.
251
252    Returns
253    -------
254    kernel : np.ndarray
255        The kernel.
256    """
257    size = np.int64(np.ceil(radius) * 2 + 1)
258    kernel = np.ones((size, size), dtype=np.float32)
259
260    if hole:
261        kernel[size // 2, size // 2] = 0.0
262
263    if circular:
264        circular_kernel = _circular_kernel_2D(radius)
265        kernel *= circular_kernel
266
267    if distance_weighted:
268        distance_weighted_kernel = _distance_weighted_kernel_2D(
269            radius,
270            method,
271            decay,
272            sigma,
273        )
274        kernel *= distance_weighted_kernel
275
276    if normalised:
277        kernel /= np.sum(kernel)
278
279    return kernel
280
281
282
283@jit(nopython=True, nogil=True, cache=True, fastmath=True, inline='always')
284def kernel_shift(
285    x_offset: float,
286    y_offset: float,
287) -> Tuple[np.ndarray, np.ndarray]:
288    """Create a 2D shift kernel.
289
290    This function returns a kernel that can be used to shift a raster by a fractional
291    number of pixels in the x and y directions. The kernel can also be used to simulate
292    channel misalignment in image augmentation.
293
294    Parameters
295    ----------
296    x_offset : float
297        The horizontal (x) offset to apply.
298
299    y_offset : float
300        The vertical (y) offset to apply.
301
302    Returns
303    -------
304    Tuple[np.ndarray, np.ndarray]
305        A tuple of two numpy arrays. The first array contains the (x, y) offsets
306        of the kernel values. The second array contains the corresponding weights
307        of each kernel value.
308    """
309    if x_offset == 0.0 and y_offset == 0.0:
310        offsets = np.array([[0, 0]], dtype=np.int64)
311        weights = np.array([1.0], dtype=np.float32)
312
313        return offsets, weights
314
315    y0 = [int(np.floor(y_offset)), int(np.ceil(y_offset))] if y_offset != 0 else [0, 0]
316    x0 = [int(np.floor(x_offset)), int(np.ceil(x_offset))] if x_offset != 0 else [0, 0]
317
318    if x_offset == 0.0 or x_offset % 1 == 0.0:
319        offsets = np.zeros((2, 2), dtype=np.int64)
320        weights = np.zeros(2, dtype=np.float32)
321
322        offsets[0] = [int(x_offset) if x_offset % 1 == 0.0 else 0, y0[0]]
323        offsets[1] = [int(x_offset) if x_offset % 1 == 0.0 else 0, y0[1]]
324
325        weights[0] = y_offset - y0[0]
326        weights[1] = 1 - weights[0]
327
328    elif y_offset == 0.0 or y_offset % 1 == 0.0:
329        offsets = np.zeros((2, 2), dtype=np.int64)
330        weights = np.zeros(2, dtype=np.float32)
331
332        offsets[0] = [x0[0], int(y_offset) if y_offset % 1 == 0.0 else 0]
333        offsets[1] = [x0[1], int(y_offset) if y_offset % 1 == 0.0 else 0]
334
335        weights[0] = x_offset - x0[0]
336        weights[1] = 1 - weights[0]
337
338    else:
339        offsets = np.zeros((4, 2), dtype=np.int64)
340        weights = np.zeros(4, dtype=np.float32)
341
342        offsets[0] = [x0[0], y0[0]]
343        offsets[1] = [x0[0], y0[1]]
344        offsets[2] = [x0[1], y0[0]]
345        offsets[3] = [x0[1], y0[1]]
346
347        weights[0] = (1 - (x_offset - offsets[0][0])) * (1 - (y_offset - offsets[0][1]))
348        weights[1] = (1 - (x_offset - offsets[1][0])) * (1 + (y_offset - offsets[1][1]))
349        weights[2] = (1 + (x_offset - offsets[2][0])) * (1 - (y_offset - offsets[2][1]))
350        weights[3] = (1 + (x_offset - offsets[3][0])) * (1 + (y_offset - offsets[3][1]))
351
352    return offsets, weights
353
354
355@jit(nopython=True, nogil=True, cache=True, fastmath=True)
356def kernel_unsharp(
357    radius: float = 1.0,
358    intensity: float = 1.0,
359) -> np.ndarray:
360    """Create a 2D unsharp kernel.
361
362    This function returns a kernel that can be used to apply an unsharp mask to an image.
363
364    Parameters
365    ----------
366    radius : float, optional
367        The radius of the kernel. Default: 1.0.
368
369    intensity : float, optional
370        The intensity of the unsharp mask. Default: 1.0.
371
372    Returns
373    -------
374    np.ndarray
375        The kernel.
376    """
377    kernel = kernel_base(
378        radius=radius,
379        circular=True,
380        distance_weighted=True,
381        method=3,
382        normalised=True,
383        hole=True,
384    )
385
386    kernel *= intensity
387    kernel_sum = np.sum(kernel) + 1.0
388    kernel *= -1.0
389    kernel[kernel.shape[0] // 2, kernel.shape[1] // 2] = kernel_sum
390
391    return kernel
392
393
394@jit(nopython=True, nogil=True, cache=True, fastmath=True, parallel=True)
395def kernel_sobel(
396    radius=1,
397    scale=2,
398) -> Tuple[np.ndarray, np.ndarray]:
399    """Creates a 2D Sobel style kernel consisting of a horizontal and vertical component.
400    This function returns a kernel that can be used to apply a Sobel filter to an image.
401
402    `kernel_gx, kernel_gy = get_kernel_sobel(radius=1, scale=2)`
403
404    The kernels for radis=2, scale=2 are:
405    ```python
406    gx = [
407        [ 0.56  0.85  0.   -0.85 -0.56],
408        [ 0.85  1.5   0.   -1.5  -0.85],
409        [ 1.    2.    0.   -2.   -1.  ],
410        [ 0.85  1.5   0.   -1.5  -0.85],
411        [ 0.56  0.85  0.   -0.85 -0.56],
412    ]
413
414    gy = [
415        [ 0.56  0.85  1.    0.85  0.56],
416        [ 0.85  1.5   2.    1.5   0.85],
417        [ 0.    0.    0.    0.    0.  ],
418        [-0.85 -1.5  -2.   -1.5  -0.85],
419        [-0.56 -0.85 -1.   -0.85 -0.56],
420    ]
421    ```
422
423    Parameters
424    ----------
425    radius : float, optional
426        The radius of the kernel. Default: 1.0.
427
428    scale : float, optional
429        The scale of the kernel. Default: 2.0.
430
431    Returns
432    -------
433    Tuple[np.ndarray, np.ndarray]
434        The kernels
435
436    """
437    size = np.int64(np.ceil(radius) * 2 + 1)
438    kernel = np.zeros((size, size), dtype=np.float32)
439    center = np.array([0.0, 0.0], dtype=np.float32)
440
441    step = size // 2
442    for idx_i, col in enumerate(range(-step, step + 1)):
443        for idx_j, row in enumerate(range(-step, step + 1)):
444            point = np.array([col, row], dtype=np.float32)
445            distance = _distance_2D(center, point)
446            if col == 0 and row == 0:
447                kernel[idx_i, idx_j] = 0
448            else:
449                weight = np.power((1 - 0.5), distance) * 2
450                kernel[idx_i, idx_j] = weight * scale
451
452    # vertical
453    kernel_gx = kernel.copy()
454    kernel_gx[:, size // 2:] *= -1
455    kernel_gx[:, size // 2] = 0
456
457    # horisontal
458    kernel_gy = kernel.copy()
459    kernel_gy[size // 2:, :] *= -1
460    kernel_gy[size // 2, :] = 0
461
462    return kernel_gx, kernel_gy
463
464
465@jit(nopython=True, nogil=True, cache=True, fastmath=True)
466def kernel_get_offsets_and_weights(
467    kernel: np.ndarray,
468    remove_zero_weights: bool = True,
469) -> Tuple[np.ndarray, np.ndarray, int]:
470    """Generates a list of offsets and weights for a given kernel.
471
472    Parameters
473    ----------
474    kernel : np.ndarray
475        The kernel to generate offsets and weights for.
476
477    remove_zero_weights : bool, optional
478        Whether to remove offsets and weights with zero weights. Default: True.
479
480    Returns
481    -------
482    Tuple[np.ndarray, np.ndarray]
483        The offsets and weights
484    """
485    count = np.count_nonzero(kernel) if remove_zero_weights else kernel.size
486
487    offsets = np.zeros((count, 2), dtype=np.int64)
488    weights = np.zeros((count), dtype=np.float32)
489
490    step = kernel.shape[0] // 2
491    index = 0
492    for col in range(-step, step + 1):
493        for row in range(-step, step + 1):
494            if kernel[col + step, row + step] != 0.0:
495                offsets[index][0] = col
496                offsets[index][1] = row
497                weights[index] = kernel[col + step, row + step]
498
499                index += 1
500
501    return offsets, weights
def kernel_base( radius: float, circular: bool = False, distance_weighted: bool = False, normalised: bool = True, hole: bool = False, method: int = 0, decay: float = 0.2, sigma: float = 2.0) -> numpy.ndarray:
210def kernel_base(
211    radius: float,
212    circular: bool = False,
213    distance_weighted: bool = False,
214    normalised: bool = True,
215    hole: bool = False,
216    method: int = 0,
217    decay: float = 0.2,
218    sigma: float = 2.0,
219) -> np.ndarray:
220    """Creates a 2D kernel for convolution.
221
222    Parameters
223    ----------
224    radius : float
225        Radius of the kernel.
226
227    circular : bool
228        Whether to use a circular kernel.
229
230    distance_weighted : bool
231        Whether to use a distance weighted kernel.
232
233    normalised : bool
234        Whether to normalise the kernel.
235
236    hole : bool
237        Whether to create a hole in the center of the kernel.
238
239    method : int
240        Method to use for weighting.
241        0. linear
242        1. sqrt
243        2. power
244        3. gaussian
245        4. constant
246
247    decay : float
248        Decay rate for distance weighted kernels. Only used if `distance_weighted` is True.
249
250    sigma : float
251        Sigma for gaussian distance weighted kernels. Only used if `distance_weighted` is True and `method` is 3.
252
253    Returns
254    -------
255    kernel : np.ndarray
256        The kernel.
257    """
258    size = np.int64(np.ceil(radius) * 2 + 1)
259    kernel = np.ones((size, size), dtype=np.float32)
260
261    if hole:
262        kernel[size // 2, size // 2] = 0.0
263
264    if circular:
265        circular_kernel = _circular_kernel_2D(radius)
266        kernel *= circular_kernel
267
268    if distance_weighted:
269        distance_weighted_kernel = _distance_weighted_kernel_2D(
270            radius,
271            method,
272            decay,
273            sigma,
274        )
275        kernel *= distance_weighted_kernel
276
277    if normalised:
278        kernel /= np.sum(kernel)
279
280    return kernel

Creates a 2D kernel for convolution.

Parameters

radius : float Radius of the kernel.

circular : bool Whether to use a circular kernel.

distance_weighted : bool Whether to use a distance weighted kernel.

normalised : bool Whether to normalise the kernel.

hole : bool Whether to create a hole in the center of the kernel.

method : int Method to use for weighting. 0. linear 1. sqrt 2. power 3. gaussian 4. constant

decay : float Decay rate for distance weighted kernels. Only used if distance_weighted is True.

sigma : float Sigma for gaussian distance weighted kernels. Only used if distance_weighted is True and method is 3.

Returns

kernel : np.ndarray The kernel.

@jit(nopython=True, nogil=True, cache=True, fastmath=True, inline='always')
def kernel_shift(x_offset: float, y_offset: float) -> Tuple[numpy.ndarray, numpy.ndarray]:
284@jit(nopython=True, nogil=True, cache=True, fastmath=True, inline='always')
285def kernel_shift(
286    x_offset: float,
287    y_offset: float,
288) -> Tuple[np.ndarray, np.ndarray]:
289    """Create a 2D shift kernel.
290
291    This function returns a kernel that can be used to shift a raster by a fractional
292    number of pixels in the x and y directions. The kernel can also be used to simulate
293    channel misalignment in image augmentation.
294
295    Parameters
296    ----------
297    x_offset : float
298        The horizontal (x) offset to apply.
299
300    y_offset : float
301        The vertical (y) offset to apply.
302
303    Returns
304    -------
305    Tuple[np.ndarray, np.ndarray]
306        A tuple of two numpy arrays. The first array contains the (x, y) offsets
307        of the kernel values. The second array contains the corresponding weights
308        of each kernel value.
309    """
310    if x_offset == 0.0 and y_offset == 0.0:
311        offsets = np.array([[0, 0]], dtype=np.int64)
312        weights = np.array([1.0], dtype=np.float32)
313
314        return offsets, weights
315
316    y0 = [int(np.floor(y_offset)), int(np.ceil(y_offset))] if y_offset != 0 else [0, 0]
317    x0 = [int(np.floor(x_offset)), int(np.ceil(x_offset))] if x_offset != 0 else [0, 0]
318
319    if x_offset == 0.0 or x_offset % 1 == 0.0:
320        offsets = np.zeros((2, 2), dtype=np.int64)
321        weights = np.zeros(2, dtype=np.float32)
322
323        offsets[0] = [int(x_offset) if x_offset % 1 == 0.0 else 0, y0[0]]
324        offsets[1] = [int(x_offset) if x_offset % 1 == 0.0 else 0, y0[1]]
325
326        weights[0] = y_offset - y0[0]
327        weights[1] = 1 - weights[0]
328
329    elif y_offset == 0.0 or y_offset % 1 == 0.0:
330        offsets = np.zeros((2, 2), dtype=np.int64)
331        weights = np.zeros(2, dtype=np.float32)
332
333        offsets[0] = [x0[0], int(y_offset) if y_offset % 1 == 0.0 else 0]
334        offsets[1] = [x0[1], int(y_offset) if y_offset % 1 == 0.0 else 0]
335
336        weights[0] = x_offset - x0[0]
337        weights[1] = 1 - weights[0]
338
339    else:
340        offsets = np.zeros((4, 2), dtype=np.int64)
341        weights = np.zeros(4, dtype=np.float32)
342
343        offsets[0] = [x0[0], y0[0]]
344        offsets[1] = [x0[0], y0[1]]
345        offsets[2] = [x0[1], y0[0]]
346        offsets[3] = [x0[1], y0[1]]
347
348        weights[0] = (1 - (x_offset - offsets[0][0])) * (1 - (y_offset - offsets[0][1]))
349        weights[1] = (1 - (x_offset - offsets[1][0])) * (1 + (y_offset - offsets[1][1]))
350        weights[2] = (1 + (x_offset - offsets[2][0])) * (1 - (y_offset - offsets[2][1]))
351        weights[3] = (1 + (x_offset - offsets[3][0])) * (1 + (y_offset - offsets[3][1]))
352
353    return offsets, weights

Create a 2D shift kernel.

This function returns a kernel that can be used to shift a raster by a fractional number of pixels in the x and y directions. The kernel can also be used to simulate channel misalignment in image augmentation.

Parameters

x_offset : float The horizontal (x) offset to apply.

y_offset : float The vertical (y) offset to apply.

Returns

Tuple[np.ndarray, np.ndarray] A tuple of two numpy arrays. The first array contains the (x, y) offsets of the kernel values. The second array contains the corresponding weights of each kernel value.

@jit(nopython=True, nogil=True, cache=True, fastmath=True)
def kernel_unsharp(radius: float = 1.0, intensity: float = 1.0) -> numpy.ndarray:
356@jit(nopython=True, nogil=True, cache=True, fastmath=True)
357def kernel_unsharp(
358    radius: float = 1.0,
359    intensity: float = 1.0,
360) -> np.ndarray:
361    """Create a 2D unsharp kernel.
362
363    This function returns a kernel that can be used to apply an unsharp mask to an image.
364
365    Parameters
366    ----------
367    radius : float, optional
368        The radius of the kernel. Default: 1.0.
369
370    intensity : float, optional
371        The intensity of the unsharp mask. Default: 1.0.
372
373    Returns
374    -------
375    np.ndarray
376        The kernel.
377    """
378    kernel = kernel_base(
379        radius=radius,
380        circular=True,
381        distance_weighted=True,
382        method=3,
383        normalised=True,
384        hole=True,
385    )
386
387    kernel *= intensity
388    kernel_sum = np.sum(kernel) + 1.0
389    kernel *= -1.0
390    kernel[kernel.shape[0] // 2, kernel.shape[1] // 2] = kernel_sum
391
392    return kernel

Create a 2D unsharp kernel.

This function returns a kernel that can be used to apply an unsharp mask to an image.

Parameters

radius : float, optional The radius of the kernel. Default: 1.0.

intensity : float, optional The intensity of the unsharp mask. Default: 1.0.

Returns

np.ndarray The kernel.

@jit(nopython=True, nogil=True, cache=True, fastmath=True, parallel=True)
def kernel_sobel(radius=1, scale=2) -> Tuple[numpy.ndarray, numpy.ndarray]:
395@jit(nopython=True, nogil=True, cache=True, fastmath=True, parallel=True)
396def kernel_sobel(
397    radius=1,
398    scale=2,
399) -> Tuple[np.ndarray, np.ndarray]:
400    """Creates a 2D Sobel style kernel consisting of a horizontal and vertical component.
401    This function returns a kernel that can be used to apply a Sobel filter to an image.
402
403    `kernel_gx, kernel_gy = get_kernel_sobel(radius=1, scale=2)`
404
405    The kernels for radis=2, scale=2 are:
406    ```python
407    gx = [
408        [ 0.56  0.85  0.   -0.85 -0.56],
409        [ 0.85  1.5   0.   -1.5  -0.85],
410        [ 1.    2.    0.   -2.   -1.  ],
411        [ 0.85  1.5   0.   -1.5  -0.85],
412        [ 0.56  0.85  0.   -0.85 -0.56],
413    ]
414
415    gy = [
416        [ 0.56  0.85  1.    0.85  0.56],
417        [ 0.85  1.5   2.    1.5   0.85],
418        [ 0.    0.    0.    0.    0.  ],
419        [-0.85 -1.5  -2.   -1.5  -0.85],
420        [-0.56 -0.85 -1.   -0.85 -0.56],
421    ]
422    ```
423
424    Parameters
425    ----------
426    radius : float, optional
427        The radius of the kernel. Default: 1.0.
428
429    scale : float, optional
430        The scale of the kernel. Default: 2.0.
431
432    Returns
433    -------
434    Tuple[np.ndarray, np.ndarray]
435        The kernels
436
437    """
438    size = np.int64(np.ceil(radius) * 2 + 1)
439    kernel = np.zeros((size, size), dtype=np.float32)
440    center = np.array([0.0, 0.0], dtype=np.float32)
441
442    step = size // 2
443    for idx_i, col in enumerate(range(-step, step + 1)):
444        for idx_j, row in enumerate(range(-step, step + 1)):
445            point = np.array([col, row], dtype=np.float32)
446            distance = _distance_2D(center, point)
447            if col == 0 and row == 0:
448                kernel[idx_i, idx_j] = 0
449            else:
450                weight = np.power((1 - 0.5), distance) * 2
451                kernel[idx_i, idx_j] = weight * scale
452
453    # vertical
454    kernel_gx = kernel.copy()
455    kernel_gx[:, size // 2:] *= -1
456    kernel_gx[:, size // 2] = 0
457
458    # horisontal
459    kernel_gy = kernel.copy()
460    kernel_gy[size // 2:, :] *= -1
461    kernel_gy[size // 2, :] = 0
462
463    return kernel_gx, kernel_gy

Creates a 2D Sobel style kernel consisting of a horizontal and vertical component. This function returns a kernel that can be used to apply a Sobel filter to an image.

kernel_gx, kernel_gy = get_kernel_sobel(radius=1, scale=2)

The kernels for radis=2, scale=2 are:

gx = [
    [ 0.56  0.85  0.   -0.85 -0.56],
    [ 0.85  1.5   0.   -1.5  -0.85],
    [ 1.    2.    0.   -2.   -1.  ],
    [ 0.85  1.5   0.   -1.5  -0.85],
    [ 0.56  0.85  0.   -0.85 -0.56],
]

gy = [
    [ 0.56  0.85  1.    0.85  0.56],
    [ 0.85  1.5   2.    1.5   0.85],
    [ 0.    0.    0.    0.    0.  ],
    [-0.85 -1.5  -2.   -1.5  -0.85],
    [-0.56 -0.85 -1.   -0.85 -0.56],
]

Parameters

radius : float, optional The radius of the kernel. Default: 1.0.

scale : float, optional The scale of the kernel. Default: 2.0.

Returns

Tuple[np.ndarray, np.ndarray] The kernels

@jit(nopython=True, nogil=True, cache=True, fastmath=True)
def kernel_get_offsets_and_weights( kernel: numpy.ndarray, remove_zero_weights: bool = True) -> Tuple[numpy.ndarray, numpy.ndarray, int]:
466@jit(nopython=True, nogil=True, cache=True, fastmath=True)
467def kernel_get_offsets_and_weights(
468    kernel: np.ndarray,
469    remove_zero_weights: bool = True,
470) -> Tuple[np.ndarray, np.ndarray, int]:
471    """Generates a list of offsets and weights for a given kernel.
472
473    Parameters
474    ----------
475    kernel : np.ndarray
476        The kernel to generate offsets and weights for.
477
478    remove_zero_weights : bool, optional
479        Whether to remove offsets and weights with zero weights. Default: True.
480
481    Returns
482    -------
483    Tuple[np.ndarray, np.ndarray]
484        The offsets and weights
485    """
486    count = np.count_nonzero(kernel) if remove_zero_weights else kernel.size
487
488    offsets = np.zeros((count, 2), dtype=np.int64)
489    weights = np.zeros((count), dtype=np.float32)
490
491    step = kernel.shape[0] // 2
492    index = 0
493    for col in range(-step, step + 1):
494        for row in range(-step, step + 1):
495            if kernel[col + step, row + step] != 0.0:
496                offsets[index][0] = col
497                offsets[index][1] = row
498                weights[index] = kernel[col + step, row + step]
499
500                index += 1
501
502    return offsets, weights

Generates a list of offsets and weights for a given kernel.

Parameters

kernel : np.ndarray The kernel to generate offsets and weights for.

remove_zero_weights : bool, optional Whether to remove offsets and weights with zero weights. Default: True.

Returns

Tuple[np.ndarray, np.ndarray] The offsets and weights