Skip to content

Comments

feat: Add z-level, timepoint, and channel selection for registration#19

Merged
hongquanli merged 3 commits intoCephla-Lab:mainfrom
Alpaca233:zt
Feb 19, 2026
Merged

feat: Add z-level, timepoint, and channel selection for registration#19
hongquanli merged 3 commits intoCephla-Lab:mainfrom
Alpaca233:zt

Conversation

@Alpaca233
Copy link
Contributor

@Alpaca233 Alpaca233 commented Feb 18, 2026

Summary

  • Add z-level selection for registration (use specific z-plane instead of always middle)
  • Add timepoint selection for registration
  • Add channel selection for registration (choose which channel to use for tile alignment)

These controls appear in the Settings section when "Enable registration refinement" is checked and the dataset has multiple z-levels, timepoints, or channels respectively.

Test plan

  • Tests pass (68/68)
  • Linting passes (black)
  • Manual testing with multi-channel dataset
  • Manual testing with multi-z dataset
  • Manual testing with time-series dataset

🤖 Generated with Claude Code

Alpaca233 and others added 3 commits February 9, 2026 21:00
Add the ability to select which z-level and timepoint to use for
registration in multi-z, multi-timepoint acquisitions. Previously,
registration always used the middle z-level and first timepoint.

Changes:
- Add registration_z and registration_t parameters to TileFusion.__init__
- Validate parameters after metadata loading when n_z/n_t are known
- Update _read_tile and _read_tile_region to use registration z/t defaults
- Add dataset_n_z and dataset_n_t state variables to GUI
- Add Z-level and Timepoint spinboxes (shown when registration enabled
  AND dataset has multi-z/t)
- Load dataset dimensions on file drop and update control visibility
- Pass registration z/t values to FusionWorker and PreviewWorker

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Add a dropdown to select which channel is used for tile registration.
The channel selector appears alongside z-level and timepoint controls
when registration is enabled and the dataset has multiple channels.

- Add dataset_n_channels and dataset_channel_names state variables
- Add channel combo to registration options widget
- Populate channel names from TileFusion metadata on file load
- Pass selected channel to PreviewWorker and FusionWorker

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
@hongquanli hongquanli requested a review from Copilot February 19, 2026 10:42
@hongquanli hongquanli merged commit d0909fc into Cephla-Lab:main Feb 19, 2026
8 of 9 checks passed
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR adds user-configurable selection of the z-plane, timepoint, and channel used during registration refinement, exposing the controls in the GUI settings when registration is enabled and the dataset supports multiple z/t/channels.

Changes:

  • Extend TileFusion to accept registration_z and registration_t, and use them as defaults for tile reads used during registration/preview.
  • Extend GUI workers to pass registration_z, registration_t, and registration channel (via channel_to_use) into TileFusion.
  • Add GUI controls (spinboxes/combobox) to choose z-level, timepoint, and channel for registration.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 3 comments.

File Description
src/tilefusion/core.py Adds registration z/t parameters, validates them, and changes default tile read behavior to use selected z/t.
gui/app.py Adds GUI state + controls for z/t/channel selection and forwards these values into background workers/TileFusion.
Comments suppressed due to low confidence (4)

src/tilefusion/core.py:469

  • _read_tile annotates time_idx as int but defaults it to None. This is an invalid type combination (and same for z_level). Update the annotations to Optional[int] (or default to an int) to keep typing consistent.
    def _read_tile(self, tile_idx: int, z_level: int = None, time_idx: int = None) -> np.ndarray:
        """Read a single tile from the input data (all channels)."""
        if z_level is None:
            z_level = self._registration_z  # Default to registration z-level
        if time_idx is None:
            time_idx = self._registration_t  # Default to registration timepoint

src/tilefusion/core.py:115

  • The new registration_z/registration_t parameters are public API, but the class docstring’s parameter list wasn’t updated to document them. Please add entries describing what they control and how defaults are chosen (middle z / t=0).
        registration_z: Optional[int] = None,
        registration_t: int = 0,
    ):

