feat: init
This commit is contained in:
0
test/model/devices/camera/__init__.py
Normal file
0
test/model/devices/camera/__init__.py
Normal file
80
test/model/devices/camera/test_camera_base.py
Normal file
80
test/model/devices/camera/test_camera_base.py
Normal file
@@ -0,0 +1,80 @@
|
||||
"""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
|
||||
|
||||
from navigate.model.devices.camera.synthetic import SyntheticCamera
|
||||
|
||||
|
||||
def test_start_camera(dummy_model):
|
||||
model = dummy_model
|
||||
for microscope_name in model.configuration["configuration"]["microscopes"].keys():
|
||||
camera = SyntheticCamera(microscope_name, None, model.configuration)
|
||||
assert (
|
||||
camera.camera_parameters["hardware"]["serial_number"]
|
||||
== model.configuration["configuration"]["microscopes"][microscope_name][
|
||||
"camera"
|
||||
]["hardware"]["serial_number"]
|
||||
), f"didn't load correct camera parameter for microscope {microscope_name}"
|
||||
|
||||
# non-exist microscope name
|
||||
microscope_name = (
|
||||
model.configuration["configuration"]["microscopes"].keys()[0] + "_random_error"
|
||||
)
|
||||
raised_error = False
|
||||
try:
|
||||
_ = SyntheticCamera(microscope_name, None, model.configuration)
|
||||
except NameError:
|
||||
raised_error = True
|
||||
assert (
|
||||
raised_error
|
||||
), "should raise NameError when the microscope name doesn't exist!"
|
||||
|
||||
|
||||
def test_camera_base_functions(dummy_model):
|
||||
import random
|
||||
|
||||
model = dummy_model
|
||||
microscope_name = model.configuration["experiment"]["MicroscopeState"][
|
||||
"microscope_name"
|
||||
]
|
||||
|
||||
camera = SyntheticCamera(microscope_name, None, model.configuration)
|
||||
funcs = ["set_readout_direction", "calculate_light_sheet_exposure_time"]
|
||||
args = [[random.random()], [random.random(), random.random()]]
|
||||
|
||||
for f, a in zip(funcs, args):
|
||||
if a is not None:
|
||||
getattr(camera, f)(*a)
|
||||
else:
|
||||
getattr(camera, f)()
|
||||
193
test/model/devices/camera/test_camera_synthetic.py
Normal file
193
test/model/devices/camera/test_camera_synthetic.py
Normal file
@@ -0,0 +1,193 @@
|
||||
# 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
|
||||
import numpy as np
|
||||
|
||||
from navigate.model.devices.camera.synthetic import (
|
||||
SyntheticCamera,
|
||||
SyntheticCameraController,
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture(scope="class")
|
||||
def synthetic_camera(dummy_model):
|
||||
dummy_model = dummy_model
|
||||
scc = SyntheticCameraController()
|
||||
microscope_name = dummy_model.configuration["experiment"]["MicroscopeState"][
|
||||
"microscope_name"
|
||||
]
|
||||
synthetic_camera = SyntheticCamera(microscope_name, scc, dummy_model.configuration)
|
||||
return synthetic_camera
|
||||
|
||||
|
||||
class TestSyntheticCamera:
|
||||
"""Unit Test for Camera Synthetic Class"""
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def _prepare_camera(self, synthetic_camera):
|
||||
self.synthetic_camera = synthetic_camera
|
||||
|
||||
def test_synthetic_camera_attributes(self):
|
||||
desired_attributes = [
|
||||
"x_pixels",
|
||||
"y_pixels",
|
||||
"is_acquiring",
|
||||
"_mean_background_count",
|
||||
"_noise_sigma",
|
||||
"camera_controller",
|
||||
"current_frame_idx",
|
||||
"data_buffer",
|
||||
"num_of_frame",
|
||||
"pre_frame_idx",
|
||||
]
|
||||
for da in desired_attributes:
|
||||
assert hasattr(self.synthetic_camera, da)
|
||||
|
||||
def test_synthetic_camera_wheel_attributes_type(self):
|
||||
desired_attributes = {
|
||||
"x_pixels": int,
|
||||
"y_pixels": int,
|
||||
"is_acquiring": bool,
|
||||
"_mean_background_count": int,
|
||||
"_noise_sigma": np.float64,
|
||||
# 'current_frame_idx': None,
|
||||
# 'data_buffer': None,
|
||||
# 'num_of_frame': None,
|
||||
# 'pre_frame_idx': None,
|
||||
}
|
||||
|
||||
for key in desired_attributes:
|
||||
attribute = getattr(self.synthetic_camera, key)
|
||||
print(key, type(attribute), desired_attributes[key])
|
||||
assert type(attribute) == desired_attributes[key]
|
||||
|
||||
def test_synthetic_camera_methods(self):
|
||||
methods = [
|
||||
"report_settings",
|
||||
"close_camera",
|
||||
"set_sensor_mode",
|
||||
"set_exposure_time",
|
||||
"set_line_interval",
|
||||
"set_binning",
|
||||
"initialize_image_series",
|
||||
"close_image_series",
|
||||
"generate_new_frame",
|
||||
"get_new_frame",
|
||||
"set_ROI",
|
||||
]
|
||||
|
||||
for m in methods:
|
||||
assert hasattr(self.synthetic_camera, m) and callable(
|
||||
getattr(self.synthetic_camera, m)
|
||||
)
|
||||
|
||||
def test_synthetic_camera_wheel_method_calls(self):
|
||||
self.synthetic_camera.report_settings()
|
||||
self.synthetic_camera.close_camera()
|
||||
self.synthetic_camera.set_sensor_mode(mode="test")
|
||||
self.synthetic_camera.set_exposure_time(exposure_time=0.2)
|
||||
self.synthetic_camera.set_line_interval(line_interval_time=1)
|
||||
self.synthetic_camera.set_binning(binning_string="2x2")
|
||||
self.synthetic_camera.initialize_image_series()
|
||||
self.synthetic_camera.close_image_series()
|
||||
self.synthetic_camera.get_new_frame()
|
||||
self.synthetic_camera.set_ROI()
|
||||
|
||||
def test_synthetic_camera_exposure(self):
|
||||
exposure_time = 200
|
||||
self.synthetic_camera.set_exposure_time(exposure_time=exposure_time / 1000)
|
||||
assert (exposure_time / 1000) == self.synthetic_camera.camera_exposure_time
|
||||
|
||||
def test_synthetic_camera_binning(self):
|
||||
x_pixels = self.synthetic_camera.x_pixels
|
||||
self.synthetic_camera.set_binning(binning_string="2x2")
|
||||
assert self.synthetic_camera.x_binning == 2
|
||||
assert self.synthetic_camera.y_binning == 2
|
||||
assert type(self.synthetic_camera.x_binning) == int
|
||||
assert type(self.synthetic_camera.y_binning) == int
|
||||
assert self.synthetic_camera.x_pixels == x_pixels / 2
|
||||
|
||||
def test_synthetic_camera_initialize_image_series(self):
|
||||
self.synthetic_camera.initialize_image_series()
|
||||
assert self.synthetic_camera.num_of_frame == 100
|
||||
assert self.synthetic_camera.data_buffer is None
|
||||
assert self.synthetic_camera.current_frame_idx == 0
|
||||
assert self.synthetic_camera.pre_frame_idx == 0
|
||||
assert self.synthetic_camera.is_acquiring is True
|
||||
|
||||
def test_synthetic_camera_close_image_series(self):
|
||||
self.synthetic_camera.close_image_series()
|
||||
assert self.synthetic_camera.pre_frame_idx == 0
|
||||
assert self.synthetic_camera.current_frame_idx == 0
|
||||
assert self.synthetic_camera.is_acquiring is False
|
||||
|
||||
def test_synthetic_camera_acquire_images(self):
|
||||
import random
|
||||
from navigate.model.concurrency.concurrency_tools import SharedNDArray
|
||||
|
||||
number_of_frames = 100
|
||||
data_buffer = [
|
||||
SharedNDArray(shape=(2048, 2048), dtype="uint16")
|
||||
for i in range(number_of_frames)
|
||||
]
|
||||
|
||||
self.synthetic_camera.initialize_image_series(data_buffer, number_of_frames)
|
||||
|
||||
assert self.synthetic_camera.is_acquiring is True, "should be acquring"
|
||||
|
||||
frame_idx = 0
|
||||
|
||||
for i in range(10):
|
||||
frame_num = random.randint(1, 30)
|
||||
for j in range(frame_num):
|
||||
self.synthetic_camera.generate_new_frame()
|
||||
frames = self.synthetic_camera.get_new_frame()
|
||||
|
||||
assert len(frames) == frame_num, "frame number isn't right!"
|
||||
assert frames[0] == frame_idx, "frame idx isn't right!"
|
||||
|
||||
frame_idx = (frame_idx + frame_num) % number_of_frames
|
||||
|
||||
self.synthetic_camera.close_image_series()
|
||||
assert (
|
||||
self.synthetic_camera.is_acquiring is False
|
||||
), "is_acquiring should be False"
|
||||
|
||||
def test_synthetic_camera_set_roi(self):
|
||||
self.synthetic_camera.set_ROI()
|
||||
assert self.synthetic_camera.x_pixels == 2048
|
||||
assert self.synthetic_camera.y_pixels == 2048
|
||||
self.synthetic_camera.set_ROI(roi_height=500, roi_width=700)
|
||||
assert self.synthetic_camera.x_pixels == 700
|
||||
assert self.synthetic_camera.y_pixels == 500
|
||||
618
test/model/devices/camera/test_daheng.py
Normal file
618
test/model/devices/camera/test_daheng.py
Normal file
@@ -0,0 +1,618 @@
|
||||
# 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.
|
||||
|
||||
# Standard Library Imports
|
||||
import pytest
|
||||
from typing import Tuple
|
||||
from unittest.mock import patch, MagicMock
|
||||
import logging
|
||||
import io
|
||||
|
||||
# Third Party Imports
|
||||
import numpy as np
|
||||
from numpy.testing import assert_array_equal
|
||||
|
||||
# Local Imports
|
||||
from navigate.model.utils.exceptions import UserVisibleException
|
||||
|
||||
try:
|
||||
import gxipy # noqa: F401
|
||||
except:
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def mock_daheng_module():
|
||||
fake_gx = MagicMock()
|
||||
with patch.dict("sys.modules", {"gxipy": fake_gx}):
|
||||
yield fake_gx
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_daheng_sdk():
|
||||
"""Patch Daheng SDK (gxipy) and return mocked device + subsystems."""
|
||||
with patch(
|
||||
"navigate.model.devices.camera.daheng.gx.DeviceManager"
|
||||
) as mock_device_manager:
|
||||
device = _create_mock_device(mock_device_manager)
|
||||
feature_control = _create_mock_feature_control()
|
||||
data_stream, raw_image = _create_mock_image_pipeline()
|
||||
|
||||
_attach_mock_interfaces_to_device(device, feature_control, data_stream)
|
||||
|
||||
yield {
|
||||
"device": device,
|
||||
"feature_control": feature_control,
|
||||
"data_stream": data_stream,
|
||||
"raw_image": raw_image,
|
||||
}
|
||||
|
||||
|
||||
def _attach_mock_interfaces_to_device(device, feature_control, data_stream):
|
||||
"""
|
||||
Attach core SDK interfaces to a mock Daheng device.
|
||||
|
||||
In the gxipy SDK, once a device is opened, it provides two key subsystems:
|
||||
|
||||
- feature_control (via get_remote_device_feature_control()):
|
||||
This is the interface for configuring camera hardware settings such as
|
||||
exposure time, gain, trigger mode, binning, resolution, and ROI.
|
||||
The SDK exposes these through feature "objects" with .get()/.set() methods.
|
||||
|
||||
- data_stream (accessed as a property):
|
||||
This handles actual image acquisition. It provides methods to start/stop
|
||||
streaming and to retrieve frames via .snap_image().
|
||||
|
||||
This function sets up mock versions of those subsystems to a MagicMock-based
|
||||
device object, enabling testable interaction without requiring physical hardware.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
device : MagicMock
|
||||
The mocked gxipy.Device object to configure.
|
||||
|
||||
feature_control : MagicMock
|
||||
Mocked feature control interface to simulate hardware parameters.
|
||||
|
||||
data_stream : MagicMock
|
||||
Mocked data stream interface to simulate image capture.
|
||||
"""
|
||||
device.get_remote_device_feature_control.return_value = feature_control
|
||||
device.data_stream = data_stream
|
||||
|
||||
|
||||
def _create_mock_device(mock_device_manager) -> MagicMock:
|
||||
"""
|
||||
Create a fake Daheng device and configure DeviceManager return values.
|
||||
|
||||
This sets up:
|
||||
- update_device_list() -> None
|
||||
- get_device_list() -> list with one fake serial number
|
||||
- open_device_by_index(i) -> the mock device
|
||||
|
||||
Returns
|
||||
-------
|
||||
MagicMock
|
||||
A fake Daheng device object.
|
||||
"""
|
||||
mock_device = MagicMock(name="FakeDevice")
|
||||
|
||||
mock_device_manager.return_value.update_device_list.return_value = None
|
||||
mock_device_manager.return_value.get_device_list.return_value = [{"sn": "1234"}]
|
||||
mock_device_manager.return_value.open_device_by_index.return_value = mock_device
|
||||
|
||||
return mock_device
|
||||
|
||||
|
||||
def _create_mock_feature_control() -> MagicMock:
|
||||
"""
|
||||
Create a fake FeatureControl interface that simulates Daheng camera settings.
|
||||
|
||||
Simulates:
|
||||
- get_string_feature("DeviceSerialNumber").get() -> "1234"
|
||||
- get_int_feature(...).get() -> 2048
|
||||
- get_enum_feature(...).set(...) -> None
|
||||
|
||||
Returns
|
||||
-------
|
||||
MagicMock
|
||||
A mock feature_control object.
|
||||
"""
|
||||
mock_feature_control = MagicMock(name="FakeFeatureControl")
|
||||
mock_feature_control.get_string_feature.return_value.get.return_value = "1234"
|
||||
mock_feature_control.get_int_feature.side_effect = lambda name: MagicMock(
|
||||
get=MagicMock(return_value=2048)
|
||||
)
|
||||
mock_feature_control.get_enum_feature.return_value.set.return_value = None
|
||||
return mock_feature_control
|
||||
|
||||
|
||||
def _create_mock_image_pipeline() -> Tuple[MagicMock, MagicMock]:
|
||||
"""
|
||||
Create a mocked data stream and raw image pipeline.
|
||||
|
||||
Simulates:
|
||||
- data_stream.snap_image() -> mock_raw_image
|
||||
- raw_image.get_numpy_array() -> np.zeros((2048, 2048), dtype=np.uint16)
|
||||
|
||||
Returns
|
||||
-------
|
||||
Tuple[MagicMock, MagicMock]
|
||||
(mock_data_stream, mock_raw_image)
|
||||
"""
|
||||
stream = MagicMock(name="FakeDataStream")
|
||||
image = MagicMock(name="FakeRawImage")
|
||||
stream.snap_image.return_value = image
|
||||
image.get_numpy_array.return_value = np.zeros((2048, 2048), dtype=np.uint16)
|
||||
return stream, image
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def camera(mock_daheng_sdk):
|
||||
"""
|
||||
Return a DahengCamera instance connected via mocked SDK.
|
||||
|
||||
The mock_daheng_sdk fixture is required to patch the SDK and simulate hardware.
|
||||
It's not used directly in this function, but must be active when connect() is called.
|
||||
"""
|
||||
# Use the patched classmethod to simulate SDK connection.
|
||||
# This is where mock_daheng_sdk enters.
|
||||
from navigate.model.devices.camera.daheng import DahengCamera
|
||||
|
||||
# Minimal config object matching Navigate's expected schema
|
||||
config = {
|
||||
"configuration": {
|
||||
"microscopes": {
|
||||
"test_scope": {
|
||||
"camera": {
|
||||
"hardware": {
|
||||
"serial_number": "1234",
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
camera = DahengCamera(
|
||||
microscope_name="test_scope",
|
||||
device_connection=mock_daheng_sdk["device"],
|
||||
configuration=config,
|
||||
)
|
||||
|
||||
# Initialize and return the test camera instance
|
||||
return camera
|
||||
|
||||
|
||||
@patch("navigate.model.devices.camera.daheng.gx.DeviceManager")
|
||||
def test_connect_without_serial(mock_dm):
|
||||
"""
|
||||
Test that DahengCamera.connect() connects to the first camera if no serial number is provided.
|
||||
|
||||
This uses patching to replace the actual DeviceManager with a mock,
|
||||
simulating a single connected camera with serial '1234'.
|
||||
"""
|
||||
|
||||
mock_device = MagicMock()
|
||||
|
||||
# Simulate SDK returning one device with serial '1234'
|
||||
mock_dm.return_value.get_device_list.return_value = [{"sn": "1234"}]
|
||||
|
||||
# Simulate opening that device returns our mock_device
|
||||
mock_dm.return_value.open_device_by_index.return_value = mock_device
|
||||
|
||||
# Call connect without specifying serial number
|
||||
from navigate.model.devices.camera.daheng import DahengCamera
|
||||
|
||||
device = DahengCamera.connect()
|
||||
|
||||
# Verify that we get the mocked device object
|
||||
assert device == mock_device
|
||||
|
||||
|
||||
@patch("navigate.model.devices.camera.daheng.gx.DeviceManager")
|
||||
def test_connect_invalid_serial_raises(mock_dm):
|
||||
"""
|
||||
Test that DahengCamera.connect() raises a UserVisibleException if the
|
||||
specified serial number does not match any connected camera.
|
||||
|
||||
This verifies the fallback else-block logic in the for-loop of connect().
|
||||
"""
|
||||
# Simulate one connected device with serial '1234'
|
||||
mock_dm.return_value.get_device_list.return_value = [{"sn": "1234"}]
|
||||
|
||||
from navigate.model.devices.camera.daheng import DahengCamera
|
||||
|
||||
# Attempt to connect with a non-existent serial number
|
||||
with pytest.raises(
|
||||
UserVisibleException, match="Daheng camera with serial INVALID_SN not found."
|
||||
):
|
||||
DahengCamera.connect(serial_number="INVALID_SN")
|
||||
|
||||
|
||||
def test_str(camera):
|
||||
"""
|
||||
Test the string representation of the DahengCamera object.
|
||||
|
||||
Ensures that the __str__ method includes the camera model name,
|
||||
serial number, and connection status in the returned string.
|
||||
"""
|
||||
result = str(camera)
|
||||
assert "MER2_1220_32U3C Camera" in result
|
||||
assert "Serial: 1234" in result
|
||||
assert "Connected" in result
|
||||
|
||||
|
||||
def test_camera_connected(camera):
|
||||
"""
|
||||
Test that the camera object reports a connected state after setup.
|
||||
|
||||
This relies on the 'camera' fixture, which internally calls DahengCamera.connect()
|
||||
and initializes the SDK state. Verifies that is_connected is True and the
|
||||
device serial number is correctly cached.
|
||||
"""
|
||||
assert camera.is_connected
|
||||
assert camera.device_serial_number == "1234"
|
||||
|
||||
|
||||
def test_disconnect_clears_state(camera):
|
||||
"""
|
||||
Test that disconnect() resets internal state and marks camera as disconnected.
|
||||
"""
|
||||
camera.disconnect()
|
||||
assert camera.device is None
|
||||
assert camera.feature_control is None
|
||||
assert camera.is_connected is False
|
||||
assert camera.serial_number == "UNKNOWN"
|
||||
|
||||
|
||||
def test_set_exposure_time(camera):
|
||||
"""
|
||||
Test that set_exposure_time() updates internal state and calls correct SDK feature.
|
||||
"""
|
||||
camera.set_exposure_time(0.1)
|
||||
|
||||
# Internal caching of exposure time (in seconds)
|
||||
assert camera._exposure_time == 0.1
|
||||
|
||||
# Verifies that the SDK was told to get the 'ExposureTime' feature
|
||||
camera.feature_control.get_float_feature.assert_called_with("ExposureTime")
|
||||
|
||||
|
||||
def test_set_gain(camera):
|
||||
"""
|
||||
Ensure set_gain() calls the Gain feature with the expected float value.
|
||||
"""
|
||||
camera.set_gain(5.0)
|
||||
camera.feature_control.get_float_feature.assert_called_with("Gain")
|
||||
camera.feature_control.get_float_feature.return_value.set.assert_called_with(5.0)
|
||||
|
||||
|
||||
def test_set_binning(camera):
|
||||
"""
|
||||
Test that set_binning() parses input string, updates binning values,
|
||||
and accesses correct SDK features.
|
||||
"""
|
||||
result = camera.set_binning("2x2")
|
||||
assert result is True
|
||||
assert camera.x_binning == 2
|
||||
assert camera.y_binning == 2
|
||||
|
||||
# Check that the SDK was asked for the correct feature names at least once
|
||||
camera.feature_control.get_int_feature.assert_any_call("BinningHorizontal")
|
||||
camera.feature_control.get_int_feature.assert_any_call("BinningVertical")
|
||||
|
||||
|
||||
def test_set_invalid_ROI(camera):
|
||||
"""
|
||||
Test that set_ROI() returns False and logs a warning when given invalid dimensions.
|
||||
"""
|
||||
# Set invalid ROI parameters
|
||||
roi_width = 9000
|
||||
roi_height = 2048
|
||||
center_x = 1000
|
||||
center_y = 1000
|
||||
|
||||
logger = logging.getLogger("model")
|
||||
logger.propagate = False # prevent sending logs to root CLI handler
|
||||
|
||||
stream = io.StringIO()
|
||||
handler = logging.StreamHandler(stream)
|
||||
logger.addHandler(handler)
|
||||
|
||||
camera.stop_acquisition()
|
||||
handler.flush()
|
||||
result = camera.set_ROI(
|
||||
roi_width=roi_width, roi_height=roi_height, center_x=center_x, center_y=center_y
|
||||
)
|
||||
assert result is False
|
||||
assert f"Invalid ROI dimensions: {roi_width}x{roi_height}" in stream.getvalue()
|
||||
logger.removeHandler(handler)
|
||||
|
||||
|
||||
def test_snap_image_returns_numpy_array(camera):
|
||||
"""
|
||||
Test that snap_image() calls the SDK and returns a NumPy array.
|
||||
|
||||
The camera fixture uses the mock_daheng_sdk fixture to simulate:
|
||||
- A data stream whose snap_image() returns a fake image object
|
||||
- An image object whose get_numpy_array() returns np.ndarray representing a fake image (zeros)
|
||||
|
||||
"""
|
||||
result = camera.snap_image()
|
||||
expected = np.zeros((2048, 2048), dtype=np.uint16)
|
||||
assert_array_equal(result, expected)
|
||||
camera.data_stream.snap_image.assert_called_once()
|
||||
|
||||
|
||||
def test_snap_software_triggered_invalid_config(camera):
|
||||
"""
|
||||
Test that snap_software_triggered() raises if trigger config is invalid.
|
||||
|
||||
This mocks the 'TriggerMode' and 'TriggerSource' enum features to return
|
||||
incorrect values ('OFF' and 'HARDWARE'), and verifies that the method
|
||||
raises a UserVisibleException with a helpful message.
|
||||
"""
|
||||
# Override trigger mode/source with bad values
|
||||
mock_enum_feature = MagicMock()
|
||||
mock_enum_feature.get_current_entry.return_value.get_symbolic.side_effect = [
|
||||
"OFF",
|
||||
"HARDWARE",
|
||||
]
|
||||
camera.feature_control.get_enum_feature.return_value = mock_enum_feature
|
||||
|
||||
with pytest.raises(
|
||||
UserVisibleException, match="TriggerMode='ON' and TriggerSource='SOFTWARE'"
|
||||
):
|
||||
camera.snap_software_triggered()
|
||||
|
||||
|
||||
def test_send_software_trigger(camera):
|
||||
"""
|
||||
Test that send_software_trigger() calls the correct Daheng SDK command.
|
||||
|
||||
Verifies that the camera issues a 'TriggerSoftware' command via the
|
||||
command feature interface and that send_command() is called exactly once.
|
||||
"""
|
||||
camera.send_software_trigger()
|
||||
camera.feature_control.get_command_feature.assert_called_with("TriggerSoftware")
|
||||
camera.feature_control.get_command_feature.return_value.send_command.assert_called_once()
|
||||
|
||||
|
||||
def test_set_trigger_mode(camera):
|
||||
"""
|
||||
Test that set_trigger_mode() calls the correct enum feature and sets it to 'ON'.
|
||||
"""
|
||||
camera.set_trigger_mode("ON")
|
||||
camera.feature_control.get_enum_feature.assert_called_with("TriggerMode")
|
||||
camera.feature_control.get_enum_feature.return_value.set.assert_called_with("ON")
|
||||
|
||||
|
||||
def test_set_trigger_source(camera):
|
||||
"""
|
||||
Test that set_trigger_source() selects the correct SDK enum feature and sets it to 'LINE1'.
|
||||
|
||||
'LINE1' refers to a physical input pin used for hardware triggering,
|
||||
typically driven by a DAQ, microcontroller, or timing controller.
|
||||
"""
|
||||
camera.set_trigger_source("LINE1")
|
||||
camera.feature_control.get_enum_feature.assert_called_with("TriggerSource")
|
||||
camera.feature_control.get_enum_feature.return_value.set.assert_called_with("LINE1")
|
||||
|
||||
|
||||
def test_initialize_and_start_acquisition(camera):
|
||||
"""
|
||||
Test that initialize_image_series and start_acquisition correctly
|
||||
update internal state and interact with the SDK.
|
||||
"""
|
||||
# Create a fake image buffer with shape matching camera resolution
|
||||
fake_buffer = [MagicMock(name=f"Frame{i}") for i in range(5)]
|
||||
number_of_frames = 5
|
||||
|
||||
# Initialize acquisition
|
||||
camera.initialize_image_series(
|
||||
data_buffer=fake_buffer, number_of_frames=number_of_frames
|
||||
)
|
||||
|
||||
# Assert acquisition is marked as started
|
||||
assert camera.is_acquiring is True
|
||||
assert camera._number_of_frames == number_of_frames
|
||||
assert camera._frames_received == 0
|
||||
assert camera._data_buffer == fake_buffer
|
||||
|
||||
# Start the acquisition and verify SDK interaction
|
||||
camera.data_stream.start_stream.assert_called_once()
|
||||
camera.feature_control.get_command_feature.assert_called_with("AcquisitionStart")
|
||||
camera.feature_control.get_command_feature.return_value.send_command.assert_called_once()
|
||||
|
||||
|
||||
def test_initialize_start_and_receive_image(camera):
|
||||
"""
|
||||
Test full acquisition flow:
|
||||
- initialize_image_series()
|
||||
- start_acquisition()
|
||||
- get_new_frame() to simulate image reception
|
||||
|
||||
Verifies that the SDK methods are called, internal state is updated,
|
||||
and image data is written to the circular buffer.
|
||||
"""
|
||||
fake_buffer = [MagicMock(name=f"Frame{i}") for i in range(3)]
|
||||
number_of_frames = 3
|
||||
|
||||
camera.initialize_image_series(
|
||||
data_buffer=fake_buffer, number_of_frames=number_of_frames
|
||||
)
|
||||
|
||||
# Simulate receiving frames
|
||||
for i in range(3):
|
||||
frame_indices = camera.get_new_frame()
|
||||
assert frame_indices == [i]
|
||||
fake_buffer[i].__setitem__.assert_called() # Simulates [:, :] = image_data
|
||||
|
||||
# Circular buffer check
|
||||
wraparound = camera.get_new_frame()
|
||||
assert wraparound == [0]
|
||||
|
||||
|
||||
def test_stop_acquisition(camera):
|
||||
"""
|
||||
Test that stop_acquisition() stops both the command and data stream,
|
||||
clears acquisition state, and accesses the correct command feature.
|
||||
"""
|
||||
# Pretend acquisition is running
|
||||
camera.is_acquiring = True
|
||||
|
||||
# Run method
|
||||
camera.stop_acquisition()
|
||||
|
||||
# Ensure the correct SDK command was accessed and triggered
|
||||
camera.feature_control.get_command_feature.assert_called_with("AcquisitionStop")
|
||||
camera.feature_control.get_command_feature.return_value.send_command.assert_called_once()
|
||||
|
||||
# Ensure the data stream was stopped
|
||||
camera.data_stream.stop_stream.assert_called_once()
|
||||
|
||||
# Verify internal state was updated
|
||||
assert camera.is_acquiring is False
|
||||
|
||||
|
||||
def test_stop_acquisition_when_disconnected(camera):
|
||||
"""
|
||||
Test that stop_acquisition() logs a warning and does not raise
|
||||
when called on a disconnected camera.
|
||||
"""
|
||||
camera.is_connected = False
|
||||
|
||||
logger = logging.getLogger("model")
|
||||
logger.propagate = False # prevent sending logs to root CLI handler
|
||||
|
||||
stream = io.StringIO()
|
||||
handler = logging.StreamHandler(stream)
|
||||
logger.addHandler(handler)
|
||||
|
||||
camera.stop_acquisition()
|
||||
handler.flush()
|
||||
|
||||
assert "not connected" in stream.getvalue()
|
||||
|
||||
logger.removeHandler(handler)
|
||||
|
||||
|
||||
def test_set_sensor_mode_logs(camera):
|
||||
"""
|
||||
Test that set_sensor_mode() logs a warning for unsupported modes.
|
||||
|
||||
If an invalid mode is specified, the camera will be set to Normal
|
||||
mode (using global shutter).
|
||||
"""
|
||||
camera.set_sensor_mode("InvalidModeName")
|
||||
camera.device.SensorShutterMode.set.assert_called_with(0)
|
||||
assert camera._scan_mode == 0
|
||||
|
||||
|
||||
def test_snap_software_triggered_success(camera):
|
||||
"""
|
||||
Test that snap_software_triggered() works when trigger config is correct.
|
||||
|
||||
Mocks TriggerMode='ON' and TriggerSource='SOFTWARE', verifies that the
|
||||
method sends a software trigger, captures an image, and returns the result.
|
||||
|
||||
Uses side_effect to return two enum values from a shared enum feature mock.
|
||||
"""
|
||||
# Patch enum feature to simulate correct trigger mode and source
|
||||
mock_enum_feature = MagicMock()
|
||||
|
||||
# First call to get_symbolic() returns 'ON', second returns 'SOFTWARE'
|
||||
mock_enum_feature.get_current_entry.return_value.get_symbolic.side_effect = [
|
||||
"ON",
|
||||
"SOFTWARE",
|
||||
]
|
||||
camera.feature_control.get_enum_feature.return_value = mock_enum_feature
|
||||
|
||||
# Snap image - behind the scenes, this calls data_stream.snap_image() which
|
||||
# is mocked during setup to return a fake image whose get_numpy_array() method
|
||||
# returns np.ndarray representing a fake image.
|
||||
result = camera.snap_software_triggered()
|
||||
|
||||
expected = np.zeros((2048, 2048), dtype=np.uint16)
|
||||
assert_array_equal(result, expected)
|
||||
|
||||
# Ensure the correct trigger command was issued via SDK
|
||||
camera.feature_control.get_command_feature.return_value.send_command.assert_called_with()
|
||||
|
||||
|
||||
def test_get_new_frame(camera):
|
||||
"""
|
||||
Test that get_new_frame() returns correct buffer index in sequence,
|
||||
and wraps around when the number of received frames exceeds the buffer length.
|
||||
|
||||
This simulates a circular buffer behavior across multiple frames.
|
||||
"""
|
||||
number_of_images = 3
|
||||
buffer = [MagicMock() for _ in range(number_of_images)]
|
||||
|
||||
# Initialize image acquisition
|
||||
camera.initialize_image_series(
|
||||
data_buffer=buffer, number_of_frames=number_of_images
|
||||
)
|
||||
camera._frames_received = 0
|
||||
|
||||
# First full loop through buffer
|
||||
for i in range(number_of_images):
|
||||
result = camera.get_new_frame()
|
||||
assert result == [i]
|
||||
|
||||
# Wraparound: next result should start from 0 again
|
||||
result = camera.get_new_frame()
|
||||
assert result == [0]
|
||||
|
||||
result = camera.get_new_frame()
|
||||
assert result == [1]
|
||||
|
||||
|
||||
def test_close_image_series(camera):
|
||||
"""
|
||||
Test that close_image_series() stops acquisition and clears buffer state.
|
||||
|
||||
This ensures the SDK stream is stopped and internal flags like
|
||||
is_acquiring and _data_buffer are reset properly.
|
||||
|
||||
The data_stream is mocked in the camera fixture (via mock_daheng_sdk).
|
||||
"""
|
||||
camera.is_acquiring = True
|
||||
camera._data_buffer = [MagicMock(), MagicMock()] # Simulate buffered frames
|
||||
|
||||
camera.close_image_series()
|
||||
|
||||
# Acquisition state should be cleared
|
||||
assert camera.is_acquiring is False
|
||||
assert camera._data_buffer == None
|
||||
|
||||
# SDK stream should be stopped
|
||||
camera.data_stream.stop_stream.assert_called_once()
|
||||
379
test/model/devices/camera/test_hamamatsu.py
Normal file
379
test/model/devices/camera/test_hamamatsu.py
Normal file
@@ -0,0 +1,379 @@
|
||||
# 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
|
||||
Reference in New Issue
Block a user