Files
navigate/test/model/devices/camera/test_hamamatsu.py
2025-12-04 16:07:30 +08:00

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