src/tilefusion/core.py:475

  • In the Zarr branch, _read_tile ignores time_idx and z_level entirely (it always reads zarr_ts[0, tile_idx, ...] and max-projects Z). That means the new registration timepoint/z-level selection won’t actually affect registration (or preview) for Zarr datasets. Update the Zarr read path to index the requested timepoint and to use the requested z-plane (or clearly document that Zarr registration is always time=0 with Z max-projection).
        if self._is_zarr_format:
            zarr_ts = self._metadata["tensorstore"]
            is_3d = self._metadata.get("is_3d", False)
            tile = read_zarr_tile(zarr_ts, tile_idx, is_3d)
        elif self._is_individual_tiffs_format:

src/tilefusion/core.py:525

  • _read_tile_region also ignores time_idx/z_level for Zarr inputs (via read_zarr_region which is hardcoded to time 0 and max-projects Z). This prevents the new registration timepoint/z-level controls from working for Zarr-backed datasets.
        if self._is_zarr_format:
            zarr_ts = self._metadata["tensorstore"]
            is_3d = self._metadata.get("is_3d", False)
            region = read_zarr_region(
                zarr_ts, tile_idx, y_slice, x_slice, self.channel_to_use, is_3d
            )

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +192 to +209
# Z-stack and time series properties
self.n_z = self._metadata.get("n_z", 1)
self.n_t = self._metadata.get("n_t", 1)
self.dz_um = self._metadata.get("dz_um", 1.0)
self._time_folders = self._metadata.get("time_folders", None)
self._middle_z = self.n_z // 2 # Use middle z-level for registration

# Registration z/t selection (validate after n_z/n_t are known)
if registration_z is None:
self._registration_z = self._middle_z
else:
if registration_z < 0 or registration_z >= self.n_z:
raise ValueError(f"registration_z={registration_z} out of range [0, {self.n_z})")
self._registration_z = registration_z

if registration_t < 0 or registration_t >= self.n_t:
raise ValueError(f"registration_t={registration_t} out of range [0, {self.n_t})")
self._registration_t = registration_t
Copy link

Copilot AI Feb 19, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For Zarr inputs, registration_t/registration_z can’t work as implemented: self.n_t/self.n_z come from metadata keys n_t/n_z, but load_zarr_metadata only provides time_dim (and no n_z), so n_t/n_z stay at 1. This makes the validation reject any non-zero registration_t and prevents multi-time / multi-z datasets from ever exposing these controls. Consider mapping time_dim -> n_t and exposing z_dim -> n_z for Zarr metadata.

Copilot uses AI. Check for mistakes.
Comment on lines +1040 to +1057
try:
from tilefusion import TileFusion

tf_temp = TileFusion(file_path)
self.dataset_n_z = tf_temp.n_z
self.dataset_n_t = tf_temp.n_t
self.dataset_n_channels = tf_temp.channels
if "channel_names" in tf_temp._metadata:
self.dataset_channel_names = tf_temp._metadata["channel_names"]
else:
self.dataset_channel_names = [
f"Channel {i}" for i in range(self.dataset_n_channels)
]
tf_temp.close()
if self.dataset_n_z > 1 or self.dataset_n_t > 1:
self.log(f"Dataset: {self.dataset_n_z} z-levels, {self.dataset_n_t} timepoints")
self._update_reg_zt_controls()
except Exception:
Copy link

Copilot AI Feb 19, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

on_file_dropped creates a temporary TileFusion instance to probe dataset dimensions, but tf_temp.close() is only called on the success path. If anything raises after construction (e.g., during metadata access), file handles may leak. Use a context manager (with TileFusion(...) as tf_temp:) or a try/finally to guarantee cleanup.

