Start splitting ScanData into workflow specific data.
This commit is contained in:
parent
5ce74cad8a
commit
b6343362b2
4 changed files with 249 additions and 161 deletions
|
|
@ -8,7 +8,7 @@ import shutil
|
|||
import threading
|
||||
import zipfile
|
||||
from datetime import datetime, timedelta
|
||||
from typing import Any, Mapping, Optional
|
||||
from typing import Any, Mapping, Optional, Self
|
||||
|
||||
from pydantic import (
|
||||
BaseModel,
|
||||
|
|
@ -16,6 +16,7 @@ from pydantic import (
|
|||
ValidationError,
|
||||
field_serializer,
|
||||
field_validator,
|
||||
model_validator,
|
||||
)
|
||||
|
||||
from openflexure_microscope_server.utilities import make_name_safe, requires_lock
|
||||
|
|
@ -29,6 +30,7 @@ STITCH_REGEX = re.compile(r"stitched\.jpe?g$")
|
|||
IMAGE_REGEX = re.compile(r"-?[0-9]+_-?[0-9]+\.jpe?g$")
|
||||
|
||||
SCAN_DATA_FILENAME = "scan_data.json"
|
||||
SCAN_DATA_SCHEMA_VERSION = 2
|
||||
|
||||
|
||||
class NotEnoughFreeSpaceError(IOError):
|
||||
|
|
@ -47,6 +49,56 @@ class ScanInfo(BaseModel):
|
|||
dzi: Optional[str]
|
||||
|
||||
|
||||
class StitchingData(BaseModel):
|
||||
"""The data needed to stitch a scan."""
|
||||
|
||||
correlation_resize: float
|
||||
"""The resize factor applied to images when the stitching program is correlating."""
|
||||
|
||||
overlap: float
|
||||
"""The overlap between adjacent images as a fraction of the image size."""
|
||||
|
||||
|
||||
def _coerce_lecacy_scan_data(data: dict) -> dict:
|
||||
"""Coerce any scan data from before version 2 into the version 2 format."""
|
||||
# Before the current version no schema_version was set
|
||||
if "schema_version" in data:
|
||||
return data
|
||||
|
||||
if "correlation_resize" and "overlap" in data:
|
||||
correlation_resize = data.pop("correlation_resize")
|
||||
# Note we don't pop overlap is a setting for the legacy workflow as well
|
||||
# as a stitching setting.
|
||||
# This is done because in future workflows the stitching overlap may be a
|
||||
# directly set setting or something that is calculated from other settings.
|
||||
overlap = data["overlap"]
|
||||
data["stitching_settings"] = StitchingData(
|
||||
correlation_resize=correlation_resize,
|
||||
overlap=overlap,
|
||||
)
|
||||
else:
|
||||
data["stitching_settings"] = None
|
||||
|
||||
# Add any legacy workflow settings that are found
|
||||
legacy_keys = [
|
||||
"overlap",
|
||||
"max_dist",
|
||||
"dx",
|
||||
"dy",
|
||||
"autofocus_dz",
|
||||
"autofocus_on",
|
||||
"skip_background",
|
||||
]
|
||||
workflow_settings = {}
|
||||
for key in legacy_keys:
|
||||
if key in data:
|
||||
workflow_settings[key] = data.pop(key)
|
||||
|
||||
data["workflow"] = "Legacy"
|
||||
data["workflow_settings"] = workflow_settings
|
||||
return data
|
||||
|
||||
|
||||
class ScanData(BaseModel):
|
||||
"""Data about a scan to be saved to a JSON file in the directory.
|
||||
|
||||
|
|
@ -60,42 +112,20 @@ class ScanData(BaseModel):
|
|||
|
||||
model_config = ConfigDict(extra="forbid")
|
||||
|
||||
schema_version: int = SCAN_DATA_SCHEMA_VERSION
|
||||
|
||||
scan_name: str
|
||||
"""The name of the scan i.e. scan_0001"""
|
||||
|
||||
starting_position: Mapping[str, int]
|
||||
"""The starting position in dictionary format."""
|
||||
|
||||
overlap: float
|
||||
"""The overlap between adjacent images as a fraction of the image size."""
|
||||
|
||||
max_dist: int
|
||||
"""The maximum distance the scan could move (in steps) from the starting position."""
|
||||
|
||||
dx: int
|
||||
"""The number of steps between adjacent images in x."""
|
||||
|
||||
dy: int
|
||||
"""The number of steps between adjacent images in y."""
|
||||
|
||||
autofocus_dz: int
|
||||
"""The z range used for autofocus (in steps)."""
|
||||
|
||||
autofocus_on: bool
|
||||
"""Whether autofocus is on."""
|
||||
|
||||
start_time: datetime
|
||||
"""The time the scan started."""
|
||||
|
||||
skip_background: bool
|
||||
"""Whether automatic background detection is on, skipping locations with no sample."""
|
||||
|
||||
stitch_automatically: bool
|
||||
"""Whether the scan is set to automatically stitch when complete."""
|
||||
|
||||
correlation_resize: float
|
||||
"""The resize factor applied to images when the stitching program is correlating."""
|
||||
|
||||
save_resolution: tuple[int, int]
|
||||
"""The resolution that scan images are saved at."""
|
||||
|
||||
|
|
@ -114,6 +144,18 @@ class ScanData(BaseModel):
|
|||
This should be set with ``set_final_data()`` to ensure duration is set.
|
||||
"""
|
||||
|
||||
workflow: str
|
||||
"""The class name of the workflow Thing."""
|
||||
|
||||
workflow_settings: dict[str, Any]
|
||||
"""The settings for this workflow."""
|
||||
|
||||
stitching_settings: Optional[StitchingData]
|
||||
"""The data needed to stitch a scan.
|
||||
|
||||
Set to None for types of scan that cannot be stitched.
|
||||
"""
|
||||
|
||||
def set_final_data(self, result: str) -> None:
|
||||
"""Set the final data for the scan, scan duration is automatically calculated.
|
||||
|
||||
|
|
@ -122,6 +164,22 @@ class ScanData(BaseModel):
|
|||
self.duration = datetime.now() - self.start_time
|
||||
self.scan_result = result
|
||||
|
||||
@model_validator(mode="before")
|
||||
@classmethod
|
||||
def coerce_legacy(cls, data: dict) -> dict:
|
||||
"""Coerce any legacy data."""
|
||||
return _coerce_lecacy_scan_data(data)
|
||||
|
||||
@model_validator(mode="after")
|
||||
def validate_schema_version(self) -> Self:
|
||||
"""Validate the schema version is as the current one."""
|
||||
if self.schema_version != SCAN_DATA_SCHEMA_VERSION:
|
||||
raise ValueError(
|
||||
f"Unsupported schema version {self.schema_version}, "
|
||||
f"expected {SCAN_DATA_SCHEMA_VERSION}"
|
||||
)
|
||||
return self
|
||||
|
||||
@field_validator("start_time", mode="before")
|
||||
@classmethod
|
||||
def parse_timestamp(cls, value: str | datetime) -> datetime:
|
||||
|
|
|
|||
|
|
@ -2,12 +2,15 @@ from typing import Mapping, Optional
|
|||
|
||||
import labthings_fastapi as lt
|
||||
|
||||
from openflexure_microscope_server.scan_directories import StitchingData
|
||||
from openflexure_microscope_server.scan_planners import ScanPlanner, SmartSpiral
|
||||
from openflexure_microscope_server.stitching import STITCHING_RESOLUTION
|
||||
from openflexure_microscope_server.things.autofocus import AutofocusThing
|
||||
from openflexure_microscope_server.things.background_detect import (
|
||||
BackgroundDetectAlgorithm,
|
||||
ChannelDeviationLUV,
|
||||
)
|
||||
from openflexure_microscope_server.things.camera_stage_mapping import CameraStageMapper
|
||||
|
||||
|
||||
class ScanWorkflow(lt.Thing):
|
||||
|
|
@ -17,9 +20,26 @@ class ScanWorkflow(lt.Thing):
|
|||
scan planning, aquisition routine.
|
||||
"""
|
||||
|
||||
_detector: Optional[BackgroundDetectAlgorithm | Mapping[str, BackgroundDetectAlgorithm]] = lt.thing_slot()
|
||||
# All workdlows must have at least one background detector and a set class for scan
|
||||
# planning
|
||||
_background_detector: Optional[
|
||||
BackgroundDetectAlgorithm | Mapping[str, BackgroundDetectAlgorithm]
|
||||
] = lt.thing_slot()
|
||||
_planner_cls: type[ScanPlanner]
|
||||
|
||||
# All workdlows set a save resolution
|
||||
save_resolution: tuple[int, int] = lt.setting(default=(1640, 1232))
|
||||
"""A tuple of the image resolution to capture."""
|
||||
|
||||
def check_before_start(self, scan_name: str) -> None:
|
||||
"""Check before the scan starts. Throw an error if the scan shouldn't start.
|
||||
|
||||
The scan_name is passed to this function to enable workflows to validate the
|
||||
scan name if needed.
|
||||
"""
|
||||
raise NotImplementedError(
|
||||
"Each specific ScanWorkflow must implement a check_before_start."
|
||||
)
|
||||
|
||||
@lt.property
|
||||
def ready(self) -> bool:
|
||||
|
|
@ -28,23 +48,141 @@ class ScanWorkflow(lt.Thing):
|
|||
"Each specific ScanWorkflow must implement a ready property."
|
||||
)
|
||||
|
||||
def all_settings(self) -> tuple[dict, Optional[StitchingData]]:
|
||||
"""Return the scan settings and the stitching settings.
|
||||
|
||||
- The specific settings for this scan workflow are returned as a dict.
|
||||
- Stitiching settings are returned either as a StitchingData object or None
|
||||
is returned if it is not possible to stitch the scan.
|
||||
"""
|
||||
raise NotImplementedError(
|
||||
"Each specific ScanWorkflow must implement a `all_settings`. "
|
||||
"method."
|
||||
)
|
||||
|
||||
def pre_scan_routine(self) -> None:
|
||||
raise NotImplementedError(
|
||||
"Each specific ScanWorkflow must implement a pre-scan routine."
|
||||
)
|
||||
|
||||
|
||||
class HistoScanWorkflow(ScanWorkflow):
|
||||
_detector: ChannelDeviationLUV = lt.thing_slot()
|
||||
# Thing Slots
|
||||
_background_detector: ChannelDeviationLUV = lt.thing_slot()
|
||||
_csm: CameraStageMapper = lt.thing_slot()
|
||||
_planner_cls: type[ScanPlanner] = SmartSpiral
|
||||
_autofocus: AutofocusThing = lt.thing_slot()
|
||||
|
||||
# Scan settings
|
||||
|
||||
skip_background: bool = lt.setting(default=True)
|
||||
"""Whether to detect and skip empty fields of view.
|
||||
|
||||
This uses the settings from the ``BackgroundDetectThing``.
|
||||
"""
|
||||
|
||||
autofocus_dz: int = lt.setting(default=1000)
|
||||
"""The z distance to perform an autofocus in steps."""
|
||||
|
||||
max_range: int = lt.setting(default=45000)
|
||||
"""The maximum distance in steps from the centre of the scan."""
|
||||
|
||||
overlap: float = lt.setting(default=0.45)
|
||||
"""The fraction (0-1) that adjacent images should overlap in x or y."""
|
||||
|
||||
# noqa, scan_name is unused but is needed for equivalence with other workflows.
|
||||
def check_before_start(self, scan_name: str) -> None: # noqa: ARG002
|
||||
"""Before starting a scan, check that background and camera-stage-mapping are set.
|
||||
|
||||
Raise error if:
|
||||
- background is to be skipped but is not set
|
||||
- camera stage mapping is not set
|
||||
|
||||
Raise warning if not using background detect that scan will go on until max steps reached
|
||||
"""
|
||||
if self._csm.calibration_required:
|
||||
raise RuntimeError("Camera Stage Mapping is not calibrated.")
|
||||
|
||||
if self.skip_background:
|
||||
if not self.background_detector.ready:
|
||||
raise RuntimeError(
|
||||
"Background is not set: you need to calibrate background detection."
|
||||
)
|
||||
else:
|
||||
self.logger.warning(
|
||||
"This scan will run in a spiral from the starting point "
|
||||
f"until you cancel it, or until it has moved by {self.max_range} steps "
|
||||
"in every direction. Make sure you watch it run to stop it leaving "
|
||||
"the area of interest, or (worse) leading the microscope's range "
|
||||
"of motion."
|
||||
)
|
||||
|
||||
@lt.property
|
||||
def ready(self) -> bool:
|
||||
"""Whether this scanworkflow is ready to start."""
|
||||
return self._detector.ready
|
||||
if self._csm.calibration_required:
|
||||
return False
|
||||
if not self.skip_background:
|
||||
return True
|
||||
return self._background_detector.ready
|
||||
|
||||
def all_settings(self) -> tuple[dict, StitchingData]:
|
||||
stitching_settings = StitchingData(
|
||||
overlap=self.overlap,
|
||||
correlation_resize=STITCHING_RESOLUTION[0] / self.save_resolution[0],
|
||||
)
|
||||
|
||||
dx, dy = self._calc_displacement_from_test_image(self.overlap)
|
||||
self.logger.info(
|
||||
f"Based on an overlap of {self.overlap}, the stage will make steps of "
|
||||
f"{dx}, {dy}"
|
||||
)
|
||||
|
||||
autofocus_dz = self.autofocus_dz
|
||||
if autofocus_dz == 0:
|
||||
self.logger.info("Running scan without autofocus")
|
||||
elif autofocus_dz <= 200:
|
||||
self.logger.warning(
|
||||
f"Your autofocus range is {autofocus_dz} steps, which is too short to "
|
||||
"attempt to focus. Running without autofocus"
|
||||
)
|
||||
autofocus_dz = 0
|
||||
|
||||
scan_settings = {
|
||||
"overlap": self.overlap,
|
||||
"max_dist": self.max_range,
|
||||
"dx": dx,
|
||||
"dy": dy,
|
||||
"autofocus_dz": autofocus_dz,
|
||||
"autofocus_on": bool(autofocus_dz),
|
||||
"skip_background": self.skip_background,
|
||||
}
|
||||
|
||||
return scan_settings, stitching_settings
|
||||
|
||||
def _calc_displacement_from_overlap(self, overlap: float) -> tuple[int, int]:
|
||||
"""Take a test image and use camera stage mapping to calculate x and y displacement.
|
||||
|
||||
:param overlap: The desired overlap as a fraction of the image. i.e. 0.5 means
|
||||
that each image should overlap its nearest neighbour by 50%.
|
||||
|
||||
:returns: (dx, dy) - the x and y displacements in steps
|
||||
"""
|
||||
csm_image_res = [int(i) for i in self._csm.image_resolution]
|
||||
|
||||
# Calculate displacements in image coordinates
|
||||
dx_img = csm_image_res[1] * (1 - overlap)
|
||||
dy_img = csm_image_res[0] * (1 - overlap)
|
||||
|
||||
x_move_stage = self._csm.convert_image_to_stage_coordinates(x=dx_img, y=0)
|
||||
y_move_stage = self._csm.convert_image_to_stage_coordinates(x=0, y=dy_img)
|
||||
|
||||
# Assume no rotation or skew and take only the aligned axis of vector.
|
||||
# Coerce to positive integer, but correct if x and y are flipped
|
||||
if abs(x_move_stage["x"]) > abs(x_move_stage["y"]):
|
||||
return x_move_stage["x"], y_move_stage["y"]
|
||||
# If not use the other stage axes
|
||||
return x_move_stage["y"], y_move_stage["x"]
|
||||
|
||||
def pre_scan_routine(self) -> None:
|
||||
self._autofocus.looping_autofocus(dz=self.autofocus_dz, start="centre")
|
||||
|
|
|
|||
|
|
@ -179,11 +179,11 @@ class SmartScanThing(lt.Thing):
|
|||
|
||||
@lt.action
|
||||
def sample_scan(self, scan_name: str = "") -> None:
|
||||
"""Move the stage to cover an area, taking images that can be tiled together.
|
||||
"""Move the stage to cover an area, taking images.
|
||||
|
||||
The stage will move in a pattern that grows outwards from the starting point,
|
||||
stopping once it is surrounded by "background" (as detected by the
|
||||
camera Thing) or reaches the "max_range" measured in steps.
|
||||
Depending on the way the stage moves depends on the selected worflow.
|
||||
If images overlap for a scan workdlow then the images can be stitched together
|
||||
into a larger composite image.
|
||||
"""
|
||||
got_lock = self._scan_lock.acquire(timeout=0.1)
|
||||
if not got_lock:
|
||||
|
|
@ -193,11 +193,13 @@ class SmartScanThing(lt.Thing):
|
|||
# the presence of `scan_data` is used during error handling to
|
||||
# determine whether the scan started.
|
||||
self._scan_data = None
|
||||
# probably make workflow a context manager with a lock?
|
||||
workflow = self._workflow
|
||||
try:
|
||||
self._check_background_and_csm_set()
|
||||
workflow.check_before_start(scan_name)
|
||||
self._ongoing_scan = self._scan_dir_manager.new_scan_dir(scan_name)
|
||||
self._latest_scan_name = self.ongoing_scan.name
|
||||
self._run_scan()
|
||||
self._run_scan(workflow)
|
||||
except Exception as e:
|
||||
# If _scan_data is set then scan started
|
||||
if self._scan_data is not None:
|
||||
|
|
@ -222,35 +224,6 @@ class SmartScanThing(lt.Thing):
|
|||
# Remove any scan folders containing zero images.
|
||||
self.purge_empty_scans()
|
||||
|
||||
@_scan_running
|
||||
def _check_background_and_csm_set(self) -> None:
|
||||
"""Before starting a scan, check that background and camera-stage-mapping are set.
|
||||
|
||||
Raise error if:
|
||||
- background is to be skipped but is not set
|
||||
- camera stage mapping is not set
|
||||
|
||||
Raise warning if not using background detect that scan will go on until max steps reached
|
||||
"""
|
||||
self._csm.assert_calibration()
|
||||
|
||||
if self.skip_background:
|
||||
if (
|
||||
self._cam.background_detector is None
|
||||
or not self._cam.background_detector.ready
|
||||
):
|
||||
raise RuntimeError(
|
||||
"Background is not set: you need to calibrate background detection."
|
||||
)
|
||||
else:
|
||||
self.logger.warning(
|
||||
"This scan will run in a spiral from the starting point "
|
||||
f"until you cancel it, or until it has moved by {self.max_range} steps "
|
||||
"in every direction. Make sure you watch it run to stop it leaving "
|
||||
"the area of interest, or (worse) leading the microscope's range "
|
||||
"of motion."
|
||||
)
|
||||
|
||||
@_scan_running
|
||||
def _move_to_next_point(
|
||||
self, next_point: tuple[int, int], z_estimate: Optional[int] = None
|
||||
|
|
@ -276,90 +249,26 @@ class SmartScanThing(lt.Thing):
|
|||
return (next_point[0], next_point[1], z_estimate)
|
||||
|
||||
@_scan_running
|
||||
def _calc_displacement_from_test_image(self, overlap: float) -> tuple[int, int]:
|
||||
"""Take a test image and use camera stage mapping to calculate x and y displacement.
|
||||
|
||||
:param overlap: The desired overlap as a fraction of the image. i.e. 0.5 means
|
||||
that each image should overlap its nearest neighbour by 50%.
|
||||
|
||||
:returns: (dx, dy) - the x and y displacements in steps
|
||||
"""
|
||||
if (
|
||||
self._csm.image_resolution is None
|
||||
or self._csm.image_to_stage_displacement_matrix is None
|
||||
):
|
||||
raise CSMUncalibratedError("Camera stage mapping is not calibrated")
|
||||
test_image = self._cam.grab_as_array()
|
||||
|
||||
test_image_res = list(test_image.shape)
|
||||
|
||||
csm_image_res = [int(i) for i in self._csm.image_resolution]
|
||||
|
||||
# If current stream width is different to csm calibration width,
|
||||
# perform the conversion here
|
||||
res_ratio = csm_image_res[0] / test_image_res[0]
|
||||
|
||||
# get displacement matrix. note it is for (y, x) not (x, y) coordinates
|
||||
csm_disp_matrix = np.array(self._csm.image_to_stage_displacement_matrix)
|
||||
csm_disp_matrix *= res_ratio
|
||||
|
||||
# Calculate displacements in image coordinates
|
||||
dx_img = test_image.shape[1] * (1 - overlap)
|
||||
dy_img = test_image.shape[0] * (1 - overlap)
|
||||
|
||||
# Calculate displacements in steps as vectors using a dot product with the matrix
|
||||
dx_vec = np.dot(np.array([0, dx_img]), csm_disp_matrix)
|
||||
dy_vec = np.dot(np.array([dy_img, 0]), csm_disp_matrix)
|
||||
|
||||
# Assume no rotation or skew and take only the aligned axis of vector.
|
||||
# Coerce to positive integer
|
||||
dx = int(np.abs(dx_vec[0]))
|
||||
dy = int(np.abs(dy_vec[1]))
|
||||
|
||||
return dx, dy
|
||||
|
||||
@_scan_running
|
||||
def _collect_scan_data(self) -> scan_directories.ScanData:
|
||||
def _collect_scan_data(self, workflow: ScanWorkflow) -> scan_directories.ScanData:
|
||||
"""Collect and return the data for this scan so it cannot be changed mid-scan."""
|
||||
# Record starting position so it can be returned to at end of scan.
|
||||
starting_position = self._stage.position
|
||||
overlap = self.overlap
|
||||
dx, dy = self._calc_displacement_from_test_image(overlap)
|
||||
correlation_resize = stitching.STITCHING_RESOLUTION[0] / self.save_resolution[0]
|
||||
|
||||
self.logger.debug(
|
||||
f"Resizing images when correlating by a factor of {correlation_resize}"
|
||||
)
|
||||
workflow_settings, stitching_settings = workflow.all_settings()
|
||||
|
||||
self.logger.info(
|
||||
f"Based on an overlap of {overlap}, we will make steps of {dx}, {dy}"
|
||||
)
|
||||
|
||||
autofocus_dz = self.autofocus_dz
|
||||
if autofocus_dz == 0:
|
||||
self.logger.info("Running scan without autofocus")
|
||||
elif autofocus_dz <= 200:
|
||||
self.logger.warning(
|
||||
f"Your autofocus range is {autofocus_dz} steps, which is too short to "
|
||||
"attempt to focus. Running without autofocus"
|
||||
)
|
||||
autofocus_dz = 0
|
||||
# If stitching settings is None then this workflow doesn't support stitching.
|
||||
auto_stitch = self.stitch_automatically and stitching_settings is not None
|
||||
|
||||
# Fix scan parameters in case UI is updated during scan.
|
||||
return scan_directories.ScanData(
|
||||
scan_name=self.ongoing_scan.name,
|
||||
starting_position=starting_position,
|
||||
overlap=overlap,
|
||||
max_dist=self.max_range,
|
||||
dx=dx,
|
||||
dy=dy,
|
||||
autofocus_dz=autofocus_dz,
|
||||
autofocus_on=bool(autofocus_dz),
|
||||
start_time=datetime.now(),
|
||||
skip_background=self.skip_background,
|
||||
stitch_automatically=self.stitch_automatically,
|
||||
correlation_resize=correlation_resize,
|
||||
save_resolution=self.save_resolution,
|
||||
stitch_automatically=auto_stitch,
|
||||
save_resolution=workflow.save_resolution,
|
||||
workflow=type(workflow).__name__,
|
||||
workflow_settings=workflow_settings,
|
||||
stitching_settings=stitching_settings,
|
||||
)
|
||||
|
||||
@_scan_running
|
||||
|
|
@ -381,7 +290,7 @@ class SmartScanThing(lt.Thing):
|
|||
self.preview_stitcher.start()
|
||||
|
||||
@_scan_running
|
||||
def _run_scan(self) -> None:
|
||||
def _run_scan(self, workflow: ScanWorkflow) -> None:
|
||||
"""Prepare and run the main scan, and perform final actions on completion.
|
||||
|
||||
The result (or exception) from the main scan loop determines whether the
|
||||
|
|
@ -389,10 +298,8 @@ class SmartScanThing(lt.Thing):
|
|||
starting x,y,z position.
|
||||
"""
|
||||
try:
|
||||
# probably make workflow a context manager with a lock?
|
||||
workflow = self._workflow
|
||||
self._cam.start_streaming(main_resolution=(3280, 2464))
|
||||
self._scan_data = self._collect_scan_data()
|
||||
self._scan_data = self._collect_scan_data(workflow)
|
||||
workflow.pre_scan_routine(self._scan_data)
|
||||
self.ongoing_scan.save_scan_data(self._scan_data)
|
||||
images_dir = self.ongoing_scan.images_dir
|
||||
|
|
@ -572,24 +479,9 @@ class SmartScanThing(lt.Thing):
|
|||
raise HTTPException(404, "File not found")
|
||||
return FileResponse(preview_path)
|
||||
|
||||
save_resolution: tuple[int, int] = lt.setting(default=(1640, 1232))
|
||||
"""A tuple of the image resolution to capture."""
|
||||
|
||||
max_range: int = lt.setting(default=45000)
|
||||
"""The maximum distance in steps from the centre of the scan."""
|
||||
|
||||
stitch_tiff: bool = lt.setting(default=False)
|
||||
"""Whether or not to also produce a pyramidal tiff at the end of a scan."""
|
||||
|
||||
skip_background: bool = lt.setting(default=True)
|
||||
"""Whether to detect and skip empty fields of view.
|
||||
|
||||
This uses the settings from the ``BackgroundDetectThing``.
|
||||
"""
|
||||
|
||||
overlap: float = lt.setting(default=0.45)
|
||||
"""The fraction (0-1) that adjacent images should overlap in x or y."""
|
||||
|
||||
stitch_automatically: bool = lt.setting(default=True)
|
||||
"""Whether to run a final stitch at the end of a successful scan."""
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue