Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Back up blob archives and fix saving blob columns #216

Merged
merged 14 commits into from
Sep 29, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion docs/release/release_v1.6.md
Original file line number Diff line number Diff line change
Expand Up @@ -72,11 +72,13 @@
- `ctrl+[n]+click` to add a channel now sets the channel directly to `n` rather than to the `n`th seleted channel (#109)
- Added a slider to choose the fraction of 3D blobs to display (#121)
- Improved blob size slider range and readability (#121)
- Blob columns can be customized, including excluding or reordering columns (#133)
- Blob columns can be customized, including excluding or reordering columns (#133, #216)
- Existing blob archives are backed up before saving (#216)
- Fixed to scale blobs' radii when viewing blobs detections on a downsampled image (#121)
- Fixed getting atlas colors for blobs for ROIs inside the main image (#121)
- Fixed blob segmentation for newer versions of Scikit-image (#91)
- Fixed verifying and resaving blobs
- Fixed loading blobs in the GUI with no blobs in the ROI or channels selected (#216)

##### Colocalization

Expand Down
1 change: 0 additions & 1 deletion magmap/cv/chunking.py
Original file line number Diff line number Diff line change
Expand Up @@ -358,7 +358,6 @@ def get_split_stack_total_shape(sub_rois, overlap=None):
channel_dim = 3
if len(shape_sub_roi) > channel_dim:
final_shape[channel_dim] = shape_sub_roi[channel_dim]
libmag.printv("final_shape: {}".format(final_shape))
return final_shape


Expand Down
93 changes: 59 additions & 34 deletions magmap/cv/detector.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@
from enum import Enum
import math
import pprint
from time import time
from typing import Callable, Dict, List, Optional, Sequence, Tuple, Union

import numpy as np
Expand Down Expand Up @@ -117,7 +116,8 @@ def __init__(
self.basename: Optional[str] = None
self.scaling: np.ndarray = np.ones(3)

# blobs have first 6 columns by default
#: Blob columns loaded from metadata. Defaults to the first 6 columns
#: in :class:`Cols`.
self.cols: Sequence[str] = [c.value for c in self.Cols][:6]

def load_blobs(self, path: str = None) -> "Blobs":
Expand Down Expand Up @@ -201,6 +201,8 @@ def load_blobs(self, path: str = None) -> "Blobs":
def save_archive(self, to_add=None, update=False):
"""Save the blobs Numpy archive file to :attr:`path`.

Backs up any existing file before saving.

Args:
to_add (dict): Dictionary of items to add; defaults to None
to use the current attributes.
Expand All @@ -221,7 +223,11 @@ def save_archive(self, to_add=None, update=False):
Blobs.Keys.ROI_OFFSET.value: self.roi_offset,
Blobs.Keys.ROI_SIZE.value: self.roi_size,
Blobs.Keys.COLOCS.value: self.colocalizations,
Blobs.Keys.COLS.value: self.cols,

# save columns ordered by the col indices
Blobs.Keys.COLS.value: [
k.value for k, v in sorted(
self.col_inds.items(), key=lambda e: e[1])],
}
else:
blobs_arc = to_add
Expand All @@ -232,6 +238,9 @@ def save_archive(self, to_add=None, update=False):
blobs_arc = np_io.read_np_archive(archive)
blobs_arc.update(to_add)

# back up any existing file
libmag.backup_file(self.path)

with open(self.path, "wb") as archive:
# save as uncompressed zip Numpy archive file
np.savez(archive, **blobs_arc)
Expand Down Expand Up @@ -276,6 +285,11 @@ def format_blobs(
extras = np.ones((shape[0], extra_cols)) * -1
blobs = np.concatenate((blobs, extras), axis=1)

# map added col names to indices, assumed to be ordered as in Cols
for i, col in enumerate(cls.Cols):
if i < shape[1]: continue
Blobs.col_inds[cls.Cols(col)] = i

# copy relative to absolute coords
blobs[:, cls._get_abs_inds()] = blobs[:, cls._get_rel_inds()]

Expand All @@ -300,7 +314,13 @@ def get_blob_col(
it is an array of blobs.

"""
if blob.ndim > 1:
is_multi_d = blob.ndim > 1
if col is None:
# no column indicates that this column has not been set up for the
# blob, so return None or an empty ndarray
return np.array([]) if is_multi_d else None

if is_multi_d:
return blob[..., col]
return blob[col]

Expand Down Expand Up @@ -383,7 +403,9 @@ def get_blob_abs_coords(cls, blobs: np.ndarray) -> np.ndarray:
@classmethod
def set_blob_col(
cls, blob: np.ndarray, col: Union[int, Sequence[int]],
val: Union[float, Sequence[float]]
val: Union[float, Sequence[float]],
mask: Union[
np.ndarray, np.lib.index_tricks.IndexExpression] = np.s_[:], **kwargs
) -> np.ndarray:
"""Set the value for the given column of a blob or blobs.

Expand All @@ -392,68 +414,75 @@ def set_blob_col(
col: Column index in ``blob``.
val: New value. If ``blob`` is 2D, can be an array the length
of ``blob``.
mask: Mask for the first axis; defaults to an index expression
for all values. Only used for multidimensional arrays.

Returns:
``blob`` after modifications.

"""
if blob.ndim > 1:
blob[..., col] = val
# set value for col in last axis, applying mask for first axis
blob[mask, ..., col] = val
else:
blob[col] = val
return blob

@classmethod
def set_blob_confirmed(
cls, blob: np.ndarray, val: Union[int, np.ndarray]) -> np.ndarray:
cls, blob: np.ndarray, *args, **kwargs) -> np.ndarray:
"""Set the confirmed flag of a blob or blobs.

Args:
blob: 1D blob array or 2D array of blobs.
val: Confirmed value. If ``blob`` is 2D, can be an array the length
of ``blob``.
args: Positional arguments passed to :meth:`set_blob_col`.
kwargs: Named arguments passed to :meth:`set_blob_col`.

Returns:
``blob`` after modifications.

"""
return cls.set_blob_col(blob, cls.col_inds[cls.Cols.CONFIRMED], val)
return cls.set_blob_col(
blob, cls.col_inds[cls.Cols.CONFIRMED], *args, **kwargs)

@classmethod
def set_blob_truth(
cls, blob: np.ndarray, val: Union[int, np.ndarray]) -> np.ndarray:
cls, blob: np.ndarray, *args, **kwargs) -> np.ndarray:
"""Set the truth flag of a blob or blobs.

Args:
blob: 1D blob array or 2D array of blobs.
val: Truth value. If ``blob`` is 2D, can be an array the length
of ``blob``.
args: Positional arguments passed to :meth:`set_blob_col`.
kwargs: Named arguments passed to :meth:`set_blob_col`.

Returns:
``blob`` after modifications.

"""
return cls.set_blob_col(blob, cls.col_inds[cls.Cols.TRUTH], val)
return cls.set_blob_col(
blob, cls.col_inds[cls.Cols.TRUTH], *args, **kwargs)

@classmethod
def set_blob_channel(
cls, blob: np.ndarray, val: Union[int, np.ndarray]) -> np.ndarray:
cls, blob: np.ndarray, *args, **kwargs) -> np.ndarray:
"""Set the channel of a blob or blobs.

Args:
blob: 1D blob array or 2D array of blobs.
val: Channel value. If ``blob`` is 2D, can be an array the length
of ``blob``.
args: Positional arguments passed to :meth:`set_blob_col`.
kwargs: Named arguments passed to :meth:`set_blob_col`.

Returns:
``blob`` after modifications.

"""
return cls.set_blob_col(blob, cls.col_inds[cls.Cols.CHANNEL], val)
return cls.set_blob_col(
blob, cls.col_inds[cls.Cols.CHANNEL], *args, **kwargs)

@classmethod
def set_blob_abs_coords(
cls, blobs: np.ndarray, coords: Sequence[int]) -> np.ndarray:
cls, blobs: np.ndarray, coords: Sequence[int], *args, **kwargs
) -> np.ndarray:
"""Set blob absolute coordinates.

Args:
Expand All @@ -465,7 +494,7 @@ def set_blob_abs_coords(
Modified ``blobs``.

"""
cls.set_blob_col(blobs, cls._get_abs_inds(), coords)
cls.set_blob_col(blobs, cls._get_abs_inds(), coords, *args, **kwargs)
return blobs

@classmethod
Expand Down Expand Up @@ -749,29 +778,29 @@ def detect_blobs(
Returns:
Array of detected blobs, each given as
``z, row, column, radius, confirmation``.

"""
time_start = time()
shape = roi.shape
multichannel, channels = plot_3d.setup_channels(roi, channel, 3)
isotropic = config.get_roi_profile(channels[0])["isotropic"]
if isotropic is not None:
# interpolate for (near) isotropy during detection, using only the
# interpolate for (near) isotropy during detection, using only the
# first process settings since applies to entire ROI
roi = cv_nd.make_isotropic(roi, isotropic)

blobs_all = []
for chl in channels:
roi_detect = roi[..., chl] if multichannel else roi
settings = config.get_roi_profile(chl)
# scaling as a factor in pixel/um, where scaling of 1um/pixel
# scaling as a factor in pixel/um, where scaling of 1um/pixel
# corresponds to factor of 1, and 0.25um/pixel corresponds to
# 1 / 0.25 = 4 pixels/um; currently simplified to be based on
# 1 / 0.25 = 4 pixels/um; currently simplified to be based on
# x scaling alone
scale = calc_scaling_factor()
scaling_factor = scale[2]

# find blobs; sigma factors can be sequences by axes for anisotropic
# detection in skimage >= 0.15, or images can be interpolated to
# find blobs; sigma factors can be sequences by axes for anisotropic
# detection in skimage >= 0.15, or images can be interpolated to
# isotropy using the "isotropic" MagellanMapper setting
min_sigma = settings["min_sigma_factor"] * scaling_factor
max_sigma = settings["max_sigma_factor"] * scaling_factor
Expand All @@ -781,13 +810,8 @@ def detect_blobs(
blobs_log = blob_log(
roi_detect, min_sigma=min_sigma, max_sigma=max_sigma,
num_sigma=num_sigma, threshold=threshold, overlap=overlap)
if config.verbose:
print("detecting blobs with min size {}, max {}, num std {}, "
"threshold {}, overlap {}"
.format(min_sigma, max_sigma, num_sigma, threshold, overlap))
print("time for 3D blob detection: {}".format(time() - time_start))
if blobs_log.size < 1:
libmag.printv("no blobs detected")
_logger.debug("No blobs detected")
continue
blobs_log[:, 3] = blobs_log[:, 3] * math.sqrt(3)
blobs = Blobs.format_blobs(blobs_log, chl)
Expand All @@ -797,12 +821,13 @@ def detect_blobs(
return None
blobs_all = np.vstack(blobs_all)
if isotropic is not None:
# if detected on isotropic ROI, need to reposition blob coordinates
# if detected on isotropic ROI, need to reposition blob coordinates
# for original, non-isotropic ROI
isotropic_factor = cv_nd.calc_isotropic_factor(isotropic)
blobs_all = Blobs.multiply_blob_rel_coords(
blobs_all, 1 / isotropic_factor)
blobs_all = Blobs.multiply_blob_abs_coords(blobs_all, 1 / isotropic_factor)
blobs_all = Blobs.multiply_blob_abs_coords(
blobs_all, 1 / isotropic_factor)

if exclude_border is not None:
# exclude blobs from the border in x,y,z
Expand Down
11 changes: 4 additions & 7 deletions magmap/cv/stack_detect.py
Original file line number Diff line number Diff line change
Expand Up @@ -110,9 +110,8 @@ class attributes, such as for spawned multiprocessing.
cli.process_cli_args()
_, orig_info = importer.make_filenames(img_path)
importer.load_metadata(orig_info)
print("detecting blobs in sub-ROI at {} of {}, offset {}, shape {}..."
.format(coord, last_coord, tuple(offset.astype(int)),
sub_roi.shape))
_logger.info(
"Detecting blobs in sub-ROI at %s of %s", coord, last_coord)

if denoise_max_shape is not None:
# further split sub-ROI for preprocessing locally
Expand All @@ -124,10 +123,8 @@ class attributes, such as for spawned multiprocessing.
denoise_coord = (z, y, x)
denoise_roi = sub_roi[denoise_roi_slices[denoise_coord]]
_logger.debug(
f"preprocessing sub-sub-ROI {denoise_coord} of "
f"{np.subtract(denoise_roi_slices.shape, 1)} "
f"(shape {denoise_roi.shape} within sub-ROI shape "
f"{sub_roi.shape})")
f"Preprocessing sub-sub-ROI {denoise_coord} of "
f"{np.subtract(denoise_roi_slices.shape, 1)}")
denoise_roi = plot_3d.saturate_roi(
denoise_roi, channel=channel)
denoise_roi = plot_3d.denoise_roi(
Expand Down
10 changes: 5 additions & 5 deletions magmap/gui/roi_editor.py
Original file line number Diff line number Diff line change
Expand Up @@ -271,8 +271,6 @@ class ROIEditor(plot_support.ImageSyncMixin):
ZLevels (:obj:`Enum`): Enum denoting the possible positions of the
z-plane shown in the overview plots.
fig (:obj:`figure.figure`): Matplotlib figure.
image5d (:obj:`np.ndarray`): Main image array in ``t,z,y,x[,c]``
format; defaults to None.
labels_img (:obj:`np.ndarray`): Atlas labels image in ``z,y,x`` format;
defaults to None.
img_region (:obj:`np.ndarray`): 3D boolean or binary array with the
Expand Down Expand Up @@ -335,7 +333,9 @@ def __init__(self, img5d, labels_img=None, img_region=None,
"""Initialize the editor."""
super().__init__(img5d)
print("Initiating ROI Editor")
self.image5d = self.img5d.img if self.img5d else None
#: Image instance to display.
self.image5d: Optional[
"np_io.Image5d"] = None if self.img5d is None else self.img5d.img
self.labels_img: Optional[np.ndarray] = labels_img
if img_region is not None:
# invert region selection image to opacify areas outside of the
Expand Down Expand Up @@ -1011,6 +1011,7 @@ def plot_roi(self, roi, segments, channel, show=True, title=""):
and only used if :attr:``config.savefig`` is set to a file
extension.
"""
# TODO: replace with modularized version from plot_2d_stack?
fig = plt.figure()
# fig.suptitle(title)
# total number of z-planes
Expand All @@ -1024,11 +1025,10 @@ def plot_roi(self, roi, segments, channel, show=True, title=""):
zoom_plot_cols += 1
zoom_plot_rows = math.ceil(z_planes / zoom_plot_cols)
col_remainder = z_planes % zoom_plot_cols
roi_size = roi.shape[::-1]
roi_size = roi.shape[:3][::-1]
zoom_offset = [0, 0, 0]
gs = gridspec.GridSpec(
zoom_plot_rows, zoom_plot_cols, wspace=0.1, hspace=0.1)
image5d = importer.roi_to_image5d(roi)

# plot the fully zoomed plots
for i in range(zoom_plot_rows):
Expand Down
11 changes: 7 additions & 4 deletions magmap/gui/visualizer.py
Original file line number Diff line number Diff line change
Expand Up @@ -2481,8 +2481,11 @@ def detect_blobs(self, segs=None, blob_matches=None):
self.blobs.blob_matches = blob_matches
cli.update_profiles()

# get selected channels for blob detection
# get selected channels for blob detection; must have at least 1
chls = sorted([int(n) for n in self._segs_chls])
if len(chls) == 0:
self.segs_feedback = "Please select a channel for blob detection"
return

# process ROI in prep for showing filtered 2D view and segmenting
self._segs_visible = [BlobsVisibilityOptions.VISIBLE.value]
Expand All @@ -2501,7 +2504,7 @@ def detect_blobs(self, segs=None, blob_matches=None):
# are relative to offset
colocs = None
if config.blobs is None or config.blobs.blobs is None:
# on-the-fly blob detection, which includes border but not
# on-the-fly blob detection, which includes border but not
# padding region; already in relative coordinates
roi = self.roi
if config.roi_profile["thresholding"]:
Expand All @@ -2521,7 +2524,7 @@ def detect_blobs(self, segs=None, blob_matches=None):
# TODO: include all channel combos
self.blobs.blob_matches = matches[tuple(matches.keys())[0]]
else:
# get all previously processed blobs in ROI plus additional
# get all previously processed blobs in ROI plus additional
# padding region to show surrounding blobs
# TODO: set segs_all to None rather than empty list if no blobs?
print("Selecting blobs in ROI from loaded blobs")
Expand Down Expand Up @@ -2581,7 +2584,7 @@ def detect_blobs(self, segs=None, blob_matches=None):
segs = detector.Blobs.blobs_in_channel(segs, chls)
segs_all = np.concatenate((segs, segs_outside), axis=0)

if segs_all is not None:
if segs_all is not None and len(segs_all) > 0:
# set confirmation flag to user-selected label for any
# un-annotated blob
confirmed = detector.Blobs.get_blob_confirmed(segs_all)
Expand Down
5 changes: 3 additions & 2 deletions magmap/io/export_rois.py
Original file line number Diff line number Diff line change
Expand Up @@ -161,9 +161,10 @@ def export_rois(
print("sitk img:\n{}".format(img3d_back[0]))
'''
sitk.WriteImage(img3d_sitk, path_img_nifti, False)
roi_ed = roi_editor.ROIEditor(img5d.img)
roi_ed = roi_editor.ROIEditor(img5d)
print("shape", img3d.shape)
roi_ed.plot_roi(
img3d, blobs, channel, show=False,
img3d, blobs, [channel], show=False,
title=os.path.splitext(path_img)[0])
libmag.show_full_arrays()

Expand Down
Loading