Skip to content

Commit

Permalink
Add gabor filter feature extraction and improves benchmark report (#36)
Browse files Browse the repository at this point in the history
Closes #24

CHANGELOG:
- Update readme.md introducing new topic segmentation by transduction
- Remove fft feature extraction method
- Reduce verbosity of benchmark notebook
- Improve pdm test usability by allowing to searh by substring
- ~~Use properly normalized wheight edges by sum of neighbors~~ (doesn't
makes difference beyond introducing bugs)
- Add gabor filter feature extraction method
- Add more details to predicted segmentation
  • Loading branch information
ryukinix authored Nov 12, 2023
1 parent f136e81 commit 7f9f6eb
Show file tree
Hide file tree
Showing 13 changed files with 649 additions and 1,739 deletions.
25 changes: 24 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,31 @@ EGSIS is acronymoun for: Exploratory Graph-Based Semi-supervised Image
Segmentation.

It's a Python implementation of a image segmentation algorithm that
combines superpixel with complex networks dynamics.
combines superpixel with complex networks dynamics. In this setup, we
classify the algorithm as transductive as well.

# What is transductive segmentation?

Transductive segmentation is a concept in machine learning and
computer vision. It refers to the process of segmenting or dividing an
unlabeled dataset into distinct groups or segments based on the
inherent structure or patterns within the data.

Formally, transductive segmentation can be defined as follows:

> Given an unlabeled dataset X = {x1, x2, ..., xn}, the goal of
transductive segmentation is to assign a label yi to each data point
xi such that the resulting segmentation optimally reflects the
inherent structure or patterns within the data. This is typically
achieved by defining a similarity measure between data points and then
grouping together data points that are similar according to this
measure.

The **key characteristic** of transductive segmentation is that it does
not require a separate training phase. Instead, it directly infers the
labels for the given dataset based on the data itself. This makes it
particularly suitable for tasks where the distribution of the data is
unknown or may change over time.

# What is graph-based image segmentation?

Expand Down
4 changes: 2 additions & 2 deletions egsis/complex_networks.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from typing import Dict, Set, Callable, Tuple, Literal
from typing import Dict, Set, Callable, Tuple

import numpy
import networkx
Expand Down Expand Up @@ -68,7 +68,7 @@ def compute_node_features(
graph: networkx.Graph,
img: numpy.ndarray,
segments: numpy.ndarray,
feature_method: Literal["fft", "comatrix"]
feature_method: features.FeaturesMethods
) -> networkx.Graph:
centroids = superpixels.superpixel_centroids(segments)
max_radius = superpixels.superpixels_max_radius(segments, centroids)
Expand Down
22 changes: 10 additions & 12 deletions egsis/features.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,12 @@

import numpy as np
from skimage.feature import graycomatrix, graycoprops
from egsis.gabor import feature_extraction_gabor

FeaturesMethods = Literal["comatrix", "gabor"]

def feature_extraction_fft(img: np.ndarray) -> np.ndarray:
"""Multidimensional fourier transform

NOTE(@lerax): dom 11 set 2022 09:49:35
Maybe I should use PCA to reduce the high dimensional space.
"""
return np.fft.fftshift(np.fft.fftn(img))


def _feature_extraction_comatrix_channel(channel) -> np.ndarray:
def _feature_extraction_comatrix_channel(channel: np.ndarray) -> np.ndarray:
glcm = graycomatrix(
channel,
distances=[10],
Expand Down Expand Up @@ -146,6 +140,10 @@ def manhattan_similarity_exp(u: np.ndarray, v: np.ndarray) -> np.floating:
return np.exp(-manhattan_distance(u, v))


def manhattan_similarity_log(u: np.ndarray, v: np.ndarray) -> np.floating:
return 1 / 1 + np.log(1 + manhattan_distance(u, v))


def cosine_similarity(u: np.ndarray, v: np.ndarray) -> np.floating:
"""Cosine similiarty function
Expand All @@ -169,15 +167,15 @@ def feature_extraction_segment(
max_radius: Optional[int] = None,
centroid: Optional[List[int]] = None,
erase_color: Optional[int] = None,
feature_method: Literal["fft", "comatrix"] = "fft"
feature_method: FeaturesMethods = "comatrix"
):
"""Return the features of the segment
If centroid and max_radius are provided, it crops the image
"""
feature_functions = {
"fft": feature_extraction_fft,
"comatrix": feature_extraction_comatrix
"comatrix": feature_extraction_comatrix,
"gabor": feature_extraction_gabor
}

if any(param is not None for param in (max_radius, centroid)):
Expand Down
50 changes: 50 additions & 0 deletions egsis/gabor.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
"""
Gabor filter feature extraction method
"""

import numpy as np
from scipy import ndimage as ndi
from skimage.filters import gabor_kernel

from functools import lru_cache


@lru_cache()
def _generate_kernels():
# prepare filter bank kernels
kernels = []
for theta in range(4):
theta = theta / 4. * np.pi
for sigma in (1, 3):
for frequency in (0.05, 0.25):
kernel = np.real(
gabor_kernel(
frequency,
theta=theta,
sigma_x=sigma,
sigma_y=sigma
)
)
kernels.append(kernel)
return kernels


def _compute_features(image, kernels):
feats = np.zeros((len(kernels), 5), dtype=np.double)
for k, kernel in enumerate(kernels):
filtered = ndi.convolve(image, kernel, mode='wrap')
feats[k, 0] = filtered.mean()
feats[k, 1] = filtered.var()
feats[k, 2] = filtered.std()
feats[k, 3] = np.quantile(filtered, q=0.25)
feats[k, 4] = np.median(filtered)

return feats.flatten()


def feature_extraction_gabor(image: np.ndarray) -> np.ndarray:
kernels = _generate_kernels()
r = _compute_features(image[:, :, 0], kernels)
g = _compute_features(image[:, :, 1], kernels)
b = _compute_features(image[:, :, 2], kernels)
return np.concatenate([r, g, b], axis=0)
7 changes: 1 addition & 6 deletions egsis/lcu.py
Original file line number Diff line number Diff line change
Expand Up @@ -181,13 +181,8 @@ def p(self, G: nx.graph, i: int, j: int, c: int) -> float:
if label != c + 1:
return 0

# NOTE: IMPORTANT!!!!!!!!!!!!!!! sáb 11 nov 2023 00:36:54
# FIXME: implement properly the weighted case
edge_weight = G.edges[i, j]["weight"]
# total_weight = sum(G.edges[i, x]["weight"] for x in G.neighbors(i))
# weight = edge_weight / total_weight
degree = G.degree[i]
walk = edge_weight / degree
walk = edge_weight / G.degree[i]
survival = 1 - self.competition_level * self.sigma(G, i, j, c)

return walk * survival
Expand Down
11 changes: 6 additions & 5 deletions egsis/model.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from typing import Dict, Callable, List, Literal
from typing import Dict, Callable, List

import numpy
import networkx
Expand All @@ -12,9 +12,10 @@


similarity_functions: Dict[str, Callable] = {
"euclidian_exp": features.euclidian_similarity_exp,
"manhattan_exp": features.manhattan_distance,
"euclidian": features.euclidian_similarity,
"euclidian_exp": features.euclidian_similarity_exp,
"manhattan_exp": features.manhattan_similarity_exp,
"manhattan_log": features.manhattan_similarity_log,
"cosine": features.cosine_similarity,
}

Expand Down Expand Up @@ -46,7 +47,7 @@ class EGSIS:
- network build method: superpixel neighbors
feature extraction:
- feature method: multidimensional fast fourier transform
- feature method: comatrix
- crop image: True | False
- erase_color
- similarity function: euclidian | cosine
Expand All @@ -63,7 +64,7 @@ def __init__(
superpixel_sigma: float,
superpixel_compactness: float,
feature_crop_image: bool = True,
feature_extraction: Literal["fft", "comatrix"] = "fft",
feature_extraction: features.FeaturesMethods = "comatrix",
feature_similarity: str = "euclidian",
network_build_method: str = "neighbors",
lcu_competition_level: float = 1,
Expand Down
83 changes: 11 additions & 72 deletions notebooks/annotator_egsis.ipynb

Large diffs are not rendered by default.

2,014 changes: 446 additions & 1,568 deletions notebooks/benchmark.ipynb

Large diffs are not rendered by default.

77 changes: 46 additions & 31 deletions notebooks/feature_extraction.ipynb

Large diffs are not rendered by default.

60 changes: 31 additions & 29 deletions notebooks/model_egsis.ipynb

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ dev = [

[tool.pdm.scripts]
lint = {composite = ["flake8", "mypy"]}
test = "pytest -vv -p no:cacheprovider --cov=egsis --cov-report xml:tests/coverage.xml --cov-report term"
test = "pytest -vv -p no:cacheprovider --cov=egsis --cov-report xml:tests/coverage.xml --cov-report term -k"
tests = {composite = ["test tests/", "coverage json"]}
format = "black egsis/ tests/"
check = {composite = ["lint egsis/", "test tests/"]}
Expand Down
10 changes: 10 additions & 0 deletions tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,16 @@ def image_a():
]
)

@pytest.fixture
def image_colorized():
return numpy.array(
[
[[1, 2, 3], [10, 20, 30], [20, 40, 60]],
[[1, 2, 3], [10, 20, 30], [20, 40, 60]],
[[1, 2, 3], [10, 20, 30], [20, 40, 60]]
]
)


@pytest.fixture
def image_b():
Expand Down
23 changes: 11 additions & 12 deletions tests/test_features.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,21 +6,20 @@
from skimage.util import img_as_ubyte


@pytest.fixture
@pytest.fixture(scope="session")
def cat():
return img_as_ubyte(_cat())


def test_feature_extraction(image_a):
x = features.feature_extraction_fft(image_a)
def test_feature_extraction(image_colorized):
x = features.feature_extraction_comatrix(image_colorized)
expected = numpy.array(
[
[0.0 + 0.0j, 0.0 + 0.0j, 0.0 + 0.0j],
[-45.0 - 25.98076211j, 180.0 + 0.0j, -45.0 + 25.98076211j],
[0.0 + 0.0j, 0.0 + 0.0j, 0.0 + 0.0j],
1., 1., 0., 0., 1., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.,
1., 1., 0., 0., 1., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.,
1., 1., 0., 0., 1., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.
]
)

assert numpy.allclose(x, expected)


Expand Down Expand Up @@ -63,17 +62,17 @@ def test_cosine_similarity_failure(image_a, image_b):
features.cosine_similarity(image_a, image_b.flatten())


def test_feature_extraction_segment(image_a, segments):
def test_feature_extraction_segment(image_colorized, segments):
x = features.feature_extraction_segment(
img=image_a,
img=image_colorized,
segments=segments,
label=2,
)
expected = numpy.array(
[
[8.8817842e-16 + 17.32050808j, -3.0e01 - 51.96152423j, 1.5e01 + 8.66025404j], # noqa
[-1.5e01 - 8.66025404j, 6.0e01 + 0.0j, -1.5e01 + 8.66025404j],
[1.5e01 - 8.66025404j, -3.0e01 + 51.96152423j, 8.8817842e-16 - 17.32050808j], # noqa
1., 1., 0., 0., 1., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.,
1., 1., 0., 0., 1., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.,
1., 1., 0., 0., 1., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.
]
)
assert numpy.allclose(x, expected)
Expand Down

0 comments on commit 7f9f6eb

Please sign in to comment.