Skip to content

Commit 79216be

Browse files
feat: true 2d contact analysis w/ fix for area calculation (#142)
* feat: true 2d contact analysis w/ fix for area calculation * perf: remove extraneous calculation * perf: don't calculate for label==label * fix: update cython code to use 4,8 correctly * fix: use correct anisotropy * fix: remove unused variable * test: check 4 and 8 connected * test: an additional orientation * fix: ensure anisotropy is good for 2d or 3d * test: check if surface area makes sense for 2d * fix: ensure surface_area=False still works
1 parent 346faa4 commit 79216be

File tree

3 files changed

+141
-12
lines changed

3 files changed

+141
-12
lines changed

automated_test.py

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -731,6 +731,15 @@ def test_contacts_surface_area():
731731
res = cc3d.contacts(labels_out, connectivity=6, surface_area=True)
732732
assert res[(1,2)] == 6
733733

734+
data = np.ones((3,3), dtype=np.uint32)
735+
data[1,1] = 2
736+
res = cc3d.contacts(data, connectivity=8, surface_area=True, anisotropy=(1.5, 2.3))
737+
assert np.isclose(res[(1,2)], (1.5 * 2 + 2.3 * 2))
738+
739+
res = cc3d.contacts(data, connectivity=4, surface_area=True, anisotropy=(1.5, 2.3))
740+
assert np.isclose(res[(1,2)], (1.5 * 2 + 2.3 * 2))
741+
742+
734743
def test_contacts_26():
735744
labels = np.zeros( (10, 10, 10), dtype=np.uint32 )
736745

@@ -846,14 +855,25 @@ def test_contacts_2d():
846855
labels[7,2] = 6
847856
labels[8,2] = 7
848857

858+
labels[9,9] = 8
859+
labels[8,8] = 9
860+
861+
labels[6,8] = 10
862+
labels[5,9] = 11
863+
849864
# not connected to anything else
850-
labels[1,:] = 10
865+
labels[1,:] = 12
851866

852-
res = cc3d.contacts(labels, connectivity=8)
867+
res = cc3d.contacts(labels, connectivity=4)
853868
assert set(res.keys()) == set([
854869
(2,3), (4,5), (6,7)
855870
])
856871

872+
res = cc3d.contacts(labels, connectivity=8)
873+
assert set(res.keys()) == set([
874+
(2,3), (4,5), (6,7), (8,9), (10,11),
875+
])
876+
857877
def test_voxel_graph_2d():
858878
labels = np.ones((3,3), dtype=np.uint8)
859879
graph = cc3d.voxel_connectivity_graph(labels, connectivity=4)

cc3d/cc3d_graphs.hpp

Lines changed: 113 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -279,6 +279,23 @@ inline void compute_neighborhood(
279279
neighborhood[12] = (connectivity > 18) * (plus_x + plus_y + minus_z) * (plus_y && minus_z);
280280
}
281281

282+
inline void compute_neighborhood(
283+
int64_t *neighborhood,
284+
const int64_t x, const int64_t y,
285+
const int64_t sx, const int64_t sy,
286+
const int64_t connectivity = 8
287+
) {
288+
289+
const int64_t plus_x = (x < (static_cast<int64_t>(sx) - 1)); // +x
290+
const int64_t minus_x = -1 * (x > 0); // -x
291+
const int64_t minus_y = -static_cast<int64_t>(sx) * (y > 0); // -y
292+
293+
neighborhood[0] = minus_x;
294+
neighborhood[1] = minus_y;
295+
neighborhood[2] = (connectivity > 4) * (minus_x + minus_y);
296+
neighborhood[3] = (connectivity > 4) * (plus_x + minus_y);
297+
}
298+
282299
struct pair_hash {
283300
inline std::size_t operator()(const std::pair<uint64_t,uint64_t> & v) const {
284301
return v.first * 31 + v.second; // arbitrary hash fn
@@ -287,7 +304,66 @@ struct pair_hash {
287304

288305
template <typename T>
289306
const std::unordered_map<std::pair<T,T>, float, pair_hash>
290-
extract_region_graph(
307+
extract_region_graph_2d(
308+
T* labels,
309+
const int64_t sx, const int64_t sy,
310+
const float wx=1, const float wy=1,
311+
const int64_t connectivity=8,
312+
const bool surface_area=true
313+
) {
314+
315+
if (connectivity != 4 && connectivity != 8) {
316+
throw std::runtime_error("Only 4 and 8 connectivities are supported.");
317+
}
318+
319+
int64_t neighborhood[4];
320+
float areas[4] = {
321+
wy, wx, // edges
322+
0, 0 // corners
323+
};
324+
if (!surface_area) {
325+
std::fill(areas, areas+4, 1.0);
326+
}
327+
328+
T cur = 0;
329+
T label = 0;
330+
331+
std::unordered_map<std::pair<T,T>, float, pair_hash> edges;
332+
333+
for (int64_t y = 0; y < sy; y++) {
334+
for (int64_t x = 0; x < sx; x++) {
335+
int64_t loc = x + sx * y;
336+
cur = labels[loc];
337+
338+
if (cur == 0) {
339+
continue;
340+
}
341+
342+
compute_neighborhood(neighborhood, x, y, sx, sy, connectivity);
343+
344+
for (int i = 0; i < connectivity / 2; i++) {
345+
int64_t neighboridx = loc + neighborhood[i];
346+
label = labels[neighboridx];
347+
348+
if (label == 0 || label == cur) {
349+
continue;
350+
}
351+
else if (cur > label) {
352+
edges[std::pair<T,T>(label, cur)] += areas[i];
353+
}
354+
else if (cur < label) {
355+
edges[std::pair<T,T>(cur, label)] += areas[i];
356+
}
357+
}
358+
}
359+
}
360+
361+
return edges;
362+
}
363+
364+
template <typename T>
365+
const std::unordered_map<std::pair<T,T>, float, pair_hash>
366+
extract_region_graph_3d(
291367
T* labels,
292368
const int64_t sx, const int64_t sy, const int64_t sz,
293369
const float wx=1, const float wy=1, const float wz=1,
@@ -339,7 +415,7 @@ extract_region_graph(
339415
int64_t neighboridx = loc + neighborhood[i];
340416
label = labels[neighboridx];
341417

342-
if (label == 0) {
418+
if (label == 0 || label == cur) {
343419
continue;
344420
}
345421
else if (cur > label) {
@@ -356,6 +432,41 @@ extract_region_graph(
356432
return edges;
357433
}
358434

435+
436+
template <typename T>
437+
const std::unordered_map<std::pair<T,T>, float, pair_hash>
438+
extract_region_graph(
439+
T* labels,
440+
const int64_t sx, const int64_t sy, const int64_t sz,
441+
const float wx=1, const float wy=1, const float wz=1,
442+
const int64_t connectivity=26,
443+
const bool surface_area=true
444+
) {
445+
if (connectivity == 4 || connectivity == 8) {
446+
if (sz != 1) {
447+
throw std::runtime_error("z thickness must be 1 for 2d region graph extraction.");
448+
}
449+
450+
return extract_region_graph_2d<T>(
451+
labels,
452+
sx, sy,
453+
wx, wy,
454+
connectivity, surface_area
455+
);
456+
}
457+
else if (connectivity == 6 || connectivity == 18 || connectivity == 26) {
458+
return extract_region_graph_3d<T>(
459+
labels,
460+
sx, sy, sz,
461+
wx, wy, wz,
462+
connectivity, surface_area
463+
);
464+
}
465+
else {
466+
throw std::runtime_error("Only (2d) 4, 8, or (3d) 6, 18, and 26 connectivities are supported.");
467+
}
468+
}
469+
359470
template <typename T>
360471
std::map<T, std::vector<std::pair<size_t, size_t>>>
361472
extract_runs(T* labels, const size_t voxels) {

cc3d/fastcc3d.pyx

Lines changed: 6 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1185,7 +1185,7 @@ def contacts(
11851185
labels:np.ndarray,
11861186
int connectivity=26,
11871187
surface_area:bool = True,
1188-
anisotropy:Tuple[int,int,int] = (1,1,1)
1188+
anisotropy:Union[Tuple[float,float,float], Tuple[float,float]] = (1,1,1)
11891189
) -> Dict[Tuple[int,int], float]:
11901190
"""
11911191
Get the N-connected region adjacancy graph of a 3D image
@@ -1207,6 +1207,9 @@ def contacts(
12071207
while len(labels.shape) < 3:
12081208
labels = labels[..., np.newaxis ]
12091209
1210+
while len(anisotropy) < 3:
1211+
anisotropy = tuple(anisotropy) + (1,)
1212+
12101213
return _contacts(labels, connectivity, surface_area, anisotropy)
12111214
12121215
def _contacts(
@@ -1215,13 +1218,8 @@ def _contacts(
12151218
surface_area=True,
12161219
anisotropy=(1,1,1),
12171220
):
1218-
if connectivity == 8 and labels.shape[2] == 1:
1219-
connectivity = 26
1220-
if connectivity == 4 and labels.shape[2] == 1:
1221-
connectivity = 6
1222-
1223-
if connectivity not in (6, 18, 26):
1224-
raise ValueError("Only 6, 18, and 26 connectivities are supported. Got: " + str(connectivity))
1221+
if connectivity not in (4, 8, 6, 18, 26):
1222+
raise ValueError(f"Only (2d) 4, 8, (3d) 6, 18, and 26 connectivities are supported. Got: {connectivity}")
12251223
12261224
labels = np.asfortranarray(labels)
12271225

0 commit comments

Comments
 (0)