diff --git a/glcm_cupy/glcm/glcm.py b/glcm_cupy/glcm/glcm.py index 097290e..a479fa9 100644 --- a/glcm_cupy/glcm/glcm.py +++ b/glcm_cupy/glcm/glcm.py @@ -46,6 +46,7 @@ def glcm( Features.CORRELATION, Features.DISSIMILARITY), normalized_features: bool = True, + skip_border: bool = False, verbose: bool = True ) -> ndarray: """ @@ -71,6 +72,7 @@ def glcm( max_threads: Maximum threads for CUDA features: Select features to be included normalized_features: Whether to normalize features to [0, 1] + skip_border: Wheter to skip border of interfacing windows. When skipping border, result is the same as a GLCM calculated on each 7x7 window, without neighbourhood information. Default is False. verbose: Whether to enable TQDM logging Returns: @@ -86,6 +88,7 @@ def glcm( normalized_features=normalized_features, step_size=step_size, directions=directions, + skip_border=skip_border, verbose=verbose ).run(im) @@ -99,6 +102,7 @@ class GLCM(GLCMBase): Direction.SOUTH, Direction.SOUTH_WEST ) + skip_border: bool = False def __post_init__(self): super().__post_init__() @@ -203,6 +207,9 @@ def make_windows(self, im_chn: cp.ndarray) -> List[ for direction in self.directions: i, j = self.pair_windows(ij, direction=direction) + if self.skip_border: + i, j = self._remove_border((i, j), direction) + i = i.reshape((-1, *i.shape[-2:])) \ .reshape((i.shape[0] * i.shape[1], -1)) j = j.reshape((-1, *j.shape[-2:])) \ @@ -211,6 +218,40 @@ def make_windows(self, im_chn: cp.ndarray) -> List[ return ijs + def _remove_border(self, ij: Tuple[cp.ndarray, cp.ndarray], direction: Direction) -> Tuple[cp.ndarray, cp.ndarray]: + if direction == Direction.EAST: + sl = ( + slice(None, None), + slice(None, None), + slice(None, None), + slice(None, -self.step_size), + ) + elif direction == Direction.SOUTH_EAST: + sl = ( + slice(None, None), + slice(None, None), + slice(None, -self.step_size), + slice(None, -self.step_size), + ) + elif direction == Direction.SOUTH: + sl = ( + slice(None, None), + slice(None, None), + slice(None, -self.step_size), + slice(None, None), + ) + elif direction == Direction.SOUTH_WEST: + sl = ( + slice(None, None), + slice(None, None), + slice(None, -self.step_size), + slice(self.step_size, None), + ) + else: + raise ValueError("Invalid Direction") + + return ij[0][sl], ij[1][sl] + def pair_windows(self, ij: ndarray, direction: Direction) -> Tuple[ndarray, ndarray]: """ Pairs the ij windows in specified direction diff --git a/glcm_cupy/glcm/glcm_py.py b/glcm_cupy/glcm/glcm_py.py index 7cc48d2..37467d7 100644 --- a/glcm_cupy/glcm/glcm_py.py +++ b/glcm_cupy/glcm/glcm_py.py @@ -4,6 +4,7 @@ import numpy as np from numpy.lib.stride_tricks import sliding_window_view from tqdm import tqdm +from typing import Tuple, Dict from glcm_cupy.conf import NO_OF_FEATURES, ndarray from glcm_cupy.glcm_py_base import GLCMPyBase @@ -12,22 +13,26 @@ def glcm_py_im(ar: ndarray, bin_from: int, bin_to: int, radius: int = 2, - step: int = 1): + step: int = 1, + skip_border = False): return GLCMPy(bin_from=bin_from, bin_to=bin_to, radius=radius, - step=step).glcm_im(ar) + step=step, + skip_border=skip_border).glcm_im(ar) def glcm_py_chn(ar: cp.ndarray, bin_from: int, bin_to: int, radius: int = 2, - step: int = 1): + step: int = 1, + skip_border = False): return GLCMPy(bin_from=bin_from, bin_to=bin_to, radius=radius, - step=step).glcm_chn(ar) + step=step, + skip_border=skip_border).glcm_chn(ar) def glcm_py_ij(i: cp.ndarray, @@ -40,28 +45,36 @@ def glcm_py_ij(i: cp.ndarray, @dataclass class GLCMPy(GLCMPyBase): step: int = 1 + skip_border: bool = False def glcm_chn(self, ar: cp.ndarray): ar = (ar / self.bin_from * self.bin_to).astype(cp.uint8) ar_w = sliding_window_view(ar, (self.diameter, self.diameter)) - + def windows(ar: ndarray): + return ar.reshape((-1, self.diameter, self.diameter)) + def flat(ar: ndarray): - ar = ar.reshape((-1, self.diameter, self.diameter)) return ar.reshape((ar.shape[0], -1)) - ar_w_i = flat(ar_w[self.step:-self.step, self.step:-self.step]) - ar_w_j_sw = flat(ar_w[self.step * 2:, :-self.step * 2]) - ar_w_j_s = flat(ar_w[self.step * 2:, self.step:-self.step]) - ar_w_j_se = flat(ar_w[self.step * 2:, self.step * 2:]) - ar_w_j_e = flat(ar_w[self.step:-self.step, self.step * 2:]) + ar_w_i = windows(ar_w[self.step:-self.step, self.step:-self.step]) + ar_w_j_sw = windows(ar_w[self.step * 2:, :-self.step * 2]) + ar_w_j_s = windows(ar_w[self.step * 2:, self.step:-self.step]) + ar_w_j_se = windows(ar_w[self.step * 2:, self.step * 2:]) + ar_w_j_e = windows(ar_w[self.step:-self.step, self.step * 2:]) feature_ar = np.zeros((ar_w_i.shape[0], 4, NO_OF_FEATURES)) for j_e, ar_w_j in enumerate( (ar_w_j_sw, ar_w_j_s, ar_w_j_se, ar_w_j_e)): - for e, (i, j) in tqdm(enumerate(zip(ar_w_i, ar_w_j)), - total=ar_w_i.shape[0]): + ar_i, ar_j = ar_w_i, ar_w_j + if self.skip_border: + ar_i, ar_j = self._remove_border((ar_i, ar_j), j_e) + ar_i = flat(ar_i) + ar_j = flat(ar_j) + for e, (i, j) in tqdm(enumerate(zip(ar_i, ar_j)), + total=ar_i.shape[0]): + feature_ar[e, j_e] = self.glcm_ij(i, j) h, w = ar_w.shape[:2] @@ -72,6 +85,36 @@ def flat(ar: ndarray): return normalize_features(feature_ar, self.bin_to) + def _remove_border(self, ij: Tuple[cp.ndarray, cp.ndarray], direction: int) -> Tuple[cp.ndarray, cp.ndarray]: + if direction == 0: # Direction.SOUTH_WEST + sl = ( + slice(None, None), + slice(None, -self.step), + slice(self.step, None), + ) + elif direction == 1: # Direction.SOUTH + sl = ( + slice(None, None), + slice(None, -self.step), + slice(None, None), + ) + elif direction == 2: # Direction.SOUTH_EAST + sl = ( + slice(None, None), + slice(None, -self.step), + slice(None, -self.step), + ) + elif direction == 3: # Direction.EAST + sl = ( + slice(None, None), + slice(None, None), + slice(None, -self.step), + ) + else: + raise ValueError("Invalid Direction") + + return ij[0][sl], ij[1][sl] + def glcm_im(self, ar: ndarray): was_cupy = False if isinstance(ar, cp.ndarray): diff --git a/glcm_cupy/glcm_base.py b/glcm_cupy/glcm_base.py index fee5f48..227c906 100644 --- a/glcm_cupy/glcm_base.py +++ b/glcm_cupy/glcm_base.py @@ -306,7 +306,7 @@ def glcm_ij(self, self.ar_features[:] = 0 no_of_windows = i.shape[0] - no_of_values = self._diameter ** 2 + no_of_values = i.shape[1] if i.dtype != cp.uint8 or j.dtype != cp.uint8: raise ValueError( diff --git a/tests/unit_tests/test_glcm.py b/tests/unit_tests/test_glcm.py index efea535..6282a82 100644 --- a/tests/unit_tests/test_glcm.py +++ b/tests/unit_tests/test_glcm.py @@ -40,11 +40,15 @@ def test_glcm_normalize(ar): "radius", [1, 2, 4] ) -def test_glcm(size, bin_from, bin_to, radius): +@pytest.mark.parametrize( + "skip_border", + [False, True] +) +def test_glcm(size, bin_from, bin_to, radius, skip_border): ar = np.random.randint(0, bin_from, [size, size, 1]) - g = GLCM(radius=radius, bin_from=bin_from, bin_to=bin_to).run(ar) - g_fn = glcm(ar, radius=radius, bin_from=bin_from, bin_to=bin_to) - expected = glcm_py_im(ar, radius=radius, bin_from=bin_from, bin_to=bin_to) + g = GLCM(radius=radius, bin_from=bin_from, bin_to=bin_to, skip_border=skip_border).run(ar) + g_fn = glcm(ar, radius=radius, bin_from=bin_from, bin_to=bin_to, skip_border=skip_border) + expected = glcm_py_im(ar, radius=radius, bin_from=bin_from, bin_to=bin_to, skip_border=skip_border) assert g == pytest.approx(expected, abs=0.001) assert g_fn == pytest.approx(expected, abs=0.001)