Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
126 changes: 126 additions & 0 deletions tests/test_thin_2d.py
Original file line number Diff line number Diff line change
Expand Up @@ -84,3 +84,129 @@ def test_maskel_vs_scikit_skeletonize_lee(self, image):
assert np.array_equal(maskel_skel, scikit_skel.astype(np.uint8)), (
"skeleton mismatch: algorithms produce different results"
)


def _isolated_pixel_2d() -> np.ndarray:
img = np.zeros((3, 3), dtype=np.uint8)
img[1, 1] = 1
return img


def _straight_line_2d() -> np.ndarray:
img = np.zeros((3, 5), dtype=np.uint8)
img[1, :] = 1
return img


def _diagonal_line_2d() -> np.ndarray:
"""A pure diagonal line - the minimal instance of the 8-vs-4-connectivity
paradox the appendix's border-point discussion is built on (a diagonal
foreground line and a diagonal background line can cross at the same
corner)."""
img = np.zeros((5, 5), dtype=np.uint8)
for i in range(5):
img[i, i] = 1
return img


def _checkerboard_corner_2d() -> np.ndarray:
"""The minimal 2x2 instance of that same corner-adjacency paradox."""
return np.array([[0, 1], [1, 0]], dtype=np.uint8)


def _t_junction_2d() -> np.ndarray:
"""A branch point (degree 3), not just a straight run or a tip."""
return np.array(
[
[0, 1, 0, 0, 0],
[0, 1, 0, 0, 0],
[1, 1, 1, 1, 1],
[0, 0, 0, 0, 0],
[0, 0, 0, 0, 0],
],
dtype=np.uint8,
)


def _x_junction_2d() -> np.ndarray:
"""A degree-4 branch point."""
return np.array(
[
[0, 0, 1, 0, 0],
[0, 0, 1, 0, 0],
[1, 1, 1, 1, 1],
[0, 0, 1, 0, 0],
[0, 0, 1, 0, 0],
],
dtype=np.uint8,
)


def _ring_2d() -> np.ndarray:
"""A single hollow square: one real hole to preserve. Tests the claim
behind Lemma 1 (impossibility of hole creation) on an object that
already has topology to lose, not just a simply-connected shape."""
return np.array(
[
[1, 1, 1, 1, 1],
[1, 0, 0, 0, 1],
[1, 0, 0, 0, 1],
[1, 0, 0, 0, 1],
[1, 1, 1, 1, 1],
],
dtype=np.uint8,
)


def _two_holes_with_bridge_2d() -> np.ndarray:
"""Two holes separated by a single-pixel-wide bridge - the "opposite
face-neighbors" configuration behind Lemma 2 (impossibility of hole
elimination): naively removing a bridge pixel would merge the two holes
into one, which the simple-point check must forbid."""
return np.array(
[
[1, 1, 1, 1, 1, 1, 1],
[1, 0, 0, 1, 0, 0, 1],
[1, 0, 0, 1, 0, 0, 1],
[1, 0, 0, 1, 0, 0, 1],
[1, 1, 1, 1, 1, 1, 1],
],
dtype=np.uint8,
)


def _solid_blob_2d() -> np.ndarray:
"""A filled region smaller than the canvas - distinct from
TestUniformInput's all-foreground case, which never erodes anything
since it fills the whole array with no boundary to erode from."""
img = np.zeros((7, 7), dtype=np.uint8)
img[1:6, 1:6] = 1
return img


class TestSyntheticPatternEquivalence:
"""Small, hand-constructed patterns compared against scikit-image's Lee
thinning - the synthetic complement to TestSkeletonizeComparison's
single real photo. Each one targets a specific edge case rather than
relying on whatever happens to occur in a real image."""

