380 lines
15 KiB
Python
380 lines
15 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.
|
|
#
|
|
|
|
# Third Party Imports
|
|
import pytest
|
|
|
|
|
|
@pytest.mark.hardware
|
|
@pytest.fixture(scope="module")
|
|
def prepare_cameras(dummy_model):
|
|
from navigate.model.devices.APIs.hamamatsu.HamamatsuAPI import DCAM, camReg
|
|
from navigate.model.devices.camera.hamamatsu import HamamatsuOrca
|
|
|
|
def start_camera(idx=0):
|
|
# open camera
|
|
for i in range(10):
|
|
assert camReg.numCameras == idx
|
|
try:
|
|
camera = DCAM(idx)
|
|
if camera.get_camera_handler() != 0:
|
|
break
|
|
camera.dev_close()
|
|
except Exception:
|
|
continue
|
|
camera = None
|
|
return camera
|
|
|
|
model = dummy_model
|
|
|
|
temp = {}
|
|
for microscope_name in model.configuration["configuration"]["microscopes"].keys():
|
|
serial_number = model.configuration["configuration"]["microscopes"][
|
|
microscope_name
|
|
]["camera"]["hardware"]["serial_number"]
|
|
temp[str(serial_number)] = microscope_name
|
|
|
|
camera_connections = {}
|
|
|
|
camera = start_camera()
|
|
for i in range(camReg.maxCameras):
|
|
if i > 0:
|
|
camera = start_camera(i)
|
|
if str(camera._serial_number) in temp:
|
|
microscope_name = temp[str(camera._serial_number)]
|
|
camera = HamamatsuOrca(microscope_name, camera, model.configuration)
|
|
camera_connections[microscope_name] = camera
|
|
|
|
yield camera_connections
|
|
|
|
# close all the cameras
|
|
for k in camera_connections:
|
|
camera_connections[k].camera_controller.dev_close()
|
|
|
|
|
|
@pytest.mark.hardware
|
|
class TestHamamatsuOrca:
|
|
"""Unit Test for HamamamatsuOrca Class"""
|
|
|
|
model = None
|
|
|
|
@pytest.fixture(autouse=True)
|
|
def _prepare_test(self, dummy_model, prepare_cameras):
|
|
self.num_of_tests = 10
|
|
self.model = dummy_model
|
|
self.cameras = prepare_cameras
|
|
|
|
self.microscope_name = self.model.configuration["experiment"][
|
|
"MicroscopeState"
|
|
]["microscope_name"]
|
|
self.camera = self.cameras[self.microscope_name]
|
|
|
|
def is_in_range(self, value, target, precision=100):
|
|
target_min = target - target / precision
|
|
target_max = target + target / precision
|
|
return value > target_min and value < target_max
|
|
|
|
def test_hamamatsu_camera_attributes(self):
|
|
from navigate.model.devices.camera.hamamatsu import HamamatsuOrca
|
|
|
|
attributes = dir(HamamatsuOrca)
|
|
desired_attributes = [
|
|
"serial_number",
|
|
"report_settings",
|
|
"close_camera",
|
|
"set_sensor_mode",
|
|
"set_readout_direction",
|
|
"calculate_light_sheet_exposure_time",
|
|
"calculate_readout_time",
|
|
"set_exposure_time",
|
|
"set_line_interval",
|
|
"set_binning",
|
|
"set_ROI",
|
|
"initialize_image_series",
|
|
"close_image_series",
|
|
"get_new_frame",
|
|
]
|
|
|
|
for da in desired_attributes:
|
|
assert da in attributes
|
|
|
|
def test_init_camera(self):
|
|
for microscope_name in self.model.configuration["configuration"][
|
|
"microscopes"
|
|
].keys():
|
|
|
|
camera = self.cameras[microscope_name]
|
|
|
|
assert camera is not None, f"Should start the camera {microscope_name}"
|
|
|
|
camera_controller = camera.camera_controller
|
|
camera_configs = self.model.configuration["configuration"]["microscopes"][
|
|
microscope_name
|
|
]["camera"]
|
|
|
|
# serial number
|
|
assert str(camera_controller._serial_number) == str(
|
|
camera_configs["hardware"]["serial_number"]
|
|
), f"the camera serial number isn't right for {microscope_name}!"
|
|
assert str(camera.serial_number) == str(
|
|
camera_configs["hardware"]["serial_number"]
|
|
), f"the camera serial number isn't right for {microscope_name}!"
|
|
|
|
# verify camera is initialized with the attributes from configuration.yaml
|
|
parameters = [
|
|
"defect_correct_mode",
|
|
"readout_speed",
|
|
"trigger_active",
|
|
"trigger_mode",
|
|
"trigger_polarity",
|
|
"trigger_source",
|
|
]
|
|
for parameter in parameters:
|
|
value = camera_controller.get_property_value(parameter)
|
|
assert value == camera_configs[parameter]
|
|
|
|
# sensor mode
|
|
sensor_mode = camera_controller.get_property_value("sensor_mode")
|
|
expected_value = 1 if camera_configs["sensor_mode"] == "Normal" else 12
|
|
assert sensor_mode == expected_value, "Sensor mode isn't right!"
|
|
|
|
# exposure time
|
|
exposure_time = camera_controller.get_property_value("exposure_time")
|
|
assert self.is_in_range(
|
|
exposure_time, camera_configs["exposure_time"] / 1000, 10
|
|
), "Exposure time isn't right!"
|
|
|
|
# binning
|
|
binning = camera_controller.get_property_value("binning")
|
|
assert int(binning) == int(
|
|
camera_configs["binning"][0]
|
|
), "Binning isn't right!"
|
|
|
|
# image width and height
|
|
width = camera_controller.get_property_value("image_width")
|
|
assert width == camera_configs["x_pixels"], "image width isn't right"
|
|
height = camera_controller.get_property_value("image_height")
|
|
assert height == camera_configs["y_pixels"], "image height isn't right"
|
|
|
|
def test_set_sensor_mode(self):
|
|
modes = {"Normal": 1, "Light-Sheet": 12, "RandomMode": None}
|
|
for mode in modes:
|
|
pre_value = self.camera.camera_controller.get_property_value("sensor_mode")
|
|
self.camera.set_sensor_mode(mode)
|
|
value = self.camera.camera_controller.get_property_value("sensor_mode")
|
|
if modes[mode] is not None:
|
|
assert value == modes[mode], f"sensor mode {mode} isn't right!"
|
|
else:
|
|
assert value == pre_value, "sensor mode shouldn't be set!"
|
|
|
|
def test_set_readout_direction(self):
|
|
readout_directions = {"Top-to-Bottom": 1, "Bottom-to-Top": 2}
|
|
for direction in readout_directions:
|
|
self.camera.set_readout_direction(direction)
|
|
value = self.camera.camera_controller.get_property_value(
|
|
"readout_direction"
|
|
)
|
|
assert (
|
|
value == readout_directions[direction]
|
|
), f"readout direction setting isn't right for {direction}"
|
|
|
|
# def test_calculate_readout_time(self):
|
|
# pass
|
|
|
|
def test_set_exposure_time(self):
|
|
import random
|
|
|
|
modes_dict = {
|
|
"Normal": 10000,
|
|
"Light-Sheet": 20,
|
|
}
|
|
for mode in modes_dict:
|
|
self.camera.set_sensor_mode(mode)
|
|
for i in range(self.num_of_tests):
|
|
exposure_time = random.randint(1, modes_dict[mode])
|
|
self.camera.set_exposure_time(exposure_time / 1000)
|
|
value = self.camera.camera_controller.get_property_value(
|
|
"exposure_time"
|
|
)
|
|
assert self.is_in_range(
|
|
value, exposure_time / 1000, 10
|
|
), f"exposure time({exposure_time}) isn't right!"
|
|
self.camera.set_sensor_mode("Normal")
|
|
|
|
def test_set_line_interval(self):
|
|
import random
|
|
|
|
self.camera.set_sensor_mode("Light-Sheet")
|
|
for i in range(self.num_of_tests):
|
|
line_interval = random.random() / 10.0
|
|
r = self.camera.set_line_interval(line_interval)
|
|
if r is True:
|
|
value = self.camera.camera_controller.get_property_value(
|
|
"internal_line_interval"
|
|
)
|
|
assert self.is_in_range(
|
|
value, line_interval
|
|
), f"line interval {line_interval} isn't right! {value}"
|
|
self.camera.set_sensor_mode("Normal")
|
|
|
|
def test_set_binning(self):
|
|
import random
|
|
|
|
binning_dict = {
|
|
"1x1": 1,
|
|
"2x2": 2,
|
|
"4x4": 4,
|
|
# '8x8': 8,
|
|
# '16x16': 16,
|
|
# '1x2': 102,
|
|
# '2x4': 204
|
|
}
|
|
for binning_string in binning_dict:
|
|
self.camera.set_binning(binning_string)
|
|
value = self.camera.camera_controller.get_property_value("binning")
|
|
assert (
|
|
int(value) == binning_dict[binning_string]
|
|
), f"binning {binning_string} isn't right!"
|
|
|
|
for i in range(self.num_of_tests):
|
|
x = random.randint(1, 20)
|
|
y = random.randint(1, 20)
|
|
binning_string = f"{x}x{y}"
|
|
assert self.camera.set_binning(binning_string) == (
|
|
binning_string in binning_dict
|
|
)
|
|
|
|
def test_set_ROI(self):
|
|
import random
|
|
|
|
self.camera.set_binning("1x1")
|
|
width = self.camera.camera_parameters["x_pixels"]
|
|
height = self.camera.camera_parameters["x_pixels"]
|
|
w = self.camera.camera_controller.get_property_value("image_width")
|
|
h = self.camera.camera_controller.get_property_value("image_height")
|
|
assert width == w, f"maximum width should be the same {width} - {w}"
|
|
assert height == h, f"maximum height should be the same {height} -{h}"
|
|
|
|
for i in range(self.num_of_tests):
|
|
pre_x, pre_y = self.camera.x_pixels, self.camera.y_pixels
|
|
x = random.randint(1, self.camera.camera_parameters["x_pixels"])
|
|
y = random.randint(1, self.camera.camera_parameters["y_pixels"])
|
|
r = self.camera.set_ROI(y, x)
|
|
if x % 2 == 1 or y % 2 == 1:
|
|
assert r is False
|
|
assert self.camera.x_pixels == pre_x, "width shouldn't be chaged!"
|
|
assert self.camera.y_pixels == pre_y, "height shouldn't be changed!"
|
|
else:
|
|
top = (height - y) / 2
|
|
bottom = top + y - 1
|
|
if top % 2 == 1 or bottom % 2 == 0:
|
|
assert r is False
|
|
else:
|
|
assert r is True, (
|
|
f"try to set{x}x{y}, but get "
|
|
f"{self.camera.x_pixels}x{self.camera.y_pixels}"
|
|
)
|
|
assert (
|
|
self.camera.x_pixels == x
|
|
), f"trying to set {x}x{y}. width should be changed to {x}"
|
|
assert self.camera.y_pixels == y, f"height should be chagned to {y}"
|
|
|
|
self.camera.set_ROI(512, 512)
|
|
assert self.camera.x_pixels == 512
|
|
assert self.camera.y_pixels == 512
|
|
|
|
self.camera.set_ROI(
|
|
self.camera.camera_parameters["x_pixels"],
|
|
self.camera.camera_parameters["y_pixels"],
|
|
)
|
|
assert self.camera.x_pixels == self.camera.camera_parameters["x_pixels"]
|
|
assert self.camera.y_pixels == self.camera.camera_parameters["y_pixels"]
|
|
|
|
self.camera.set_ROI(
|
|
self.camera.camera_parameters["x_pixels"] + 100,
|
|
self.camera.camera_parameters["y_pixels"] + 100,
|
|
)
|
|
assert self.camera.x_pixels == self.camera.camera_parameters["x_pixels"]
|
|
assert self.camera.y_pixels == self.camera.camera_parameters["y_pixels"]
|
|
|
|
def test_acquire_image(self):
|
|
import random
|
|
import time
|
|
from navigate.model.concurrency.concurrency_tools import SharedNDArray
|
|
|
|
# set software trigger
|
|
self.camera.camera_controller.set_property_value("trigger_source", 3)
|
|
|
|
assert self.camera.is_acquiring is False
|
|
|
|
number_of_frames = 100
|
|
data_buffer = [
|
|
SharedNDArray(shape=(2048, 2048), dtype="uint16")
|
|
for i in range(number_of_frames)
|
|
]
|
|
|
|
# initialize without release/close the camera
|
|
self.camera.initialize_image_series(data_buffer, number_of_frames)
|
|
assert self.camera.is_acquiring is True
|
|
|
|
self.camera.initialize_image_series(data_buffer, number_of_frames)
|
|
assert self.camera.is_acquiring is True
|
|
|
|
exposure_time = self.camera.camera_controller.get_property_value(
|
|
"exposure_time"
|
|
)
|
|
readout_time = self.camera.camera_controller.get_property_value("readout_time")
|
|
|
|
for i in range(self.num_of_tests):
|
|
triggers = random.randint(1, 100)
|
|
for j in range(triggers):
|
|
self.camera.camera_controller.fire_software_trigger()
|
|
time.sleep(exposure_time + readout_time)
|
|
|
|
time.sleep(0.01)
|
|
frames = self.camera.get_new_frame()
|
|
assert len(frames) == triggers
|
|
|
|
self.camera.close_image_series()
|
|
assert self.camera.is_acquiring is False
|
|
|
|
for i in range(self.num_of_tests):
|
|
self.camera.initialize_image_series(data_buffer, number_of_frames)
|
|
assert self.camera.is_acquiring is True
|
|
self.camera.close_image_series()
|
|
assert self.camera.is_acquiring is False
|
|
|
|
# close a closed camera
|
|
self.camera.close_image_series()
|
|
self.camera.close_image_series()
|
|
assert self.camera.is_acquiring is False
|