240 lines
9.0 KiB
Python
240 lines
9.0 KiB
Python
# Copyright (c) 2021-2025 The University of Texas Southwestern Medical Center.
|
|
# All rights reserved.
|
|
|
|
# Redistribution and use in source and binary forms, with or without
|
|
# modification, are permitted for academic and research use only (subject to the
|
|
# limitations in the disclaimer below) provided that the following conditions are met:
|
|
|
|
# * Redistributions of source code must retain the above copyright notice,
|
|
# this list of conditions and the following disclaimer.
|
|
|
|
# * Redistributions in binary form must reproduce the above copyright
|
|
# notice, this list of conditions and the following disclaimer in the
|
|
# documentation and/or other materials provided with the distribution.
|
|
|
|
# * Neither the name of the copyright holders nor the names of its
|
|
# contributors may be used to endorse or promote products derived from this
|
|
# software without specific prior written permission.
|
|
|
|
# NO EXPRESS OR IMPLIED LICENSES TO ANY PARTY'S PATENT RIGHTS ARE GRANTED BY
|
|
# THIS LICENSE. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND
|
|
# CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
|
# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
|
|
# PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR
|
|
# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
|
|
# EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
|
|
# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR
|
|
# BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER
|
|
# IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
|
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
|
# POSSIBILITY OF SUCH DAMAGE.
|
|
import queue
|
|
|
|
import pytest
|
|
import numpy as np
|
|
|
|
from navigate.model.features.volume_search import VolumeSearch
|
|
|
|
|
|
class TestVolumeSearch:
|
|
@pytest.fixture(
|
|
autouse=True,
|
|
params=[[False, False], [True, False], [False, True], [True, True]],
|
|
)
|
|
def _prepare_test(self, request, dummy_model_to_test_features):
|
|
flipx, flipy = request.param
|
|
self.model = dummy_model_to_test_features
|
|
self.config = self.model.configuration["experiment"]["MicroscopeState"]
|
|
self.record_num = 0
|
|
self.overlap = 0.0 # TODO: Make this consistently work for
|
|
# overlap > 0.0 (add fudge factor)
|
|
self.feature_list = [
|
|
[
|
|
{
|
|
"name": VolumeSearch,
|
|
"args": ("Nanoscale", "N/A", flipx, flipy, self.overlap),
|
|
}
|
|
]
|
|
]
|
|
|
|
self.sinx = 1 if flipx else -1
|
|
self.siny = 1 if flipy else -1
|
|
|
|
self.model.active_microscope_name = self.config["microscope_name"]
|
|
curr_zoom = self.model.configuration["experiment"]["MicroscopeState"]["zoom"]
|
|
self.curr_pixel_size = float(
|
|
self.model.configuration["configuration"]["microscopes"][
|
|
self.model.active_microscope_name
|
|
]["zoom"]["pixel_size"][curr_zoom]
|
|
)
|
|
self.target_pixel_size = float(
|
|
self.model.configuration["configuration"]["microscopes"]["Nanoscale"][
|
|
"zoom"
|
|
]["pixel_size"]["N/A"]
|
|
)
|
|
|
|
self.N = 128
|
|
# The target image size in pixels
|
|
self.mag_ratio = int(self.curr_pixel_size / self.target_pixel_size)
|
|
self.target_grid_pixels = int(self.N // self.mag_ratio)
|
|
# The target image size in microns
|
|
self.target_grid_width = self.N * self.target_pixel_size * (1 - self.overlap)
|
|
|
|
self.model.event_queue = queue.Queue(10)
|
|
|
|
self.model.configuration["experiment"]["StageParameters"]["z"] = 100
|
|
self.model.configuration["experiment"]["StageParameters"]["f"] = 100
|
|
|
|
self.config["start_position"] = np.random.randint(-200, 0)
|
|
self.config["end_position"] = self.config["start_position"] + 200
|
|
self.config["number_z_steps"] = np.random.randint(5, 10)
|
|
self.config["step_size"] = (
|
|
self.config["end_position"] - self.config["start_position"]
|
|
) / self.config["number_z_steps"]
|
|
self.config["start_focus"] = -10
|
|
self.config["end_focus"] = 10
|
|
|
|
self.focus_step = (
|
|
self.config["end_focus"] - self.config["start_focus"]
|
|
) / self.config["number_z_steps"]
|
|
|
|
def get_next_record(self, record_prefix, idx):
|
|
idx += 1
|
|
while self.model.signal_records[idx][0] != record_prefix:
|
|
idx += 1
|
|
if idx >= self.record_num:
|
|
break
|
|
return idx
|
|
|
|
def verify_volume_search(self):
|
|
self.record_num = len(self.model.signal_records)
|
|
|
|
idx = -1
|
|
|
|
z_pos = (
|
|
self.model.configuration["experiment"]["StageParameters"]["z"]
|
|
+ self.config["number_z_steps"] // 2 * self.config["step_size"]
|
|
)
|
|
f_pos = (
|
|
self.model.configuration["experiment"]["StageParameters"]["f"]
|
|
+ self.config["number_z_steps"] // 2 * self.focus_step
|
|
)
|
|
|
|
for j in range(self.config["number_z_steps"]):
|
|
idx = self.get_next_record("move_stage", idx)
|
|
if idx >= self.record_num:
|
|
# volume search ended early
|
|
break
|
|
pos_moved = self.model.signal_records[idx][1][0]
|
|
|
|
fact = (
|
|
j
|
|
if j < (self.config["number_z_steps"] + 1) // 2
|
|
else (self.config["number_z_steps"] + 1) // 2 - j - 1
|
|
)
|
|
|
|
# z, f
|
|
assert (
|
|
pos_moved["z_abs"] - (z_pos + fact * self.config["step_size"])
|
|
) < 1e-6, (
|
|
f"should move to z: {z_pos + fact * self.config['step_size']}, "
|
|
f"but moved to {pos_moved['z_abs']}"
|
|
)
|
|
assert (pos_moved["f_abs"] - (f_pos + fact * self.focus_step)) < 1e-6, (
|
|
f"should move to f: {f_pos + fact * self.focus_step}, "
|
|
f"but moved to {pos_moved['f_abs']}"
|
|
)
|
|
|
|
for _ in range(100):
|
|
event, value = self.model.event_queue.get()
|
|
if event == "multiposition":
|
|
break
|
|
|
|
positions = np.vstack(value) # Columns: X, Y, Z, Theta, F
|
|
|
|
# Check the bounding box. TODO: Make exact.
|
|
min_x = (
|
|
self.model.configuration["experiment"]["StageParameters"]["x"]
|
|
- self.sinx * self.lxy
|
|
)
|
|
max_x = self.model.configuration["experiment"]["StageParameters"][
|
|
"x"
|
|
] + self.sinx * (self.lxy + self.N // 2 * self.curr_pixel_size) * (
|
|
1 - self.overlap
|
|
)
|
|
min_y = (
|
|
self.model.configuration["experiment"]["StageParameters"]["y"]
|
|
- self.siny * self.lxy
|
|
)
|
|
max_y = self.model.configuration["experiment"]["StageParameters"][
|
|
"y"
|
|
] + self.siny * (self.lxy + self.N // 2 * self.curr_pixel_size) * (
|
|
1 - self.overlap
|
|
)
|
|
|
|
if self.sinx == -1:
|
|
tmp = max_x
|
|
max_x = min_x
|
|
min_x = tmp
|
|
|
|
if self.siny == -1:
|
|
tmp = max_y
|
|
max_y = min_y
|
|
min_y = tmp
|
|
|
|
min_z = self.model.configuration["experiment"]["StageParameters"]["z"] - self.lz
|
|
max_z = self.model.configuration["experiment"]["StageParameters"]["z"] + (
|
|
self.lz + self.N // 2 * self.curr_pixel_size
|
|
) * (1 - self.overlap)
|
|
|
|
assert np.min(positions[:, 0]) >= min_x
|
|
assert np.max(positions[:, 0]) <= max_x
|
|
assert np.min(positions[:, 1]) >= min_y
|
|
assert np.max(positions[:, 1]) <= max_y
|
|
assert np.min(positions[:, 2]) >= min_z
|
|
assert np.max(positions[:, 2]) <= max_z
|
|
|
|
def test_box_volume_search(self):
|
|
from navigate.tools.sdf import volume_from_sdf, box
|
|
|
|
M = int(self.config["number_z_steps"])
|
|
microscope_name = self.model.configuration["experiment"]["MicroscopeState"][
|
|
"microscope_name"
|
|
]
|
|
self.model.configuration["experiment"]["CameraParameters"][microscope_name][
|
|
"x_pixels"
|
|
] = self.N
|
|
self.lxy = (
|
|
np.random.randint(int(0.1 * self.N), int(0.4 * self.N))
|
|
* self.curr_pixel_size
|
|
)
|
|
self.lz = (
|
|
np.random.randint(int(0.1 * self.N), int(0.4 * self.N))
|
|
* self.curr_pixel_size
|
|
)
|
|
|
|
vol = volume_from_sdf(
|
|
lambda p: box(
|
|
p,
|
|
(
|
|
self.lxy,
|
|
self.lxy,
|
|
self.lz,
|
|
),
|
|
),
|
|
self.N,
|
|
pixel_size=self.curr_pixel_size,
|
|
subsample_z=self.N // M,
|
|
)
|
|
vol = (vol <= 0) * 100
|
|
vol = vol[np.r_[(M // 2) : M, 0 : (M // 2)]]
|
|
self.model.data_buffer = vol
|
|
|
|
def get_offset_variance_maps():
|
|
return np.zeros((self.N, self.N)), np.ones((self.N, self.N))
|
|
|
|
self.model.get_offset_variance_maps = get_offset_variance_maps
|
|
|
|
self.model.start(self.feature_list)
|
|
self.verify_volume_search()
|