Copilot uses AI. Check for mistakes.
Comment on lines +1097 to +1117
# Update z spinbox
self.reg_z_label.setVisible(has_multi_z)
self.reg_z_spin.setVisible(has_multi_z)
if has_multi_z:
self.reg_z_spin.setRange(0, self.dataset_n_z - 1)
self.reg_z_spin.setValue(self.dataset_n_z // 2) # Default to middle

# Update t spinbox
self.reg_t_label.setVisible(has_multi_t)
self.reg_t_spin.setVisible(has_multi_t)
if has_multi_t:
self.reg_t_spin.setRange(0, self.dataset_n_t - 1)
self.reg_t_spin.setValue(0) # Default to first timepoint

# Update channel combo
self.reg_channel_label.setVisible(has_multi_channel)
self.reg_channel_combo.setVisible(has_multi_channel)
if has_multi_channel:
self.reg_channel_combo.clear()
self.reg_channel_combo.addItems(self.dataset_channel_names)
self.reg_channel_combo.setCurrentIndex(0)
Copy link

Copilot AI Feb 19, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

_update_reg_zt_controls() resets the z/time/channel selections every time it runs (setValue(...) / setCurrentIndex(0)). Since it’s called when registration is toggled and when files are dropped, a user’s selection can be lost unexpectedly. Preserve the current values when still in range, and only apply defaults when the dataset changes or on first show.

Suggested change
# Update z spinbox
self.reg_z_label.setVisible(has_multi_z)
self.reg_z_spin.setVisible(has_multi_z)
if has_multi_z:
self.reg_z_spin.setRange(0, self.dataset_n_z - 1)
self.reg_z_spin.setValue(self.dataset_n_z // 2) # Default to middle
# Update t spinbox
self.reg_t_label.setVisible(has_multi_t)
self.reg_t_spin.setVisible(has_multi_t)
if has_multi_t:
self.reg_t_spin.setRange(0, self.dataset_n_t - 1)
self.reg_t_spin.setValue(0) # Default to first timepoint
# Update channel combo
self.reg_channel_label.setVisible(has_multi_channel)
self.reg_channel_combo.setVisible(has_multi_channel)
if has_multi_channel:
self.reg_channel_combo.clear()
self.reg_channel_combo.addItems(self.dataset_channel_names)
self.reg_channel_combo.setCurrentIndex(0)
# Preserve previous selections where possible
prev_z = self.reg_z_spin.value() if self.reg_z_spin.isVisible() else None
prev_t = self.reg_t_spin.value() if self.reg_t_spin.isVisible() else None
prev_channel_index = (
self.reg_channel_combo.currentIndex()
if self.reg_channel_combo.count() > 0
else None
)
existing_channels = [
self.reg_channel_combo.itemText(i)
for i in range(self.reg_channel_combo.count())
]
# Update z spinbox
self.reg_z_label.setVisible(has_multi_z)
self.reg_z_spin.setVisible(has_multi_z)
if has_multi_z:
self.reg_z_spin.setRange(0, self.dataset_n_z - 1)
max_z = self.dataset_n_z - 1
if prev_z is not None and 0 <= prev_z <= max_z:
self.reg_z_spin.setValue(prev_z)
else:
# Default to middle slice when no previous valid selection
self.reg_z_spin.setValue(self.dataset_n_z // 2)
# Update t spinbox
self.reg_t_label.setVisible(has_multi_t)
self.reg_t_spin.setVisible(has_multi_t)
if has_multi_t:
self.reg_t_spin.setRange(0, self.dataset_n_t - 1)
max_t = self.dataset_n_t - 1
if prev_t is not None and 0 <= prev_t <= max_t:
self.reg_t_spin.setValue(prev_t)
else:
# Default to first timepoint when no previous valid selection
self.reg_t_spin.setValue(0)
# Update channel combo
self.reg_channel_label.setVisible(has_multi_channel)
self.reg_channel_combo.setVisible(has_multi_channel)
if has_multi_channel:
# Only rebuild items if the dataset channels changed
if existing_channels != self.dataset_channel_names:
self.reg_channel_combo.clear()
self.reg_channel_combo.addItems(self.dataset_channel_names)
# On dataset change, default to first channel
self.reg_channel_combo.setCurrentIndex(0)
else:
# Preserve previous channel selection if still valid
max_channel_index = self.reg_channel_combo.count() - 1
if (
prev_channel_index is not None
and 0 <= prev_channel_index <= max_channel_index
):
self.reg_channel_combo.setCurrentIndex(prev_channel_index)
else:
self.reg_channel_combo.setCurrentIndex(0)

Copilot uses AI. Check for mistakes.
Alpaca233 added a commit to Alpaca233/stitcher-tile-fusion that referenced this pull request Feb 20, 2026
- Add docstring entries for registration_z and registration_t parameters
- Fix type hints: z_level and time_idx now Optional[int] instead of int
- Update read_zarr_tile and read_zarr_region to accept z_level and time_idx
- Zarr reads now respect z-level and timepoint selection instead of hardcoding

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants