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
2 changes: 1 addition & 1 deletion examples/object_classifier.py
Original file line number Diff line number Diff line change
Expand Up @@ -220,7 +220,7 @@ def histopathology_annotator():
segmentations.append(segmentation)
embedding_paths.append(embedding_path)

clf.image_series_object_classifier(
clf.batch_object_classifier(
images, segmentations, output_folder="./clf-test-data/histo-results",
embedding_paths=embedding_paths, model_type="vit_b_histopathology", ndim=2,
)
Expand Down
4 changes: 2 additions & 2 deletions micro_sam/napari.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ contributions:
- id: micro_sam.annotator_tracking
python_name: micro_sam.sam_annotator.annotator_tracking:AnnotatorTracking
title: Start the tracking annotator
- id: micro_sam.image_series_annotator
- id: micro_sam.batch_annotator
python_name: micro_sam.sam_annotator.batch_annotator:BatchAnnotator
title: Start the batch annotator
- id: micro_sam.object_classifier
Expand Down Expand Up @@ -79,7 +79,7 @@ contributions:
display_name: Segmentation Annotator
- command: micro_sam.annotator_tracking
display_name: Tracking Annotator
- command: micro_sam.image_series_annotator
- command: micro_sam.batch_annotator
display_name: Batch Annotator
- command: micro_sam.object_classifier
display_name: Object Classifier
Expand Down
2 changes: 1 addition & 1 deletion micro_sam/sam_annotator/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,5 @@

from .annotator import annotator, Annotator
from .annotator_tracking import annotator_tracking
from .batch_annotator import image_folder_annotator, image_series_annotator
from .batch_annotator import batch_annotator, image_folder_annotator
# from .object_classifier import object_classifier
4 changes: 2 additions & 2 deletions micro_sam/sam_annotator/_annotator.py
Original file line number Diff line number Diff line change
Expand Up @@ -428,8 +428,8 @@ def _resolve_anyup(self):
image = self._viewer.layers["image"].data
return image, upsampler, True

def accumulate_series_features(self):
"""Add the current image's labeled features to the running training set (image series).
def accumulate_batch_features(self):
"""Add the current image's labeled features to the running batch training set.

Uses the per-tool feature and label hooks so it works for both classifiers. No-op when the
features cannot be computed or when the current image has no annotations.
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
"""Shared navigation harness for the image series annotator.
"""Shared navigation harness for the batch annotator.

Hosts any per-task annotator (segmentation, tracking, classification) and drives
forward navigation, the skip-already-done check and per-item loading/saving
through a small task-adapter interface (`SeriesAnnotatorTask`). The harness is
through a small task-adapter interface (`BatchAnnotatorTask`). The harness is
task-agnostic; everything task-specific lives in the concrete adapter.
"""

Expand All @@ -19,13 +19,13 @@


def _hide_embedding_widget(annotator):
"""Hide the docked annotator's embedding section during a series session.
"""Hide the docked annotator's embedding section during a batch session.

The launcher's advanced settings are the single source of truth for the model / tiling / device /
embedding-path / ndim, and the harness computes embeddings itself in 'start'/'advance', so the
annotator's embedding panel (and its 'Compute Embeddings' button) is redundant here. The widget
object is kept alive (just hidden) because '_sync_embedding_widget' and the classifier spec read
from it. Standalone (non-series) annotators never call this, so their panel stays visible.
from it. Standalone (non-batch) annotators never call this, so their panel stays visible.
"""
ew = getattr(annotator, "_embedding_widget", None)
if ew is None:
Expand All @@ -39,15 +39,15 @@ def _hide_embedding_widget(annotator):


def _embed_navigation(viewer, annotator, nav_container):
"""Add the navigation controls as a 'Series Navigation' section inside the docked annotator.
"""Add the navigation controls as a 'Batch Navigation' section inside the docked annotator.

Falls back to a standalone dock widget if the annotator has no embeddable inner layout.
"""
inner = getattr(annotator, "_annotator_widget", None)
if inner is None or inner.layout() is None:
viewer.window.add_dock_widget(nav_container, name="Series Navigation")
viewer.window.add_dock_widget(nav_container, name="Batch Navigation")
return
group = QtWidgets.QGroupBox("Series Navigation")
group = QtWidgets.QGroupBox("Batch Navigation")
group_layout = QtWidgets.QVBoxLayout()
# Add a top margin so the group title is not cramped against the navigation buttons.
group_layout.setContentsMargins(8, 14, 8, 8)
Expand Down Expand Up @@ -86,10 +86,10 @@ def _resize():
QTimer.singleShot(0, _resize)


class SeriesAnnotatorTask:
"""Adapter encoding the task-specific parts of an image series session.
class BatchAnnotatorTask:
"""Adapter encoding the task-specific parts of a batch annotation session.

The harness owns navigation, the skip-already-done check and the end-of-series dialog.
The harness owns navigation, the skip-already-done check and the end-of-batch dialog.
The adapter owns precomputing the model and embeddings, loading an item into the viewer,
deciding whether there is content worth saving, and saving the per-item result. Concrete
tasks: segmentation, tracking, object/pixel classification.
Expand All @@ -98,14 +98,14 @@ class SeriesAnnotatorTask:
#: Folder where per-item results are written. Set by the harness before the session starts.
output_folder = None

#: Whether the series inputs are in-memory arrays (True) or file paths (False). Set by the harness.
#: Whether the batch inputs are in-memory arrays (True) or file paths (False). Set by the harness.
have_inputs_as_arrays = False

def result_filename(self, entry, index: int) -> str:
"""Return the filename (relative to `output_folder`) of this item's saved result.

Used to skip items that are already done. `entry` is the raw series entry
(an array or a file path), `index` its position in the series.
Used to skip items that are already done. `entry` is the raw batch entry
(an array or a file path), `index` its position in the batch.
"""
raise NotImplementedError

Expand Down Expand Up @@ -139,27 +139,27 @@ def on_leave_item(self, viewer, entry, index: int) -> None:
empty_item_message = "Nothing is annotated yet. Do you wish to continue to the next image?"

def nav_extra_widgets(self):
"""Extra magicgui widgets to place next to the Next button in the Series Navigation container.
"""Extra magicgui widgets to place next to the Next button in the Batch Navigation container.

Task-specific (e.g. the classifiers' 'Forward Classifier State' checkbox); none by default.
"""
return []


def run_image_series(
def run_batch(
images,
output_folder,
task: SeriesAnnotatorTask,
task: BatchAnnotatorTask,
*,
have_inputs_as_arrays: bool,
viewer=None,
return_viewer: bool = False,
skip_done: bool = True,
):
"""Drive an image series annotation session for any task.
"""Drive a batch annotation session for any task.

Args:
images: The series entries (in-memory arrays or file paths).
images: The batch entries (in-memory arrays or file paths).
output_folder: The folder where per-item results are saved.
task: The task adapter that encodes the task-specific behavior.
have_inputs_as_arrays: Whether `images` holds arrays (True) or file paths (False).
Expand Down Expand Up @@ -193,7 +193,11 @@ def _load_pixels(index):
while current_index < n_images and _is_done(current_index):
current_index += 1
if current_index == n_images:
print("All images have already been annotated and 'skip_done' is set. Nothing to do.")
# The batch launcher reports this in a dialog and stays open so its settings can be
# changed. Keep the terminal message for direct Python / CLI use, where no viewer was
# supplied to display that feedback.
if viewer is None:
print("All images have already been annotated and 'skip_done' is set. Nothing to do.")
return
if current_index != 0:
print("The first image to annotate is image number", current_index)
Expand All @@ -204,7 +208,7 @@ def _load_pixels(index):
image = _load_pixels(current_index)
annotator = task.start(viewer, images[current_index], image, embedding_paths[current_index], current_index)

# The launcher owns the model / embedding settings in a series session, so hide the annotator's
# The launcher owns the model / embedding settings in a batch session, so hide the annotator's
# (now redundant) embedding section to avoid duplicating those controls.
_hide_embedding_widget(annotator)

Expand Down Expand Up @@ -241,14 +245,14 @@ def _do_next(*args):
return
_go_to(index)

# Embed the navigation controls in the docked annotator, so they travel with the image series
# Embed the navigation controls in the docked annotator, so they travel with the batch
# annotator instead of as a separate floating dock widget. The action is also tracked in the
# shared state, so it can be triggered programmatically (e.g. in tests) just like the annotator's
# own widgets.
state = AnnotatorState()
next_button = PushButton(text="Next Image [N]")
next_button.clicked.connect(lambda: _do_next())
state.widgets["series_next"] = _do_next
state.widgets["batch_next"] = _do_next

nav_buttons = [next_button]
# Task-specific controls placed next to Next (e.g. the classifiers' 'Keep Classifier').
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
"""Shared image-series task for the object and pixel classifiers.
"""Shared batch-annotation task for the object and pixel classifiers.

Both classifiers share the per-item series flow: (re)initialize the predictor, build the classifier
Both classifiers share the per-item batch flow: (re)initialize the predictor, build the classifier
widget, accumulate the per-item labeled features into the running training set when leaving an item,
and save the prediction plus the trained classifier. Subclasses bind the concrete classifier widget
class, the cached state-attribute names and any extra per-item layers (the object classifier adds a
Expand All @@ -14,14 +14,14 @@
from joblib import dump
from magicgui.widgets import CheckBox

from ._series import SeriesAnnotatorTask
from ._batch import BatchAnnotatorTask
from ._state import AnnotatorState
from ._tooltips import get_tooltip
from .util import _sync_embedding_widget


class ClassificationSeriesTask(SeriesAnnotatorTask):
"""Series task base for the classifiers; subclasses bind the concrete widget and state attrs."""
class ClassificationBatchTask(BatchAnnotatorTask):
"""Batch task base for the classifiers; subclasses bind the concrete widget and state attrs."""

# Bound by subclasses.
classifier_class = None # ObjectClassifier | PixelClassifier
Expand Down Expand Up @@ -51,7 +51,7 @@ def result_filename(self, entry, index):
return os.path.splitext(os.path.basename(entry))[0] + "_prediction.tif"

def precompute(self, images):
# Start the series with a fresh running training set and no cached features/classifier, so a
# Start the batch with a fresh running training set and no cached features/classifier, so a
# new session does not inherit accumulated state from a previous one (the state is a singleton).
state = AnnotatorState()
state.previous_features, state.previous_labels = None, None
Expand Down Expand Up @@ -94,10 +94,10 @@ def start(self, viewer, entry, image, embedding_path, index):

def nav_extra_widgets(self):
# A checkbox next to the Next button (classification tasks only), on by default, to carry the
# classifier state forward across the series. Tracked in the state so it is reachable in tests.
# classifier state forward across the batch. Tracked in the state so it is reachable in tests.
self._forward_state = CheckBox(value=True, text="Keep Classifier")
self._forward_state.native.setToolTip(get_tooltip("classification", "forward_classifier_state"))
AnnotatorState().widgets["series_forward_state"] = self._forward_state
AnnotatorState().widgets["batch_forward_state"] = self._forward_state
return [self._forward_state]

def _forward_state_enabled(self):
Expand Down Expand Up @@ -127,7 +127,7 @@ def on_leave_item(self, viewer, entry, index):
state = AnnotatorState()
if self._forward_state_enabled():
# Stack this image's annotated features into the running training set, forwarded to the next.
state.annotator.accumulate_series_features()
state.annotator.accumulate_batch_features()
if state.previous_features is not None:
np.save(os.path.join(self.output_folder, "features.npy"), state.previous_features)
np.save(os.path.join(self.output_folder, "labels.npy"), state.previous_labels)
Expand Down
2 changes: 1 addition & 1 deletion micro_sam/sam_annotator/_state.py
Original file line number Diff line number Diff line change
Expand Up @@ -124,7 +124,7 @@ class AnnotatorState(metaclass=Singleton):
object_rf: Optional[Any] = None
# TODO use proper class
segmentation_selection: Optional[Any] = None
# For image_series_object_classifier
# For batch_object_classifier
previous_features: Optional[np.ndarray] = None
previous_labels: Optional[np.ndarray] = None

Expand Down
6 changes: 3 additions & 3 deletions micro_sam/sam_annotator/_tooltips.py
Original file line number Diff line number Diff line change
Expand Up @@ -79,13 +79,13 @@
"track_state": "Select the state of the current annotation. Choose 'division' if the object is dviding in the current frame.", # noqa
"export_button": "Export the committed tracking result in the chosen format (CTC, GEFF or TrackMate XML).", # noqa
},
"image_series_annotator": {
"batch_annotator": {
"folder": "Select the folder with the images to annotate.",
"output_folder": "Select the folder for saving the segmentation results.",
"continue_annotation": "Resume at the first image without a saved result in the output folder. Uncheck to restart at the first image and load existing segmentations for review or editing.", # noqa
"pattern": "Select a pattern for selecting files. E.g. '*.tif' to only select tif files. By default all files in the input folder are selected.", # noqa
"ndim": "The spatial dimensionality of the data.",
"task": "The annotation task to run over the series: interactive segmentation, tracking (each file is a timeseries), or object / pixel classification.", # noqa
"task": "The annotation task to run over the batch: interactive segmentation, tracking (each file is a timeseries), or object / pixel classification.", # noqa
"segmentation_folder": "Object classification only: a folder with one segmentation per image to classify. Leave empty to produce the segmentations in the tool.", # noqa
},
"training": {
Expand Down Expand Up @@ -113,7 +113,7 @@
},
"classification": {
"settings": "Optional classifier settings: PCA feature reduction, AnyUp upsampling, the random seed, and loading or exporting a trained classifier.", # noqa
"forward_classifier_state": "Carry the classifier across images in the series: annotated features from previous images are stacked with the current one, a fresh random forest is trained on the combined set, and it is applied to the next image automatically (even without new annotations). Uncheck to classify each image independently.", # noqa
"forward_classifier_state": "Carry the classifier across images in the batch: annotated features from previous images are stacked with the current one, a fresh random forest is trained on the combined set, and it is applied to the next image automatically (even without new annotations). Uncheck to classify each image independently.", # noqa
"segmentation": "Select the segmentation (labels) layer whose objects will be classified.",
"train_button": "Train the random forest on all current annotations and predict on the image. Shortcut: Shift + T.", # noqa
"clear_button": "Clear the annotation scribbles and the prediction (whole volume, or the current slice for 3d data when 'Apply to Volume' is unchecked). Shortcut: C.", # noqa
Expand Down
2 changes: 1 addition & 1 deletion micro_sam/sam_annotator/_widgets.py
Original file line number Diff line number Diff line change
Expand Up @@ -1794,7 +1794,7 @@ def _update_tiling_visibility(self, index=None):
def _apply_default_tiling_for_shape(self, shape):
# Enable tiling by default for large in-plane images, using the central v2 tiling defaults.
# 'shape' is the spatial image shape (channel axis already removed). Shared by the layer-based
# auto-tiling and the image series launcher (which judges from the first file in the folder).
# auto-tiling and the batch launcher (which judges from the first file in the folder).
from micro_sam.v2.util import needs_default_tiling, DEFAULT_TILE_SHAPE, DEFAULT_HALO

if shape is not None and needs_default_tiling(shape):
Expand Down
16 changes: 8 additions & 8 deletions micro_sam/sam_annotator/annotator_tracking.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
from . import _widgets as widgets
from . import util as vutil
from ._annotator import _AnnotatorBase
from ._series import SeriesAnnotatorTask, run_image_series
from ._batch import BatchAnnotatorTask, run_batch
from ._state import AnnotatorState
from ._tooltips import get_tooltip

Expand Down Expand Up @@ -479,8 +479,8 @@ def annotator_tracking(
napari.run()


class TrackingSeriesTask(SeriesAnnotatorTask):
"""Series task for tracking: each item is a TYX timeseries tracked independently."""
class TrackingBatchTask(BatchAnnotatorTask):
"""Batch task for tracking: each item is a TYX timeseries tracked independently."""

empty_item_message = "Nothing is tracked yet. Do you wish to continue to the next timeseries?"

Expand Down Expand Up @@ -540,7 +540,7 @@ def start(self, viewer, entry, image, embedding_path, index):
annotator._update_image()

state = AnnotatorState()
viewer.window.add_dock_widget(annotator, name="Segment Anything for Microscopy (Image Series Tracking)")
viewer.window.add_dock_widget(annotator, name="Segment Anything for Microscopy (Batch Tracking)")
vutil._sync_embedding_widget(
widget=state.widgets["embeddings"],
model_type=self.model_type if self.checkpoint_path is None else state.predictor.model_type,
Expand All @@ -563,7 +563,7 @@ def save_item(self, viewer, entry, index):
imageio.imwrite(save_path, viewer.layers["committed_objects"].data, compression="zlib")


def image_series_tracking_annotator(
def batch_tracking_annotator(
images: Union[List[Union[os.PathLike, str]], List[np.ndarray]],
output_folder: str,
*,
Expand All @@ -579,7 +579,7 @@ def image_series_tracking_annotator(
return_viewer: bool = False,
skip_done: bool = True,
) -> Optional["napari.viewer.Viewer"]:
"""Run the tracking annotation tool for a series of timeseries (each item is one TYX video).
"""Run the tracking annotation tool for a batch of timeseries (each item is one TYX video).

Args:
images: List of timeseries (TYX arrays) or file paths, each tracked independently.
Expand All @@ -602,12 +602,12 @@ def image_series_tracking_annotator(
The napari viewer, only returned if `return_viewer=True`.
"""
have_inputs_as_arrays = isinstance(images[0], np.ndarray)
task = TrackingSeriesTask(
task = TrackingBatchTask(
model_type=model_type, embedding_path=embedding_path, tile_shape=tile_shape, halo=halo,
checkpoint_path=checkpoint_path, decoder_path=decoder_path, device=device,
precompute_amg_state=precompute_amg_state,
)
return run_image_series(
return run_batch(
images, output_folder, task, have_inputs_as_arrays=have_inputs_as_arrays,
viewer=viewer, return_viewer=return_viewer, skip_done=skip_done,
)
Expand Down
Loading
Loading