Skip to content

Commit d24d595

Browse files
perf: num_transitions rather than num_foreground + return_N + automatic out_dtype + remove sparse (#55)
* feat(voxel_connectivity_graph): extracts directed voxel connectivity graph Each voxel of the output is a bitfield that represents the allowed transit directions. * docs: added description of bitfields to python help * refactor: move region_graph tests to bottom * test: add simple tests for 4 and 8 graph connectivity * test: skip big memory test for 32-bit windows * fix: voxel size + fix compiler warnings * test: adds test for 3d voxel connectivity graph * fix: the masks dont make sense in row-major format so just drop it for now * docs: update README with voxel_connectivity_graph * docs: describe how to use the voxel graph in C++ * docs: correction to data width of black cube * perf(sparse): use num transitions rather than num foreground voxels * fix: if first voxel is foreground, add one to transitions * BREAKING: radical revision that eliminates max_labels, sparse, and out_dtype Uses num_transitions to select appropriate out_dtype and obviates sparse. * fix: compiler warnings around printf w/ templates * test: fix and add tests, drop sparse, out_dtype, add zeroth_pass * fix: needed to account for boundaries in transition estimation * docs: describe automatic out dtype assignment * chore: add fastremap to setup.py * fix: actually cc3d doesn't have a limitation on the input labels... * feat: add return_N argument * docs: show how to use the new library features * test: remove py3.9 from appveyor * test: add py3.8 to travis * fix: wrong invocation for C++
1 parent ff1f74c commit d24d595

File tree

8 files changed

+5541
-3671
lines changed

8 files changed

+5541
-3671
lines changed

.travis.yml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,8 @@ python:
33
- '2.7'
44
- '3.5'
55
- '3.6'
6-
- '3.7-dev'
6+
- '3.7'
7+
- '3.8'
78
before_install:
89
- PYTHON_MAJOR_VERSION=`echo $TRAVIS_PYTHON_VERSION | head -c 1`
910
- if [[ $PYTHON_MAJOR_VERSION == 3 ]]; then sudo apt-get install python3-pip; fi

README.md

Lines changed: 12 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -41,8 +41,6 @@ python setup.py develop
4141

4242
## Python Use
4343

44-
Important limitation: Only label values less than or equal to the size of the image in voxels (pixels) are supported currently. If you want to use larger values, consider using [fastremap.renumber](https://github.com/seung-lab/fastremap/).
45-
4644
```python
4745
import cc3d
4846
import numpy as np
@@ -53,20 +51,20 @@ labels_out = cc3d.connected_components(labels_in) # 26-connected
5351
connectivity = 6 # only 4,8 (2D) and 26, 18, and 6 (3D) are allowed
5452
labels_out = cc3d.connected_components(labels_in, connectivity=connectivity)
5553

56-
# You can adjust the bit width of the output to accomodate
57-
# different expected image statistics with memory usage tradeoffs.
58-
# uint16, uint32 (default), and uint64 are supported. Note that
59-
# uint16 will limit you to 2^16 provisional labels.
60-
labels_out = cc3d.connected_components(labels_in, out_dtype=np.uint16)
54+
# We use a third pass to estimate the number of provisional labels prior
55+
# to running CCL. This is usually faster and lower memory for large datasets.
56+
# If you have a special situation, you can switch this feature off. Note that
57+
# np.bool datatypes can use fewer labels without this estimation scan.
58+
labels_out = cc3d.connected_components(labels_in, zeroth_pass=False)
6159

62-
# If you know that the number of foreground voxels is relatively
63-
# low, you can save memory and sometimes time by enabling sparse mode
64-
# which will shrink the memory allocation of the Union-Find datastructure
65-
# to exactly match the number of foreground voxels if not lower.
66-
labels_out = cc3d.connected_components(labels_in, sparse=True)
60+
# You can extract the number of labels (which is also the maximum
61+
# label value) like so:
62+
labels_out, N = cc3d.connected_components(labels_in, return_N=True) # free
63+
# -- OR --
64+
labels_out = cc3d.connected_components(labels_in)
65+
N = np.max(labels_out) # costs a full read
6766

6867
# You can extract individual components like so:
69-
N = np.max(labels_out)
7068
for segid in range(1, N+1):
7169
extracted_image = labels_out * (labels_out == segid)
7270
process(extracted_image)
@@ -84,7 +82,7 @@ graph = cc3d.voxel_connectivity_graph(labels, connectivity=connectivity)
8482

8583
```
8684

87-
If you know approximately how many labels you are going to generate, you can save some memory by specifying a number a safety factor above that range. The max label ID in your input labels must be less than `max_labels`.
85+
If you know approximately how many labels you are going to generate, you can save some memory by specifying a number a safety factor above that range. The max label ID in your input labels must be less than `max_labels`. *This option is only recommended for expert users.*
8886

8987
```python
9088
labels_out = connected_components(labels_in, max_labels=20000)

appveyor.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,11 @@ environment:
44
- PYTHON: "C:\\Python36"
55
- PYTHON: "C:\\Python37"
66
- PYTHON: "C:\\Python38"
7-
- PYTHON: "C:\\Python39"
7+
# - PYTHON: "C:\\Python39"
88
- PYTHON: "C:\\Python36-x64"
99
- PYTHON: "C:\\Python37-x64"
1010
- PYTHON: "C:\\Python38-x64"
11-
- PYTHON: "C:\\Python39-x64"
11+
# - PYTHON: "C:\\Python39-x64"
1212

1313
install:
1414
# We need wheel installed to build wheels

automated_test.py

Lines changed: 41 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,7 @@ def gt_c2f(gt):
2121

2222
@pytest.mark.parametrize("connectivity", (4, 6, 8, 18, 26))
2323
@pytest.mark.parametrize("dtype", TEST_TYPES)
24-
@pytest.mark.parametrize("out_dtype", OUT_TYPES)
25-
def test_2d_square(out_dtype, dtype, connectivity):
24+
def test_2d_square(dtype, connectivity):
2625
def test(order, ground_truth):
2726
input_labels = np.zeros( (16,16), dtype=dtype, order=order )
2827
input_labels[:8,:8] = 8
@@ -31,7 +30,7 @@ def test(order, ground_truth):
3130
input_labels[8:,8:] = 11
3231

3332
output_labels = cc3d.connected_components(
34-
input_labels, out_dtype=out_dtype, connectivity=connectivity
33+
input_labels, connectivity=connectivity
3534
).astype(dtype)
3635

3736
print(output_labels)
@@ -62,8 +61,7 @@ def test(order, ground_truth):
6261

6362
@pytest.mark.parametrize("connectivity", (4, 6, 8, 18, 26))
6463
@pytest.mark.parametrize("dtype", TEST_TYPES)
65-
@pytest.mark.parametrize("out_dtype", OUT_TYPES)
66-
def test_2d_rectangle(out_dtype, dtype, connectivity):
64+
def test_2d_rectangle(dtype, connectivity):
6765
def test(order, ground_truth):
6866
input_labels = np.zeros( (16,13), dtype=dtype, order=order )
6967
input_labels[:8,:8] = 8
@@ -72,7 +70,7 @@ def test(order, ground_truth):
7270
input_labels[8:,8:] = 11
7371

7472
output_labels = cc3d.connected_components(
75-
input_labels, out_dtype=out_dtype, connectivity=connectivity
73+
input_labels, connectivity=connectivity
7674
).astype(dtype)
7775
print(output_labels.shape)
7876
output_labels = output_labels[:,:]
@@ -412,9 +410,7 @@ def test_max_labels_nonsensical():
412410
assert np.all(real_labels == zero_labels)
413411
assert np.all(real_labels == negative_labels)
414412

415-
@pytest.mark.parametrize("out_dtype", OUT_TYPES)
416-
@pytest.mark.parametrize("sparse", (True, False))
417-
def test_compare_scipy_26(out_dtype, sparse):
413+
def test_compare_scipy_26():
418414
import scipy.ndimage.measurements
419415

420416
sx, sy, sz = 128, 128, 128
@@ -426,16 +422,16 @@ def test_compare_scipy_26(out_dtype, sparse):
426422
[[1,1,1], [1,1,1], [1,1,1]]
427423
]
428424

429-
cc3d_labels = cc3d.connected_components(labels, connectivity=26, out_dtype=out_dtype, sparse=sparse)
430-
scipy_labels, wow = scipy.ndimage.measurements.label(labels, structure=structure)
425+
cc3d_labels, Ncc3d = cc3d.connected_components(labels, connectivity=26, return_N=True)
426+
scipy_labels, Nscipy = scipy.ndimage.measurements.label(labels, structure=structure)
431427

432428
print(cc3d_labels)
433429
print(scipy_labels)
434430

431+
assert Ncc3d == Nscipy
435432
assert np.all(cc3d_labels == scipy_labels)
436433

437-
@pytest.mark.parametrize("sparse", (True, False))
438-
def test_compare_scipy_18(sparse):
434+
def test_compare_scipy_18():
439435
import scipy.ndimage.measurements
440436

441437
sx, sy, sz = 256, 256, 256
@@ -447,27 +443,28 @@ def test_compare_scipy_18(sparse):
447443
[[0,1,0], [1,1,1], [0,1,0]]
448444
]
449445

450-
cc3d_labels = cc3d.connected_components(labels, connectivity=18, sparse=sparse)
451-
scipy_labels, wow = scipy.ndimage.measurements.label(labels, structure=structure)
446+
cc3d_labels, Ncc3d = cc3d.connected_components(labels, connectivity=18, return_N=True)
447+
scipy_labels, Nscipy = scipy.ndimage.measurements.label(labels, structure=structure)
452448

453449
print(cc3d_labels)
454450
print(scipy_labels)
455451

452+
assert Ncc3d == Nscipy
456453
assert np.all(cc3d_labels == scipy_labels)
457454

458-
@pytest.mark.parametrize("sparse", (True, False))
459-
def test_compare_scipy_6(sparse):
455+
def test_compare_scipy_6():
460456
import scipy.ndimage.measurements
461457

462458
sx, sy, sz = 256, 256, 256
463459
labels = np.random.randint(0,2, (sx,sy,sz), dtype=np.bool)
464460

465-
cc3d_labels = cc3d.connected_components(labels, connectivity=6, sparse=sparse)
466-
scipy_labels, wow = scipy.ndimage.measurements.label(labels)
461+
cc3d_labels, Ncc3d = cc3d.connected_components(labels, connectivity=6, return_N=True)
462+
scipy_labels, Nscipy = scipy.ndimage.measurements.label(labels)
467463

468464
print(cc3d_labels)
469465
print(scipy_labels)
470466

467+
assert Ncc3d == Nscipy
471468
assert np.all(cc3d_labels == scipy_labels)
472469

473470
@pytest.mark.skipif("sys.maxsize <= 2**33")
@@ -477,22 +474,24 @@ def test_sixty_four_bit():
477474
cc3d.connected_components(input_labels, max_labels=3)
478475

479476
@pytest.mark.parametrize("size", (255,256))
480-
def test_stress_upper_bound_for_binary_6(size):
477+
@pytest.mark.parametrize("zeroth_pass", (True,False))
478+
def test_stress_upper_bound_for_binary_6(size, zeroth_pass):
481479
labels = np.zeros((size,size,size), dtype=np.bool)
482480
for z in range(labels.shape[2]):
483481
for y in range(labels.shape[1]):
484482
off = (y + (z % 2)) % 2
485483
labels[off::2,y,z] = True
486484

487-
out = cc3d.connected_components(labels, connectivity=6)
485+
out = cc3d.connected_components(labels, connectivity=6, zeroth_pass=zeroth_pass)
488486
assert np.max(out) + 1 <= (256**3) // 2 + 1
489487

490488
@pytest.mark.parametrize("size", (255,256))
491-
def test_stress_upper_bound_for_binary_8(size):
489+
@pytest.mark.parametrize("zeroth_pass", (True,False))
490+
def test_stress_upper_bound_for_binary_8(size, zeroth_pass):
492491
labels = np.zeros((size,size), dtype=np.bool)
493492
labels[0::2,0::2] = True
494493

495-
out = cc3d.connected_components(labels, connectivity=8)
494+
out = cc3d.connected_components(labels, connectivity=8, zeroth_pass=zeroth_pass)
496495
assert np.max(out) + 1 <= (256**2) // 4 + 1
497496

498497
for _ in range(10):
@@ -501,49 +500,51 @@ def test_stress_upper_bound_for_binary_8(size):
501500
assert np.max(out) + 1 <= (256**2) // 4 + 1
502501

503502
@pytest.mark.parametrize("size", (255,256))
504-
def test_stress_upper_bound_for_binary_18(size):
503+
@pytest.mark.parametrize("zeroth_pass", (True,False))
504+
def test_stress_upper_bound_for_binary_18(size, zeroth_pass):
505505
labels = np.zeros((size,size,size), dtype=np.bool)
506506
labels[::2,::2,::2] = True
507507
labels[1::2,1::2,::2] = True
508508

509-
out = cc3d.connected_components(labels, connectivity=26)
509+
out = cc3d.connected_components(labels, connectivity=18, zeroth_pass=zeroth_pass)
510510
assert np.max(out) + 1 <= (256**3) // 4 + 1
511511

512512
for _ in range(10):
513513
labels = np.random.randint(0,2, (256,256,256), dtype=np.bool)
514-
out = cc3d.connected_components(labels, connectivity=26)
515-
assert np.max(out) + 1 <= (256**2) // 4 + 1
514+
out = cc3d.connected_components(labels, connectivity=18, zeroth_pass=zeroth_pass)
515+
assert np.max(out) + 1 <= (256**3) // 4 + 1
516516

517517
@pytest.mark.parametrize("size", (255,256))
518-
def test_stress_upper_bound_for_binary_26(size):
518+
@pytest.mark.parametrize("zeroth_pass", (True,False))
519+
def test_stress_upper_bound_for_binary_26(size, zeroth_pass):
519520
labels = np.zeros((size,size,size), dtype=np.bool)
520521
labels[::2,::2,::2] = True
521522

523+
out = cc3d.connected_components(labels, connectivity=26, zeroth_pass=zeroth_pass)
524+
assert np.max(out) + 1 <= (256**3) // 8 + 1
525+
526+
for _ in range(10):
527+
labels = np.random.randint(0,2, (256,256,256), dtype=np.bool)
528+
out = cc3d.connected_components(labels, connectivity=26, zeroth_pass=zeroth_pass)
529+
assert np.max(out) + 1 <= (256**3) // 8 + 1
530+
522531
@pytest.mark.parametrize("connectivity", (8, 18, 26))
523532
@pytest.mark.parametrize("dtype", TEST_TYPES)
524-
@pytest.mark.parametrize("out_dtype", OUT_TYPES)
525533
@pytest.mark.parametrize("order", ("C", "F"))
526-
@pytest.mark.parametrize("sparse", (True, False))
527-
def test_all_zeros_3d(connectivity, dtype, out_dtype, order, sparse):
534+
def test_all_zeros_3d(connectivity, dtype, order):
528535
labels = np.zeros((128,128,128), dtype=dtype, order=order)
529-
out = cc3d.connected_components(labels, out_dtype=out_dtype, sparse=sparse)
536+
out = cc3d.connected_components(labels)
530537
assert np.all(out == 0)
531538

532539
@pytest.mark.parametrize("connectivity", (8, 18, 26))
533540
@pytest.mark.parametrize("dtype", TEST_TYPES)
534-
@pytest.mark.parametrize("out_dtype", OUT_TYPES)
535541
@pytest.mark.parametrize("order", ("C", "F"))
536542
@pytest.mark.parametrize("lbl", (1, 100, 7))
537-
@pytest.mark.parametrize("sparse", (True, False))
538-
def test_all_single_foreground(connectivity, dtype, out_dtype, order, lbl, sparse):
543+
def test_all_single_foreground(connectivity, dtype, order, lbl):
539544
labels = np.zeros((64,64,64), dtype=dtype, order=order) + lbl
540-
out = cc3d.connected_components(labels, out_dtype=out_dtype, sparse=sparse)
545+
out = cc3d.connected_components(labels)
541546
assert np.all(out == 1)
542547

543-
# def test_sixty_four_bit():
544-
# input_labels = np.ones((1626,1626,1626), dtype=np.uint8)
545-
# cc3d.connected_components(input_labels, max_labels=0)
546-
547548
def test_region_graph_26():
548549
labels = np.zeros( (10, 10, 10), dtype=np.uint32 )
549550

0 commit comments

Comments
 (0)