Skip to content

Commit

Permalink
adding demo auto-calibration for testing
Browse files Browse the repository at this point in the history
  • Loading branch information
edyoshikun committed Feb 7, 2024
1 parent 0adf31f commit 312b722
Show file tree
Hide file tree
Showing 5 changed files with 211 additions and 65 deletions.
45 changes: 33 additions & 12 deletions copylot/assemblies/photom/demo/demo_photom_assembly.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,20 @@
from copylot.assemblies.photom.utils import affine_transform
from copylot.hardware.mirrors.optotune.mirror import OptoMirror
from copylot.assemblies.photom.photom_mock_devices import MockLaser, MockMirror
from copylot.hardware.cameras.flir.flir_camera import FlirCamera
import time

# %%
config_file = './photom_VIS_config.yml'
# Mock imports for the mirror and the lasers
laser = MockLaser('Mock Laser', power=0)
mirror = OptoMirror(com_port='COM8')


camera_array = FlirCamera()
print(camera_array.list_available_cameras())
camera = camera_array.open(index=0)

# %%
# Test the moving of the mirrors
mirror.position = (0.009, 0.0090)
Expand All @@ -30,27 +37,41 @@
affine_matrix_path=[
r'C:\Users\ZebraPhysics\Documents\GitHub\coPylot\copylot\assemblies\photom\demo\test_tmp.yml'
],
camera=[camera],
)
photom_device.set_position(
mirror_index=0, position=[camera_sensor_width // 2, camera_sensor_height // 2]
)
curr_pos = photom_device.get_position(mirror_index=0)
print(curr_pos)

# %%
mirror_roi = [
[0.004, 0.004],
[0.006, 0.006],
] # Top-left and Bottom-right corners of the mirror ROI
photom_device.camera[0]
photom_device.calibrate_w_camera(
mirror_index=0,
camera_index=0,
rectangle_boundaries=mirror_roi,
config_file='./affine_T.yml',
save_calib_stack_path='./calib_stack',
)

# %%
# TODO: Test the calibration without GUI
import time
# # TODO: Test the calibration without GUI
# import time

start_time = time.time()
photom_device._calibrating = True
while time.time() - start_time < 5:
# Your code here
elapsed_time = time.time() - start_time
print(f'starttime: {start_time} elapsed_time: {elapsed_time}')
photom_device.calibrate(
mirror_index=0, rectangle_size_xy=[0.002, 0.002], center=[0.000, 0.000]
)
photom_device._calibrating = False
# start_time = time.time()
# photom_device._calibrating = True
# while time.time() - start_time < 5:
# # Your code here
# elapsed_time = time.time() - start_time
# print(f'starttime: {start_time} elapsed_time: {elapsed_time}')
# photom_device.calibrate(
# mirror_index=0, rectangle_size_xy=[0.002, 0.002], center=[0.000, 0.000]
# )
# photom_device._calibrating = False

# %%
2 changes: 1 addition & 1 deletion copylot/assemblies/photom/demo/photom_calibration.py
Original file line number Diff line number Diff line change
Expand Up @@ -380,7 +380,7 @@ def done_calibration(self):
self.target_pts = self.photom_window.get_coordinates()

# Mirror calibration size
mirror_calib_size = self.photom_assembly._calibration_rectangle_size_xy
mirror_calib_size = self.photom_assembly._calibration_rectangle_boundaries
origin = np.array(
[[pt.x(), pt.y()] for pt in self.target_pts],
dtype=np.float32,
Expand Down
145 changes: 103 additions & 42 deletions copylot/assemblies/photom/photom.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
from dataclasses import dataclass
from re import T
from copylot.hardware.cameras.abstract_camera import AbstractCamera
from copylot.hardware.mirrors.abstract_mirror import AbstractMirror
from copylot.hardware.lasers.abstract_laser import AbstractLaser
Expand All @@ -8,14 +6,19 @@
from copylot.assemblies.photom.utils.affine_transform import AffineTransform
from copylot.assemblies.photom.utils.scanning_algorithms import (
calculate_rectangle_corners,
generate_grid_points,
)
from pathlib import Path
from copylot import logger
from typing import Tuple
import time
from typing import Optional
import numpy as np
import tifffile
from copylot.assemblies.photom.utils import image_analysis as ia

# TODO: add the logger from copylot
# TODO: add mirror's confidence ROI or update with calibration in OptotuneDocumentation


class PhotomAssembly:
Expand All @@ -32,52 +35,34 @@ def __init__(
self.laser = laser # list of lasers
self.mirror = mirror
self.DAC = dac

self.affine_matrix_path = affine_matrix_path
self._calibrating = False
# TODO: These are hardcoded values. Unsure if they should come from a config file
self._calibration_rectangle_boundaries = None

# TODO: replace these hardcoded values to mirror's scan steps given the magnification
# and the mirrors angles
self._calibration_rectangle_size_xy = [0.004, 0.004]

assert len(self.mirror) == len(affine_matrix_path)
def init_mirrors(self):
assert len(self.mirror) == len(self.affine_matrix_path)

self._calibration_rectangle_boundaries = np.zeros((len(self.mirror), 2, 2))
# Apply AffineTransform to each mirror
for i, tx_path in enumerate(affine_matrix_path):
self.mirror[i].affine_transform_obj = AffineTransform(config_file=tx_path)
for i in range(len(self.mirror)):
self.mirror[i].affine_transform_obj = AffineTransform(
config_file=self.affine_matrix_path[i]
)
self._calibration_rectangle_boundaries[i] = [[None, None], [None, None]]

def calibrate(
self, mirror_index: int, rectangle_size_xy: tuple[int, int], center=[0.0, 0.0]
):
if mirror_index < len(self.mirror):
print("Calibrating mirror...")
rectangle_coords = calculate_rectangle_corners(rectangle_size_xy, center)
# offset the rectangle coords by the center
# iterate over each corner and move the mirror
i = 0
while self._calibrating:
# Logic for calibrating the mirror
self.set_position(mirror_index, rectangle_coords[i])
time.sleep(1)
i += 1
if i == 4:
i = 0
time.sleep(1)
# moving the mirror in a rectangle
else:
raise IndexError("Mirror index out of range.")
# TODO probably will replace the camera with zyx or yx image array input
## Camera Functions
def capture(self, camera_index: int, exposure_time: float) -> list:
pass

## Mirror Functions
def stop_mirror(self, mirror_index: int):
if mirror_index < len(self.mirror):
self._calibrating = False
else:
raise IndexError("Mirror index out of range.")

# TODO probably will replace the camera with zyx or yx image array input
## Camera Functions
def capture(self):
pass

## Mirror Functions
def get_position(self, mirror_index: int) -> list[float]:
if mirror_index < len(self.mirror):
if self.DAC is not None:
Expand Down Expand Up @@ -108,6 +93,88 @@ def set_position(self, mirror_index: int, position: list[float]):
else:
raise IndexError("Mirror index out of range.")

def calibrate(
self, mirror_index: int, rectangle_size_xy: tuple[int, int], center=[0.0, 0.0]
):
if mirror_index < len(self.mirror):
print("Calibrating mirror...")
rectangle_coords = calculate_rectangle_corners(rectangle_size_xy, center)
# offset the rectangle coords by the center
# iterate over each corner and move the mirror
i = 0
while self._calibrating:
# Logic for calibrating the mirror
self.set_position(mirror_index, rectangle_coords[i])
time.sleep(1)
i += 1
if i == 4:
i = 0
time.sleep(1)
# moving the mirror in a rectangle
else:
raise IndexError("Mirror index out of range.")

def calibrate_w_camera(
self,
mirror_index: int,
camera_index: int,
rectangle_boundaries: Tuple[Tuple[int, int], Tuple[int, int]],
config_file: Path = './affine_matrix.yml',
save_calib_stack_path: Path = None,
):
assert self.camera is not None
assert config_file.endswith('.yaml')
self._calibration_rectangle_boundaries[mirror_index] = rectangle_boundaries

x_min, x_max, y_min, y_max = self.camera.image_size_limits
# assuming the minimum is always zero, which is typically that case
assert mirror_index < len(self.mirror)
assert camera_index < len(self.camera)
print("Calibrating mirror_idx <{mirror_idx}> with camera_idx <{camera_idx}>")
# TODO: replace these values with something from the config
# Generate grid of points
grid_points = generate_grid_points(
rectangle_size=rectangle_boundaries, n_points=5
)
# Acquire sequence of images with points
img_sequence = np.zeros((len(grid_points), x_max, y_max))
for idx, coord in enumerate(grid_points):
self.set_position(mirror_index, coord)
img_sequence[idx] = self.camera[camera_index].snap()

# Find the coordinates of peak per image
peak_coords = np.zeros((len(grid_points), 2))
for idx, img in enumerate(img_sequence):
peak_coords[idx] = ia.find_objects_centroids(
img, sigma=5, threshold_rel=0.5, min_distance=10
)

# Find the affine transform
T_affine = self.mirror[mirror_index].affine_transform_obj.get_affine_matrix(
peak_coords, grid_points
)
print(f"Affine matrix: {T_affine}")

# Save the matrix
config_file = Path(config_file)
if not config_file.exists():
config_file.mkdir(parents=True, exist_ok=True)

self.photom_assembly.mirror[
self._current_mirror_idx
].affine_transform_obj.save_matrix(matrix=T_affine, config_file=config_file)

if save_calib_stack_path is not None:
save_calib_stack_path = Path(save_calib_stack_path)
save_calib_stack_path = Path(save_calib_stack_path)
if not save_calib_stack_path.exists():
save_calib_stack_path.mkdir(parents=True, exist_ok=True)
timestamp = time.strftime("%Y%m%d_%H%M%S")
output_path_name = (
save_calib_stack_path / f'calibration_images_{timestamp}.tif'
)
tifffile.imwrite(output_path_name)

## LASER Fuctions
def get_laser_power(self, laser_index: int) -> float:
power = self.laser[laser_index].power
Expand Down Expand Up @@ -159,9 +226,3 @@ def convert_values(
output_values.append(normalized_val * output_span + output_min)

return output_values

def get_sensor_size(self):
if self.camera is not None:
self.camera_sensor_size = self.camera[0].sensor_size
else:
raise NotImplementedError("No camera found.")
65 changes: 57 additions & 8 deletions copylot/assemblies/photom/utils/scanning_algorithms.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
import math

"""
Borrowing from Hirofumi's photom-code
"""
import numpy as np
from typing import ArrayLike


class ScanAlgorithm:
"""
Borrowing from Hirofumi's photom-code
"""

def __init__(self, initial_cord, size, gap, shape, sec_per_cycle):
"""
Generates lists of x & y coordinates for various shapes with different scanning curves.
Expand Down Expand Up @@ -196,7 +197,7 @@ def generate_rect(self):
return cord_x, cord_y


def calculate_rectangle_corners(window_size: tuple[int, int],center=[0.0,0.0]):
def calculate_rectangle_corners(window_size: tuple[int, int], center=[0.0, 0.0]):
# window_size is a tuple of (width, height)

# Calculate the coordinates of the rectangle corners
Expand All @@ -207,6 +208,54 @@ def calculate_rectangle_corners(window_size: tuple[int, int],center=[0.0,0.0]):
x1y0 = [x0y0[0] + window_size[0], x0y0[1]]
x1y1 = [x0y0[0] + window_size[0], x0y0[1] + window_size[1]]
x0y1 = [x0y0[0], x0y0[1] + window_size[1]]



return [x0y0, x1y0, x1y1, x0y1]


def generate_grid_points(
rectangle_size: ArrayLike[int, int], n_points: int = 5
) -> np.ndarray:
"""
Generate grid points for a given rectangle.
Parameters:
- rectangle_size: ArrayLike, containing the start and end coordinates of the rectangle
as [[start_x, start_y], [end_x, end_y]].
- n_points: The number of points per row and column in the grid.
Returns:
- An array of coordinates for the grid points, evenly distributed across the rectangle.
Example:
>>> rectangle_size = [[-1, -1], [1, 1]]
>>> n_points = 3
>>> generate_grid_points(rectangle_size, n_points)
array([[-1. , -1. ],
[ 0. , -1. ],
[ 1. , -1. ],
[-1. , 0. ],
[ 0. , 0. ],
[ 1. , 0. ],
[-1. , 1. ],
[ 0. , 1. ],
[ 1. , 1. ]], dtype=float32)
"""
start_x, start_y = rectangle_size[0]
end_x, end_y = rectangle_size[1]

# Calculate intervals between points in the grid
interval_x = (end_x - start_x) / (n_points - 1)
interval_y = (end_y - start_y) / (n_points - 1)

# Initialize an array to store the coordinates of the grid points
grid_points = np.zeros((n_points * n_points, 2), dtype=np.float32)

# Populate the array with the coordinates of the grid points
for i in range(n_points):
for j in range(n_points):
index = i * n_points + j
x = start_x + j * interval_x
y = start_y + i * interval_y
grid_points[index] = [y, x]

return grid_points
Loading

0 comments on commit 312b722

Please sign in to comment.