Skip to content

Commit 1f64e45

Browse files
committed
Add spatial annotations example
1 parent 04ca1f6 commit 1f64e45

3 files changed

Lines changed: 138 additions & 7 deletions

File tree

examples/cells.ipynb

Lines changed: 129 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
},
1313
{
1414
"cell_type": "code",
15-
"execution_count": 1,
15+
"execution_count": null,
1616
"id": "59e8a2c3",
1717
"metadata": {
1818
"execution": {
@@ -27,7 +27,9 @@
2727
},
2828
"outputs": [],
2929
"source": [
30-
"import libcarna"
30+
"import libcarna\n",
31+
"import numpy as np\n",
32+
"import scipy.ndimage as ndi"
3133
]
3234
},
3335
{
@@ -85,7 +87,7 @@
8587
},
8688
{
8789
"cell_type": "code",
88-
"execution_count": 3,
90+
"execution_count": null,
8991
"id": "ed16156a",
9092
"metadata": {
9193
"execution": {
@@ -166,7 +168,7 @@
166168
}
167169
],
168170
"source": [
169-
"GEOMETRY_TYPE_VOLUME = 2\n",
171+
"GEOMETRY_TYPE_VOLUME = 1\n",
170172
"\n",
171173
"# Create and configure frame renderer\n",
172174
"mip = libcarna.mip(GEOMETRY_TYPE_VOLUME, cmap='jet', sr=500)\n",
@@ -607,7 +609,129 @@
607609
"Note that we use `dvr.replicate()` when adding the previously defined DVR to the renderer. This is because each\n",
608610
"rendering stage can only be added to one renderer, hence, we replicate it this time. Of course, we could have used the\n",
609611
"`dvr.replicate()` method the first time that we added the DVR to a renderer, too, but this is not mandatory. All\n",
610-
"rendering stages provide such a method."
612+
"rendering stages provide such a method.\n",
613+
"\n",
614+
"## Segmentation\n",
615+
"\n",
616+
"Perform segmentation of the nuclei by applying an intensity threshold:"
617+
]
618+
},
619+
{
620+
"cell_type": "code",
621+
"execution_count": null,
622+
"id": "417a2589",
623+
"metadata": {
624+
"vscode": {
625+
"languageId": "plaintext"
626+
}
627+
},
628+
"outputs": [],
629+
"source": [
630+
"data_denoised = ndi.gaussian_filter(data, 1)"
631+
]
632+
},
633+
{
634+
"cell_type": "code",
635+
"execution_count": null,
636+
"id": "b8af663f",
637+
"metadata": {
638+
"vscode": {
639+
"languageId": "plaintext"
640+
}
641+
},
642+
"outputs": [],
643+
"source": [
644+
"GEOMETRY_TYPE_MASK = 2\n",
645+
"\n",
646+
"seg = libcarna.volume(\n",
647+
" GEOMETRY_TYPE_MASK,\n",
648+
" ndi.gaussian_filter(data, 10) > 10_000,\n",
649+
" parent=volume,\n",
650+
" spacing=volume.spacing,\n",
651+
")\n",
652+
"\n",
653+
"libcarna.imshow(\n",
654+
" libcarna.animate(\n",
655+
" libcarna.animate.swing_local(camera, amplitude=22),\n",
656+
" n_frames=50,\n",
657+
" ).render(\n",
658+
" libcarna.renderer(600, 450, [\n",
659+
" dvr.replicate(),\n",
660+
" libcarna.mask_renderer(GEOMETRY_TYPE_MASK),\n",
661+
" ]),\n",
662+
" camera,\n",
663+
" ),\n",
664+
")"
665+
]
666+
},
667+
{
668+
"cell_type": "markdown",
669+
"id": "97991990",
670+
"metadata": {},
671+
"source": [
672+
"## Pointwise Annotations"
673+
]
674+
},
675+
{
676+
"cell_type": "code",
677+
"execution_count": null,
678+
"id": "9931ba22",
679+
"metadata": {
680+
"vscode": {
681+
"languageId": "plaintext"
682+
}
683+
},
684+
"outputs": [],
685+
"source": [
686+
"data_max = ndi.maximum_filter(data_denoised, size=5)\n",
687+
"detections = np.where(\n",
688+
" np.logical_and(\n",
689+
" data_denoised == data_max,\n",
690+
" data_denoised >= 30_000,\n",
691+
" )\n",
692+
")"
693+
]
694+
},
695+
{
696+
"cell_type": "code",
697+
"execution_count": null,
698+
"id": "537cfdd2",
699+
"metadata": {
700+
"vscode": {
701+
"languageId": "plaintext"
702+
}
703+
},
704+
"outputs": [],
705+
"source": [
706+
"GEOMETRY_TYPE_OPAQUE = 3\n",
707+
"\n",
708+
"ball = libcarna.meshes.create_ball(5)\n",
709+
"red = libcarna.material('solid', color=libcarna.color.RED)\n",
710+
"\n",
711+
"for xyz in zip(*detections):\n",
712+
" libcarna.geometry(\n",
713+
" GEOMETRY_TYPE_OPAQUE,\n",
714+
" parent=volume,\n",
715+
" features={\n",
716+
" libcarna.mesh_renderer.ROLE_DEFAULT_MESH: ball,\n",
717+
" libcarna.mesh_renderer.ROLE_DEFAULT_MATERIAL: red,\n",
718+
" },\n",
719+
" ).translate(\n",
720+
" *volume.transform_from_voxels_into(volume).point(xyz)\n",
721+
" )\n",
722+
"\n",
723+
"libcarna.imshow(\n",
724+
" libcarna.animate(\n",
725+
" libcarna.animate.swing_local(camera, amplitude=22),\n",
726+
" n_frames=50,\n",
727+
" ).render(\n",
728+
" libcarna.renderer(600, 450, [\n",
729+
" dvr.replicate(),\n",
730+
" libcarna.opaque_renderer(GEOMETRY_TYPE_OPAQUE),\n",
731+
" ]),\n",
732+
" camera,\n",
733+
" ),\n",
734+
")"
611735
]
612736
}
613737
],

misc/libcarna/_spatial.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -282,6 +282,13 @@ def transform_into_voxels_from(self, rhs: libcarna.base.Spatial) -> np.array:
282282
self.transform_from(rhs).mat
283283
)
284284

285+
def transform_from_voxels_into(self, lhs: libcarna.base.Spatial) -> np.array:
286+
"""
287+
Compute the transformation from the voxel coordinate system of this volume into the local coordinate system
288+
of a spatial object `lhs`.
289+
"""
290+
return transform(np.linalg.inv(self.transform_into_voxels_from(lhs).mat))
291+
285292
def normalized(self, array: np.ndarray) -> np.ndarray:
286293
"""
287294
Convert raw array intensities to the normalized intensities in [0, 1] used for rendering.

misc/libcarna/_transform.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,10 @@ def __init__(self, mat: np.ndarray):
77
self.mat = mat
88

99
def point(self, xyz: np.ndarray = np.zeros(3)) -> np.ndarray:
10-
return self.mat @ np.array([*xyz, 1.0])
10+
return (self.mat @ np.array([*xyz, 1.0]))[:3]
1111

1212
def intpoint(self, *args, **kwargs) -> tuple[int, int, int]:
13-
return tuple(self.point(*args, **kwargs).round().astype(int))[:3]
13+
return tuple(self.point(*args, **kwargs).round().astype(int))
1414

1515
def direction(self, xyz: np.ndarray = np.zeros(3)) -> np.ndarray:
1616
return self.mat @ np.array([*xyz, 0.0])

0 commit comments

Comments
 (0)