-
Notifications
You must be signed in to change notification settings - Fork 51
fix: NDViewer black screen during live TIFF acquisition #521
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
base: master
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -150,24 +150,52 @@ def _acquire_file_lock(lock_path: str, context: str = ""): | |||||||||||||||||||||
| ) from exc | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
|
|
||||||||||||||||||||||
| @dataclass | ||||||||||||||||||||||
| class TiffWriteResult: | ||||||||||||||||||||||
| """Result from a SaveImageJob, containing frame info for viewer notification.""" | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| filepath: str | ||||||||||||||||||||||
| fov: int | ||||||||||||||||||||||
| time_point: int | ||||||||||||||||||||||
| z_index: int | ||||||||||||||||||||||
| channel_name: str | ||||||||||||||||||||||
| region_id: int = 0 | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
|
Comment on lines
+153
to
+163
|
||||||||||||||||||||||
|
|
||||||||||||||||||||||
| class SaveImageJob(Job): | ||||||||||||||||||||||
| _log: ClassVar = squid.logging.get_logger("SaveImageJob") | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| def run(self) -> bool: | ||||||||||||||||||||||
| def _make_result(self) -> TiffWriteResult: | ||||||||||||||||||||||
| info = self.capture_info | ||||||||||||||||||||||
| filepath = utils_acquisition.get_image_filepath( | ||||||||||||||||||||||
| info.save_directory, info.file_id, info.configuration.name, np.uint16 | ||||||||||||||||||||||
|
Comment on lines
+170
to
+171
|
||||||||||||||||||||||
| filepath = utils_acquisition.get_image_filepath( | |
| info.save_directory, info.file_id, info.configuration.name, np.uint16 | |
| # Use the actual image dtype (if available) so the filepath matches the one used by save_image(). | |
| image_dtype = ( | |
| self.capture_image.image_array.dtype | |
| if self.capture_image.image_array is not None | |
| else np.uint16 | |
| ) | |
| filepath = utils_acquisition.get_image_filepath( | |
| info.save_directory, info.file_id, info.configuration.name, image_dtype |
Copilot
AI
Mar 24, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In simulated disk I/O mode, SaveImageJob returns a TiffWriteResult (triggering viewer notification) but simulated_tiff_write() explicitly discards the buffer and does not create a file on disk. This will cause NDViewer to try to read a filepath that will never exist when SIMULATED_DISK_IO_ENABLED is true. Consider suppressing signal_tiff_frame_written in this mode or writing a real temporary file when the viewer is active.
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
|
|
@@ -125,3 +125,6 @@ class MultiPointControllerFunctions: | |||||
| # Zarr frame written callback - called when subprocess completes writing a frame | ||||||
| # Args: (fov, time_point, z_index, channel_name, region_idx) | ||||||
| signal_zarr_frame_written: Callable[[int, int, int, str, int], None] = lambda *a, **kw: None | ||||||
| # TIFF frame written callback - called when subprocess completes writing a frame | ||||||
| # Args: (filepath, fov, time_point, z_index, channel_name, region_id) | ||||||
| signal_tiff_frame_written: Callable[[str, int, int, int, str, int], None] = lambda *a, **kw: None | ||||||
|
||||||
| signal_tiff_frame_written: Callable[[str, int, int, int, str, int], None] = lambda *a, **kw: None | |
| signal_tiff_frame_written: Callable[[str, int, int, int, str, Union[str, int]], None] = lambda *a, **kw: None |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -252,6 +252,7 @@ def __init__( | |
| signal_slack_timepoint_notification=self._signal_slack_timepoint_notification_fn, | ||
| signal_slack_acquisition_finished=self._signal_slack_acquisition_finished_fn, | ||
| signal_zarr_frame_written=self._signal_zarr_frame_written_fn, | ||
| signal_tiff_frame_written=self._signal_tiff_frame_written_fn, | ||
| ), | ||
| scan_coordinates=scan_coordinates, | ||
| laser_autofocus_controller=laser_autofocus_controller, | ||
|
|
@@ -362,30 +363,9 @@ def _signal_new_image_fn(self, frame: squid.abc.CameraFrame, info: CaptureInfo): | |
| frame.frame, info.position.x_mm, info.position.y_mm, info.z_index, napri_layer_name | ||
| ) | ||
|
|
||
| # NDViewer push-based API: register image | ||
| # Compute flat FOV index from region and fov within region | ||
| region_offset = self._ndviewer_region_fov_offset.get(info.region_id) | ||
| if region_offset is None: | ||
| # This should not happen if start_acquisition was called correctly | ||
| self.log.warning( | ||
| f"Unknown region_id '{info.region_id}' in NDViewer registration. " | ||
| f"Available: {list(self._ndviewer_region_fov_offset.keys())}. Skipping." | ||
| ) | ||
| return | ||
| flat_fov_idx = region_offset + info.fov | ||
|
|
||
| if self._ndviewer_mode in (NDViewerMode.ZARR_6D, NDViewerMode.ZARR_5D): | ||
| # Zarr modes: notification happens via signal_zarr_frame_written callback | ||
| # when the subprocess completes writing, not here (too early). | ||
| pass | ||
| else: | ||
| # TIFF mode: register with filepath (synchronous write, notification is correct here) | ||
| filepath = control.utils_acquisition.get_image_filepath( | ||
| info.save_directory, info.file_id, info.configuration.name, frame.frame.dtype | ||
| ) | ||
| self.ndviewer_register_image.emit( | ||
| info.time_point, flat_fov_idx, info.z_index, info.configuration.name, filepath | ||
| ) | ||
| # NDViewer notification for both TIFF and Zarr modes happens via post-write | ||
| # callbacks (signal_tiff_frame_written / signal_zarr_frame_written) to avoid | ||
| # race conditions with async file writes. Do not emit here. | ||
|
|
||
| def _signal_current_configuration_fn(self, config: AcquisitionChannel): | ||
| self.signal_current_configuration.emit(config) | ||
|
|
@@ -449,6 +429,19 @@ def _signal_zarr_frame_written_fn( | |
| flat_fov = fov | ||
| self.ndviewer_notify_zarr_frame.emit(time_point, flat_fov, z_index, channel_name, 0) | ||
|
|
||
| def _signal_tiff_frame_written_fn( | ||
| self, filepath: str, fov: int, time_point: int, z_index: int, channel_name: str, region_id: int | ||
| ): | ||
| """Called when subprocess completes writing a TIFF frame. | ||
|
|
||
| This is the correct time to notify the viewer - after data is on disk. | ||
| """ | ||
| region_offset = self._ndviewer_region_fov_offset.get(region_id) | ||
| if region_offset is None: | ||
| return | ||
|
Comment on lines
+439
to
+441
|
||
| flat_fov_idx = region_offset + fov | ||
| self.ndviewer_register_image.emit(time_point, flat_fov_idx, z_index, channel_name, filepath) | ||
|
|
||
| # ------------------------------------------------------------------------- | ||
| # Helper methods for Zarr FOV path building | ||
| # ------------------------------------------------------------------------- | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
TiffWriteResult is used by both SaveImageJob and SaveOMETiffJob, but the docstring currently only mentions SaveImageJob. Updating the docstring to reflect all producers (and that filepath may include a page selector like "#") would prevent confusion for future consumers.