Skip to content

cds2.image

Grayscale image processing: convolution, edges, morphology.

Grayscale image processing: convolution, filtering and morphology.

EdgeResult dataclass

Gradient magnitude and orientation returned by :func:sobel_edges.

Source code in src/cds2/image.py
87
88
89
90
91
92
@dataclass(frozen=True)
class EdgeResult:
    """Gradient magnitude and orientation returned by :func:`sobel_edges`."""

    magnitude: FloatArray
    direction: FloatArray

convolve2d(image, kernel, mode='same')

True 2-D convolution (kernel flipped) with zero padding in same mode.

Source code in src/cds2/image.py
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
def convolve2d(image: ImageLike, kernel: ImageLike, mode: str = "same") -> FloatArray:
    """True 2-D convolution (kernel flipped) with zero padding in ``same`` mode."""
    img = _as_image(image)
    ker = _as_square_kernel(kernel)
    if mode not in ("same", "valid"):
        msg = "mode must be same or valid"
        raise ValueError(msg)
    side = ker.shape[0]
    pad = side // 2
    flipped = np.ascontiguousarray(ker[::-1, ::-1])
    if mode == "same":
        padded = np.pad(img, pad, mode="constant", constant_values=0.0)
    else:
        padded = img
    return _sliding_dot(padded, flipped)

gaussian_kernel(size=5, sigma=1.0)

Separable Gaussian kernel normalized to unit sum.

Source code in src/cds2/image.py
68
69
70
71
72
73
74
75
76
77
78
79
def gaussian_kernel(size: int = 5, sigma: float = 1.0) -> FloatArray:
    """Separable Gaussian kernel normalized to unit sum."""
    if size < 3 or size % 2 == 0:
        msg = "kernel size must be odd and at least 3"
        raise ValueError(msg)
    if sigma <= 0:
        msg = "sigma must be positive"
        raise ValueError(msg)
    offsets = np.arange(size, dtype=float) - (size - 1) / 2
    profile = np.exp(-(offsets**2) / (2.0 * sigma**2))
    profile /= profile.sum()
    return np.asarray(np.outer(profile, profile), dtype=float)

gaussian_blur(image, sigma=1.0, size=5)

Smooth image by convolving with a size x size Gaussian kernel.

Source code in src/cds2/image.py
82
83
84
def gaussian_blur(image: ImageLike, sigma: float = 1.0, size: int = 5) -> FloatArray:
    """Smooth ``image`` by convolving with a ``size`` x ``size`` Gaussian kernel."""
    return convolve2d(image, gaussian_kernel(size=size, sigma=sigma), mode="same")

sobel_edges(image)

Sobel gradient magnitude and direction via correlation (no flip).

Source code in src/cds2/image.py
102
103
104
105
106
107
108
109
110
111
112
113
114
def sobel_edges(image: ImageLike) -> EdgeResult:
    """Sobel gradient magnitude and direction via correlation (no flip)."""
    img = _as_image(image)
    sobel_x = np.array([[-1.0, 0.0, 1.0], [-2.0, 0.0, 2.0], [-1.0, 0.0, 1.0]])
    sobel_y = sobel_x.T.copy()
    gx = _correlate2d(img, sobel_x)
    gy = _correlate2d(img, sobel_y)
    magnitude = np.hypot(gx, gy)
    direction = np.arctan2(gy, gx)
    return EdgeResult(
        magnitude=np.asarray(magnitude, dtype=float),
        direction=np.asarray(direction, dtype=float),
    )

downsample(image, factor=2, method='mean')

Reduce resolution by non-overlapping factor blocks after cropping.

Source code in src/cds2/image.py
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
def downsample(image: ImageLike, factor: int = 2, method: str = "mean") -> FloatArray:
    """Reduce resolution by non-overlapping ``factor`` blocks after cropping."""
    img = _as_image(image)
    if method not in ("mean", "max", "min"):
        msg = "method must be mean, max or min"
        raise ValueError(msg)
    if factor < 2:
        msg = "factor must be at least 2"
        raise ValueError(msg)
    height = (img.shape[0] // factor) * factor
    width = (img.shape[1] // factor) * factor
    blocks = img[:height, :width].reshape(height // factor, factor, width // factor, factor)
    if method == "mean":
        reduced = blocks.mean(axis=(1, 3))
    elif method == "max":
        reduced = blocks.max(axis=(1, 3))
    else:
        reduced = blocks.min(axis=(1, 3))
    return np.asarray(reduced, dtype=float)

binarize(image, threshold)

Threshold an image into a 0/1 mask of pixels strictly above threshold.

Source code in src/cds2/image.py
138
139
140
141
def binarize(image: ImageLike, threshold: float) -> FloatArray:
    """Threshold an image into a 0/1 mask of pixels strictly above ``threshold``."""
    img = _as_image(image)
    return np.asarray((img > threshold).astype(float), dtype=float)

erode(binary, structure_size=3)

Morphological erosion as a min filter over a square window.

Source code in src/cds2/image.py
159
160
161
162
163
164
165
166
def erode(binary: ImageLike, structure_size: int = 3) -> FloatArray:
    """Morphological erosion as a min filter over a square window."""
    arr = _as_binary(binary)
    side = _as_structure_size(structure_size)
    pad = side // 2
    padded = np.pad(arr, pad, mode="constant", constant_values=1.0)
    windows = np.lib.stride_tricks.sliding_window_view(padded, (side, side))
    return np.asarray(windows.min(axis=(-2, -1)), dtype=float)

dilate(binary, structure_size=3)

Morphological dilation as a max filter over a square window.

Source code in src/cds2/image.py
169
170
171
172
173
174
175
176
def dilate(binary: ImageLike, structure_size: int = 3) -> FloatArray:
    """Morphological dilation as a max filter over a square window."""
    arr = _as_binary(binary)
    side = _as_structure_size(structure_size)
    pad = side // 2
    padded = np.pad(arr, pad, mode="constant", constant_values=0.0)
    windows = np.lib.stride_tricks.sliding_window_view(padded, (side, side))
    return np.asarray(windows.max(axis=(-2, -1)), dtype=float)