Saving Data¶
To save the images from an acquisition, pass output= to
core.mda.run() (or
core.run_mda()). Frames are written to
disk as they arrive, by ome-writers,
and the extension of the path picks the format:
from pymmcore_plus import CMMCorePlus
import useq
core = CMMCorePlus()
core.loadSystemConfiguration()
sequence = useq.MDASequence(
stage_positions=[(0, 0, 0), (100, 100, 0)],
channels=["DAPI", "FITC"],
time_plan={"interval": 1, "loops": 3},
z_plan={"range": 4, "step": 1},
)
core.mda.run(sequence, output="experiment.ome.zarr") # (1)!
- Or
output="experiment.ome.tiff". Note thatmda.runblocks until the acquisition is done, whilerun_mdaruns it on another thread (see The Acquisition Engine).
There are two formats to choose from:
.ome.zarrwrites an OME-Zarr (v0.5, which is built on Zarr v3). This works with the default install.-
.ome.tiffwrites OME-TIFF. This needs tifffile, which is what theioextra adds:(Without it, you'll get
ValueError: No available backends found for format 'ome-tiff'.) OME-TIFF is a good choice if you want to open the files in tools like Fiji (via Bio-Formats).
The shape of the data comes from the sequence. In the example above, each position
gets a (t, c, z, y, x) array of shape (3, 2, 5, 512, 512). The dimensions are
stored in the order OME expects (time, channel, z, y, x), whatever axis_order
you acquired them in.
Where did the data go?¶
The path you pass isn't always a single file:
| output | one position | several positions (or a grid plan) |
|---|---|---|
experiment.ome.zarr |
a zarr folder holding one image | a zarr folder with one image per position, named after the position if it has a name (A1/, ...), or numbered (0/, 1/, ...). With a grid plan, each tile is its own image (0_0_0/, 0_1_0/, ...: position, row, column) |
experiment.ome.tiff |
a single file | a folder, experiment/, with one file per position (or grid tile): experiment_p000.ome.tiff, experiment_p001.ome.tiff, ... |
Running it again (overwrite)¶
If the output already exists, the run fails right away:
FileExistsError: Zarr group already exists at .../experiment.ome.zarr. Use overwrite=True to replace it.
Pass overwrite=True to replace it:
run_mda doesn't take an overwrite argument (yet). To overwrite from
run_mda, put it in an AcquisitionSettings
object instead: output=AcquisitionSettings(root_path="experiment.ome.zarr",
overwrite=True).
Opening the data in Python¶
An OME-TIFF reads straight into a numpy array with tifffile:
import tifffile
data = tifffile.imread("experiment/experiment_p000.ome.tiff")
print(data.shape) # (3, 2, 5, 512, 512), i.e. (t, c, z, y, x)
An OME-Zarr can be read with any reader that supports Zarr v3, for example
zarr-python (pip install zarr):
import zarr
root = zarr.open_group("experiment.ome.zarr", mode="r")
data = root["0/0"][:] # (1)!
print(data.shape) # (3, 2, 5, 512, 512), i.e. (t, c, z, y, x)
- With several positions,
root["0"]is the image for the first position (orroot["A1"], if you named it), and"0"inside it is the full-resolution array. With a single position there's no position level, so the array is justroot["0"]. (list(root)shows what's there.)
Some metadata is saved along with the pixels:
- The summary metadata (devices, properties, pixel size, the
MDASequenceitself, etc...; seeSummaryMetaV1) is stored under a"pymmcore_plus"key, in the attributes of the root zarr group (root.attrs["pymmcore_plus"]["summary_metadata"]) or as an OME-XML annotation in the TIFF. - For each frame, the time (
delta_t), exposure time, and stage position are written into the OME metadata. The rest of the per-frame metadata thatframeReadyemits (FrameMetaV1) is not saved. If you need it, connect toframeReadyand store it yourself (see below).
More control with AcquisitionSettings¶
Instead of a path, output also accepts an
ome_writers.AcquisitionSettings object, which lets you choose things like
compression, the backend, and whether to overwrite:
from ome_writers import AcquisitionSettings
settings = AcquisitionSettings(
root_path="experiment.ome.zarr",
compression="blosc-zstd",
overwrite=True,
)
core.run_mda(sequence, output=settings)
The dimensions (and well-plate layout, if any) are always filled in from the
sequence, so you don't need to (and can't) set them here. To change the
chunking, use the dimension_overrides argument of mda.run / run_mda,
keyed by dimension name:
core.mda.run(
sequence,
output="experiment.ome.zarr",
dimension_overrides={"y": {"chunk_size": 256}, "x": {"chunk_size": 256}},
)
See the ome-writers documentation
for the rest of the AcquisitionSettings options.
While an acquisition is running, core.mda.get_view()
returns an array-like view of the data written so far. (output="memory" keeps
the data in memory instead of writing a file, which is handy for viewers and
tests.)
Lists and generators are saved as a flat stack¶
The array shape is taken from the MDASequence. If you pass anything else (a
list of MDAEvents, a generator, a queue iterator as in Event-Driven
Acquisition, or a useq.v2.MDASequence), there's no
shape to go on, so the frames are written in the order they arrive into one
(t, y, x) array of unknown length:
The same happens (with a warning in the log) for an MDASequence whose positions
have their own sub-sequence with anything other than a grid_plan (for example,
a different z-stack at one
position),
since the positions then don't share one shape.
Each frame's MDAEvent (and its index) is still available in the frameReady
signal, if you need to sort the frames out later.
Processing frames yourself¶
To do something else with the images (analysis, a different file format, sending
them somewhere, etc...), connect a callback to the
frameReady signal. It receives up
to three arguments: the image (a numpy array), the MDAEvent, and the frame
metadata dict:
import numpy as np
@core.mda.events.frameReady.connect
def on_frame(image: np.ndarray, event: useq.MDAEvent, metadata: dict) -> None:
print(f"received frame {event.index}: {image.shape}, {image.dtype}")
Keep it quick
With the default (psygnal) event backend, a frameReady callback runs in the
acquisition thread, so the next event waits until your callback returns. See
Threads for details.
If the processing is slow, write a handler object instead, and pass it in
output=. A handler is any object with a frameReady method (it may also have
sequenceStarted and sequenceFinished methods). Handlers are called from a
separate thread, so they don't hold up the acquisition (mda.run waits for them
to catch up before it returns). Here we make a maximum-intensity projection of
each z-stack, while also saving the raw data:
class MaxProjector:
"""Keep a maximum-intensity projection of each z-stack."""
def sequenceStarted(self, sequence: useq.MDASequence, metadata: dict) -> None:
self.projections: dict[tuple, np.ndarray] = {}
def frameReady(
self, image: np.ndarray, event: useq.MDAEvent, metadata: dict
) -> None:
idx = event.index
key = (idx.get("p", 0), idx.get("t", 0), idx.get("c", 0))
if key in self.projections:
np.maximum(self.projections[key], image, out=self.projections[key])
else:
self.projections[key] = image.copy()
def sequenceFinished(self, sequence: useq.MDASequence) -> None:
print(f"made {len(self.projections)} projections")
projector = MaxProjector() # (1)!
core.mda.run(sequence, output=["experiment.ome.zarr", projector], overwrite=True)
- Keep a reference to the handler, so that you can use
projector.projectionsafter the run.
output may be a list holding any number of handlers, plus at most one path (or
AcquisitionSettings).
Deprecated: pymmcore_plus.mda.handlers
Older versions of pymmcore-plus wrote data with the handler classes in
pymmcore_plus.mda.handlers (OMEZarrWriter, OMETiffWriter,
TensorStoreHandler and ImageSequenceWriter). These are deprecated in favor
of output= with a path, as described above, and importing the module emits a
FutureWarning. They will be removed in a future version; if you rely on one,
please copy it into your own code.
Next steps¶
- Events lists every signal emitted during an acquisition.
- Event-Driven Acquisition builds the sequence on the fly, based on the data.
- The
pymmcore_plus.metadatareference describes the summary and frame metadata.