from abc import abstractmethod
import numpy as np
from typing import Tuple
from typing import List
from merlin.core import analysistask
[docs]class GlobalAlignment(analysistask.AnalysisTask):
"""
An abstract analysis task that determines the relative position of
different field of views relative to each other in order to construct
a global alignment.
"""
[docs] @abstractmethod
def fov_coordinates_to_global(
self, fov: int, fovCoordinates: Tuple[float, float]) \
-> Tuple[float, float]:
"""Calculates the global coordinates based on the local coordinates
in the specified field of view.
Args:
fov: the fov where the coordinates are measured
fovCoordinates: a tuple containing the x and y coordinates
or z, x, and y coordinates (in pixels) in the specified fov.
Returns:
A tuple containing the global x and y coordinates or
z, x, and y coordinates (in microns)
"""
pass
[docs] @abstractmethod
def global_coordinates_to_fov(
self, fov: int, globalCoordinates: List[Tuple[float, float]]) \
-> List[Tuple[float, float]]:
"""Calculates the fov pixel coordinates for a list of global coordinates
in the specified field of view.
Args:
fov: the fov where the coordinates are measured
globalCoordinates: a list of tuples containing the x and
y coordinates (in pixels) in the specified fov.
Returns:
A list of tuples containing the global x and y coordinates
(in microns)
"""
pass
# TODO this can be updated to take either a list or a single coordinate
# and to convert z position
[docs] @abstractmethod
def get_global_extent(self) -> Tuple[float, float, float, float]:
"""Get the extent of the global coordinate system.
Returns:
a tuple where the first two indexes correspond to the minimum
and x and y extents and the last two indexes correspond to the
maximum x and y extents. All are in units of microns.
"""
pass
[docs]class SimpleGlobalAlignment(GlobalAlignment):
"""A global alignment that uses the theoretical stage positions in
order to determine the relative positions of each field of view.
"""
def __init__(self, dataSet, parameters=None, analysisName=None):
super().__init__(dataSet, parameters, analysisName)
[docs] def get_estimated_memory(self):
return 1
[docs] def get_estimated_time(self):
return 0
def _run_analysis(self):
# This analysis task does not need computation
pass
[docs] def get_dependencies(self):
return []
[docs] def fov_coordinates_to_global(self, fov, fovCoordinates):
fovStart = self.dataSet.get_fov_offset(fov)
micronsPerPixel = self.dataSet.get_microns_per_pixel()
if len(fovCoordinates) == 2:
return (fovStart[0] + fovCoordinates[0]*micronsPerPixel,
fovStart[1] + fovCoordinates[1]*micronsPerPixel)
elif len(fovCoordinates) == 3:
zPositions = self.dataSet.get_z_positions()
return (np.interp(fovCoordinates[0], np.arange(len(zPositions)),
zPositions),
fovStart[0] + fovCoordinates[1]*micronsPerPixel,
fovStart[1] + fovCoordinates[2]*micronsPerPixel)
[docs] def fov_global_extent(self, fov: int) -> List[float]:
"""
Returns the global extent of an fov, output interleaved as
xmin, ymin, xmax, ymax
Args:
fov: the fov of interest
Returns:
a list of four floats, representing the xmin, xmax, ymin, ymax
"""
return [x for y in (self.fov_coordinates_to_global(fov, (0, 0)),
self.fov_coordinates_to_global(fov, (2048, 2048)))
for x in y]
[docs] def global_coordinates_to_fov(self, fov, globalCoordinates):
tform = np.linalg.inv(self.fov_to_global_transform(fov))
def convert_coordinate(coordinateIn):
coords = np.array([coordinateIn[0], coordinateIn[1], 1])
return np.matmul(tform, coords).astype(int)[:2]
pixels = [convert_coordinate(x) for x in globalCoordinates]
return pixels
[docs] def get_global_extent(self):
fovSize = self.dataSet.get_image_dimensions()
fovBounds = [self.fov_coordinates_to_global(x, (0, 0))
for x in self.dataSet.get_fovs()] + \
[self.fov_coordinates_to_global(x, fovSize)
for x in self.dataSet.get_fovs()]
minX = np.min([x[0] for x in fovBounds])
maxX = np.max([x[0] for x in fovBounds])
minY = np.min([x[1] for x in fovBounds])
maxY = np.max([x[1] for x in fovBounds])
return minX, minY, maxX, maxY
[docs]class CorrelationGlobalAlignment(GlobalAlignment):
"""
A global alignment that uses the cross-correlation between
overlapping regions in order to determine the relative positions
of each field of view.
"""
# TODO - implement. I expect rotation might be needed for this alignment
# if the x-y orientation of the camera is not perfectly oriented with
# the microscope stage
def __init__(self, dataSet, parameters=None, analysisName=None):
super().__init__(dataSet, parameters, analysisName)
[docs] def get_estimated_memory(self):
return 1000
[docs] def get_estimated_time(self):
return 60
[docs] def fov_coordinates_to_global(self, fov, fovCoordinates):
raise NotImplementedError
[docs] def get_global_extent(self):
raise NotImplementedError
@staticmethod
def _calculate_overlap_area(x1, y1, x2, y2, width, height):
"""Calculates the overlapping area between two rectangles with
equal dimensions.
"""
dx = min(x1+width, x2+width) - max(x1, x2)
dy = min(y1+height, y2+height) - max(y1, y2)
if dx > 0 and dy > 0:
return dx*dy
else:
return 0
def _get_overlapping_regions(self, fov: int, minArea: int = 2000):
"""Get a list of all the fovs that overlap with the specified fov.
"""
positions = self.dataSet.get_stage_positions()
pixelToMicron = self.dataSet.get_microns_per_pixel()
fovMicrons = [x*pixelToMicron
for x in self.dataSet.get_image_dimensions()]
fovPosition = positions.loc[fov]
overlapAreas = [i for i, p in positions.iterrows()
if self._calculate_overlap_area(
p['X'], p['Y'], fovPosition['X'], fovPosition['Y'],
fovMicrons[0], fovMicrons[1]) > minArea and i != fov]
return overlapAreas
def _run_analysis(self):
fov1 = self.dataSet.get_fiducial_image(0, 0)
fov2 = self.dataSet.get_fiducial_image(0, 1)
return fov1, fov2