Skip to content
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
701 changes: 223 additions & 478 deletions plugins/tileacq.py

Large diffs are not rendered by default.

127 changes: 79 additions & 48 deletions src/odemis/acq/stitching/_tiledacq.py
Original file line number Diff line number Diff line change
Expand Up @@ -120,8 +120,8 @@ def __init__(self, streams, stage, region, overlap, settings_obs=None, log_path=
:param zlevels: (list(float) or None) focus z positions required zstack acquisition.
Currently, can only be used if focusing_method == MAX_INTENSITY_PROJECTION.
If focus_points is defined, zlevels is adjusted relative to the focus_points.
:param registrar: (REGISTER_*) type of registration method
:param weaver: (WEAVER_*) type of weaving method
:param registrar: (REGISTER_* or None) type of registration method. If registrar and weaver are None, do not stitch.
Comment thread
tepals marked this conversation as resolved.
:param weaver: (WEAVER_* or None) type of weaving method. If registrar and weaver are None, do not stitch.
Comment thread
coderabbitai[bot] marked this conversation as resolved.
:param focusing_method: (FocusingMethod) Defines when will the autofocuser be run.
The autofocuser uses the first stream with a .focuser.
If MAX_INTENSITY_PROJECTION is used, zlevels must be provided too.
Expand Down Expand Up @@ -161,7 +161,7 @@ def __init__(self, streams, stage, region, overlap, settings_obs=None, log_path=
# stream.horizontalFoV.value = stream.horizontalFoV.clip(max(self._area_size))

# Get the smallest field of view
self._sfov = self._guessSmallestFov(streams)
self._sfov = self.guessSmallestFov(streams)
logging.debug("Smallest FoV: %s", self._sfov)

self._starting_pos, self._tile_indices = self._get_tile_coverage()
Expand Down Expand Up @@ -279,7 +279,8 @@ def _convert_region_to_polygon(
raise ValueError("The provided region does not form a valid polygon.")
return polygon

def _getFov(self, sd):
@staticmethod
def getFov(sd):
"""
sd (Stream or DataArray): If it's a stream, it must be a live stream,
and the FoV will be estimated based on the settings.
Expand All @@ -295,13 +296,14 @@ def _getFov(self, sd):
else:
raise TypeError("Unsupported object")

def _guessSmallestFov(self, ss):
@classmethod
def guessSmallestFov(cls, ss):
"""
Return (float, float): smallest width and smallest height of all the FoV
Note: they are not necessarily from the same FoV.
raise ValueError: If no stream selected
"""
fovs = [self._getFov(s) for s in ss]
fovs = [cls.getFov(s) for s in ss]
if not fovs:
raise ValueError("No stream so no FoV, so no minimum one")

Expand Down Expand Up @@ -543,7 +545,7 @@ def _updateFov(self, das, sfov):
sfov: previous estimate for the fov
:returns same fov or updated from the data arrays
"""
afovs = [self._getFov(d) for d in das]
afovs = [self.getFov(d) for d in das]
asfov = (min(f[0] for f in afovs),
min(f[1] for f in afovs))
if not all(util.almost_equal(e, a) for e, a in zip(sfov, asfov)):
Expand Down Expand Up @@ -598,17 +600,20 @@ def estimateMemory(self):
stitching and compares it to the available memory on the computer.
:returns (bool) True if sufficient memory available, (float) estimated memory
"""
# Number of pixels for acquisition
pxs = sum(self._estimateStreamPixels(s) for s in self._streams)
pxs *= self._number_of_tiles

# Memory calculation
mem_est = pxs * self.MEMPP
mem_computer = psutil.virtual_memory().total
logging.debug("Estimating %g GB needed, while %g GB available",
mem_est / 1024 ** 3, mem_computer / 1024 ** 3)
# Assume computer is using 2 GB RAM for odemis and other programs
mem_sufficient = mem_est < mem_computer - (2 * 1024 ** 3)
mem_sufficient = True
mem_est = 0.0
if self._registrar is not None and self._weaver is not None:
# Number of pixels for acquisition
pxs = sum(self._estimateStreamPixels(s) for s in self._streams)
pxs *= self._number_of_tiles

# Memory calculation
mem_est = pxs * self.MEMPP
mem_computer = psutil.virtual_memory().total
logging.debug("Estimating %g GB needed, while %g GB available",
mem_est / 1024 ** 3, mem_computer / 1024 ** 3)
# Assume computer is using 2 GB RAM for odemis and other programs
mem_sufficient = mem_est < mem_computer - (2 * 1024 ** 3)

return mem_sufficient, mem_est

Expand All @@ -632,18 +637,21 @@ def estimateTime(self, remaining=None):
acq_time = acq_time * remaining

# Estimate stitching time based on number of pixels in the overlapping part
max_pxs = 0
for s in self._streams:
for sda in s.raw:
pxs = sda.shape[0] * sda.shape[1]
if pxs > max_pxs:
max_pxs = pxs

stitch_time = (self._number_of_tiles * max_pxs * self._overlap) / STITCH_SPEED
stitch_time = 0
if self._registrar is not None and self._weaver is not None:
max_pxs = 0
for s in self._streams:
for sda in s.raw:
pxs = sda.shape[0] * sda.shape[1]
if pxs > max_pxs:
max_pxs = pxs

stitch_time = (self._number_of_tiles * max_pxs * self._overlap) / STITCH_SPEED

try:
# move_speed is a default speed but not an actual stage speed due to which
# extra time is added based on observed time taken to move stage from one tile position to another
move_time = max(self._guessSmallestFov(self._streams)) * (remaining - 1) / self._move_speed + 0.3 * remaining
move_time = max(self._sfov) * (remaining - 1) / self._move_speed + 0.3 * remaining
# current tile is part of remaining, so no need to move there
except ValueError: # no current streams
move_time = 0.5
Expand Down Expand Up @@ -730,26 +738,48 @@ def _acquireStreamTile(self, i, ix, iy, stream):
except IndexError:
raise IndexError(f"Failure in acquiring tile {ix}x{iy}, stream {stream.name}.")

def _acquireStreamsTile(self, i, ix, iy, streams):
"""
Calls acquire function and blocks until the data is returned.
:return: list(DataArray) acquired das for the current tile streams
"""
# Update the progress bar
self._future.set_progress(end=self.estimateTime(self._number_of_tiles - i) + time.time())
# Acquire data array for passed stream
self._future.running_subf = acqmng.acquire(streams, self._settings_obs)
das, e = self._future.running_subf.result() # blocks until all the acquisitions are finished
if e:
logging.warning(f"Acquisition for tile {ix}x{iy}, streams partially failed: {e}")

if self._future._task_state == CANCELLED:
raise CancelledError()

return das # return all das for the tile
Comment thread
coderabbitai[bot] marked this conversation as resolved.

def _getTileDAs(self, i, ix, iy):
"""
Iterate over each tile stream and construct their data arrays list
:return: list(DataArray) list of each stream DataArray
"""
das = []
for stream in self._streams:
if stream.focuser is not None and len(self._zlevels) > 1:
# Acquire zstack images based on the given zlevels, and compress them into a single da
da = self._acquireStreamCompressedZStack(i, ix, iy, stream)
elif stream.focuser and len(self._zlevels) == 1:
z = self._zlevels[0]
logging.debug(f"Moving focus for tile {ix}x{iy} to {z}.")
stream.focuser.moveAbsSync({'z': z})
# Acquire a single image of the stream
da = self._acquireStreamTile(i, ix, iy, stream)
else:
# Acquire a single image of the stream
da = self._acquireStreamTile(i, ix, iy, stream)
das.append(da)

if len(self._zlevels):
for stream in self._streams:
if stream.focuser is not None and len(self._zlevels) > 1:
# Acquire zstack images based on the given zlevels, and compress them into a single da
da = self._acquireStreamCompressedZStack(i, ix, iy, stream)
elif stream.focuser is not None and len(self._zlevels) == 1:
z = self._zlevels[0]
logging.debug(f"Moving focus for tile {ix}x{iy} to {z}.")
stream.focuser.moveAbsSync({'z': z})
da = self._acquireStreamTile(i, ix, iy, stream)
else:
# Acquire a single image of the stream
da = self._acquireStreamTile(i, ix, iy, stream)
das.append(da)
else:
das = self._acquireStreamsTile(i, ix, iy, self._streams)

Comment thread
nandishjpatel marked this conversation as resolved.
return das
Comment thread
coderabbitai[bot] marked this conversation as resolved.

def _acquireTiles(self):
Expand Down Expand Up @@ -1018,13 +1048,14 @@ def run(self):
# Acquire the needed tiles
da_list = self._acquireTiles()

if not da_list or not da_list[0]:
logging.warning("No stream acquired that can be used for stitching.")
else:
logging.info("Acquisition completed, now stitching...")
# Stitch the acquired tiles
self._future.set_progress(end=self.estimateTime(0) + time.time())
st_data = self._stitchTiles(da_list)
if self._registrar is not None and self._weaver is not None:
if not da_list or not da_list[0]:
logging.warning("No stream acquired that can be used for stitching.")
else:
logging.info("Acquisition completed, now stitching...")
Comment thread
pieleric marked this conversation as resolved.
# Stitch the acquired tiles
self._future.set_progress(end=self.estimateTime(0) + time.time())
st_data = self._stitchTiles(da_list)

if self._future._task_state == CANCELLED:
raise CancelledError()
Expand Down
7 changes: 7 additions & 0 deletions src/odemis/acq/stream/_helper.py
Original file line number Diff line number Diff line change
Expand Up @@ -1889,6 +1889,13 @@ def result_as_da(timeout=None) -> Tuple[List[model.DataArray], Optional[Exceptio

return result_as_da

def guessFoV(self):
"""
Estimate the field-of-view based on the current settings.
:return: (float, float): width, height in meters
"""
# Calculate FoV based on the emitter's shape and pixel size
return tuple(s * p for s, p in zip(self._emitter.shape, self._emitter.pixelSize.value))

class ScannedTCSettingsStream(RepetitionStream):

Expand Down
13 changes: 12 additions & 1 deletion src/odemis/acq/stream/_sync.py
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,8 @@ def __init__(self, name, streams):
should be the e-beam, and will be used to scan). Streams should have
a different detector. The order matters.
The first stream with .repetition will be used to define
the region of acquisition (ROA), with the .roi and .fuzzing VAs.
the region of acquisition (ROA), with the .roi, .rotation, .fuzzing VAs
and .guessFoV will be used to define the field-of-view (FoV).
The first stream with .useScanStage will be used to define a scanning
stage.
The first leech of type AnchorDriftCorrector will be used for drift correction.
Expand Down Expand Up @@ -124,6 +125,7 @@ def __init__(self, name, streams):
logging.debug("Using ROA from %s", s)
self.repetition = s.repetition
self.roi = s.roi
self.guessFoV = s.guessFoV
if model.hasVA(s, "rotation"):
self.rotation = s.rotation
if model.hasVA(s, "fuzzing"):
Expand Down Expand Up @@ -167,6 +169,15 @@ def __init__(self, name, streams):
self._integrationTime = s.integrationTime
self._integrationCounts = s.integrationCounts

# Get focuser if found on any of the substreams
self._focuser = None
Comment thread
nandishjpatel marked this conversation as resolved.
for s in streams:
if hasattr(s, "_focuser") and s._focuser:
if self._focuser and self._focuser != s._focuser:
logging.warning("Multiple different focusers were found.")
break
self._focuser = s._focuser

# Information about the scanning, computed just before running an acquisition
self._pxs = None # (float, float): pixel size in the CCD data (so, independent of fuzzing)
self._scanner_pxs = None # (float, float): pixel size of the scanner (only different from the pixel size if fuzzing)
Expand Down
18 changes: 10 additions & 8 deletions src/odemis/gui/cont/stream.py
Original file line number Diff line number Diff line change
Expand Up @@ -67,13 +67,14 @@ class StreamController(object):
""" Manage a stream and its accompanying stream panel """

def __init__(self, stream_bar, stream, tab_data_model, show_panel=True, view=None,
sb_ctrl=None):
sb_ctrl=None, sp_options=None):
"""
view (MicroscopeView or None): Link stream to a view. If view is None, the stream
will be linked to the focused view. Passing a view to the controller ensures
that the visibility button functions correctly when multiple views are present.
sb_ctrl (StreamBarController or None): the StreamBarController which (typically)
created this StreamController. Only needed for ROA repetition display.
sp_options: (int or None) combination of OPT_* values for the StreamPanel or None for default.
"""
Comment thread
pieleric marked this conversation as resolved.

self.stream = stream
Expand All @@ -83,25 +84,26 @@ def __init__(self, stream_bar, stream, tab_data_model, show_panel=True, view=Non

self._stream_config = data.get_stream_settings_config().get(type(stream), {})

options = (OPT_BTN_REMOVE | OPT_BTN_SHOW | OPT_BTN_UPDATE)
if sp_options is None:
sp_options = OPT_BTN_REMOVE | OPT_BTN_SHOW | OPT_BTN_UPDATE
# Add tint/colormap option if there is a tint VA and adjust based on the stream type
if hasattr(stream, "tint"):
options |= OPT_BTN_TINT
sp_options |= OPT_BTN_TINT
if isinstance(stream, acqstream.RGBStream):
options |= OPT_NO_COLORMAPS
sp_options |= OPT_NO_COLORMAPS
# (Temporal)SpectrumStreams *with spectrum data* accept the FIT_TO_RGB option
if isinstance(stream, acqstream.SpectrumStream) and stream.raw[0].shape[0] > 1:
options |= OPT_FIT_RGB
sp_options |= OPT_FIT_RGB

# Allow changing the name of dyes (aka FluoStreams)
if isinstance(stream, acqstream.FluoStream):
options |= OPT_NAME_EDIT
sp_options |= OPT_NAME_EDIT

# Special display for spectrum (aka SpectrumStream)
if isinstance(stream, acqstream.SpectrumStream) and hasattr(stream, "peak_method"):
options |= OPT_BTN_PEAK
sp_options |= OPT_BTN_PEAK
Comment thread
pieleric marked this conversation as resolved.

self.stream_panel = StreamPanel(stream_bar, stream, options)
self.stream_panel = StreamPanel(stream_bar, stream, sp_options)
self.tab_data_model = tab_data_model

# To update the local resolution without hardware feedback
Expand Down
9 changes: 6 additions & 3 deletions src/odemis/gui/cont/stream_bar.py
Original file line number Diff line number Diff line change
Expand Up @@ -482,7 +482,8 @@ def addStream(self, stream, **kwargs):
"""
return self._add_stream(stream, **kwargs)

def _add_stream(self, stream, add_to_view=False, visible=True, play=None, stream_cont_cls=StreamController):
def _add_stream(self, stream, add_to_view=False, visible=True, play=None, stream_cont_cls=StreamController,
sp_options=None):
""" Add the given stream to the tab data model and appropriate views

Args:
Expand All @@ -496,6 +497,7 @@ def _add_stream(self, stream, add_to_view=False, visible=True, play=None, stream
play (None or boolean): If True, immediately start it, if False, let it stopped, and if
None, only play if already a stream is playing.
stream_cont_cls: The stream controller class.
sp_options: (int or None) combination of OPT_* values for the StreamPanel or None for default.

Returns:
(StreamController or Stream): the stream controller or stream (if visible is False) that
Expand Down Expand Up @@ -554,21 +556,22 @@ def _add_stream(self, stream, add_to_view=False, visible=True, play=None, stream
static=self.static_mode,
view=linked_view,
cls=stream_cont_cls,
sp_options=sp_options,
)
return stream_cont
else:
return stream

def _add_stream_cont(self, stream, show_panel=True, locked=False, static=False,
view=None, cls=StreamController):
view=None, cls=StreamController, sp_options=None):
""" Create and add a stream controller for the given stream

:return: (StreamController)

"""

stream_cont = cls(self._stream_bar, stream, self._tab_data_model,
show_panel, view, sb_ctrl=self)
show_panel, view, sb_ctrl=self, sp_options=sp_options)

Comment thread
nandishjpatel marked this conversation as resolved.
if locked:
stream_cont.to_locked_mode()
Expand Down
7 changes: 4 additions & 3 deletions src/odemis/gui/plugin/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -420,7 +420,7 @@ def __init__(self, plugin, title, text=None):
self.spectrum_viewport.setView(self.spectrum_view, self._dmodel)
self._dmodel.focussedView.value = self.view
self._dmodel.views.value = [self.view, self.view_r,
self.spectrum_view]
self.spectrum_view, self.hidden_view]
self._viewports = (self.viewport_l, self.viewport_r, self.spectrum_viewport)

self.streambar_controller = StreamBarController(
Expand Down Expand Up @@ -505,7 +505,7 @@ def button_callback_wrapper(evt, btnid=btnid):
self.pnl_buttons.Layout()

@call_in_wx_main
def addStream(self, stream, index=0):
def addStream(self, stream, index=0, sp_options=None):
"""
Adds a stream to the viewport, and a stream entry to the stream panel.
It also ensures the panel box and viewport are shown.
Expand All @@ -517,6 +517,7 @@ def addStream(self, stream, index=0):
index (0, 1, 2, or None): Index of the viewport to add the stream. 0 = left,
1 = right, 2 = spectrum viewport. If None, it will not show the stream
on any viewport (and it will be added to the .hidden_view)
sp_options: (int or None) combination of OPT_* values for the StreamPanel or None for default.
"""
need_layout = False

Expand All @@ -535,7 +536,7 @@ def addStream(self, stream, index=0):
if not self.fp_streams.IsShown():
self.fp_streams.Show()
need_layout = True
self.streambar_controller.addStream(stream, add_to_view=v)
self.streambar_controller.addStream(stream, add_to_view=v, sp_options=sp_options)

if need_layout:
self.Layout()
Expand Down
Loading