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
  • 3D_UNet
  • 3d_watershed
  • conv_zarr_tiff_folders
  • convert_tiff_folders
  • layered_surface_segmentation
  • main
  • memmap_txrm
  • notebook_update
  • notebooks
  • notebooksv1
  • optimize_scaleZYXdask
  • save_files_function
  • scaleZYX_mean
  • test
  • threshold-exploration
  • tr_val_te_splits
  • v0.2.0
  • v0.3.0
  • v0.3.1
  • v0.3.2
  • v0.3.3
  • v0.3.9
  • v0.4.0
  • v0.4.1
24 results

Target

Select target project
  • QIM/tools/qim3d
1 result
Select Git revision
  • 3D_UNet
  • 3d_watershed
  • conv_zarr_tiff_folders
  • convert_tiff_folders
  • layered_surface_segmentation
  • main
  • memmap_txrm
  • notebook_update
  • notebooks
  • notebooksv1
  • optimize_scaleZYXdask
  • save_files_function
  • scaleZYX_mean
  • test
  • threshold-exploration
  • tr_val_te_splits
  • v0.2.0
  • v0.3.0
  • v0.3.1
  • v0.3.2
  • v0.3.3
  • v0.3.9
  • v0.4.0
  • v0.4.1
24 results
Show changes
Showing
with 418 additions and 253 deletions
import numpy as np
import qim3d.filters as filters
from qim3d.utils import log
__all__ = ["remove_background", "fade_mask", "overlay_rgb_images"]
__all__ = ['remove_background', 'fade_mask', 'overlay_rgb_images']
def remove_background(
vol: np.ndarray,
median_filter_size: int = 2,
min_object_radius: int = 3,
background: str = "dark",
background: str = 'dark',
**median_kwargs,
) -> np.ndarray:
"""
......@@ -41,6 +42,7 @@ def remove_background(
fig2 = qim3d.viz.slices_grid(vol_filtered, value_min=0, value_max=255, num_slices=5, display_figure=True)
```
![operations-remove_background_after](../../assets/screenshots/operations-remove_background_after.png)
"""
# Create a pipeline with a median filter and a tophat filter
......@@ -53,12 +55,11 @@ def remove_background(
return pipeline(vol)
def fade_mask(
vol: np.ndarray,
decay_rate: float = 10,
ratio: float = 0.5,
geometry: str = "spherical",
geometry: str = 'spherical',
invert: bool = False,
axis: int = 0,
**kwargs,
......@@ -96,9 +97,9 @@ def fade_mask(
![operations-edge_fade_after](../../assets/screenshots/operations-edge_fade_after.png)
"""
if 0 > axis or axis >= vol.ndim:
if axis < 0 or axis >= vol.ndim:
raise ValueError(
"Axis must be between 0 and the number of dimensions of the volume"
'Axis must be between 0 and the number of dimensions of the volume'
)
# Generate the coordinates of each point in the array
......@@ -112,9 +113,9 @@ def fade_mask(
center = np.array([(s - 1) / 2 for s in shape])
# Calculate the distance of each point from the center
if geometry == "spherical":
if geometry == 'spherical':
distance = np.linalg.norm([z - center[0], y - center[1], x - center[2]], axis=0)
elif geometry == "cylindrical":
elif geometry == 'cylindrical':
distance_list = np.array([z - center[0], y - center[1], x - center[2]])
# remove the axis along which the fading is not applied
distance_list = np.delete(distance_list, axis, axis=0)
......@@ -127,8 +128,8 @@ def fade_mask(
# Compute ratio to make synthetic blobs exactly cylindrical
# target_max_normalized_distance = 1.4 works well to make the blobs cylindrical
if "target_max_normalized_distance" in kwargs:
target_max_normalized_distance = kwargs["target_max_normalized_distance"]
if 'target_max_normalized_distance' in kwargs:
target_max_normalized_distance = kwargs['target_max_normalized_distance']
ratio = np.max(distance) / (target_max_normalized_distance * max_distance)
# Normalize the distances so that they go from 0 at the center to 1 at the farthest point
......@@ -154,7 +155,10 @@ def fade_mask(
def overlay_rgb_images(
background: np.ndarray, foreground: np.ndarray, alpha: float = 0.5, hide_black: bool = True,
background: np.ndarray,
foreground: np.ndarray,
alpha: float = 0.5,
hide_black: bool = True,
) -> np.ndarray:
"""
Overlay an RGB foreground onto an RGB background using alpha blending.
......@@ -176,6 +180,7 @@ def overlay_rgb_images(
- It ensures that the background and foreground have the same first two dimensions (image size matches).
- It can handle greyscale images, values from 0 to 1, raw values which are negative or bigger than 255.
- It calculates the maximum projection of the foreground and blends them onto the background.
"""
def to_uint8(image: np.ndarray):
......@@ -193,7 +198,9 @@ def overlay_rgb_images(
elif image.ndim == 3:
image = image[..., :3] # Ignoring alpha channel
else:
raise ValueError(F'Input image can not have higher dimension than 3. Yours have {image.ndim}')
raise ValueError(
f'Input image can not have higher dimension than 3. Yours have {image.ndim}'
)
return image.astype(np.uint8)
......@@ -202,7 +209,9 @@ def overlay_rgb_images(
# Ensure both images have the same shape
if background.shape != foreground.shape:
raise ValueError(F"Input images must have the same first two dimensions. But background is of shape {background.shape} and foreground is of shape {foreground.shape}")
raise ValueError(
f'Input images must have the same first two dimensions. But background is of shape {background.shape} and foreground is of shape {foreground.shape}'
)
# Perform alpha blending
foreground_max_projection = np.amax(foreground, axis=2)
......@@ -215,16 +224,20 @@ def overlay_rgb_images(
)
# Check alpha validity
if alpha < 0:
raise ValueError(F'Alpha has to be positive number. You used {alpha}')
raise ValueError(f'Alpha has to be positive number. You used {alpha}')
elif alpha > 1:
alpha = 1
# If the pixel is black, its alpha value is set to 0, so it has no effect on the image
if hide_black:
alpha = np.full((background.shape[0], background.shape[1], 1), alpha)
alpha[np.apply_along_axis(lambda x: (x == [0,0,0]).all(), axis = 2, arr = foreground)] = 0
alpha[
np.apply_along_axis(
lambda x: (x == [0, 0, 0]).all(), axis=2, arr=foreground
)
] = 0
composite = background * (1 - alpha) + foreground * alpha
composite = np.clip(composite, 0, 255).astype("uint8")
composite = np.clip(composite, 0, 255).astype('uint8')
return composite.astype("uint8")
\ No newline at end of file
return composite.astype('uint8')
from ._layers import get_lines, segment_layers
from ._local_thickness import local_thickness
from ._structure_tensor import structure_tensor
from ._layers import segment_layers, get_lines
import numpy as np
from slgbuilder import GraphObject
from slgbuilder import MaxflowBuilder
from slgbuilder import GraphObject, MaxflowBuilder
def segment_layers(data: np.ndarray,
def segment_layers(
data: np.ndarray,
inverted: bool = False,
n_layers: int = 1,
delta: float = 1,
min_margin: int = 10,
max_margin: int = None,
wrap: bool = False
wrap: bool = False,
) -> list:
"""
Works on 2D and 3D data.
......@@ -56,11 +57,15 @@ def segment_layers(data: np.ndarray,
if inverted:
data = ~data
else:
raise TypeError(F"Data has to be type np.ndarray. Your data is of type {type(data)}")
raise TypeError(
f'Data has to be type np.ndarray. Your data is of type {type(data)}'
)
helper = MaxflowBuilder()
if not isinstance(n_layers, int):
raise TypeError(F"Number of layers has to be positive integer. You passed {type(n_layers)}")
raise TypeError(
f'Number of layers has to be positive integer. You passed {type(n_layers)}'
)
if n_layers == 1:
layer = GraphObject(data)
......@@ -69,17 +74,21 @@ def segment_layers(data: np.ndarray,
layers = [GraphObject(data) for _ in range(n_layers)]
helper.add_objects(layers)
for i in range(len(layers) - 1):
helper.add_layered_containment(layers[i], layers[i+1], min_margin=min_margin, max_margin=max_margin)
helper.add_layered_containment(
layers[i], layers[i + 1], min_margin=min_margin, max_margin=max_margin
)
else:
raise ValueError(F"Number of layers has to be positive integer. You passed {n_layers}")
raise ValueError(
f'Number of layers has to be positive integer. You passed {n_layers}'
)
helper.add_layered_boundary_cost()
if delta > 1:
delta = int(delta)
elif delta <= 0:
raise ValueError(F'Delta has to be positive number. You passed {delta}')
raise ValueError(f'Delta has to be positive number. You passed {delta}')
helper.add_layered_smoothness(delta=delta, wrap=bool(wrap))
helper.solve()
if n_layers == 1:
......@@ -89,6 +98,7 @@ def segment_layers(data: np.ndarray,
return segmentations
def get_lines(segmentations: list[np.ndarray]) -> list:
"""
Expects list of arrays where each array is 2D segmentation with only 2 classes. This function gets the border between those two
......@@ -99,6 +109,7 @@ def get_lines(segmentations:list[np.ndarray]) -> list:
Returns:
segmentation_lines (list): List of 1D numpy arrays
"""
segmentation_lines = [np.argmin(s, axis=0) - 0.5 for s in segmentations]
return segmentation_lines
"""Wrapper for the local thickness function from the localthickness package including visualization functions."""
import numpy as np
from typing import Optional
from qim3d.utils import log
import qim3d
import numpy as np
from IPython.display import display
import qim3d
from qim3d.utils import log
def local_thickness(
image: np.ndarray,
scale: float = 1,
mask: Optional[np.ndarray] = None,
visualize: bool = False,
**viz_kwargs
**viz_kwargs,
) -> np.ndarray:
"""Wrapper for the local thickness function from the [local thickness package](https://github.com/vedranaa/local-thickness)
"""
Wrapper for the local thickness function from the [local thickness package](https://github.com/vedranaa/local-thickness)
The "Fast Local Thickness" by Vedrana Andersen Dahl and Anders Bjorholm Dahl from the Technical University of Denmark is a efficient algorithm for computing local thickness in 2D and 3D images.
Their method significantly reduces computation time compared to traditional algorithms by utilizing iterative dilation with small structuring elements, rather than the large ones typically used.
......@@ -90,9 +94,7 @@ def local_thickness(
# If not, binarize it using Otsu's method, log the threshold and compute the local thickness
threshold = threshold_otsu(image=image)
log.warning(
"Input image is not binary. It will be binarized using Otsu's method with threshold: {}".format(
threshold
)
f"Input image is not binary. It will be binarized using Otsu's method with threshold: {threshold}"
)
local_thickness = lt.local_thickness(image > threshold, scale=scale, mask=mask)
else:
......
"""Wrapper for the structure tensor function from the structure_tensor package"""
from typing import Tuple
import logging
from typing import Tuple
import numpy as np
from qim3d.utils._logger import log
from IPython.display import display
def structure_tensor(
vol: np.ndarray,
sigma: float = 1.0,
......@@ -13,9 +14,10 @@ def structure_tensor(
base_noise: bool = True,
full: bool = False,
visualize: bool = False,
**viz_kwargs
**viz_kwargs,
) -> Tuple[np.ndarray, np.ndarray]:
"""Wrapper for the 3D structure tensor implementation from the [structure_tensor package](https://github.com/Skielex/structure-tensor/).
"""
Wrapper for the 3D structure tensor implementation from the [structure_tensor package](https://github.com/Skielex/structure-tensor/).
The structure tensor algorithm is a method for analyzing the orientation of fiber-like structures in 3D images.
......@@ -83,7 +85,7 @@ def structure_tensor(
logging.getLogger().setLevel(previous_logging_level)
if vol.ndim != 3:
raise ValueError("The input volume must be 3D")
raise ValueError('The input volume must be 3D')
# Ensure volume is a float
if vol.dtype != np.float32 and vol.dtype != np.float64:
......
import numpy as np
from qim3d.utils._logger import log
__all__ = ["watershed"]
__all__ = ['watershed']
def watershed(bin_vol: np.ndarray, min_distance: int = 5) -> tuple[np.ndarray, int]:
"""
......@@ -37,11 +39,13 @@ def watershed(bin_vol: np.ndarray, min_distance: int = 5) -> tuple[np.ndarray, i
![operations-watershed_after](../../assets/screenshots/operations-watershed_after.png)
"""
import skimage
import scipy
import skimage
if len(np.unique(bin_vol)) > 2:
raise ValueError("bin_vol has to be binary volume - it must contain max 2 unique values.")
raise ValueError(
'bin_vol has to be binary volume - it must contain max 2 unique values.'
)
# Compute distance transform of binary volume
distance = scipy.ndimage.distance_transform_edt(bin_vol)
......@@ -65,6 +69,6 @@ def watershed(bin_vol: np.ndarray, min_distance: int = 5) -> tuple[np.ndarray, i
# Extract number of objects found
num_labels = len(np.unique(labeled_volume)) - 1
log.info(f"Total number of objects found: {num_labels}")
log.info(f'Total number of objects found: {num_labels}')
return labeled_volume, num_labels
import numpy as np
from scipy.ndimage import find_objects, label
from qim3d.utils._logger import log
......@@ -11,6 +12,7 @@ class CC:
Args:
connected_components (np.ndarray): The connected components.
num_connected_components (int): The number of connected components.
"""
self._connected_components = connected_components
self.cc_count = num_connected_components
......@@ -35,14 +37,17 @@ class CC:
Returns:
np.ndarray: The connected component as a binary mask.
"""
if index is None:
volume = self._connected_components
elif index == "random":
elif index == 'random':
index = np.random.randint(1, self.cc_count + 1)
volume = self._connected_components == index
else:
assert 1 <= index <= self.cc_count, "Index out of range. Needs to be in range [1, cc_count]."
assert (
1 <= index <= self.cc_count
), 'Index out of range. Needs to be in range [1, cc_count].'
volume = self._connected_components == index
if crop:
......@@ -53,24 +58,27 @@ class CC:
return volume
def get_bounding_box(self, index: int | None = None) -> list[tuple]:
"""Get the bounding boxes of the connected components.
"""
Get the bounding boxes of the connected components.
Args:
index (int, optional): The index of the connected component. If none selects all components.
Returns:
list: A list of bounding boxes.
"""
if index:
assert 1 <= index <= self.cc_count, "Index out of range."
assert 1 <= index <= self.cc_count, 'Index out of range.'
return find_objects(self._connected_components == index)
else:
return find_objects(self._connected_components)
def get_3d_cc(image: np.ndarray) -> CC:
""" Returns an object (CC) containing the connected components of the input volume. Use plot_cc to visualize the connected components.
"""
Returns an object (CC) containing the connected components of the input volume. Use plot_cc to visualize the connected components.
Args:
image (np.ndarray): An array-like object to be labeled. Any non-zero values in `input` are
......@@ -85,7 +93,8 @@ def get_3d_cc(image: np.ndarray) -> CC:
vol = qim3d.examples.cement_128x128x128[50:150]<60
cc = qim3d.segmentation.get_3d_cc(vol)
```
"""
connected_components, num_connected_components = label(image)
log.info(f"Total number of connected components found: {num_connected_components}")
log.info(f'Total number of connected components found: {num_connected_components}')
return CC(connected_components, num_connected_components)
"Helper functions for testing"
"""Helper functions for testing"""
import os
import matplotlib
import matplotlib.pyplot as plt
from pathlib import Path
import shutil
from PIL import Image
import socket
from pathlib import Path
import matplotlib
import matplotlib.pyplot as plt
import numpy as np
from PIL import Image
from qim3d.utils._logger import log
def mock_plot():
"""Creates a mock plot of a sine wave.
"""
Creates a mock plot of a sine wave.
Returns:
matplotlib.figure.Figure: The generated plot figure.
......@@ -22,9 +25,10 @@ def mock_plot():
>>> fig = mock_plot()
>>> plt.show()
"""
matplotlib.use("Agg")
matplotlib.use('Agg')
fig = plt.figure(figsize=(5, 4))
axes = fig.add_axes([0.1, 0.1, 0.8, 0.8])
......@@ -34,7 +38,7 @@ def mock_plot():
return fig
def mock_write_file(path, content="File created by qim3d"):
def mock_write_file(path, content='File created by qim3d'):
"""
Creates a file at the specified path and writes a predefined text into it.
......@@ -43,8 +47,9 @@ def mock_write_file(path, content="File created by qim3d"):
Example:
>>> mock_write_file("example.txt")
"""
_file = open(path, "w", encoding="utf-8")
_file = open(path, 'w', encoding='utf-8')
_file.write(content)
_file.close()
......@@ -60,7 +65,8 @@ def is_server_running(ip, port):
def temp_data(folder, remove=False, n=3, img_shape=(32, 32)):
"""Creates a temporary folder to test deep learning tools.
"""
Creates a temporary folder to test deep learning tools.
Creates two folders, 'train' and 'test', who each also have two subfolders 'images' and 'labels'.
n random images are then added to all four subfolders.
......@@ -74,9 +80,10 @@ def temp_data(folder, remove=False, n=3, img_shape=(32, 32)):
Example:
>>> tempdata('temporary_folder',n = 10, img_shape = (16,16))
"""
folder_trte = ["train", "test"]
sub_folders = ["images", "labels"]
folder_trte = ['train', 'test']
sub_folders = ['images', 'labels']
# Creating train/test folder
path_train = Path(folder) / folder_trte[0]
......@@ -98,10 +105,10 @@ def temp_data(folder, remove=False, n=3, img_shape=(32, 32)):
os.makedirs(path_train_lab)
os.makedirs(path_test_lab)
for i in range(n):
img.save(path_train_im / f"img_train{i}.png")
img.save(path_train_lab / f"img_train{i}.png")
img.save(path_test_im / f"img_test{i}.png")
img.save(path_test_lab / f"img_test{i}.png")
img.save(path_train_im / f'img_train{i}.png')
img.save(path_train_lab / f'img_train{i}.png')
img.save(path_test_im / f'img_test{i}.png')
img.save(path_test_lab / f'img_test{i}.png')
if remove:
for filename in os.listdir(folder):
......@@ -112,6 +119,6 @@ def temp_data(folder, remove=False, n=3, img_shape=(32, 32)):
elif os.path.isdir(file_path):
shutil.rmtree(file_path)
except Exception as e:
log.warning("Failed to delete %s. Reason: %s" % (file_path, e))
log.warning('Failed to delete %s. Reason: %s' % (file_path, e))
os.rmdir(folder)
import qim3d
import re
import numpy as np
import pytest
import re
import qim3d
def test_filter_base_initialization():
filter_base = qim3d.filters.FilterBase(3, size=2)
assert filter_base.args == (3,)
assert filter_base.kwargs == {'size': 2}
def test_gaussian_filter():
input_image = np.random.rand(50, 50)
......@@ -23,6 +27,7 @@ def test_gaussian_filter():
assert np.array_equal(filtered_image_fn, filtered_image_cls)
assert not np.array_equal(filtered_image_fn, input_image)
def test_median_filter():
input_image = np.random.rand(50, 50)
......@@ -38,6 +43,7 @@ def test_median_filter():
assert np.array_equal(filtered_image_fn, filtered_image_cls)
assert not np.array_equal(filtered_image_fn, input_image)
def test_maximum_filter():
input_image = np.random.rand(50, 50)
......@@ -53,6 +59,7 @@ def test_maximum_filter():
assert np.array_equal(filtered_image_fn, filtered_image_cls)
assert not np.array_equal(filtered_image_fn, input_image)
def test_minimum_filter():
input_image = np.random.rand(50, 50)
......@@ -68,6 +75,7 @@ def test_minimum_filter():
assert np.array_equal(filtered_image_fn, filtered_image_cls)
assert not np.array_equal(filtered_image_fn, input_image)
def test_sequential_filter_pipeline():
input_image = np.random.rand(50, 50)
......@@ -77,17 +85,23 @@ def test_sequential_filter_pipeline():
maximum_filter = qim3d.filters.Maximum(size=3)
# Testing the sequential pipeline
sequential_pipeline = qim3d.filters.Pipeline(gaussian_filter, median_filter, maximum_filter)
sequential_pipeline = qim3d.filters.Pipeline(
gaussian_filter, median_filter, maximum_filter
)
filtered_image_pipeline = sequential_pipeline(input_image)
# Testing the equivalence to maximum(median(gaussian(input,**kwargs),**kwargs),**kwargs)
expected_output = qim3d.filters.maximum(qim3d.filters.median(qim3d.filters.gaussian(input_image, sigma=1.5), size=3), size=3)
expected_output = qim3d.filters.maximum(
qim3d.filters.median(qim3d.filters.gaussian(input_image, sigma=1.5), size=3),
size=3,
)
# Assertions
assert filtered_image_pipeline.shape == expected_output.shape == input_image.shape
assert not np.array_equal(filtered_image_pipeline, input_image)
assert np.array_equal(filtered_image_pipeline, expected_output)
def test_sequential_filter_appending():
input_image = np.random.rand(50, 50)
......@@ -97,29 +111,41 @@ def test_sequential_filter_appending():
maximum_filter = qim3d.filters.Maximum(size=3)
# Sequential pipeline with filter initialized at the beginning
sequential_pipeline_initial = qim3d.filters.Pipeline(gaussian_filter, median_filter, maximum_filter)
sequential_pipeline_initial = qim3d.filters.Pipeline(
gaussian_filter, median_filter, maximum_filter
)
filtered_image_initial = sequential_pipeline_initial(input_image)
# Sequential pipeline with filter appended
sequential_pipeline_appended = qim3d.filters.Pipeline(gaussian_filter, median_filter)
sequential_pipeline_appended = qim3d.filters.Pipeline(
gaussian_filter, median_filter
)
sequential_pipeline_appended.append(maximum_filter)
filtered_image_appended = sequential_pipeline_appended(input_image)
# Assertions
assert filtered_image_initial.shape == filtered_image_appended.shape == input_image.shape
assert (
filtered_image_initial.shape
== filtered_image_appended.shape
== input_image.shape
)
assert not np.array_equal(filtered_image_appended, input_image)
assert np.array_equal(filtered_image_initial, filtered_image_appended)
def test_assertion_error_not_filterbase_subclass():
# Get valid filter classes
valid_filters = [subclass.__name__ for subclass in qim3d.filters.FilterBase.__subclasses__()]
valid_filters = [
subclass.__name__ for subclass in qim3d.filters.FilterBase.__subclasses__()
]
# Create invalid object
invalid_filter = object() # An object that is not an instance of FilterBase
# Construct error message
message = f"filters should be instances of one of the following classes: {valid_filters}"
message = (
f'filters should be instances of one of the following classes: {valid_filters}'
)
# Use pytest.raises to catch the AssertionError
with pytest.raises(AssertionError, match=re.escape(message)):
......
import qim3d
import multiprocessing
import time
import qim3d
def test_starting_class():
app = qim3d.gui.annotation_tool.Interface()
assert app.title == "Annotation Tool"
assert app.title == 'Annotation Tool'
def start_server(ip, port):
......@@ -15,7 +16,7 @@ def start_server(ip, port):
def test_app_launch():
ip = "localhost"
ip = 'localhost'
port = 65432
proc = multiprocessing.Process(target=start_server, args=(ip, port))
......
import qim3d
import multiprocessing
import time
import qim3d
def test_starting_class():
app = qim3d.gui.iso3d.Interface()
assert app.title == "Isosurfaces for 3D visualization"
assert app.title == 'Isosurfaces for 3D visualization'
def start_server(ip, port):
......@@ -15,7 +16,7 @@ def start_server(ip, port):
def test_app_launch():
ip = "localhost"
ip = 'localhost'
port = 65432
proc = multiprocessing.Process(target=start_server, args=(ip, port))
......
import qim3d
import os
import pytest
from pathlib import Path
import shutil
from pathlib import Path
import pytest
import qim3d
@pytest.fixture
@pytest.fixture()
def setup_temp_folder():
"""Fixture to create and clean up a temporary folder for tests."""
folder = "Cowry_Shell"
file = "Cowry_DOWNSAMPLED.tif"
folder = 'Cowry_Shell'
file = 'Cowry_DOWNSAMPLED.tif'
path = Path(folder) / file
# Ensure clean environment before running tests
......@@ -30,7 +32,7 @@ def test_download(setup_temp_folder):
dl.Cowry_Shell.Cowry_DOWNSAMPLED()
# Verify the file was downloaded correctly
assert path.exists(), f"{path} does not exist after download."
assert path.exists(), f'{path} does not exist after download.'
img = qim3d.io.load(str(path))
assert img.shape == (500, 350, 350)
......@@ -40,22 +42,24 @@ def test_download(setup_temp_folder):
def test_get_file_size():
"""Tests for correct and incorrect file size retrieval."""
coal_file = "https://archive.compute.dtu.dk/download/public/projects/viscomp_data_repository/Coal/CoalBrikett.tif"
folder_url = "https://archive.compute.dtu.dk/files/public/projects/viscomp_data_repository/"
coal_file = 'https://archive.compute.dtu.dk/download/public/projects/viscomp_data_repository/Coal/CoalBrikett.tif'
folder_url = (
'https://archive.compute.dtu.dk/files/public/projects/viscomp_data_repository/'
)
# Correct file size
size = qim3d.io._downloader._get_file_size(coal_file)
assert size == 2_400_082_900, f"Expected size mismatch for {coal_file}."
assert size == 2_400_082_900, f'Expected size mismatch for {coal_file}.'
# Wrong URL (not a file)
size = qim3d.io._downloader._get_file_size(folder_url)
assert size == -1, "Expected size -1 for non-file URL."
assert size == -1, 'Expected size -1 for non-file URL.'
def test_extract_html():
url = "https://archive.compute.dtu.dk/files/public/projects/viscomp_data_repository"
url = 'https://archive.compute.dtu.dk/files/public/projects/viscomp_data_repository'
html = qim3d.io._downloader._extract_html(url)
assert 'data-path="/files/public/projects/viscomp_data_repository"' in html, \
"Expected HTML content not found in extracted HTML."
assert (
'data-path="/files/public/projects/viscomp_data_repository"' in html
), 'Expected HTML content not found in extracted HTML.'
import qim3d
import numpy as np
from pathlib import Path
import os
import pytest
import re
from pathlib import Path
import numpy as np
import pytest
import qim3d
# Load volume into memory
vol = qim3d.examples.bone_128x128x128
# Ceate memory map to blobs
volume_path = Path(qim3d.__file__).parents[0] / "examples" / "bone_128x128x128.tif"
volume_path = Path(qim3d.__file__).parents[0] / 'examples' / 'bone_128x128x128.tif'
vol_memmap = qim3d.io.load(volume_path, virtual_stack=True)
......@@ -26,7 +28,7 @@ def test_load_type_memmap():
def test_invalid_path():
invalid_path = os.path.join("this", "path", "doesnt", "exist.tif")
invalid_path = os.path.join('this', 'path', 'doesnt', 'exist.tif')
with pytest.raises(FileNotFoundError):
qim3d.io.load(invalid_path)
......
......@@ -15,7 +15,7 @@ def test_image_exist():
# Create temporary directory
with tempfile.TemporaryDirectory() as temp_dir:
image_path = os.path.join(temp_dir,"test_image.tif")
image_path = os.path.join(temp_dir, 'test_image.tif')
# Save to temporary directory
qim3d.io.save(image_path, test_image)
......@@ -23,14 +23,15 @@ def test_image_exist():
# Assert that test image has been saved
assert os.path.exists(image_path)
def test_compression():
# Get test image (should not be random in order for compression to function)
test_image = qim3d.examples.bone_128x128x128
# Create temporary directory
with tempfile.TemporaryDirectory() as temp_dir:
image_path = os.path.join(temp_dir,"test_image.tif")
compressed_image_path = os.path.join(temp_dir,"compressed_test_image.tif")
image_path = os.path.join(temp_dir, 'test_image.tif')
compressed_image_path = os.path.join(temp_dir, 'compressed_test_image.tif')
# Save to temporary directory with and without compression
qim3d.io.save(image_path, test_image)
......@@ -43,13 +44,14 @@ def test_compression():
# Assert that compressed file size is smaller than non-compressed file size
assert compressed_file_size < file_size
def test_image_matching():
# Create random test image
original_image = np.random.randint(0, 256, (100, 100, 100), 'uint8')
# Create temporary directory
with tempfile.TemporaryDirectory() as temp_dir:
image_path = os.path.join(temp_dir,"original_image.tif")
image_path = os.path.join(temp_dir, 'original_image.tif')
# Save to temporary directory
qim3d.io.save(image_path, original_image)
......@@ -64,13 +66,14 @@ def test_image_matching():
# Assert that original image is identical to saved_image
assert original_hash == saved_hash
def test_compressed_image_matching():
# Get test image (should not be random in order for compression to function)
original_image = qim3d.examples.bone_128x128x128
# Create temporary directory
with tempfile.TemporaryDirectory() as temp_dir:
image_path = os.path.join(temp_dir,"original_image.tif")
image_path = os.path.join(temp_dir, 'original_image.tif')
# Save to temporary directory
qim3d.io.save(image_path, original_image, compression=True)
......@@ -85,6 +88,7 @@ def test_compressed_image_matching():
# Assert that original image is identical to saved_image
assert original_hash == compressed_hash
def test_file_replace():
# Create random test image
test_image1 = np.random.randint(0, 256, (100, 100, 100), 'uint8')
......@@ -92,7 +96,7 @@ def test_file_replace():
# Create temporary directory
with tempfile.TemporaryDirectory() as temp_dir:
image_path = os.path.join(temp_dir,"test_image.tif")
image_path = os.path.join(temp_dir, 'test_image.tif')
# Save first test image to temporary directory
qim3d.io.save(image_path, test_image1)
......@@ -107,6 +111,7 @@ def test_file_replace():
# Assert that the file was modified by checking if the second modification time is newer than the first
assert hash1 != hash2
def test_file_already_exists():
# Create random test image
test_image1 = np.random.randint(0, 256, (100, 100, 100), 'uint8')
......@@ -114,15 +119,19 @@ def test_file_already_exists():
# Create temporary directory
with tempfile.TemporaryDirectory() as temp_dir:
image_path = os.path.join(temp_dir,"test_image.tif")
image_path = os.path.join(temp_dir, 'test_image.tif')
# Save first test image to temporary directory
qim3d.io.save(image_path, test_image1)
with pytest.raises(ValueError,match="A file with the provided path already exists. To replace it set 'replace=True'"):
with pytest.raises(
ValueError,
match="A file with the provided path already exists. To replace it set 'replace=True'",
):
# Try to save another image to the existing path
qim3d.io.save(image_path, test_image2)
def test_no_file_ext():
# Create random test image
test_image = np.random.randint(0, 256, (100, 100, 100), 'uint8')
......@@ -140,6 +149,7 @@ def test_no_file_ext():
# Try to save the test image to a path witout file extension
qim3d.io.save(image_path, test_image)
def test_folder_doesnt_exist():
# Create random test image
test_image = np.random.randint(0, 256, (100, 100, 100), 'uint8')
......@@ -154,6 +164,7 @@ def test_folder_doesnt_exist():
# Try to save test image to an invalid path
qim3d.io.save(invalid_path, test_image)
def test_unsupported_file_format():
# Create random test image
test_image = np.random.randint(0, 256, (100, 100, 100), 'uint8')
......@@ -168,6 +179,7 @@ def test_unsupported_file_format():
# Try to save test image with an unsupported file extension
qim3d.io.save(image_path, test_image)
def test_no_basename():
# Create random test image
test_image = np.random.randint(0, 256, (100, 100, 100), 'uint8')
......@@ -181,6 +193,7 @@ def test_no_basename():
# that you want to save as several files) without providing a basename
qim3d.io.save(temp_dir, test_image)
def test_mkdir_tiff_stack():
# Create random test image
test_image = np.random.randint(0, 256, (10, 100, 100), 'uint8')
......@@ -196,13 +209,16 @@ def test_mkdir_tiff_stack():
# Assert that folder is created
assert os.path.isdir(path2save)
def test_tiff_stack_naming():
# Create random test image
test_image = np.random.randint(0, 256, (10, 100, 100), 'uint8')
# Define expected filenames
basename = 'test'
expected_filenames = [basename + str(i).zfill(2) + '.tif' for i,_ in enumerate(test_image)]
expected_filenames = [
basename + str(i).zfill(2) + '.tif' for i, _ in enumerate(test_image)
]
# create temporary directory
with tempfile.TemporaryDirectory() as temp_dir:
......@@ -224,13 +240,14 @@ def test_tiff_stack_slicing_dim():
qim3d.io.save(path2save, test_image, basename='test', sliced_dim=dim)
assert len(os.listdir(path2save)) == test_image.shape[dim]
def test_tiff_save_load():
# Create random test image
original_image = qim3d.examples.bone_128x128x128
# Create temporary directory
with tempfile.TemporaryDirectory() as temp_dir:
image_path = os.path.join(temp_dir,"test_image.tif")
image_path = os.path.join(temp_dir, 'test_image.tif')
# Save to temporary directory
qim3d.io.save(image_path, original_image)
......@@ -245,13 +262,14 @@ def test_tiff_save_load():
# Assert that original image is identical to saved_image
assert original_hash == saved_hash
def test_vol_save_load():
# Create random test image
original_image = qim3d.examples.bone_128x128x128
# Create temporary directory
with tempfile.TemporaryDirectory() as temp_dir:
image_path = os.path.join(temp_dir,"test_image.vol")
image_path = os.path.join(temp_dir, 'test_image.vol')
# Save to temporary directory
qim3d.io.save(image_path, original_image)
......@@ -266,14 +284,15 @@ def test_vol_save_load():
# Assert that original image is identical to saved_image
assert original_hash == saved_hash
def test_pil_save_load():
# Create random test image
original_image = qim3d.examples.bone_128x128x128[0]
# Create temporary directory
with tempfile.TemporaryDirectory() as temp_dir:
image_path_png = os.path.join(temp_dir,"test_image.png")
image_path_jpg = os.path.join(temp_dir,"test_image.jpg")
image_path_png = os.path.join(temp_dir, 'test_image.png')
image_path_jpg = os.path.join(temp_dir, 'test_image.jpg')
# Save to temporary directory
qim3d.io.save(image_path_png, original_image)
......@@ -293,14 +312,15 @@ def test_pil_save_load():
# jpg is lossy so the hashes will not match, checks that the image is the same size and similar values
assert original_image.shape == saved_image_jpg.shape
def test_nifti_save_load():
# Create random test image
original_image = qim3d.examples.bone_128x128x128
# Create temporary directory
with tempfile.TemporaryDirectory() as temp_dir:
image_path = os.path.join(temp_dir,"test_image.nii")
image_path_compressed = os.path.join(temp_dir,"test_image_compressed.nii.gz")
image_path = os.path.join(temp_dir, 'test_image.nii')
image_path_compressed = os.path.join(temp_dir, 'test_image_compressed.nii.gz')
# Save to temporary directory
qim3d.io.save(image_path, original_image)
......@@ -333,8 +353,8 @@ def test_h5_save_load():
# Create temporary directory
with tempfile.TemporaryDirectory() as temp_dir:
image_path = os.path.join(temp_dir,"test_image.h5")
image_path_compressed = os.path.join(temp_dir,"test_image_compressed.nii.gz")
image_path = os.path.join(temp_dir, 'test_image.h5')
image_path_compressed = os.path.join(temp_dir, 'test_image_compressed.nii.gz')
# Save to temporary directory
qim3d.io.save(image_path, original_image)
......@@ -361,6 +381,7 @@ def test_h5_save_load():
# Assert that compressed file size is smaller than non-compressed file size
assert compressed_file_size < file_size
def calculate_image_hash(image):
image_bytes = image.tobytes()
hash_object = hashlib.md5(image_bytes)
......
import pytest
import numpy as np
from pygel3d import hmesh
import qim3d
def test_from_volume_valid_input():
"""Test that from_volume returns a hmesh.Manifold object for a valid 3D input."""
volume = np.random.rand(50, 50, 50).astype(np.float32) # Generate a random 3D volume
mesh = qim3d.mesh.from_volume(volume)
assert isinstance(mesh, hmesh.Manifold) # Check if output is a Manifold object
def test_from_volume_invalid_input():
"""Test that from_volume raises ValueError for non-3D input."""
volume = np.random.rand(50, 50) # A 2D array
with pytest.raises(ValueError, match="The input volume must be a 3D numpy array."):
qim3d.mesh.from_volume(volume)
def test_from_volume_empty_array():
"""Test how from_volume handles an empty 3D array."""
volume = np.empty((0, 0, 0)) # Empty 3D array
with pytest.raises(ValueError): # It should fail because it doesn't make sense to generate a mesh from empty data
qim3d.mesh.from_volume(volume)
def test_from_volume_with_kwargs():
"""Test that from_volume correctly passes kwargs."""
volume = np.random.rand(50, 50, 50).astype(np.float32)
# Mock volumetric_isocontour to check if kwargs are passed
def mock_volumetric_isocontour(vol, **kwargs):
assert "isovalue" in kwargs
assert kwargs["isovalue"] == 0.5
return hmesh.Manifold()
# Replace the function temporarily
original_function = hmesh.volumetric_isocontour
hmesh.volumetric_isocontour = mock_volumetric_isocontour
try:
qim3d.mesh.from_volume(volume, isovalue=0.5)
finally:
hmesh.volumetric_isocontour = original_function # Restore original function
import qim3d
import torch
import qim3d
# unit tests for UNet()
def test_starting_unet():
unet = qim3d.ml.models.UNet()
......@@ -17,6 +19,7 @@ def test_forward_pass():
output = unet(x)
assert x.shape == output.shape
# unit tests for Hyperparameters()
def test_hyper():
unet = qim3d.ml.models.UNet()
......@@ -24,6 +27,7 @@ def test_hyper():
assert hyperparams.n_epochs == 10
def test_hyper_dict():
unet = qim3d.ml.models.UNet()
hyperparams = qim3d.ml.models.Hyperparameters(unet)
......
import qim3d
import pytest
from torch import ones
import qim3d
from qim3d.tests import temp_data
......@@ -9,10 +9,10 @@ from qim3d.tests import temp_data
def test_model_summary():
n = 10
img_shape = (32, 32)
folder = "folder_data"
folder = 'folder_data'
temp_data(folder, img_shape=img_shape, n=n)
unet = qim3d.ml.models.UNet(size="small")
unet = qim3d.ml.models.UNet(size='small')
augment = qim3d.ml.Augmentation(transform_train=None)
train_set, val_set, test_set = qim3d.ml.prepare_datasets(
folder, 1 / 3, unet, augment
......@@ -30,10 +30,10 @@ def test_model_summary():
# unit test for inference()
def test_inference():
folder = "folder_data"
folder = 'folder_data'
temp_data(folder)
unet = qim3d.ml.models.UNet(size="small")
unet = qim3d.ml.models.UNet(size='small')
augment = qim3d.ml.Augmentation(transform_train=None)
train_set, _, _ = qim3d.ml.prepare_datasets(folder, 1 / 3, unet, augment)
......@@ -46,13 +46,13 @@ def test_inference():
# unit test for tuple ValueError().
def test_inference_tuple():
folder = "folder_data"
folder = 'folder_data'
temp_data(folder)
unet = qim3d.ml.models.UNet(size="small")
unet = qim3d.ml.models.UNet(size='small')
data = [1, 2, 3]
with pytest.raises(ValueError, match="Data items must be tuples"):
with pytest.raises(ValueError, match='Data items must be tuples'):
qim3d.ml.inference(data, unet)
temp_data(folder, remove=True)
......@@ -60,13 +60,13 @@ def test_inference_tuple():
# unit test for tensor ValueError().
def test_inference_tensor():
folder = "folder_data"
folder = 'folder_data'
temp_data(folder)
unet = qim3d.ml.models.UNet(size="small")
unet = qim3d.ml.models.UNet(size='small')
data = [(1, 2)]
with pytest.raises(ValueError, match="Data items must consist of tensors"):
with pytest.raises(ValueError, match='Data items must consist of tensors'):
qim3d.ml.inference(data, unet)
temp_data(folder, remove=True)
......@@ -74,14 +74,14 @@ def test_inference_tensor():
# unit test for dimension ValueError().
def test_inference_dim():
folder = "folder_data"
folder = 'folder_data'
temp_data(folder)
unet = qim3d.ml.models.UNet(size="small")
unet = qim3d.ml.models.UNet(size='small')
data = [(ones(1), ones(1))]
# need the r"" for special characters
with pytest.raises(ValueError, match=r"Input image must be \(C,H,W\) format"):
with pytest.raises(ValueError, match=r'Input image must be \(C,H,W\) format'):
qim3d.ml.inference(data, unet)
temp_data(folder, remove=True)
......@@ -89,12 +89,12 @@ def test_inference_dim():
# unit test for train_model()
def test_train_model():
folder = "folder_data"
folder = 'folder_data'
temp_data(folder)
n_epochs = 1
unet = qim3d.ml.models.UNet(size="small")
unet = qim3d.ml.models.UNet(size='small')
augment = qim3d.ml.Augmentation(transform_train=None)
hyperparams = qim3d.ml.Hyperparameters(unet, n_epochs=n_epochs)
train_set, val_set, test_set = qim3d.ml.prepare_datasets(
......@@ -108,6 +108,6 @@ def test_train_model():
unet, hyperparams, train_loader, val_loader, plot=False, return_loss=True
)
assert len(train_loss["loss"]) == n_epochs
assert len(train_loss['loss']) == n_epochs
temp_data(folder, remove=True)
from testbook import testbook
def test_blob_detection_notebook():
with testbook('./docs/notebooks/blob_detection.ipynb', execute=True) as tb:
pass
def test_filters_notebook():
with testbook('./docs/notebooks/filters.ipynb', execute=True) as tb:
pass
def test_local_thickness_notebook():
with testbook('./docs/notebooks/local_thickness.ipynb', execute=True) as tb:
pass
def test_logging_notebook():
with testbook('./docs/notebooks/Logging.ipynb', execute=True) as tb:
pass
def test_references_from_doi_notebook():
with testbook('./docs/notebooks/References from DOI.ipynb', execute=True) as tb:
pass
def test_structure_tensor_notebook():
with testbook('./docs/notebooks/structure_tensor.ipynb', execute=True) as tb:
pass
import qim3d
import numpy as np
from skimage.draw import disk, ellipsoid
import pytest
import qim3d
def test_local_thickness_2d():
# Create a binary 2D image
......@@ -21,12 +22,17 @@ def test_local_thickness_2d():
assert np.allclose(lt, lt_manual, rtol=1e-1)
def test_local_thickness_3d():
disk3d = ellipsoid(15, 15, 15)
# Remove weird border pixels
border_thickness = 2
disk3d = disk3d[border_thickness:-border_thickness, border_thickness:-border_thickness, border_thickness:-border_thickness]
disk3d = disk3d[
border_thickness:-border_thickness,
border_thickness:-border_thickness,
border_thickness:-border_thickness,
]
disk3d = np.pad(disk3d, border_thickness, mode='constant')
lt = qim3d.processing.local_thickness(disk3d)
......
import pytest
import numpy as np
import pytest
import qim3d
def test_wrong_ndim():
img_2d = np.random.rand(50, 50)
with pytest.raises(ValueError, match = "The input volume must be 3D"):
with pytest.raises(ValueError, match='The input volume must be 3D'):
qim3d.processing.structure_tensor(img_2d, 1.5, 1.5)
def test_structure_tensor():
volume = np.random.rand(50, 50, 50)
val, vec = qim3d.processing.structure_tensor(volume, 1.5, 1.5)
......@@ -16,6 +19,7 @@ def test_structure_tensor():
assert np.all(val[1] <= val[2])
assert np.all(val[0] <= val[2])
def test_structure_tensor_full():
volume = np.random.rand(50, 50, 50)
val, vec = qim3d.processing.structure_tensor(volume, 1.5, 1.5, full=True)
......