Skip to content

Commit ff1f74c

Browse files
feat(voxel_connectivity_graph): extracts directed voxel connectivity … (#48)
* 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
1 parent 2d8f6e9 commit ff1f74c

File tree

8 files changed

+4452
-1889
lines changed

8 files changed

+4452
-1889
lines changed

.gitignore

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -140,4 +140,4 @@ saved_images
140140
save_images
141141

142142
.DS_Store
143-
callgrind*
143+
callgrind*

README.md

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ import numpy as np
5050
labels_in = np.ones((512, 512, 512), dtype=np.int32)
5151
labels_out = cc3d.connected_components(labels_in) # 26-connected
5252

53-
connectivity = 6 # only 26, 18, and 6 are allowed
53+
connectivity = 6 # only 4,8 (2D) and 26, 18, and 6 (3D) are allowed
5454
labels_out = cc3d.connected_components(labels_in, connectivity=connectivity)
5555

5656
# You can adjust the bit width of the output to accomodate
@@ -73,7 +73,15 @@ for segid in range(1, N+1):
7373

7474
# We also include a region adjacency graph function
7575
# that returns a set of undirected edges.
76-
graph = cc3d.region_graph(labels_out, connectivity=connectivity)
76+
edges = cc3d.region_graph(labels_out, connectivity=connectivity)
77+
78+
# You can also generate a voxel connectivty graph that encodes
79+
# which directions are passable from a given voxel as a bitfield.
80+
# This could also be seen as a method of eroding voxels fractionally
81+
# based on their label adjacencies.
82+
# See help(cc3d.voxel_connectivity_graph) for details.
83+
graph = cc3d.voxel_connectivity_graph(labels, connectivity=connectivity)
84+
7785
```
7886

7987
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`.
@@ -106,11 +114,22 @@ uint16_t* cc_labels = cc3d::connected_components3d<int, uint16_t>(
106114
/*connectivity=*/18 // default is 26 connected
107115
);
108116

117+
#include "cc3d_graphs.hpp"
118+
109119
// edges is [ e11, e12, e21, e22, ... ]
110120
std::vector<uint64_t> edges = cc3d::extract_region_graph<uint64_t>(
111121
labels, /*sx=*/512, /*sy=*/512, /*sz=*/512,
112122
/*connectivity=*/18 // default is 26 connected
113123
);
124+
125+
// graph is a series of bitfields that describe inter-voxel
126+
// connectivity based on adjacent labels. See "cc3d_graphs.hpp"
127+
// for details on the bitfield.
128+
uint32_t* graph = extract_voxel_connectivity_graph<T>(
129+
labels, /*sx=*/512, /*sy=*/512, /*sz=*/512,
130+
/*connectivity=*/6 // default is 26 connected
131+
);
132+
114133
```
115134

116135
## 26-Connected CCL Algorithm

automated_test.py

Lines changed: 133 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import pytest
2+
import sys
23

34
import cc3d
45
import numpy as np
@@ -469,6 +470,55 @@ def test_compare_scipy_6(sparse):
469470

470471
assert np.all(cc3d_labels == scipy_labels)
471472

473+
@pytest.mark.skipif("sys.maxsize <= 2**33")
474+
@pytest.mark.xfail(raises=MemoryError, reason="Some build tools don't have enough memory for this.")
475+
def test_sixty_four_bit():
476+
input_labels = np.ones((1626,1626,1626), dtype=np.uint8)
477+
cc3d.connected_components(input_labels, max_labels=3)
478+
479+
@pytest.mark.parametrize("size", (255,256))
480+
def test_stress_upper_bound_for_binary_6(size):
481+
labels = np.zeros((size,size,size), dtype=np.bool)
482+
for z in range(labels.shape[2]):
483+
for y in range(labels.shape[1]):
484+
off = (y + (z % 2)) % 2
485+
labels[off::2,y,z] = True
486+
487+
out = cc3d.connected_components(labels, connectivity=6)
488+
assert np.max(out) + 1 <= (256**3) // 2 + 1
489+
490+
@pytest.mark.parametrize("size", (255,256))
491+
def test_stress_upper_bound_for_binary_8(size):
492+
labels = np.zeros((size,size), dtype=np.bool)
493+
labels[0::2,0::2] = True
494+
495+
out = cc3d.connected_components(labels, connectivity=8)
496+
assert np.max(out) + 1 <= (256**2) // 4 + 1
497+
498+
for _ in range(10):
499+
labels = np.random.randint(0,2, (256,256), dtype=np.bool)
500+
out = cc3d.connected_components(labels, connectivity=8)
501+
assert np.max(out) + 1 <= (256**2) // 4 + 1
502+
503+
@pytest.mark.parametrize("size", (255,256))
504+
def test_stress_upper_bound_for_binary_18(size):
505+
labels = np.zeros((size,size,size), dtype=np.bool)
506+
labels[::2,::2,::2] = True
507+
labels[1::2,1::2,::2] = True
508+
509+
out = cc3d.connected_components(labels, connectivity=26)
510+
assert np.max(out) + 1 <= (256**3) // 4 + 1
511+
512+
for _ in range(10):
513+
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
516+
517+
@pytest.mark.parametrize("size", (255,256))
518+
def test_stress_upper_bound_for_binary_26(size):
519+
labels = np.zeros((size,size,size), dtype=np.bool)
520+
labels[::2,::2,::2] = True
521+
472522
@pytest.mark.parametrize("connectivity", (8, 18, 26))
473523
@pytest.mark.parametrize("dtype", TEST_TYPES)
474524
@pytest.mark.parametrize("out_dtype", OUT_TYPES)
@@ -595,53 +645,89 @@ def test_region_graph_6():
595645
(1,2), (1,3), (1,4), (1,5), (1,6), (1,7)
596646
])
597647

598-
@pytest.mark.parametrize("size", (255,256))
599-
def test_stress_upper_bound_for_binary_6(size):
600-
labels = np.zeros((size,size,size), dtype=np.bool)
601-
for z in range(labels.shape[2]):
602-
for y in range(labels.shape[1]):
603-
off = (y + (z % 2)) % 2
604-
labels[off::2,y,z] = True
605-
606-
out = cc3d.connected_components(labels, connectivity=6)
607-
assert np.max(out) + 1 <= (256**3) // 2 + 1
608-
609-
@pytest.mark.parametrize("size", (255,256))
610-
def test_stress_upper_bound_for_binary_8(size):
611-
labels = np.zeros((size,size), dtype=np.bool)
612-
labels[0::2,0::2] = True
613-
614-
out = cc3d.connected_components(labels, connectivity=8)
615-
assert np.max(out) + 1 <= (256**2) // 4 + 1
616-
617-
for _ in range(10):
618-
labels = np.random.randint(0,2, (256,256), dtype=np.bool)
619-
out = cc3d.connected_components(labels, connectivity=8)
620-
assert np.max(out) + 1 <= (256**2) // 4 + 1
621-
622-
@pytest.mark.parametrize("size", (255,256))
623-
def test_stress_upper_bound_for_binary_18(size):
624-
labels = np.zeros((size,size,size), dtype=np.bool)
625-
labels[::2,::2,::2] = True
626-
labels[1::2,1::2,::2] = True
627-
628-
out = cc3d.connected_components(labels, connectivity=26)
629-
assert np.max(out) + 1 <= (256**3) // 4 + 1
630-
631-
for _ in range(10):
632-
labels = np.random.randint(0,2, (256,256,256), dtype=np.bool)
633-
out = cc3d.connected_components(labels, connectivity=26)
634-
assert np.max(out) + 1 <= (256**2) // 4 + 1
648+
def test_voxel_graph_2d():
649+
labels = np.ones((3,3), dtype=np.uint8)
650+
graph = cc3d.voxel_connectivity_graph(labels, connectivity=4)
651+
assert graph.dtype == np.uint8
652+
assert np.all(graph)
653+
654+
graph = cc3d.voxel_connectivity_graph(labels, connectivity=8)
655+
assert graph.dtype == np.uint8
656+
assert np.all(graph)
657+
658+
labels[1,1] = 0
659+
graph = cc3d.voxel_connectivity_graph(labels, connectivity=4)
660+
gt = np.array([
661+
[0x0f, 0b00001011, 0x0f ],
662+
[0b00001110, 0x00, 0b00001101],
663+
[0x0f, 0b00000111, 0x0f ]
664+
], dtype=np.uint8)
665+
assert np.all(gt.T == graph)
635666

636-
@pytest.mark.parametrize("size", (255,256))
637-
def test_stress_upper_bound_for_binary_26(size):
638-
labels = np.zeros((size,size,size), dtype=np.bool)
639-
labels[::2,::2,::2] = True
667+
graph = cc3d.voxel_connectivity_graph(labels, connectivity=8)
668+
gt = np.array([
669+
[0b11101111, 0b11111011, 0b11011111],
670+
[0b11111110, 0x00, 0b11111101],
671+
[0b10111111, 0b11110111, 0b01111111]
672+
], dtype=np.uint8)
673+
assert np.all(gt.T == graph)
674+
675+
def test_voxel_graph_3d():
676+
labels = np.ones((3,3,3), dtype=np.uint8)
677+
graph = cc3d.voxel_connectivity_graph(labels, connectivity=26)
678+
assert graph.dtype == np.uint32
679+
assert np.all(graph)
680+
681+
E = {
682+
(-1,-1,-1): 0b01111111111111111111111111,
683+
(+1,-1,-1): 0b10111111111111111111111111,
684+
(-1,+1,-1): 0b11011111111111111111111111,
685+
(+1,+1,-1): 0b11101111111111111111111111,
686+
(-1,-1,+1): 0b11110111111111111111111111,
687+
(+1,-1,+1): 0b11111011111111111111111111,
688+
(-1,+1,+1): 0b11111101111111111111111111,
689+
(+1,+1,+1): 0b11111110111111111111111111,
690+
( 0,-1,-1): 0b11111111011111111111111111,
691+
( 0,+1,-1): 0b11111111101111111111111111,
692+
(-1, 0,-1): 0b11111111110111111111111111,
693+
(+1, 0,-1): 0b11111111111011111111111111,
694+
( 0,-1,+1): 0b11111111111101111111111111,
695+
( 0,+1,+1): 0b11111111111110111111111111,
696+
(-1, 0,+1): 0b11111111111111011111111111,
697+
(+1, 0,+1): 0b11111111111111101111111111,
698+
(-1,-1, 0): 0b11111111111111110111111111,
699+
(+1,-1, 0): 0b11111111111111111011111111,
700+
(-1,+1, 0): 0b11111111111111111101111111,
701+
(+1,+1, 0): 0b11111111111111111110111111,
702+
( 0, 0,-1): 0b11111111111111111111011111,
703+
( 0, 0,+1): 0b11111111111111111111101111,
704+
( 0,-1, 0): 0b11111111111111111111110111,
705+
( 0,+1, 0): 0b11111111111111111111111011,
706+
(-1, 0, 0): 0b11111111111111111111111101,
707+
(+1, 0, 0): 0b11111111111111111111111110,
708+
}
709+
710+
711+
labels[1,1,1] = 0
712+
graph = cc3d.voxel_connectivity_graph(labels, connectivity=26)
713+
gt = np.array([
714+
[
715+
[ E[(1, 1,1)], E[(0, 1,1)], E[(-1, 1,1)] ],
716+
[ E[(1, 0,1)], E[(0, 0,1)], E[(-1, 0,1)] ],
717+
[ E[(1,-1,1)], E[(0,-1,1)], E[(-1,-1,1)] ],
718+
],
719+
[
720+
[ E[(1, 1,0)], E[(0,1,0)], E[(-1, 1,0)] ],
721+
[ E[(1, 0,0)], 0x00, E[(-1, 0,0)] ],
722+
[ E[(1,-1,0)], E[(0,-1,0)], E[(-1,-1,0)] ]
723+
],
724+
[
725+
[ E[(1,1,-1)], E[(0,1,-1)], E[(-1,1,-1)] ],
726+
[ E[(1,0,-1)], E[(0,0,-1)], E[(-1,0,-1)] ],
727+
[ E[(1,-1,-1)], E[(0,-1,-1)], E[(-1,-1,-1)] ]
728+
]
729+
], dtype=np.uint32)
730+
731+
assert np.all(gt.T == graph)
640732

641-
out = cc3d.connected_components(labels, connectivity=26)
642-
assert np.max(out) + 1 <= (256**3) // 8 + 1
643733

644-
for _ in range(10):
645-
labels = np.random.randint(0,2, (256,256,256), dtype=np.bool)
646-
out = cc3d.connected_components(labels, connectivity=26)
647-
assert np.max(out) + 1 <= (256**2) // 8 + 1

benchmarks/README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -136,7 +136,7 @@ Here there's a slight difference in the memory usage. SciPy uses about 850 MB wh
136136

137137
<p style="font-style: italics;" align="center">
138138
<img height=384 src="https://raw.githubusercontent.com/seung-lab/connected-components-3d/master/benchmarks/cc3d_sparse_black.png" alt="Fig. 4: Different configurations run against a uint64 512x512x512 black cube using 26-connectivity. (black) SciPy 1.5.2 (blue) cc3d 1.14.0 (red) cc3d 1.14.0 in sparse mode." /><br>
139-
Fig. 4: Different configurations run against a uint32 512x512x512 black cube using 26-connectivity. (black) SciPy 1.5.2 (blue) cc3d 1.14.0 (red) cc3d 1.14.0 in sparse mode.
139+
Fig. 4: Different configurations run against a uint64 512x512x512 black cube using 26-connectivity. (black) SciPy 1.5.2 (blue) cc3d 1.14.0 (red) cc3d 1.14.0 in sparse mode.
140140
</p>
141141

142142
Sometimes empty data shows up in your pipeline. Sometimes a lot of it. How do your libraries handle it? At full speed? Slower? Faster than normal?

0 commit comments

Comments
 (0)