440 lines
16 KiB
Python
440 lines
16 KiB
Python
|
import numpy as np
|
||
|
from skimage.segmentation import random_walker
|
||
|
from skimage.transform import resize
|
||
|
from skimage._shared._warnings import expected_warnings
|
||
|
from skimage._shared import testing
|
||
|
from skimage._shared.testing import xfail, arch32
|
||
|
import scipy
|
||
|
from distutils.version import LooseVersion as Version
|
||
|
|
||
|
|
||
|
# older versions of scipy raise a warning with new NumPy because they use
|
||
|
# numpy.rank() instead of arr.ndim or numpy.linalg.matrix_rank.
|
||
|
SCIPY_RANK_WARNING = r'numpy.linalg.matrix_rank|\A\Z'
|
||
|
PYAMG_MISSING_WARNING = r'pyamg|\A\Z'
|
||
|
PYAMG_OR_SCIPY_WARNING = SCIPY_RANK_WARNING + '|' + PYAMG_MISSING_WARNING
|
||
|
|
||
|
if Version(scipy.__version__) < '1.3':
|
||
|
NUMPY_MATRIX_WARNING = 'matrix subclass'
|
||
|
else:
|
||
|
NUMPY_MATRIX_WARNING = None
|
||
|
|
||
|
|
||
|
def make_2d_syntheticdata(lx, ly=None):
|
||
|
if ly is None:
|
||
|
ly = lx
|
||
|
np.random.seed(1234)
|
||
|
data = np.zeros((lx, ly)) + 0.1 * np.random.randn(lx, ly)
|
||
|
small_l = int(lx // 5)
|
||
|
data[lx // 2 - small_l:lx // 2 + small_l,
|
||
|
ly // 2 - small_l:ly // 2 + small_l] = 1
|
||
|
data[lx // 2 - small_l + 1:lx // 2 + small_l - 1,
|
||
|
ly // 2 - small_l + 1:ly // 2 + small_l - 1] = (
|
||
|
0.1 * np.random.randn(2 * small_l - 2, 2 * small_l - 2))
|
||
|
data[lx // 2 - small_l, ly // 2 - small_l // 8:ly // 2 + small_l // 8] = 0
|
||
|
seeds = np.zeros_like(data)
|
||
|
seeds[lx // 5, ly // 5] = 1
|
||
|
seeds[lx // 2 + small_l // 4, ly // 2 - small_l // 4] = 2
|
||
|
return data, seeds
|
||
|
|
||
|
|
||
|
def make_3d_syntheticdata(lx, ly=None, lz=None):
|
||
|
if ly is None:
|
||
|
ly = lx
|
||
|
if lz is None:
|
||
|
lz = lx
|
||
|
np.random.seed(1234)
|
||
|
data = np.zeros((lx, ly, lz)) + 0.1 * np.random.randn(lx, ly, lz)
|
||
|
small_l = int(lx // 5)
|
||
|
data[lx // 2 - small_l:lx // 2 + small_l,
|
||
|
ly // 2 - small_l:ly // 2 + small_l,
|
||
|
lz // 2 - small_l:lz // 2 + small_l] = 1
|
||
|
data[lx // 2 - small_l + 1:lx // 2 + small_l - 1,
|
||
|
ly // 2 - small_l + 1:ly // 2 + small_l - 1,
|
||
|
lz // 2 - small_l + 1:lz // 2 + small_l - 1] = 0
|
||
|
# make a hole
|
||
|
hole_size = np.max([1, small_l // 8])
|
||
|
data[lx // 2 - small_l,
|
||
|
ly // 2 - hole_size:ly // 2 + hole_size,
|
||
|
lz // 2 - hole_size:lz // 2 + hole_size] = 0
|
||
|
seeds = np.zeros_like(data)
|
||
|
seeds[lx // 5, ly // 5, lz // 5] = 1
|
||
|
seeds[lx // 2 + small_l // 4,
|
||
|
ly // 2 - small_l // 4,
|
||
|
lz // 2 - small_l // 4] = 2
|
||
|
return data, seeds
|
||
|
|
||
|
|
||
|
def test_2d_bf():
|
||
|
lx = 70
|
||
|
ly = 100
|
||
|
data, labels = make_2d_syntheticdata(lx, ly)
|
||
|
with expected_warnings([NUMPY_MATRIX_WARNING]):
|
||
|
labels_bf = random_walker(data, labels, beta=90, mode='bf')
|
||
|
assert (labels_bf[25:45, 40:60] == 2).all()
|
||
|
assert data.shape == labels.shape
|
||
|
with expected_warnings([NUMPY_MATRIX_WARNING]):
|
||
|
full_prob_bf = random_walker(data, labels, beta=90, mode='bf',
|
||
|
return_full_prob=True)
|
||
|
assert (full_prob_bf[1, 25:45, 40:60] >=
|
||
|
full_prob_bf[0, 25:45, 40:60]).all()
|
||
|
assert data.shape == labels.shape
|
||
|
# Now test with more than two labels
|
||
|
labels[55, 80] = 3
|
||
|
with expected_warnings([NUMPY_MATRIX_WARNING]):
|
||
|
full_prob_bf = random_walker(data, labels, beta=90, mode='bf',
|
||
|
return_full_prob=True)
|
||
|
assert (full_prob_bf[1, 25:45, 40:60] >=
|
||
|
full_prob_bf[0, 25:45, 40:60]).all()
|
||
|
assert len(full_prob_bf) == 3
|
||
|
assert data.shape == labels.shape
|
||
|
|
||
|
|
||
|
def test_2d_cg():
|
||
|
lx = 70
|
||
|
ly = 100
|
||
|
data, labels = make_2d_syntheticdata(lx, ly)
|
||
|
with expected_warnings(['"cg" mode' + '|' + SCIPY_RANK_WARNING,
|
||
|
NUMPY_MATRIX_WARNING]):
|
||
|
labels_cg = random_walker(data, labels, beta=90, mode='cg')
|
||
|
assert (labels_cg[25:45, 40:60] == 2).all()
|
||
|
assert data.shape == labels.shape
|
||
|
with expected_warnings(['"cg" mode' + '|' + SCIPY_RANK_WARNING,
|
||
|
NUMPY_MATRIX_WARNING]):
|
||
|
full_prob = random_walker(data, labels, beta=90, mode='cg',
|
||
|
return_full_prob=True)
|
||
|
assert (full_prob[1, 25:45, 40:60] >=
|
||
|
full_prob[0, 25:45, 40:60]).all()
|
||
|
assert data.shape == labels.shape
|
||
|
return data, labels_cg
|
||
|
|
||
|
|
||
|
def test_2d_cg_mg():
|
||
|
lx = 70
|
||
|
ly = 100
|
||
|
data, labels = make_2d_syntheticdata(lx, ly)
|
||
|
anticipated_warnings = [
|
||
|
'scipy.sparse.sparsetools|%s' % PYAMG_OR_SCIPY_WARNING,
|
||
|
NUMPY_MATRIX_WARNING]
|
||
|
with expected_warnings(anticipated_warnings):
|
||
|
labels_cg_mg = random_walker(data, labels, beta=90, mode='cg_mg')
|
||
|
assert (labels_cg_mg[25:45, 40:60] == 2).all()
|
||
|
assert data.shape == labels.shape
|
||
|
with expected_warnings(anticipated_warnings):
|
||
|
full_prob = random_walker(data, labels, beta=90, mode='cg_mg',
|
||
|
return_full_prob=True)
|
||
|
assert (full_prob[1, 25:45, 40:60] >=
|
||
|
full_prob[0, 25:45, 40:60]).all()
|
||
|
assert data.shape == labels.shape
|
||
|
return data, labels_cg_mg
|
||
|
|
||
|
|
||
|
def test_2d_cg_j():
|
||
|
lx = 70
|
||
|
ly = 100
|
||
|
data, labels = make_2d_syntheticdata(lx, ly)
|
||
|
with expected_warnings([NUMPY_MATRIX_WARNING]):
|
||
|
labels_cg = random_walker(data, labels, beta=90, mode='cg_j')
|
||
|
assert (labels_cg[25:45, 40:60] == 2).all()
|
||
|
assert data.shape == labels.shape
|
||
|
with expected_warnings([NUMPY_MATRIX_WARNING]):
|
||
|
full_prob = random_walker(data, labels, beta=90, mode='cg_j',
|
||
|
return_full_prob=True)
|
||
|
assert (full_prob[1, 25:45, 40:60]
|
||
|
>= full_prob[0, 25:45, 40:60]).all()
|
||
|
assert data.shape == labels.shape
|
||
|
|
||
|
|
||
|
def test_types():
|
||
|
lx = 70
|
||
|
ly = 100
|
||
|
data, labels = make_2d_syntheticdata(lx, ly)
|
||
|
data = 255 * (data - data.min()) // (data.max() - data.min())
|
||
|
data = data.astype(np.uint8)
|
||
|
with expected_warnings([PYAMG_OR_SCIPY_WARNING, NUMPY_MATRIX_WARNING]):
|
||
|
labels_cg_mg = random_walker(data, labels, beta=90, mode='cg_mg')
|
||
|
assert (labels_cg_mg[25:45, 40:60] == 2).all()
|
||
|
assert data.shape == labels.shape
|
||
|
return data, labels_cg_mg
|
||
|
|
||
|
|
||
|
def test_reorder_labels():
|
||
|
lx = 70
|
||
|
ly = 100
|
||
|
data, labels = make_2d_syntheticdata(lx, ly)
|
||
|
labels[labels == 2] = 4
|
||
|
with expected_warnings([NUMPY_MATRIX_WARNING]):
|
||
|
labels_bf = random_walker(data, labels, beta=90, mode='bf')
|
||
|
assert (labels_bf[25:45, 40:60] == 2).all()
|
||
|
assert data.shape == labels.shape
|
||
|
return data, labels_bf
|
||
|
|
||
|
|
||
|
def test_2d_inactive():
|
||
|
lx = 70
|
||
|
ly = 100
|
||
|
data, labels = make_2d_syntheticdata(lx, ly)
|
||
|
labels[10:20, 10:20] = -1
|
||
|
labels[46:50, 33:38] = -2
|
||
|
with expected_warnings([NUMPY_MATRIX_WARNING]):
|
||
|
labels = random_walker(data, labels, beta=90)
|
||
|
assert (labels.reshape((lx, ly))[25:45, 40:60] == 2).all()
|
||
|
assert data.shape == labels.shape
|
||
|
return data, labels
|
||
|
|
||
|
|
||
|
def test_3d():
|
||
|
n = 30
|
||
|
lx, ly, lz = n, n, n
|
||
|
data, labels = make_3d_syntheticdata(lx, ly, lz)
|
||
|
with expected_warnings(['"cg" mode' + '|' + SCIPY_RANK_WARNING,
|
||
|
NUMPY_MATRIX_WARNING]):
|
||
|
labels = random_walker(data, labels, mode='cg')
|
||
|
assert (labels.reshape(data.shape)[13:17, 13:17, 13:17] == 2).all()
|
||
|
assert data.shape == labels.shape
|
||
|
return data, labels
|
||
|
|
||
|
|
||
|
def test_3d_inactive():
|
||
|
n = 30
|
||
|
lx, ly, lz = n, n, n
|
||
|
data, labels = make_3d_syntheticdata(lx, ly, lz)
|
||
|
old_labels = np.copy(labels)
|
||
|
labels[5:25, 26:29, 26:29] = -1
|
||
|
after_labels = np.copy(labels)
|
||
|
with expected_warnings(['"cg" mode|CObject type' + '|'
|
||
|
+ SCIPY_RANK_WARNING, NUMPY_MATRIX_WARNING]):
|
||
|
labels = random_walker(data, labels, mode='cg')
|
||
|
assert (labels.reshape(data.shape)[13:17, 13:17, 13:17] == 2).all()
|
||
|
assert data.shape == labels.shape
|
||
|
return data, labels, old_labels, after_labels
|
||
|
|
||
|
|
||
|
def test_multispectral_2d():
|
||
|
lx, ly = 70, 100
|
||
|
data, labels = make_2d_syntheticdata(lx, ly)
|
||
|
data = data[..., np.newaxis].repeat(2, axis=-1) # Expect identical output
|
||
|
with expected_warnings(['"cg" mode' + '|' + SCIPY_RANK_WARNING,
|
||
|
NUMPY_MATRIX_WARNING]):
|
||
|
multi_labels = random_walker(data, labels, mode='cg',
|
||
|
multichannel=True)
|
||
|
assert data[..., 0].shape == labels.shape
|
||
|
with expected_warnings(['"cg" mode' + '|' + SCIPY_RANK_WARNING,
|
||
|
NUMPY_MATRIX_WARNING]):
|
||
|
single_labels = random_walker(data[..., 0], labels, mode='cg')
|
||
|
assert (multi_labels.reshape(labels.shape)[25:45, 40:60] == 2).all()
|
||
|
assert data[..., 0].shape == labels.shape
|
||
|
return data, multi_labels, single_labels, labels
|
||
|
|
||
|
|
||
|
def test_multispectral_3d():
|
||
|
n = 30
|
||
|
lx, ly, lz = n, n, n
|
||
|
data, labels = make_3d_syntheticdata(lx, ly, lz)
|
||
|
data = data[..., np.newaxis].repeat(2, axis=-1) # Expect identical output
|
||
|
with expected_warnings(['"cg" mode' + '|' + SCIPY_RANK_WARNING,
|
||
|
NUMPY_MATRIX_WARNING]):
|
||
|
multi_labels = random_walker(data, labels, mode='cg',
|
||
|
multichannel=True)
|
||
|
assert data[..., 0].shape == labels.shape
|
||
|
with expected_warnings(['"cg" mode' + '|' + SCIPY_RANK_WARNING,
|
||
|
NUMPY_MATRIX_WARNING]):
|
||
|
single_labels = random_walker(data[..., 0], labels, mode='cg')
|
||
|
assert (multi_labels.reshape(labels.shape)[13:17, 13:17, 13:17] == 2).all()
|
||
|
assert (single_labels.reshape(labels.shape)[13:17, 13:17, 13:17] == 2).all()
|
||
|
assert data[..., 0].shape == labels.shape
|
||
|
return data, multi_labels, single_labels, labels
|
||
|
|
||
|
|
||
|
def test_spacing_0():
|
||
|
n = 30
|
||
|
lx, ly, lz = n, n, n
|
||
|
data, _ = make_3d_syntheticdata(lx, ly, lz)
|
||
|
|
||
|
# Rescale `data` along Z axis
|
||
|
data_aniso = np.zeros((n, n, n // 2))
|
||
|
for i, yz in enumerate(data):
|
||
|
data_aniso[i, :, :] = resize(yz, (n, n // 2),
|
||
|
mode='constant',
|
||
|
anti_aliasing=False)
|
||
|
|
||
|
# Generate new labels
|
||
|
small_l = int(lx // 5)
|
||
|
labels_aniso = np.zeros_like(data_aniso)
|
||
|
labels_aniso[lx // 5, ly // 5, lz // 5] = 1
|
||
|
labels_aniso[lx // 2 + small_l // 4,
|
||
|
ly // 2 - small_l // 4,
|
||
|
lz // 4 - small_l // 8] = 2
|
||
|
|
||
|
# Test with `spacing` kwarg
|
||
|
with expected_warnings(['"cg" mode' + '|' + SCIPY_RANK_WARNING,
|
||
|
NUMPY_MATRIX_WARNING]):
|
||
|
labels_aniso = random_walker(data_aniso, labels_aniso, mode='cg',
|
||
|
spacing=(1., 1., 0.5))
|
||
|
|
||
|
assert (labels_aniso[13:17, 13:17, 7:9] == 2).all()
|
||
|
|
||
|
|
||
|
@xfail(condition=arch32,
|
||
|
reason=('Known test failure on 32-bit platforms. See links for '
|
||
|
'details: '
|
||
|
'https://github.com/scikit-image/scikit-image/issues/3091 '
|
||
|
'https://github.com/scikit-image/scikit-image/issues/3092'))
|
||
|
def test_spacing_1():
|
||
|
n = 30
|
||
|
lx, ly, lz = n, n, n
|
||
|
data, _ = make_3d_syntheticdata(lx, ly, lz)
|
||
|
|
||
|
# Rescale `data` along Y axis
|
||
|
# `resize` is not yet 3D capable, so this must be done by looping in 2D.
|
||
|
data_aniso = np.zeros((n, n * 2, n))
|
||
|
for i, yz in enumerate(data):
|
||
|
data_aniso[i, :, :] = resize(yz, (n * 2, n),
|
||
|
mode='constant',
|
||
|
anti_aliasing=False)
|
||
|
|
||
|
# Generate new labels
|
||
|
small_l = int(lx // 5)
|
||
|
labels_aniso = np.zeros_like(data_aniso)
|
||
|
labels_aniso[lx // 5, ly // 5, lz // 5] = 1
|
||
|
labels_aniso[lx // 2 + small_l // 4,
|
||
|
ly - small_l // 2,
|
||
|
lz // 2 - small_l // 4] = 2
|
||
|
|
||
|
# Test with `spacing` kwarg
|
||
|
# First, anisotropic along Y
|
||
|
with expected_warnings(['"cg" mode' + '|' + SCIPY_RANK_WARNING,
|
||
|
NUMPY_MATRIX_WARNING]):
|
||
|
labels_aniso = random_walker(data_aniso, labels_aniso, mode='cg',
|
||
|
spacing=(1., 2., 1.))
|
||
|
assert (labels_aniso[13:17, 26:34, 13:17] == 2).all()
|
||
|
|
||
|
# Rescale `data` along X axis
|
||
|
# `resize` is not yet 3D capable, so this must be done by looping in 2D.
|
||
|
data_aniso = np.zeros((n, n * 2, n))
|
||
|
for i in range(data.shape[1]):
|
||
|
data_aniso[i, :, :] = resize(data[:, 1, :], (n * 2, n),
|
||
|
mode='constant',
|
||
|
anti_aliasing=False)
|
||
|
|
||
|
# Generate new labels
|
||
|
small_l = int(lx // 5)
|
||
|
labels_aniso2 = np.zeros_like(data_aniso)
|
||
|
labels_aniso2[lx // 5, ly // 5, lz // 5] = 1
|
||
|
labels_aniso2[lx - small_l // 2,
|
||
|
ly // 2 + small_l // 4,
|
||
|
lz // 2 - small_l // 4] = 2
|
||
|
|
||
|
# Anisotropic along X
|
||
|
with expected_warnings(['"cg" mode' + '|' + SCIPY_RANK_WARNING,
|
||
|
NUMPY_MATRIX_WARNING]):
|
||
|
labels_aniso2 = random_walker(data_aniso,
|
||
|
labels_aniso2,
|
||
|
mode='cg', spacing=(2., 1., 1.))
|
||
|
assert (labels_aniso2[26:34, 13:17, 13:17] == 2).all()
|
||
|
|
||
|
|
||
|
def test_trivial_cases():
|
||
|
# When all voxels are labeled
|
||
|
img = np.ones((10, 10))
|
||
|
labels = np.ones((10, 10))
|
||
|
|
||
|
with expected_warnings(["Returning provided labels"]):
|
||
|
pass_through = random_walker(img, labels)
|
||
|
np.testing.assert_array_equal(pass_through, labels)
|
||
|
|
||
|
# When all voxels are labeled AND return_full_prob is True
|
||
|
labels[:, :5] = 3
|
||
|
expected = np.concatenate(((labels == 1)[..., np.newaxis],
|
||
|
(labels == 3)[..., np.newaxis]), axis=2)
|
||
|
with expected_warnings(["Returning provided labels"]):
|
||
|
test = random_walker(img, labels, return_full_prob=True)
|
||
|
np.testing.assert_array_equal(test, expected)
|
||
|
|
||
|
# Unlabeled voxels not connected to seed, so nothing can be done
|
||
|
img = np.full((10, 10), False)
|
||
|
object_A = np.array([(6,7), (6,8), (7,7), (7,8)])
|
||
|
object_B = np.array([(3,1), (4,1), (2,2), (3,2), (4,2), (2,3), (3,3)])
|
||
|
for x, y in np.vstack((object_A, object_B)):
|
||
|
img[y][x] = True
|
||
|
|
||
|
markers = np.zeros((10, 10), dtype=np.int8)
|
||
|
for x, y in object_B:
|
||
|
markers[y][x] = 1
|
||
|
|
||
|
markers[img == 0] = -1
|
||
|
with expected_warnings(["All unlabeled pixels are isolated"]):
|
||
|
output_labels = random_walker(img, markers)
|
||
|
assert np.all(output_labels[markers == 1] == 1)
|
||
|
# Here 0-labeled pixels could not be determined (no connexion to seed)
|
||
|
assert np.all(output_labels[markers == 0] == -1)
|
||
|
with expected_warnings(["All unlabeled pixels are isolated"]):
|
||
|
test = random_walker(img, markers, return_full_prob=True)
|
||
|
|
||
|
|
||
|
def test_length2_spacing():
|
||
|
# If this passes without raising an exception (warnings OK), the new
|
||
|
# spacing code is working properly.
|
||
|
np.random.seed(42)
|
||
|
img = np.ones((10, 10)) + 0.2 * np.random.normal(size=(10, 10))
|
||
|
labels = np.zeros((10, 10), dtype=np.uint8)
|
||
|
labels[2, 4] = 1
|
||
|
labels[6, 8] = 4
|
||
|
with expected_warnings([NUMPY_MATRIX_WARNING]):
|
||
|
random_walker(img, labels, spacing=(1., 2.))
|
||
|
|
||
|
|
||
|
def test_bad_inputs():
|
||
|
# Too few dimensions
|
||
|
img = np.ones(10)
|
||
|
labels = np.arange(10)
|
||
|
with testing.raises(ValueError):
|
||
|
random_walker(img, labels)
|
||
|
with testing.raises(ValueError):
|
||
|
random_walker(img, labels, multichannel=True)
|
||
|
|
||
|
# Too many dimensions
|
||
|
np.random.seed(42)
|
||
|
img = np.random.normal(size=(3, 3, 3, 3, 3))
|
||
|
labels = np.arange(3 ** 5).reshape(img.shape)
|
||
|
with testing.raises(ValueError):
|
||
|
random_walker(img, labels)
|
||
|
with testing.raises(ValueError):
|
||
|
random_walker(img, labels, multichannel=True)
|
||
|
|
||
|
# Spacing incorrect length
|
||
|
img = np.random.normal(size=(10, 10))
|
||
|
labels = np.zeros((10, 10))
|
||
|
labels[2, 4] = 2
|
||
|
labels[6, 8] = 5
|
||
|
with testing.raises(ValueError):
|
||
|
random_walker(img, labels, spacing=(1,))
|
||
|
|
||
|
# Invalid mode
|
||
|
img = np.random.normal(size=(10, 10))
|
||
|
labels = np.zeros((10, 10))
|
||
|
with testing.raises(ValueError):
|
||
|
random_walker(img, labels, mode='bad')
|
||
|
|
||
|
|
||
|
def test_isolated_seeds():
|
||
|
np.random.seed(0)
|
||
|
a = np.random.random((7, 7))
|
||
|
mask = - np.ones(a.shape)
|
||
|
# This pixel is an isolated seed
|
||
|
mask[1, 1] = 1
|
||
|
# Unlabeled pixels
|
||
|
mask[3:, 3:] = 0
|
||
|
# Seeds connected to unlabeled pixels
|
||
|
mask[4, 4] = 2
|
||
|
mask[6, 6] = 1
|
||
|
|
||
|
# Test that no error is raised, and that labels of isolated seeds are OK
|
||
|
with expected_warnings([NUMPY_MATRIX_WARNING]):
|
||
|
res = random_walker(a, mask)
|
||
|
assert res[1, 1] == 1
|
||
|
with expected_warnings([NUMPY_MATRIX_WARNING]):
|
||
|
res = random_walker(a, mask, return_full_prob=True)
|
||
|
assert res[0, 1, 1] == 1
|
||
|
assert res[1, 1, 1] == 0
|