diff --git a/src/openflexure_microscope_server/things/__init__.py b/src/openflexure_microscope_server/things/__init__.py index c05a34ee..975be827 100644 --- a/src/openflexure_microscope_server/things/__init__.py +++ b/src/openflexure_microscope_server/things/__init__.py @@ -64,14 +64,19 @@ class OFMThing(lt.Thing): ) return self._data_dir - def create_data_path(self, path: str) -> "RelativeDataPath": + def create_data_path(self, path: str, absolute: bool = False) -> "RelativeDataPath": """Create a ``RelativeDataPath`` object with this Thing set as the saving Thing. :param path: The relative path within the data directory of this Thing's data dir that the data shudl be saved. + :path absolute: Set to True if the current path is absolute. A relative path + will be returned. An validation error will be raised if the absolute path + is not within the data directory. :return: A ``RelativeDataPath`` object with the saving Thing already set. """ + if absolute: + path = os.path.relpath(path, self.data_dir) rel_data_path = RelativeDataPath(path) rel_data_path.set_saving_thing(self) return rel_data_path @@ -136,12 +141,15 @@ class RelativeDataPath(RootModel[str]): :return: A new ``RelativeDataPath`` object with the path appended. """ - return RelativeDataPath(os.path.join(self.root, sub_path)) + new_path = RelativeDataPath(os.path.join(self.root, sub_path)) + if self._saving_thing is not None: + new_path.set_saving_thing(self._saving_thing) + return new_path @property def abs_data_path(self) -> str: """The absolute data directory to save to.""" - if self.save_location_set: + if self._saving_thing is None: raise RuntimeError("The saving Thing for the relative path was never set") if isinstance(self._saving_thing, TemporaryDirectory): return os.path.join(self._saving_thing.name, self.root) diff --git a/src/openflexure_microscope_server/things/camera/__init__.py b/src/openflexure_microscope_server/things/camera/__init__.py index 6da91133..9f71188b 100644 --- a/src/openflexure_microscope_server/things/camera/__init__.py +++ b/src/openflexure_microscope_server/things/camera/__init__.py @@ -591,7 +591,7 @@ class BaseCamera(OFMThing, ABC): self.capture_and_save_to_path(path, capture_mode) if tmpdir is None: - blob = lt.blob.Blob.from_file(path) + blob = lt.blob.Blob.from_file(path.abs_data_path) else: blob = lt.blob.Blob.from_temporary_directory(tmpdir, fname) blob.media_type = format_info.media_type diff --git a/src/openflexure_microscope_server/things/scan_workflows.py b/src/openflexure_microscope_server/things/scan_workflows.py index ec3f20a2..d691bdab 100644 --- a/src/openflexure_microscope_server/things/scan_workflows.py +++ b/src/openflexure_microscope_server/things/scan_workflows.py @@ -111,19 +111,27 @@ class ScanWorkflow(Generic[SettingModelType], lt.Thing): ) def all_settings( - self, images_dir: str - ) -> tuple[SettingModelType, Optional[StitchingSettings]]: + self, images_dir: RelativeDataPath + ) -> tuple[SettingModelType, Optional[StitchingSettings], tuple[int, int]]: """Return the scan settings and the stitching settings. - The specific settings for this scan workflow are returned as a Base Model of the type set when defining the class. - Stitiching settings are returned either as a StitchingSettings object or None is returned if it is not possible to stitch the scan. + - The save resolution as determined by a test image. """ raise NotImplementedError( "Each specific ScanWorkflow must implement a `all_settings` method." ) + def _get_save_resolution(self) -> tuple[int, int]: + """Return the save resolution as determined by a test image.""" + # Capture an example image. + image = self._cam._capture_image(capture_mode=self.capture_mode) + # Check side to create a unit faction for downsampling. + return image.size + def pre_scan_routine(self, settings: SettingModelType) -> None: """Overload to set the routine that happens before each scan.""" raise NotImplementedError( @@ -268,12 +276,11 @@ class RectGridWorkflow( ) return y_move_stage["x"], x_move_stage["y"] - def _get_stitching_settings_model(self) -> StitchingSettings: + def _get_stitching_settings_model( + self, save_resolution: tuple[int, int] + ) -> StitchingSettings: """Return a stitching settings model based on current settings.""" - # Use the save resolution and target stitch resolution to choose a unit fraction, - # which makes correlating faster - # TODO Calculate this using a camera method. - width, height = self.save_resolution + width, height = save_resolution # Target area in pixels target_area = TARGET_STITCHING_DIMENSION**2 # Find N so that (width/N) * (height/N) ~ target_area @@ -293,24 +300,24 @@ class RectGridWorkflow( return self._settings_model(**base_kwargs) def all_settings( - self, images_dir: str - ) -> tuple[RectGridSettingModelType, Optional[StitchingSettings]]: + self, images_dir: RelativeDataPath + ) -> tuple[RectGridSettingModelType, Optional[StitchingSettings], tuple[int, int]]: """Return scan settings and the stitching settings. :param images_dir: The directory that images are to be written to. - :return: A tuple containing the settings model for this workflow and the - settings model for stitching. + :return: A tuple containing the settings model for this workflow, the + settings model for stitching, and the save resolution. """ + save_resolution = self._get_save_resolution() # Developer Note: When subclassing RectGridWorkflow rather than override # this method first consider overriding _build_scan_settings - stitching_settings = self._get_stitching_settings_model() + stitching_settings = self._get_stitching_settings_model(save_resolution) dx, dy = self._calc_displacement_from_overlap(self.overlap) base_kwargs = { "overlap": self.overlap, "dx": dx, "dy": dy, - # TODO set images dir correctly as a RelDataPath "capture_params": CaptureParams( images_dir=images_dir, capture_mode=self.capture_mode ), @@ -319,7 +326,7 @@ class RectGridWorkflow( scan_settings = self._build_scan_settings(base_kwargs) - return scan_settings, stitching_settings + return scan_settings, stitching_settings, save_resolution @lt.property def ready(self) -> bool: diff --git a/src/openflexure_microscope_server/things/smart_scan.py b/src/openflexure_microscope_server/things/smart_scan.py index 84463e17..328a76cb 100644 --- a/src/openflexure_microscope_server/things/smart_scan.py +++ b/src/openflexure_microscope_server/things/smart_scan.py @@ -304,8 +304,8 @@ class SmartScanThing(OFMThing): # Ensure any PreviewStitcher created cannot be reused. self._preview_stitcher = None - # Remove any scan folders containing zero images. - self.purge_empty_scans() + # Remove any scan folders containing zero images. + self.purge_empty_scans() @_scan_running def _move_to_next_point( @@ -343,8 +343,8 @@ class SmartScanThing(OFMThing): if images_dir is None: raise RuntimeError("Couldn't run scan, images directory was not created.") - workflow_settings, stitching_settings = workflow.all_settings( - images_dir=images_dir + workflow_settings, stitching_settings, save_resolution = workflow.all_settings( + images_dir=self.create_data_path(images_dir, absolute=True) ) # If stitching settings is None then this workflow doesn't support stitching. @@ -356,7 +356,7 @@ class SmartScanThing(OFMThing): starting_position=starting_position, start_time=datetime.now(), stitch_automatically=auto_stitch, - save_resolution=workflow.save_resolution, + save_resolution=save_resolution, workflow=type(workflow).__name__, workflow_settings=workflow_settings, stitching_settings=stitching_settings, @@ -392,6 +392,9 @@ class SmartScanThing(OFMThing): starting x,y,z position. """ try: + # Change into streaming mode instantly so mode is correct when collecting + # Scan Data + self._cam.change_streaming_mode(mode="full_resolution") self._scan_data = self._collect_scan_data(workflow) images_dir = self.ongoing_scan.images_dir # Type narrowing @@ -401,7 +404,6 @@ class SmartScanThing(OFMThing): ) self.ongoing_scan.save_scan_data(self._scan_data) - self._cam.change_streaming_mode(mode="full_resolution") workflow.pre_scan_routine(self._scan_data.workflow_settings) # If stitching settings are None then this type of scan can't be stitched