diff --git a/situr/image/situ_image.py b/situr/image/situ_image.py new file mode 100644 index 0000000..75c41d5 --- /dev/null +++ b/situr/image/situ_image.py @@ -0,0 +1,132 @@ +def extend_dim(array): + ones = np.ones((array.shape[0], 1)) + return np.append(array, ones, axis=1) + +def remove_dim(array): + return array[:,:-1] + + +class SituImage: + """ + A class to representing one situ image with different focus levels. + + ... + + Attributes + ---------- + data : numpy.array + the image data containing all the channels of shape (channels, focus_levels, image_size_y, image_size_x) + files: (list(list(str))) + A list of lists. Each inner list corresponds to one focus level. Its contents correspons to a file for each channel. + nucleaus_channel : int + tells which channel is used for showing where the cell nucleuses are. + """ + + def __init__(self, file_list, nucleaus_channel=4): + self.files = file_list + self.data = None + self.nucleaus_channel = nucleaus_channel + + def get_data(self): + if self.data is None: + self._load_image() + return self.data + + def get_channel(self, channel): + ''' + Loads and returns the specified channel for all focus_levels. + + Returns: + numpy.array: The loaded image of shape (focus_level, width, height) + ''' + return self.get_data()[channel,:,:,:] + + def _load_image(self): + ''' + Loads the channels of an image from seperate files and returns them as numpy array. + + Parameters: + channel (int): + The channel that should be used + Returns: + numpy.array: The loaded image of shape (channels, focus_level, width, height) + ''' + image_list = [] + for focus_level_list in self.files: + channels = [] + for file in focus_level_list: + channels.append(np.array(Image.open(file))) + image_list.append(channels) + self.data = np.array(image_list) + + def unload_image(self): + ''' + Unloads the image data to free up memory + ''' + self.data = None + + def show_channel(self, channel, focus_level=0): + ''' + Prints and returns the specified channel and focus_level of the image. + + Parameters: + channel (int): + The channel that should be used when printing + focus_level (int) default: 0: + The focus level that should be used + Returns: + image: The image of the specified focus level and channel + ''' + img = Image.fromarray(self.get_data()[0,0,:,:]) + img.show() + return img + + def get_channel_peaks(self, channel, focus_level=0, min_sigma=0.75, max_sigma=3, threshold=0.1): + ''' + Returns the coordinates of peaks (local maxima) in the specified channel and focus_level. + This method uses skimage blob_dog, therefore using difference of gaussian. + + Parameters: + channel (int): + The channel that should be used when printing + focus_level (int) default: 0: + The focus level that should be used + min_sigma (float) default: 0.75: + The minimum standard deviation for Gaussian kernel. Keep this low to detect smaller blobs. The standard deviations of the Gaussian filter are given for each axis as a sequence, or as a single number, in which case it is equal for all axes. + max_sigma (float) default: 3: + The maximum standard deviation for Gaussian kernel. Keep this high to detect larger blobs. The standard deviations of the Gaussian filter are given for each axis as a sequence, or as a single number, in which case it is equal for all axes. + threshold (float) default: 0.1: + The absolute lower bound for scale space maxima. Local maxima smaller than thresh are ignored. Reduce this to detect blobs with less intensities. + Returns: + np.array: The peaks found by this method as np.array of shape (n, 2) + ''' + img = img_as_float(self.get_data()[channel, focus_level, :, :]) + peaks = blob_dog(img, min_sigma=min_sigma, max_sigma=max_sigma, threshold=threshold) + return peaks[:, 0:2] + + def show_channel_peaks(self, channel, focus_level=0, min_sigma=0.75, max_sigma=3, threshold=0.1): + ''' + Returns and shows the found. Uses get_channel_peaks internally. + + Parameters: + channel (int): + The channel that should be used when printing + focus_level (int) default: 0: + The focus level that should be used + min_sigma (float) default: 0.75: + The minimum standard deviation for Gaussian kernel. Keep this low to detect smaller blobs. The standard deviations of the Gaussian filter are given for each axis as a sequence, or as a single number, in which case it is equal for all axes. + max_sigma (float) default: 3: + The maximum standard deviation for Gaussian kernel. Keep this high to detect larger blobs. The standard deviations of the Gaussian filter are given for each axis as a sequence, or as a single number, in which case it is equal for all axes. + threshold (float) default: 0.1: + The absolute lower bound for scale space maxima. Local maxima smaller than thresh are ignored. Reduce this to detect blobs with less intensities. + Returns: + image: The image of the specified focus level and channel with encircled peaks. + ''' + peaks = self.get_channel_peaks(channel, focus_level, min_sigma, max_sigma, threshold) + + img = Image.fromarray(self.get_data()[channel, focus_level, :, :]) + draw = ImageDraw.Draw(img) + for x, y in zip(peaks[:,0], peaks[:,1]): + draw.ellipse((x - 5, y - 5, x + 5, y + 5), outline ='white', width = 3) + img.show() + return img \ No newline at end of file diff --git a/situr/image/tile.py b/situr/image/tile.py new file mode 100644 index 0000000..dde8495 --- /dev/null +++ b/situr/image/tile.py @@ -0,0 +1,10 @@ +class Tile: + ''' + * Rounds 5 + * Channels 4+1 - spot colours + nuclei + * Z 1 to 30 - focus level + * Y 2048 + * X 2048 + ''' + def __init__(self): + pass \ No newline at end of file diff --git a/situr/image/tile_collection.py b/situr/image/tile_collection.py new file mode 100644 index 0000000..9e1c2ac --- /dev/null +++ b/situr/image/tile_collection.py @@ -0,0 +1,15 @@ +class TileCollection: + ''' + The idea here is about a class that knows where to find all the images and then being able to load the tile that is wanted on demand. + Therefore it also needs to know about the metadata. + We need to keep track off transformatons for each channel and tile. + + * Tiles ~100 + * Rounds 5 + * Channels 4+1 - spot colours + nuclei + * Z 1 to 30 - focus level + * Y 2048 + * X 2048 + ''' + def __init__(self): + pass \ No newline at end of file diff --git a/situr/registration/channel_registration/registration.py b/situr/registration/channel_registration/registration.py new file mode 100644 index 0000000..5d63dbe --- /dev/null +++ b/situr/registration/channel_registration/registration.py @@ -0,0 +1,23 @@ +class ChannelRegistration: + __metaclass__ = abc.ABCMeta + + def do_registration(self, situ_img , reference_channel=0): + # For each channel (except nucleus) compute transform compared to reference_channel + # Add Channel transformation to Channel + pass + @abc.abstractmethod + def register_single_channel(self, peaks_data, reference_peaks): + """Performs the channel registration on an image. Expects the peaks in each image as input.""" + raise NotImplementedError(self.__class__.__name__ + '.register_single_channel') + +class FilterregChannelRegistration(ChannelRegistration): + def register_single_channel(self, data_peaks, reference_peaks): + source = o3.geometry.PointCloud() + source.points = o3.utility.Vector3dVector(extend_dim(data_peaks)) + target = o3.geometry.PointCloud() + target.points = o3.utility.Vector3dVector(extend_dim(reference_peaks)) + + registration_method=filterreg.registration_filterreg + tf_param, _, _ = filterreg.registration_filterreg(source, target) + + return ScaleRotateTranslateChannelTransform(transform_matrix=tf_param.rot[0:2, 0:2], scale=tf_param.scale, offset=tf_param.t[0:2]) diff --git a/situr/transformation/channel_transformation.py b/situr/transformation/channel_transformation.py new file mode 100644 index 0000000..c05acce --- /dev/null +++ b/situr/transformation/channel_transformation.py @@ -0,0 +1,28 @@ +class ChannelTransform: + __metaclass__ = abc.ABCMeta + + @abc.abstractmethod + def apply_transformation(self, situ_img , channel): + """Performs a transformation on one channel, all focus_levels are transformed the same way""" + raise NotImplementedError(self.__class__.__name__ + '.apply_transformation') + + +class ScaleRotateTranslateChannelTransform(ChannelTransform): + def __init__(self, transform_matrix, scale=1, offset=np.array([0, 0])): + # TODO: check + # * transform matrix is 2x2 + # * offset is array (2,) + self.transform_matrix = transform_matrix + self.offset = offset + self.scale = scale + + def apply_tranformation(self, situ_img , channel): + channel_img = situ_img.get_channel(channel) + focus_levels = channel_img.shape[0] + + for focus_level in range(focus_levels): + img = channel_img [focus_level, :, :] + + img [:, :] = scipy.ndimage.affine_transform(img, self.transform_matrix) + img [:, :] = scipy.ndimage.zoom(img, self.scale) + img [:, :] = scipy.ndimage.shift(img, self.offset) \ No newline at end of file