@pytest.mark.parametrize(
"pattern",
[
pytest.param(_isolated_pixel_2d(), id="isolated_pixel"),
pytest.param(_straight_line_2d(), id="straight_line"),
pytest.param(_diagonal_line_2d(), id="diagonal_line"),
pytest.param(_checkerboard_corner_2d(), id="checkerboard_corner"),
pytest.param(_t_junction_2d(), id="t_junction"),
pytest.param(_x_junction_2d(), id="x_junction"),
pytest.param(_ring_2d(), id="ring_one_hole"),
pytest.param(_two_holes_with_bridge_2d(), id="two_holes_with_bridge"),
pytest.param(_solid_blob_2d(), id="solid_blob"),
],
)
def test_maskel_vs_scikit_skeletonize_lee(self, pattern):
maskel_skel = lee94_thin(pattern)
scikit_skel = skeletonize(pattern, method="lee").astype(np.uint8)
assert np.array_equal(maskel_skel, scikit_skel), (
"skeleton mismatch: algorithms produce different results"
)
74 changes: 74 additions & 0 deletions tests/test_thin_3d.py
Original file line number Diff line number Diff line change
Expand Up @@ -152,3 +152,77 @@ def test_volume_needing_more_than_255_batches(self):

assert np.array_equal(result.astype(bool), skeletonize(vol))
assert result.any(), "the rod must survive as skeleton"


def _isolated_voxel_3d() -> np.ndarray:
vol = np.zeros((3, 3, 3), dtype=np.uint8)
vol[1, 1, 1] = 1
return vol


def _axis_line_3d() -> np.ndarray:
vol = np.zeros((3, 3, 5), dtype=np.uint8)
vol[1, 1, :] = 1
return vol


def _space_diagonal_line_3d() -> np.ndarray:
"""The 3D analogue of test_thin_2d's diagonal_line: a line along the
space diagonal, exercising the 26-vs-6 connectivity conventions rather
than a face-aligned run."""
vol = np.zeros((5, 5, 5), dtype=np.uint8)
for i in range(5):
vol[i, i, i] = 1
return vol


def _plus_junction_3d() -> np.ndarray:
"""A degree-6 branch point: one line along each axis, all crossing at
the centre voxel."""
vol = np.zeros((5, 5, 5), dtype=np.uint8)
vol[2, 2, :] = 1
vol[2, :, 2] = 1
vol[:, 2, 2] = 1
return vol


def _hollow_shell_3d() -> np.ndarray:
"""A hollow cube: one real cavity to preserve, the 3D analogue of
test_thin_2d's ring_one_hole."""
vol = np.ones((5, 5, 5), dtype=np.uint8)
vol[1:4, 1:4, 1:4] = 0
return vol


class TestSyntheticPatternEquivalence:
"""Small, hand-constructed patterns compared against scikit-image's Lee
thinning - the synthetic complement to TestForegroundSizedBuffers' and
TestRemovedEpochTagWrap's real-shaped volumes. Each one targets a
specific edge case from the Euler-invariant-redundancy appendix rather
than relying on whatever topology happens to occur in a real scan.

Deliberately excluded: a volume exactly 1 voxel thick along one axis.
scikit-image's shared 3D routine special-cases that shape (dropping to
4 border directions, treating it as an embedded 2D image), which
thin_3d does not - it always sweeps all 6 directions, since real 3D
data is never expected to be flat across its entire extent. The two
implementations legitimately disagree on that shape; it isn't one
thin_3d was designed to match.
"""

@pytest.mark.parametrize(
"vol",
[
pytest.param(_isolated_voxel_3d(), id="isolated_voxel"),
pytest.param(_axis_line_3d(), id="axis_line"),
pytest.param(_space_diagonal_line_3d(), id="space_diagonal_line"),
pytest.param(_plus_junction_3d(), id="plus_junction"),
pytest.param(_hollow_shell_3d(), id="hollow_shell_one_cavity"),
],
)
def test_maskel_vs_scikit_skeletonize_lee(self, vol):
maskel_skel = thin_3d(vol)
scikit_skel = skeletonize(vol).astype(np.uint8)
assert np.array_equal(maskel_skel, scikit_skel), (
"skeleton mismatch: algorithms produce different results"
)