Skip to content

Commit

Permalink
feat(filters): add more linear filters
Browse files Browse the repository at this point in the history
- Add laplacian, sobel, and robert cross filters

- Add a utility function to apply padding and convolution
  • Loading branch information
Preet-Sojitra committed Jun 4, 2024
1 parent f2a2e9a commit 6958032
Show file tree
Hide file tree
Showing 2 changed files with 180 additions and 16 deletions.
110 changes: 94 additions & 16 deletions src/imgcv/filters/linear.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import numpy as np
from imgcv.common import check_image
from imgcv.filters.utils import apply_convolution


def box_filter(img, filter_size):
Expand All @@ -20,32 +21,109 @@ def box_filter(img, filter_size):
raise ValueError("filter_size should be a tuple")

if len(img.shape) == 2:
kernel = np.ones(filter_size)
final_img = apply_convolution(img, [kernel], seperate=False, take_mean=True)

pad_height = filter_size[0] // 2
pad_width = filter_size[1] // 2
return final_img
else:
# apply filter to each channel
final_img = np.zeros(img.shape)
for i in range(img.shape[2]):
final_img[:, :, i] = box_filter(img[:, :, i], filter_size)
return final_img

padded_img = np.pad(
img, ((pad_height, pad_height), (pad_width, pad_width)), mode="constant"
)

kernel = np.ones(filter_size)
def laplacian_filter(img, diagonal=False, return_edges=True):
"""Apply Laplacian filter to the image.
Args:
img (np.ndarray): Input Image array
diagonal (bool, optional): If True, apply diagonal laplacian filter. Defaults to False.
return_edges (bool, optional): If True, return image with edges detected else return the sharpened image. Defaults to True.
Raises:
ValueError: If diagonal is not a boolean value.
Returns:
np.ndarray: Image with edges detected
"""
check_image(img)

# convolve
if not isinstance(diagonal, bool):
raise ValueError(f"diagonal should be a boolean value. Got {diagonal}")

if len(img.shape) == 2:
if diagonal:
if return_edges:
kernel = np.array([[-1, -1, -1], [-1, 8, -1], [-1, -1, -1]])
else:
kernel = np.array([[-1, -1, -1], [-1, 9, -1], [-1, -1, -1]])
if not diagonal:
if return_edges:
kernel = np.array([[0, -1, 0], [-1, 4, -1], [0, -1, 0]])
else:
kernel = np.array([[0, -1, 0], [-1, 5, -1], [0, -1, 0]])

final_img = apply_convolution(img, [kernel], seperate=False)

return final_img
else:
# apply filter to each channel
final_img = np.zeros(img.shape)
for row in range(img.shape[0]):
for col in range(img.shape[1]):
window = padded_img[
row : row + filter_size[0], col : col + filter_size[1]
]
result = np.mean(window * kernel)
result = np.clip(result, 0, 255)
final_img[row, col] = np.round(result)
for i in range(img.shape[2]):
final_img[:, :, i] = laplacian_filter(img[:, :, i], diagonal, return_edges)
return final_img.astype(np.uint8)


def robert_cross_filter(img):
"""Apply Robert Cross filter to the image. This uses 2x2 kernels. Extending this to 3x3 kernel will give us Sobel filter.
Args:
img (np.ndarray): Input Image array
Returns:
np.ndarray: Image with edges detected
"""
check_image(img)

if len(img.shape) == 2:
kernel_x = np.array([[-1, 0], [0, 1]]) # kerenel for x direction
kernel_y = np.array([[0, -1], [1, 0]]) # kernel for y direction

final_img = apply_convolution(img, [kernel_x, kernel_y], seperate=True)

return final_img
else:
# apply filter to each channel
final_img = np.zeros(img.shape)
for i in range(img.shape[2]):
final_img[:, :, i] = robert_cross_filter(img[:, :, i])
return final_img.astype(np.uint8)


def sobel_filter(img):
"""
Apply Sobel filter to the image. This uses 3x3 kernels. This is extension of Robert Cross filter. Center of kerenel is weighted more than the corners. This gives us more accurate edge detection.
Args:
img (np.ndarray): Input Image array
Returns:
np.ndarray: Image with edges detected
"""

check_image(img)

if len(img.shape) == 2:
kernel_x = np.array([[-1, -2, -1], [0, 0, 0], [1, 2, 1]])
kernel_y = np.array([[-1, 0, 1], [-2, 0, 2], [-1, 0, 1]])

final_img = apply_convolution(img, [kernel_x, kernel_y], seperate=True)

return final_img
else:
# apply filter to each channel
final_img = np.zeros(img.shape)
for i in range(img.shape[2]):
final_img[:, :, i] = box_filter(img[:, :, i], filter_size)
final_img[:, :, i] = sobel_filter(img[:, :, i])
return final_img.astype(np.uint8)
86 changes: 86 additions & 0 deletions src/imgcv/filters/utils.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
import numpy as np


def pad_image(img, filter_size):
"""Pad the image with zeros.
Args:
img (np.ndarray): Input Image array
filter_size (Tuple[int, int]): Size of the filter.
Returns:
np.ndarray: Padded image array.
"""
pad_height = filter_size[0] // 2
pad_width = filter_size[1] // 2

return np.pad(
img, ((pad_height, pad_height), (pad_width, pad_width)), mode="constant"
)


def apply_convolution(img, kernels, seperate, take_mean=False):
"""Apply convolution operation to the image.
Args:
img (np.ndarray): Input Image array
kernels (List[np.ndarray, np.ndarray]): List of kernels to apply.
seperate (bool): If True, apply the kernels seperately in x and y direction respectively.
Raises:
ValueError: If kernels is not a list of length 1 or 2.
ValueError: If seperate is not a boolean value.
ValueError: If both kernels are not of same shape.
Returns:
np.ndarray: Image array after applying convolution.
"""

# kernels must be a list
if not isinstance(kernels, list):
raise ValueError("kernels should be a list")

# kernels should be a list of length 1 or 2
if len(kernels) not in [1, 2]:
raise ValueError("Kernels should be a of length 2")

# seperate should be a boolean value
if not isinstance(seperate, bool):
raise ValueError("seperate should be a boolean value")

# if kernels are of length 2, they should be applied seperately. Thus both kernels should be of same shape
if len(kernels) == 2 and seperate:
if kernels[0].shape != kernels[1].shape:
raise ValueError("Both kernels should be of same shape")

# if kernels are of length 1 or 2, padding is done based on the first kernel. Because if there is second kernel then it will be of same shape as first kernel
padded_img = pad_image(img, kernels[0].shape)

final_img = np.zeros(img.shape)
for row in range(img.shape[0]):
for col in range(img.shape[1]):
# if kernels are of length 1, then apply the kernel directly
if len(kernels) == 1:
kernel = kernels[0]
window = padded_img[
row : row + kernel.shape[0], col : col + kernel.shape[1]
]
# if kernels are of length 2 and seperate is True, then apply the kernels seperately in x and y direction
elif len(kernels) == 2 and seperate:
kernel_x, kernel_y = kernels
window = padded_img[
row : row + kernel_x.shape[0], col : col + kernel_x.shape[1]
]
result1 = np.sum(window * kernel_x)
result2 = np.sum(window * kernel_y)

result = np.abs(result1) + np.abs(result2)

if take_mean: # useful for box filter
result = np.mean(window * kernel)
if not take_mean and len(kernels) == 1:
result = np.sum(window * kernel)
result = np.clip(result, 0, 255)
final_img[row, col] = np.round(result)

return final_img.astype(np.uint8)

0 comments on commit 6958032

Please sign in to comment.