feat: init

This commit is contained in:
2025-12-04 16:07:30 +08:00
commit 262583a57f
681 changed files with 117578 additions and 0 deletions

View File

@@ -0,0 +1,185 @@
import os
import numpy as np
import pytest
@pytest.mark.parametrize("ext", ["h5", "n5", "tiff"])
def test_bdv_metadata(ext):
from navigate.model.metadata_sources.bdv_metadata import BigDataViewerMetadata
md = BigDataViewerMetadata()
views = []
for _ in range(10):
views.append(
{
"x": np.random.randint(-1000, 1000),
"y": np.random.randint(-1000, 1000),
"z": np.random.randint(-1000, 1000),
"theta": np.random.randint(-1000, 1000),
"f": np.random.randint(-1000, 1000),
}
)
for view in views:
arr = md.stage_positions_to_affine_matrix(**view)
assert arr[0, 3] == view["y"] / md.dy
assert arr[1, 3] == view["x"] / md.dx
assert arr[2, 3] == view["z"] / md.dz
md.write_xml(f"test_bdv.{ext}", views)
os.remove("test_bdv.xml")
# Test defaults for shear transform.
assert md.rotate_data is False
assert md.shear_data is False
assert np.shape(md.rotate_transform) == (3, 4)
assert np.shape(md.shear_transform) == (3, 4)
# Confirm that the shear/rotation transforms are identity matrices by default
assert np.all(md.shear_transform == np.eye(3, 4))
assert np.all(md.rotate_transform == np.eye(3, 4))
# Confirm that the shear/rotation transforms are identity matrices by default
# even after calling calculate_shear_transform and calculate_rotate_transform
md.bdv_shear_transform()
md.bdv_rotate_transform()
assert np.all(md.shear_transform == np.eye(3, 4))
assert np.all(md.rotate_transform == np.eye(3, 4))
# Test that the shear/rotation transforms are correctly calculated.
md.shear_data = True
md.shear_dimension = "XZ"
md.shear_angle = 15
md.dx, md.dy, md.dz = 1, 1, 1
md.bdv_shear_transform()
assert md.shear_transform[0, 2] == np.tan(np.deg2rad(15))
md.rotate_data = True
md.rotate_angle_x = 15
md.rotate_angle_y = 0
md.rotate_angle_z = 0
md.bdv_rotate_transform()
assert md.rotate_transform[1, 1] == np.cos(np.deg2rad(15))
assert md.rotate_transform[1, 2] == -np.sin(np.deg2rad(15))
assert md.rotate_transform[2, 1] == np.sin(np.deg2rad(15))
assert md.rotate_transform[2, 2] == np.cos(np.deg2rad(15))
# Make sure we can still write the data.
md.write_xml(f"test_bdv.{ext}", views)
os.remove("test_bdv.xml")
@pytest.mark.parametrize("stack_cycling_mode", ["per_stack", "per_z"])
def test_bdv_xml_dict(dummy_model, stack_cycling_mode):
from navigate.model.metadata_sources.bdv_metadata import BigDataViewerMetadata
md = BigDataViewerMetadata()
md.configuration = dummy_model.configuration.copy()
# set shape from configuration and experiment
md.configuration["experiment"]["MicroscopeState"]["image_mode"] = "z-stack"
# timepoints, channels, z-slices, positions
for tp in [1]: #, 2, 3, 5]:
for pos in [1, 3, 5]:
for ch in [1, 2, 3]:
for z in [1, 5, 10, 20]:
md.configuration["experiment"]["MicroscopeState"]["timepoints"] = tp
# channel settings
channel_dict = md.configuration["experiment"]["MicroscopeState"]["channels"]
for i in range(1, ch+1):
channel_name = f"channel_{i}"
if channel_name not in channel_dict:
channel_dict[channel_name] = {
"is_selected": True,
"laser": "488nm",
"laser_index": 0,
"camera_exposure_time": 200.0,
"laser_power": 20.0,
"interval_time": 1.0,
"defocus": 104.0,
"filter_wheel_0": "Empty-Alignment",
"filter_position_0": 6,
"filter_wheel_1": "Empty-Alignment",
"filter_position_1": 6
}
else:
channel_dict[channel_name]["is_selected"] = True
for i in range(ch+1, 5):
channel_name = f"channel_{i}"
if channel_name in channel_dict:
channel_dict[channel_name]["is_selected"] = False
# z-stack settings
start_z_position = md.configuration["experiment"]["MicroscopeState"]["start_position"]
md.configuration["experiment"]["MicroscopeState"]["number_z_steps"] = z
md.configuration["experiment"]["MicroscopeState"]["step_size"] = 0.2
md.configuration["experiment"]["MicroscopeState"]["end_position"] = z * 0.2 + start_z_position
# multiposition settings
md.configuration["experiment"]["MicroscopeState"]["is_multiposition"] = (
True if pos > 1 else False
)
multipositions = [
["X", "Y", "Z", "THETA", "F"]
]
for p in range(pos):
position = [
np.random.uniform(0, 100), # X
np.random.uniform(0, 100), # Y
np.random.uniform(0, 100), # Z
np.random.uniform(0, 360), # THETA
np.random.uniform(0, 10), # F
]
multipositions.append(position)
md.configuration["multi_positions"] = multipositions
md.set_from_configuration_experiment()
assert md.shape_t == tp
assert md.shape_c == ch
assert md.shape_z == z
assert md.positions == pos
if pos > 1:
assert md._multiposition is True
else:
assert md._multiposition is False
# view
views = []
for p in range(1, pos):
for c in range(ch):
position = md.configuration["multi_positions"][p + 1]
for _z in range(z):
views.append(
{
"x": position[0],
"y": position[1],
"z": position[2] + start_z_position + _z * 0.2,
"theta": position[3],
"f": position[4],
}
)
xml_dict = md.bdv_xml_dict("test_file.h5", views)
assert "ImageLoader" in xml_dict["SequenceDescription"]
assert "ViewSetups" in xml_dict["SequenceDescription"]
assert "Timepoints" in xml_dict["SequenceDescription"]
assert "ViewRegistrations" in xml_dict
# verify affine values are the same for a position
view_registrations = xml_dict["ViewRegistrations"]["ViewRegistration"]
for i in range(len(view_registrations)):
# assert view id
view_id = view_registrations[i]["setup"]
if ch > 1:
assert view_id == ((i % ch) * pos + (i // ch))
# assert affine position consistency between channels
if i % ch == 0:
affine = view_registrations[i]["ViewTransform"][0]["affine"]["text"]
else:
affine_c = view_registrations[i]["ViewTransform"][0]["affine"]["text"]
assert affine == affine_c

View File

@@ -0,0 +1,154 @@
import pytest
import random
def test_metadata_voxel_size(dummy_model):
from navigate.model.metadata_sources.metadata import Metadata
md = Metadata()
md.configuration = dummy_model.configuration
zoom = dummy_model.configuration["experiment"]["MicroscopeState"]["zoom"]
active_microscope = dummy_model.configuration["experiment"]["MicroscopeState"][
"microscope_name"
]
pixel_size = float(
dummy_model.configuration["configuration"]["microscopes"][active_microscope][
"zoom"
]["pixel_size"][zoom]
)
dx, dy, dz = md.voxel_size
assert (
(dx == pixel_size)
and (dy == pixel_size)
and (
dz
== float(
dummy_model.configuration["experiment"]["MicroscopeState"]["step_size"]
)
)
)
def test_metadata_shape(dummy_model):
from navigate.model.metadata_sources.metadata import Metadata
dummy_model.configuration["experiment"]["MicroscopeState"]["image_mode"] = "z-stack"
md = Metadata()
md.configuration = dummy_model.configuration
microscope_name = dummy_model.configuration["experiment"]["MicroscopeState"][
"microscope_name"
]
txs = dummy_model.configuration["experiment"]["CameraParameters"][microscope_name][
"img_x_pixels"
]
tys = dummy_model.configuration["experiment"]["CameraParameters"][microscope_name][
"img_y_pixels"
]
tzs = dummy_model.configuration["experiment"]["MicroscopeState"]["number_z_steps"]
tts = dummy_model.configuration["experiment"]["MicroscopeState"]["timepoints"]
tcs = sum(
[
v["is_selected"] is True
for k, v in dummy_model.configuration["experiment"]["MicroscopeState"][
"channels"
].items()
]
)
xs, ys, cs, zs, ts = md.shape
assert (xs == txs) and (ys == tys) and (zs == tzs) and (ts == tts) and (cs == tcs)
@pytest.mark.parametrize(
"image_mode",
[
"single",
"Confocal Projection",
"z-stack",
],
)
@pytest.mark.parametrize("stack_cycling_mode", ["per_stack", "per_z"])
@pytest.mark.parametrize("conpro_cycling_mode", ["per_stack", "per_plane"])
def test_metadata_set_stack_order_from_configuration_experiment(
dummy_model, image_mode, stack_cycling_mode, conpro_cycling_mode
):
from navigate.model.metadata_sources.metadata import Metadata
dummy_model.configuration["experiment"]["MicroscopeState"][
"image_mode"
] = image_mode
dummy_model.configuration["experiment"]["MicroscopeState"][
"stack_cycling_mode"
] = stack_cycling_mode
dummy_model.configuration["experiment"]["MicroscopeState"][
"conpro_cycling_mode"
] = conpro_cycling_mode
md = Metadata()
md.configuration = dummy_model.configuration
if image_mode == "z-stack" and stack_cycling_mode == "per_stack":
assert md._per_stack is True
elif image_mode == "Confocal Projection" and stack_cycling_mode == "per_stack":
assert md._per_stack is True
else:
assert md._per_stack is False
def set_shape_from_configuration_experiment(dummy_model):
from navigate.model.metadata_sources.metadata import Metadata
md = Metadata()
md.configuration = dummy_model.configuration.copy()
# set up experiment with multiposition
# no position
md.configuration["experiment"]["MicroscopeState"]["image_mode"] = "z-stack"
md.configuration["multi_positions"] = [
["X", "Y", "Z", "THETA", "F"]
]
md.configuration["expriment"]["MicroscopeState"]["is_multiposition"] = False
md._set_shape_from_configuration_experiment()
assert md._multiposition is False
assert md.positions == 1
# customized mode
md.configuration["experiment"]["MicroscopeState"]["image_mode"] = "customized"
assert md._multiposition is True
assert md.positions == 1
# random multiposition
md.configuration["experiment"]["MicroscopeState"]["image_mode"] = "z-stack"
for i in range(5):
num_positions = random.randint(2, 10)
md.configuration["multi_positions"] = [["X", "Y", "Z", "THETA", "F"]]
for p in range(num_positions):
pos = [
random.uniform(0, 100), # X
random.uniform(0, 100), # Y
random.uniform(0, 100), # Z
random.uniform(0, 360), # THETA
random.uniform(0, 10), # F
]
md.configuration["multi_positions"].append(pos)
md.configuration["experiment"]["MicroscopeState"]["is_multiposition"] = True
md._set_shape_from_configuration_experiment()
assert md._multiposition is True
assert md.positions == num_positions

View File

@@ -0,0 +1,46 @@
import urllib.request
import os
import platform
from navigate.tools.file_functions import delete_folder
def test_ome_metadata_valid(dummy_model):
from navigate.model.metadata_sources.ome_tiff_metadata import OMETIFFMetadata
# First, download OME-XML validation tools
# new_path = https://downloads.openmicroscopy.org/bio-formats/8.1.0/artifacts/bftools.zip
# old_path = https://downloads.openmicroscopy.org/bio-formats/6.0.1/artifacts/bftools.zip
urllib.request.urlretrieve(
"https://downloads.openmicroscopy.org/bio-formats/8.1.0/artifacts/bftools.zip",
"bftools.zip",
)
# Unzip
_ = os.popen("tar -xzvf bftools.zip").read()
# Create metadata
md = OMETIFFMetadata()
md.configuration = dummy_model.configuration
# Write metadata to file
md.write_xml("test.xml")
# Validate the XML
if platform.system() == "Windows":
output = os.popen("bftools\\xmlvalid.bat test.xml").read()
else:
output = os.popen("./bftools/xmlvalid test.xml").read()
print(output)
# Delete bftools
delete_folder("./bftools")
os.remove("bftools.zip")
# Delete XML
os.remove("test.xml")
assert "No validation errors found." in output

View File

@@ -0,0 +1,189 @@
import pytest
SPACE_UNITS = [
"angstrom",
"attometer",
"centimeter",
"decimeter",
"exameter",
"femtometer",
"foot",
"gigameter",
"hectometer",
"inch",
"kilometer",
"megameter",
"meter",
"micrometer",
"mile",
"millimeter",
"nanometer",
"parsec",
"petameter",
"picometer",
"terameter",
"yard",
"yoctometer",
"yottameter",
"zeptometer",
"zettameter",
]
TIME_UNITS = [
"attosecond",
"centisecond",
"day",
"decisecond",
"exasecond",
"femtosecond",
"gigasecond",
"hectosecond",
"hour",
"kilosecond",
"megasecond",
"microsecond",
"millisecond",
"minute",
"nanosecond",
"petasecond",
"picosecond",
"second",
"terasecond",
"yoctosecond",
"yottasecond",
"zeptosecond",
"zettasecond",
]
@pytest.fixture
def dummy_metadata(dummy_model):
from navigate.model.metadata_sources.zarr_metadata import OMEZarrMetadata
# Create metadata
md = OMEZarrMetadata()
md.configuration = dummy_model.configuration
return md
def test_axes(dummy_metadata):
axes = dummy_metadata._axes
# Check length
assert (len(axes) > 1) and (len(axes) < 6)
# Check list types and count
time_count = 0
space_count = 0
channel_count = 0
custom_count = 0
for d in axes:
if d["type"] == "time":
assert d["unit"] in TIME_UNITS
time_count += 1
elif d["type"] == "space":
assert d["unit"] in SPACE_UNITS
space_count += 1
elif d["type"] == "channel":
channel_count += 1
else:
custom_count += 1
assert (space_count > 1) and (space_count < 4)
assert time_count < 2
assert ((channel_count < 2) and (custom_count == 0)) or (
(channel_count == 0) and (custom_count < 2)
)
# Check order
order_type = [x["type"] for x in axes]
if "time" in order_type:
# Time must be first, if present
assert order_type.index("time") == 0
if "channel" in order_type:
# Channel must be before all the space axes, if present
ci = order_type.index("channel")
for i, el in enumerate(order_type):
if el == "space":
assert i > ci
# Skip zyx order spec as the naming of axes is not enforcable.
def test_stage_positions_to_translation_transform(dummy_metadata):
import random
pos = [random.random() for _ in range(5)]
translation = dummy_metadata._stage_positions_to_translation_transform(*pos)
axes = dummy_metadata._axes
assert len(translation) == len(axes)
def test_scale_transform(dummy_metadata):
scale = dummy_metadata._scale_transform()
axes = dummy_metadata._axes
assert len(scale) == len(axes)
def test_coordinate_transformations(dummy_metadata):
import random
pos = [random.random() for _ in range(5)]
translation = dummy_metadata._stage_positions_to_translation_transform(*pos)
scale = dummy_metadata._scale_transform()
assert len(dummy_metadata._coordinate_transformations(scale)) == 1
combo = dummy_metadata._coordinate_transformations(scale, translation)
assert len(combo) == 2
assert combo[0]["type"] == "scale" and combo[1]["type"] == "translation"
with pytest.raises(UserWarning):
dummy_metadata._coordinate_transformations(translation=translation)
def test_multiscale_metadata(dummy_metadata):
"""https://ngff.openmicroscopy.org/0.4/#multiscale-md"""
import numpy as np
import random
resolutions = np.array([[1, 1, 1], [2, 2, 1], [4, 4, 1], [8, 8, 1]], dtype=int)
paths = [f"path{i}" for i in range(resolutions.shape[0])]
view = {k: random.random() for k in ["x", "y", "z", "theta", "f"]}
msd = dummy_metadata.multiscales_dict("test", paths, resolutions, view)
# Each "multiscales" dictionary MUST contain the field "axes"
assert "axes" in msd.keys()
# Each "multiscales" dictionary MUST contain the field "datasets"
assert "datasets" in msd.keys()
# Each dictionary in "datasets" MUST contain the field "path",
# whose value contains the path to the array for this resolution
# relative to the current zarr group. The "path"s MUST be ordered
# from largest (i.e. highest resolution) to smallest.
# Each "datasets" dictionary MUST have the same number of dimensions
# and MUST NOT have more than 5 dimensions.
# Each "multiscales" dictionary SHOULD contain the field "name"
assert "name" in msd.keys()
# Each "multiscales" dictionary MAY contain the field "coordinateTransformations"
assert "coordinateTransformations" in msd.keys()
# It SHOULD contain the field "version"
assert "version" in msd.keys()
# Each "multiscales" dictionary SHOULD contain the field "type", which gives
# the type of downscaling method used to generate the multiscale image pyramid.
# It SHOULD contain the field "metadata", which contains a dictionary with
# additional information about the downscaling method.