Skip to content
Snippets Groups Projects

Compare revisions

Changes are shown as if the source revision was being merged into the target revision. Learn more about comparing revisions.

Source

Select target project
No results found
Select Git revision
Loading items

Target

Select target project
  • QIM/tools/qim3d
1 result
Select Git revision
Loading items
Show changes
Commits on Source (8)
......@@ -30,3 +30,10 @@ Here, we provide functionalities designed specifically for 3D image analysis and
- watershed
- fade_mask
- overlay_rgb_images
::: qim3d.processing.features
options:
members:
- area
- volume
- sphericity
\ No newline at end of file
File suppressed by a .gitattributes entry or the file's encoding is unsupported.
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
width="12.806507mm"
height="10.91249mm"
viewBox="0 0 12.806507 10.91249"
version="1.1"
id="svg1"
inkscape:version="1.3.2 (1:1.3.2+202311252150+091e20ef0f)"
sodipodi:docname="qim3d-icon.svg"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<sodipodi:namedview
id="namedview1"
pagecolor="#ffffff"
bordercolor="#000000"
borderopacity="0.25"
inkscape:showpageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
inkscape:document-units="mm"
inkscape:zoom="1.1082211"
inkscape:cx="95.648783"
inkscape:cy="267.09472"
inkscape:window-width="914"
inkscape:window-height="1323"
inkscape:window-x="4182"
inkscape:window-y="56"
inkscape:window-maximized="0"
inkscape:current-layer="layer1" />
<defs
id="defs1" />
<g
inkscape:label="Layer 1"
inkscape:groupmode="layer"
id="layer1"
transform="translate(-79.904184,-77.787494)">
<g
id="g129"
transform="translate(384.99568,-124.53625)">
<path
id="path111"
d="m -299.2688,207.63632 -5.27832,2.18097 c -0.37814,0.14893 -0.41489,0.7496 -0.0409,0.91436 l 5.17239,2.13082 c 0.4877,0.18116 1.00239,0.18129 1.45479,0 l 5.17239,-2.13082 c 0.37397,-0.16476 0.33723,-0.76543 -0.0409,-0.91436 l -5.27831,-2.18097 c -0.44426,-0.18069 -0.70409,-0.18273 -1.1611,0 z"
fill="#ffab61"
paint-order="normal"
style="fill:#990000;fill-opacity:1;stroke:#000000;stroke-width:0.476;stroke-dasharray:none;stroke-opacity:1" />
<g
id="g128"
transform="translate(0,0.52916663)">
<path
id="path112"
d="m -295.10452,208.46249 c -0.77318,0.32871 -2.00929,0.87897 -2.77928,1.20984 -0.5027,0.18965 -0.83385,0.28613 -1.44516,0.0622 -0.56836,-0.24279 -1.13861,-0.48709 -1.70755,-0.73059 -0.51356,-0.24139 -0.85224,0.5504 -0.32297,0.75507 l 1.68789,0.72178 c 0.69391,0.30329 1.34389,0.27111 2.1577,-0.0742 l 3.47659,-1.50312 z"
fill="#ff8f64"
font-variant-ligatures="normal"
font-variant-position="normal"
font-variant-caps="normal"
font-variant-numeric="normal"
font-variant-alternates="normal"
font-feature-settings="normal"
text-indent="0"
text-align="start"
text-decoration-line="none"
text-decoration-style="solid"
text-decoration-color="#000000"
text-transform="none"
text-orientation="mixed"
white-space="normal"
shape-padding="0"
isolation="auto"
mix-blend-mode="normal"
solid-color="#000000"
solid-opacity="1"
vector-effect="none"
paint-order="normal"
style="fill:#000000;fill-opacity:0.425063;stroke-width:0.776388"
sodipodi:nodetypes="ccccccccc" />
<path
id="path114"
d="m -299.2688,204.6337 -5.27832,2.18097 c -0.37814,0.14893 -0.41489,0.7496 -0.0409,0.91436 l 5.17239,2.13082 c 0.4877,0.18116 1.00239,0.18129 1.45479,0 l 5.17239,-2.13082 c 0.37397,-0.16476 0.33723,-0.76543 -0.0409,-0.91436 l -5.27831,-2.18097 c -0.44426,-0.18069 -0.70409,-0.18272 -1.1611,0 z"
fill="#ffc861"
paint-order="normal"
style="fill:#cd4d00;fill-opacity:1;stroke:#000000;stroke-width:0.476;stroke-dasharray:none;stroke-opacity:1" />
</g>
<g
id="g127"
transform="translate(0,1.0583333)">
<path
id="path115"
d="m -295.30437,205.52095 c -0.7744,0.32922 -1.79262,0.8291 -2.56383,1.16048 -0.50349,0.18996 -0.83516,0.28658 -1.44743,0.0623 -0.56926,-0.24318 -1.1404,-0.48787 -1.71024,-0.73176 -0.51438,-0.24176 -0.85359,0.55127 -0.32348,0.75627 l 1.69054,0.72291 c 0.69501,0.30377 1.34602,0.27153 2.16111,-0.0743 l 3.48241,-1.48907 z"
fill="#ffab61"
font-variant-ligatures="normal"
font-variant-position="normal"
font-variant-caps="normal"
font-variant-numeric="normal"
font-variant-alternates="normal"
font-feature-settings="normal"
text-indent="0"
text-align="start"
text-decoration-line="none"
text-decoration-style="solid"
text-decoration-color="#000000"
text-transform="none"
text-orientation="mixed"
white-space="normal"
shape-padding="0"
isolation="auto"
mix-blend-mode="normal"
solid-color="#000000"
solid-opacity="1"
vector-effect="none"
paint-order="normal"
style="fill:#000000;fill-opacity:0.425063;stroke-width:0.205743"
sodipodi:nodetypes="ccccccccc" />
<path
id="path125"
d="m -299.26881,201.63132 -5.27832,2.18098 c -0.37814,0.14892 -0.41489,0.74959 -0.0409,0.91436 l 5.17239,2.13082 c 0.4877,0.18115 1.00239,0.18128 1.45479,0 l 5.17239,-2.13082 c 0.37397,-0.16477 0.33723,-0.76544 -0.0409,-0.91436 l -5.27831,-2.18098 c -0.44426,-0.18069 -0.70409,-0.18272 -1.1611,0 z"
fill="#55a1ff"
paint-order="normal"
style="fill:#ff9900;fill-opacity:1;stroke:#000000;stroke-width:0.476;stroke-dasharray:none;stroke-opacity:1" />
</g>
</g>
</g>
</svg>
......@@ -72,7 +72,7 @@ class BaseInterface(ABC):
height=self.height,
width=self.width,
favicon_path=Path(qim3d.__file__).parents[0]
/ "../docs/assets/qim3d-icon.svg",
/ "gui/assets/qim3d-icon.svg",
**kwargs,
)
......
......@@ -6,3 +6,4 @@ from .operations import *
from .cc import get_3d_cc
from .layers2d import segment_layers, get_lines
from .mesh import create_mesh
from .features import volume, area, sphericity
import numpy as np
import qim3d.processing
from qim3d.utils.logger import log
import trimesh
import qim3d
def volume(obj, **mesh_kwargs) -> float:
"""
Compute the volume of a 3D volume or mesh.
Args:
obj: Either a np.ndarray volume or a mesh object of type trimesh.Trimesh.
**mesh_kwargs: Additional arguments for mesh creation if the input is a volume.
Returns:
volume: The volume of the object.
Example:
Compute volume from a mesh:
```python
import qim3d
# Load a mesh from a file
mesh = qim3d.io.load_mesh('path/to/mesh.obj')
# Compute the volume of the mesh
volume = qim3d.processing.volume(mesh)
print('Volume:', volume)
```
Compute volume from a np.ndarray:
```python
import qim3d
# Generate a 3D blob
synthetic_blob = qim3d.generate.blob(noise_scale = 0.015)
# Compute the volume of the blob
volume = qim3d.processing.volume(synthetic_blob, level=0.5)
print('Volume:', volume)
```
"""
if isinstance(obj, np.ndarray):
log.info("Converting volume to mesh.")
obj = qim3d.processing.create_mesh(obj, **mesh_kwargs)
return obj.volume
def area(obj, **mesh_kwargs) -> float:
"""
Compute the surface area of a 3D volume or mesh.
Args:
obj: Either a np.ndarray volume or a mesh object of type trimesh.Trimesh.
**mesh_kwargs: Additional arguments for mesh creation if the input is a volume.
Returns:
area: The surface area of the object.
Example:
Compute area from a mesh:
```python
import qim3d
# Load a mesh from a file
mesh = qim3d.io.load_mesh('path/to/mesh.obj')
# Compute the surface area of the mesh
area = qim3d.processing.area(mesh)
print(f"Area: {area}")
```
Compute area from a np.ndarray:
```python
import qim3d
# Generate a 3D blob
synthetic_blob = qim3d.generate.blob(noise_scale = 0.015)
# Compute the surface area of the blob
volume = qim3d.processing.area(synthetic_blob, level=0.5)
print('Area:', volume)
```
"""
if isinstance(obj, np.ndarray):
log.info("Converting volume to mesh.")
obj = qim3d.processing.create_mesh(obj, **mesh_kwargs)
return obj.area
def sphericity(obj, **mesh_kwargs) -> float:
"""
Compute the sphericity of a 3D volume or mesh.
Sphericity is a measure of how spherical an object is. It is defined as the ratio
of the surface area of a sphere with the same volume as the object to the object's
actual surface area.
Args:
obj: Either a np.ndarray volume or a mesh object of type trimesh.Trimesh.
**mesh_kwargs: Additional arguments for mesh creation if the input is a volume.
Returns:
sphericity: A float value representing the sphericity of the object.
Example:
Compute sphericity from a mesh:
```python
import qim3d
# Load a mesh from a file
mesh = qim3d.io.load_mesh('path/to/mesh.obj')
# Compute the sphericity of the mesh
sphericity = qim3d.processing.sphericity(mesh)
```
Compute sphericity from a np.ndarray:
```python
import qim3d
# Generate a 3D blob
synthetic_blob = qim3d.generate.blob(noise_scale = 0.015)
# Compute the sphericity of the blob
sphericity = qim3d.processing.sphericity(synthetic_blob, level=0.5)
```
!!! info "Limitations due to pixelation"
Sphericity is particularly sensitive to the resolution of the mesh, as it directly impacts the accuracy of surface area and volume calculations.
Since the mesh is generated from voxel-based 3D volume data, the discrete nature of the voxels leads to pixelation effects that reduce the precision of sphericity measurements.
Higher resolution meshes may mitigate these errors but often at the cost of increased computational demands.
"""
if isinstance(obj, np.ndarray):
log.info("Converting volume to mesh.")
obj = qim3d.processing.create_mesh(obj, **mesh_kwargs)
volume = qim3d.processing.volume(obj)
area = qim3d.processing.area(obj)
if area == 0:
log.warning("Surface area is zero, sphericity is undefined.")
return np.nan
sphericity = (np.pi ** (1 / 3) * (6 * volume) ** (2 / 3)) / area
log.info(f"Sphericity: {sphericity}")
return sphericity
......@@ -9,6 +9,7 @@ def create_mesh(
volume: np.ndarray,
level: float = None,
step_size=1,
allow_degenerate=False,
padding: Tuple[int, int, int] = (2, 2, 2),
**kwargs: Any,
) -> trimesh.Trimesh:
......@@ -18,6 +19,9 @@ def create_mesh(
Args:
volume (np.ndarray): The 3D numpy array representing the volume.
level (float, optional): The threshold value for Marching Cubes. If None, Otsu's method is used.
step_size (int, optional): The step size for the Marching Cubes algorithm.
allow_degenerate (bool, optional): Whether to allow degenerate (i.e. zero-area) triangles in the end-result.
If False, degenerate triangles are removed, at the cost of making the algorithm slower. Default False.
padding (tuple of int, optional): Padding to add around the volume.
**kwargs: Additional keyword arguments to pass to `skimage.measure.marching_cubes`.
......@@ -39,7 +43,7 @@ def create_mesh(
mesh = qim3d.processing.create_mesh(vol, step_size=3)
qim3d.viz.mesh(mesh.vertices, mesh.faces)
```
<iframe src="https://platform.qim.dk/k3d/mesh_visualization.html" width="100%" height="500" frameborder="0"></iframe>
"""
if volume.ndim != 3:
raise ValueError("The input volume must be a 3D numpy array.")
......@@ -63,10 +67,13 @@ def create_mesh(
# Call skimage.measure.marching_cubes with user-provided kwargs
verts, faces, normals, values = measure.marching_cubes(
volume, level=level, step_size=step_size, **kwargs
volume, level=level, step_size=step_size, allow_degenerate=allow_degenerate, **kwargs
)
# Create the Trimesh object
mesh = trimesh.Trimesh(vertices=verts, faces=faces)
# Fix face orientation to ensure normals point outwards
trimesh.repair.fix_inversion(mesh, multibody=True)
return mesh
......@@ -3,11 +3,14 @@ Provides a collection of visualization functions.
"""
import math
import warnings
from typing import List, Optional, Union
import dask.array as da
import ipywidgets as widgets
import matplotlib.pyplot as plt
import matplotlib
import numpy as np
import qim3d
......@@ -28,6 +31,7 @@ def slices(
show_position: bool = True,
interpolation: Optional[str] = "none",
img_size=None,
cbar: bool = False,
**imshow_kwargs,
) -> plt.Figure:
"""Displays one or several slices from a 3d volume.
......@@ -50,6 +54,7 @@ def slices(
show (bool, optional): If True, displays the plot (i.e. calls plt.show()). Defaults to False.
show_position (bool, optional): If True, displays the position of the slices. Defaults to True.
interpolation (str, optional): Specifies the interpolation method for the image. Defaults to None.
cbar (bool, optional): Adds a colorbar positioned in the top-right for the corresponding colormap and data range. Defaults to False.
Returns:
fig (matplotlib.figure.Figure): The figure with the slices from the 3d array.
......@@ -127,6 +132,7 @@ def slices(
figsize=(ncols * img_height, nrows * img_width),
constrained_layout=True,
)
if nrows == 1:
axs = [axs] # Convert to a list for uniformity
......@@ -134,6 +140,11 @@ def slices(
if isinstance(vol, da.core.Array):
vol = vol.compute()
if cbar:
# In this case, we want the vrange to be constant across the slices, which makes them all comparable to a single cbar.
new_vmin = vmin if vmin else np.min(vol)
new_vmax = vmax if vmax else np.max(vol)
# Run through each ax of the grid
for i, ax_row in enumerate(axs):
for j, ax in enumerate(np.atleast_1d(ax_row)):
......@@ -141,10 +152,12 @@ def slices(
try:
slice_img = vol.take(slice_idxs[slice_idx], axis=axis)
if not cbar:
# If vmin is higher than the highest value in the image ValueError is raised
# We don't want to override the values because next slices might be okay
new_vmin = None if (isinstance(vmin, (float, int)) and vmin > np.max(slice_img)) else vmin
new_vmax = None if (isinstance(vmax, (float, int)) and vmax < np.min(slice_img)) else vmax
ax.imshow(
slice_img, cmap=cmap, interpolation=interpolation,vmin = new_vmin, vmax = new_vmax, **imshow_kwargs
)
......@@ -181,6 +194,19 @@ def slices(
# Hide the axis, so that we have a nice grid
ax.axis("off")
if cbar:
with warnings.catch_warnings():
warnings.simplefilter("ignore", category=UserWarning)
fig.tight_layout()
norm = matplotlib.colors.Normalize(vmin=new_vmin, vmax=new_vmax, clip=True)
mappable = matplotlib.cm.ScalarMappable(norm=norm, cmap=cmap)
# Figure coordinates of top-right axis
tr_pos = np.atleast_1d(axs[0])[-1].get_position()
# The width is divided by ncols to make it the same relative size to the images
cbar_ax = fig.add_axes([tr_pos.x1 + 0.05/ncols, tr_pos.y0, 0.05/ncols, tr_pos.height])
fig.colorbar(mappable=mappable, cax=cbar_ax, orientation='vertical')
if show:
plt.show()
......@@ -216,6 +242,7 @@ def slicer(
show_position: bool = False,
interpolation: Optional[str] = "none",
img_size=None,
cbar: bool = False,
**imshow_kwargs,
) -> widgets.interactive:
"""Interactive widget for visualizing slices of a 3D volume.
......@@ -230,6 +257,7 @@ def slicer(
img_width (int, optional): Width of the figure. Defaults to 3.
show_position (bool, optional): If True, displays the position of the slices. Defaults to False.
interpolation (str, optional): Specifies the interpolation method for the image. Defaults to None.
cbar (bool, optional): Adds a colorbar for the corresponding colormap and data range. Defaults to False.
Returns:
slicer_obj (widgets.interactive): The interactive widget for visualizing slices of a 3D volume.
......@@ -263,6 +291,7 @@ def slicer(
position=position,
n_slices=1,
show=True,
cbar=cbar,
**imshow_kwargs,
)
return fig
......
......@@ -128,7 +128,7 @@ class Installer:
output = subprocess.run(command, shell = True, capture_output=True)
def _windows():
subprocess.run(["powershell.exe", SOURCE_FNM, F"fnm use --fnm-dir {self.dir} --install-if-missing 22"])
subprocess.run(["powershell.exe",F'$env:XDG_DATA_HOME = "{self.dir}";', SOURCE_FNM, F"fnm use --fnm-dir {self.dir} --install-if-missing 22"])
print(F'Installing node.js...')
run_for_platform(linux_func = _linux, windows_func=_windows, macos_func=_linux)
......
......@@ -191,6 +191,7 @@ def mesh(
mesh = qim3d.processing.create_mesh(vol, step_size=3)
qim3d.viz.mesh(mesh.vertices, mesh.faces)
```
<iframe src="https://platform.qim.dk/k3d/mesh_visualization.html" width="100%" height="500" frameborder="0"></iframe>
"""
import k3d
......