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 (4)
Showing
with 288 additions and 108 deletions
docs/assets/screenshots/blob_detection.gif

1.93 MiB

docs/assets/screenshots/blob_get_mask.gif

815 KiB

docs/assets/screenshots/filter_original.png

219 KiB

docs/assets/screenshots/filter_processed.png

51.8 KiB

docs/assets/screenshots/local_thickness_2d.png

108 KiB

docs/assets/screenshots/local_thickness_3d.gif

1.44 MiB

docs/assets/screenshots/structure_tensor.gif

3.86 MiB

......@@ -3,32 +3,10 @@ Dealing with volumetric data can be done by `qim3d` for the most common image fo
Currently, it is possible to directly load `tiff`, `h5`, `nii`,`txm`, `vol` and common `PIL` formats using one single function.
!!! Example
```python
import qim3d
# Get some data from examples
vol = qim3d.examples.blobs_256x256x256
# Save in a local file
qim3d.io.save("blobs.tif", vol)
# Load data from file
loaded_vol = qim3d.io.load("blobs.tif")
```
::: qim3d.io.load
::: qim3d.io
options:
members:
- load
::: qim3d.io.save
options:
members:
- save
::: qim3d.io.downloader
options:
members:
- Downloader
- ImgExamples
\ No newline at end of file
# Processing data
::: qim3d.processing.filters
options:
members:
-
`qim3d` provides various tools for 3D image processing. Here, we provide a suite of powerful functionalities designed specifically for 3D image analysis and processing. From filter pipelines to structure tensor computation and blob detection, `qim3d` equips you with the tools you need to extract meaningful insights from your data.
::: qim3d.processing.filters.Pipeline
::: qim3d.processing
options:
members:
- append
::: qim3d.processing.structure_tensor
\ No newline at end of file
- structure_tensor
- local_thickness
- get_3d_cc
- Pipeline
- Blob
\ No newline at end of file
......@@ -12,6 +12,12 @@ And remember to keep your pip installation [up to date](/qim3d/#upgrade) so that
### v0.3.3 (coming soon!)
- Introduction of `qim3d.viz.slicer` (and also `qim3d.viz.orthogonal` ) 🎉
- Introduction of `qim3d.gui.annotation_tool` 🎉
- Introduction of `qim3d.processing.Blob` for blob detection 🎉
- Introduction of `qim3d.processing.local_thickness` 🎉
- Introduction of `qim3d.processing.structure_tensor` 🎉
- Support for loading DICOM files with `qim3d.io.load`🎉
- Introduction of `qim3d.processing.get_3d_cc` for 3D connected components and `qim3d.viz.plot_cc` for associated visualization 🎉
- Introduction of `qim3d.viz.colormaps` for easy visualization of e.g. multi-label segmentation results 🎉
### v0.3.2 (23/02/2024)
......@@ -20,9 +26,9 @@ This version focus on the increased usability of the `qim3d` library
- Online documentation available at [https://platform.qim.dk/qim3d](https://platform.qim.dk/qim3d)
- Virtual stacks also available for `txm` files
- Updated GUI launch pipeline
- New functionalities for `qim3d.vix.slices`
- New functionalities for `qim3d.viz.slices`
- Introduction of `qim3d.processing.filters` 🎉
- Introduction of `qim3d.viz.k3d` 🎉
- Introduction of `qim3d.viz.vol` 🎉
### v0.3.1 (01/02/2024)
......
# Data visualization
The `qim3d`libray aims to provide easy ways to explore and get insights from volumetric data.
The `qim3d` library aims to provide easy ways to explore and get insights from volumetric data.
!!! Example
```python
import qim3d
img = qim3d.examples.shell_225x128x128
qim3d.viz.slices(img, n_slices=15)
```
![Grid of slices](assets/screenshots/viz-slices.png)
!!! Example
```python
import qim3d
vol = qim3d.examples.bone_128x128x128
qim3d.viz.slicer(vol)
```
![viz slicer](assets/screenshots/viz-slicer.gif)
!!! Example
```python
import qim3d
vol = qim3d.examples.fly_150x256x256
qim3d.viz.orthogonal(vol, cmap="magma")
```
![viz orthogonal](assets/screenshots/viz-orthogonal.gif)
!!! Example
```python
import qim3d
vol = qim3d.examples.bone_128x128x128
qim3d.viz.vol(vol)
```
<iframe src="https://platform.qim.dk/k3d/fima-bone_128x128x128-20240221113459.html" width="100%" height="500" frameborder="0"></iframe>
::: qim3d.viz.img
::: qim3d.viz
options:
members:
- slices
- slicer
- orthogonal
::: qim3d.viz.k3d
options:
members:
- vol
- local_thickness
- vectors
- plot_cc
- objects
from .loading import DataLoader, load, ImgExamples
from .downloader import Downloader
from .load import DataLoader, load, ImgExamples
from .save import DataSaver, save
from .saving import DataSaver, save
from .sync import Sync
from . import logger
\ No newline at end of file
......@@ -7,16 +7,16 @@ from urllib.parse import quote
from tqdm import tqdm
from pathlib import Path
from qim3d.io.load import load
from qim3d.io import load
from qim3d.io.logger import log
import outputformat as ouf
class Downloader:
"""Class for downloading large data files available on the [QIM data repository](https://data.qim.dk/data-repository/).
"""Class for downloading large data files available on the [QIM data repository](https://data.qim.dk/).
Attributes:
[folder_name] (str): folder class with the name of the folder in https://data.qim.dk/data-repository/
[folder_name] (str): folder class with the name of the folder in <https://data.qim.dk/>
Example:
```python
......
......@@ -12,16 +12,16 @@ Example:
import difflib
import os
import re
import struct
from pathlib import Path
import dask.array as da
import h5py
import nibabel as nib
import numpy as np
import olefile
import struct
import re
import dask.array as da
from pathlib import Path
import pydicom
import tifffile
from PIL import Image, UnidentifiedImageError
......@@ -30,6 +30,7 @@ from qim3d.io.logger import log
from qim3d.utils.internal_tools import sizeof, stringify_path
from qim3d.utils.system import Memory
class DataLoader:
"""Utility class for loading data from different file formats.
......@@ -438,6 +439,39 @@ class DataLoader:
else:
return vol
def load_dicom(self, path):
""" Load a DICOM file
Args:
path (str): Path to file
"""
dcm_data = pydicom.dcmread(path)
if self.return_metadata:
return dcm_data.pixel_array, dcm_data
else:
return dcm_data.pixel_array
def load_dicom_dir(self, path):
""" Load a directory of DICOM files into a numpy 3d array
Args:
path (str): Directory path
"""
# loop over all .dcm files in the directory
files = [f for f in os.listdir(path) if f.endswith('.dcm')]
files.sort()
# dicom_list contains the dicom objects with metadata
dicom_list = [pydicom.dcmread(os.path.join(path, f)) for f in files]
# vol contains the pixel data
vol = np.stack([dicom.pixel_array for dicom in dicom_list], axis=0)
if self.return_metadata:
return vol, dicom_list
else:
return vol
def load(self, path):
"""
Load a file or directory based on the given path.
......@@ -446,9 +480,11 @@ class DataLoader:
path (str or os.PathLike): The path to the file or directory.
Returns:
numpy.ndarray, numpy.memmap, h5py._hl.dataset.Dataset, nibabel.arrayproxy.ArrayProxy or tuple: The loaded volume.
If 'self.virtual_stack' is True, returns numpy.memmap, h5py._hl.dataset.Dataset or nibabel.arrayproxy.ArrayProxy depending on file format
If 'self.return_metadata' is True and file format is either HDF5, NIfTI or TXRM/TXM/XRM, returns a tuple (volume, metadata).
vol (numpy.ndarray, numpy.memmap, h5py._hl.dataset.Dataset, nibabel.arrayproxy.ArrayProxy or tuple): The loaded volume
If `virtual_stack=True`, returns `numpy.memmap`, `h5py._hl.dataset.Dataset` or `nibabel.arrayproxy.ArrayProxy` depending on file format
If `return_metadata=True` and file format is either HDF5, NIfTI or TXRM/TXM/XRM, returns `tuple` (volume, metadata).
Raises:
ValueError: If the format is not supported
ValueError: If the file or directory does not exist.
......@@ -474,6 +510,8 @@ class DataLoader:
return self.load_nifti(path)
elif path.endswith((".vol",".vgi")):
return self.load_vol(path)
elif path.endswith((".dcm",".DCM")):
return self.load_dicom(path)
else:
try:
return self.load_pil(path)
......@@ -482,6 +520,10 @@ class DataLoader:
# Load a directory
elif os.path.isdir(path):
# load dicom if directory contains dicom files else load tiff stack as default
if any([f.endswith('.dcm') for f in os.listdir(path)]):
return self.load_dicom_dir(path)
else:
return self.load_tiff_stack(path)
# Fails
......@@ -545,11 +587,10 @@ def load(
to the DataLoader constructor.
Returns:
numpy.ndarray, numpy.memmap, h5py._hl.dataset.Dataset, nibabel.arrayproxy.ArrayProxy or tuple: The loaded volume.
If 'virtual_stack' is True, returns numpy.memmap, h5py._hl.dataset.Dataset or nibabel.arrayproxy.ArrayProxy depending on file format
vol (numpy.ndarray, numpy.memmap, h5py._hl.dataset.Dataset, nibabel.arrayproxy.ArrayProxy or tuple): The loaded volume
If 'return_metadata' is True and file format is either HDF5, NIfTI or TXRM/TXM/XRM, returns a tuple (volume, metadata).
If `virtual_stack=True`, returns `numpy.memmap`, `h5py._hl.dataset.Dataset` or `nibabel.arrayproxy.ArrayProxy` depending on file format
If `return_metadata=True` and file format is either HDF5, NIfTI or TXRM/TXM/XRM, returns `tuple` (volume, metadata).
Example:
```python
......@@ -595,7 +636,30 @@ def load(
class ImgExamples:
"""Image examples"""
"""Image examples
Attributes:
blobs_256x256 (numpy.ndarray): A 2D image of blobs.
blobs_256x256x256 (numpy.ndarray): A 3D volume of blobs.
bone_128x128x128 (numpy.ndarray): A 3D volume of bone.
cement_128x128x128 (numpy.ndarray): A 3D volume of cement.
fly_150x256x256 (numpy.ndarray): A 3D volume of a fly.
NT_10x200x100 (numpy.ndarray): A 3D volume of a neuron.
NT_128x128x128 (numpy.ndarray): A 3D volume of a neuron.
shell_225x128x128 (numpy.ndarray): A 3D volume of a shell.
Tip:
Call `qim3d.examples.<name>` to access the image examples easily as this class is instantiated when importing `qim3d`
Example:
```python
import qim3d
data = qim3d.examples.blobs_256x256
```
"""
def __init__(self):
img_examples_path = Path(qim3d.__file__).parents[0] / "img_examples"
......@@ -607,4 +671,4 @@ class ImgExamples:
# Generate loader for each image found
for idx, name in enumerate(img_names):
exec(f"self.{name} = qim3d.io.load(path = img_paths[idx])")
\ No newline at end of file
exec(f"self.{name} = load(path = img_paths[idx])")
\ No newline at end of file
......@@ -21,13 +21,17 @@ Example:
```
"""
import datetime
import os
import h5py
import nibabel as nib
import numpy as np
import PIL
import pydicom
import tifffile
from pydicom.dataset import FileDataset, FileMetaDataset
from pydicom.uid import UID
from qim3d.io.logger import log
from qim3d.utils.internal_tools import sizeof, stringify_path
......@@ -179,6 +183,59 @@ class DataSaver:
with h5py.File(path, "w") as f:
f.create_dataset("dataset", data=data, compression="gzip" if self.compression else None)
def save_dicom(self, path, data):
""" Save data to a DICOM file to the given path.
Args:
path (str): The path to save file to
data (numpy.ndarray): The data to be saved
"""
# based on https://pydicom.github.io/pydicom/stable/auto_examples/input_output/plot_write_dicom.html
# Populate required values for file meta information
file_meta = FileMetaDataset()
file_meta.MediaStorageSOPClassUID = UID('1.2.840.10008.5.1.4.1.1.2')
file_meta.MediaStorageSOPInstanceUID = UID("1.2.3")
file_meta.ImplementationClassUID = UID("1.2.3.4")
# Create the FileDataset instance (initially no data elements, but file_meta
# supplied)
ds = FileDataset(path, {},
file_meta=file_meta, preamble=b"\0" * 128)
ds.PatientName = "Test^Firstname"
ds.PatientID = "123456"
ds.StudyInstanceUID = "1.2.3.4.5"
ds.SamplesPerPixel = 1
ds.PixelRepresentation = 0
ds.BitsStored = 16
ds.BitsAllocated = 16
ds.PhotometricInterpretation = "MONOCHROME2"
ds.Rows = data.shape[1]
ds.Columns = data.shape[2]
ds.NumberOfFrames = data.shape[0]
# Set the transfer syntax
ds.is_little_endian = True
ds.is_implicit_VR = True
# Set creation date/time
dt = datetime.datetime.now()
ds.ContentDate = dt.strftime('%Y%m%d')
timeStr = dt.strftime('%H%M%S.%f') # long format with micro seconds
ds.ContentTime = timeStr
# Needs to be here because of bug in pydicom
ds.file_meta.TransferSyntaxUID = pydicom.uid.ImplicitVRLittleEndian
# Reshape the data into a 1D array and convert to uint16
data_1d = data.ravel().astype(np.uint16)
# Convert the data to bytes
data_bytes = data_1d.tobytes()
# Add the data to the DICOM file
ds.PixelData = data_bytes
ds.save_as(path)
def save_PIL(self, path, data):
""" Save data to a PIL file to the given path.
......@@ -271,6 +328,8 @@ class DataSaver:
return self.save_h5(path, data)
elif path.endswith((".vol",".vgi")):
return self.save_vol(path, data)
elif path.endswith((".dcm",".DCM")):
return self.save_dicom(path, data)
elif path.endswith((".jpeg",".jpg", ".png")):
return self.save_PIL(path, data)
else:
......
from .local_thickness_ import local_thickness
from .structure_tensor_ import structure_tensor
from .filters import *
from .local_thickness import local_thickness
from .structure_tensor import structure_tensor
from .detection import *
from .cc import get_3d_cc
File moved
......@@ -6,6 +6,9 @@ __all__ = ["Blob"]
class Blob:
"""
Extract blobs from a volume using Difference of Gaussian (DoG) method
"""
def __init__(
self,
background="dark",
......@@ -19,6 +22,7 @@ class Blob:
):
"""
Initialize the blob detection object
Args:
background: 'dark' if background is darker than the blobs, 'bright' if background is lighter than the blobs
min_sigma: The minimum standard deviation for Gaussian kernel
......@@ -43,10 +47,37 @@ class Blob:
def detect(self, vol):
"""
Detect blobs in the volume
Args:
vol: The volume to detect blobs in
Returns:
blobs: The blobs found in the volume as (p, r, c, radius)
Example:
```python
import qim3d
# Get data
vol = qim3d.examples.cement_128x128x128
vol_blurred = qim3d.processing.gaussian(vol, sigma=2)
# Initialize Blob detector
blob_detector = qim3d.processing.Blob(
min_sigma=1,
max_sigma=8,
threshold=0.001,
overlap=0.1,
background="bright"
)
# Detect blobs
blobs = blob_detector.detect(vol_blurred)
# Visualize results
qim3d.viz.circles(blobs,vol,alpha=0.8,color='blue')
```
![blob detection](assets/screenshots/blob_detection.gif)
"""
self.vol_shape = vol.shape
if self.background == "bright":
......@@ -70,8 +101,32 @@ class Blob:
def get_mask(self):
'''
Retrieve a binary volume with the blobs marked as True
Returns:
binary_volume: A binary volume with the blobs marked as True
Example:
```python
import qim3d
# Get data
vol = qim3d.examples.cement_128x128x128
vol_blurred = qim3d.processing.gaussian(vol, sigma=2)
# Initialize Blob detector
blob_detector = qim3d.processing.Blob(
min_sigma=1,
max_sigma=8,
threshold=0.001,
overlap=0.1,
background="bright"
)
# Get mask and visualize
mask = blob_detector.get_mask()
qim3d.viz.slicer(mask)
```
![blob detection](assets/screenshots/blob_get_mask.gif)
'''
binary_volume = np.zeros(self.vol_shape, dtype=bool)
......
......@@ -87,6 +87,37 @@ class Minimum(FilterBase):
class Pipeline:
"""
Example:
```python
import qim3d
from qim3d.processing import Pipeline, Median, Gaussian, Maximum, Minimum
# Get data
vol = qim3d.examples.fly_150x256x256
# Show original
qim3d.viz.slices(vol, axis=0, show=True)
# Create filter pipeline
pipeline = Pipeline(
Median(size=5),
Gaussian(sigma=3)
)
# Append a third filter to the pipeline
pipeline.append(Maximum(size=3))
# Apply filter pipeline
vol_filtered = pipeline(vol)
# Show filtered
qim3d.viz.slices(vol_filtered, axis=0)
```
![original volume](assets/screenshots/filter_original.png)
![filtered volume](assets/screenshots/filter_processed.png)
"""
def __init__(self, *args: Type[FilterBase]):
"""
Represents a sequence of image filters.
......@@ -125,6 +156,20 @@ class Pipeline:
Args:
fn: An instance of a FilterBase subclass to be appended.
Example:
```python
import qim3d
from qim3d.processing import Pipeline, Maximum, Median
# Create filter pipeline
pipeline = Pipeline(
Maximum(size=3)
)
# Append a second filter to the pipeline
pipeline.append(Median(size=5))
```
"""
self._add_filter(str(len(self.filters)), fn)
......
......@@ -15,7 +15,7 @@ def local_thickness(
visualize=False,
**viz_kwargs
) -> np.ndarray:
"""Wrapper for the local thickness function from the localthickness package (https://github.com/vedranaa/local-thickness)
"""Wrapper for the local thickness function from the [local thickness package](https://github.com/vedranaa/local-thickness)
Args:
image (np.ndarray): 2D or 3D NumPy array representing the image/volume.
......@@ -26,11 +26,27 @@ def local_thickness(
mask (np.ndarray, optional): binary mask of the same size of the image defining parts of the
image to be included in the computation of the local thickness. Default is None.
visualize (bool, optional): Whether to visualize the local thickness. Default is False.
**viz_kwargs: Additional keyword arguments for the visualization function. Only used if visualize=True.
**viz_kwargs: Additional keyword arguments passed to `qim3d.viz.local_thickness`. Only used if `visualize=True`.
Returns:
local_thickness (np.ndarray): 2D or 3D NumPy array representing the local thickness of the input image/volume.
Example:
```python
import qim3d
fly = qim3d.examples.fly_150x256x256 # 3D volume
lt_fly = qim3d.processing.local_thickness(fly, visualize=True, axis=0)
```
![local thickness 3d](assets/screenshots/local_thickness_3d.gif)
```python
import qim3d
blobs = qim3d.examples.blobs_256x256 # 2D image
lt_blobs = qim3d.processing.local_thickness(blobs, visualize=True)
```
![local thickness 2d](assets/screenshots/local_thickness_2d.png)
!!! quote "Reference"
Dahl, V. A., & Dahl, A. B. (2023, June). Fast Local Thickness. 2023 IEEE/CVF Conference on Computer Vision and Pattern Recognition Workshops (CVPRW).
......