Add punctuation to docstrings
This commit is contained in:
parent
4dc41bb008
commit
80beeea07b
34 changed files with 232 additions and 235 deletions
|
|
@ -87,7 +87,7 @@ class OFMHandler(logging.Handler):
|
|||
self._log.pop(0)
|
||||
|
||||
def emit(self, record):
|
||||
"""Emit will save the logged record to the log"""
|
||||
"""Emit will save the logged record to the log."""
|
||||
# Basic filter for now that simply stops uvicorn.access logs
|
||||
# These are the logs each time an API endpoint is accessed
|
||||
# This is only the log for the UI.
|
||||
|
|
@ -105,7 +105,7 @@ class OFMHandler(logging.Handler):
|
|||
|
||||
@property
|
||||
def log_history(self):
|
||||
"""Return the log history up to the maximum number of logs"""
|
||||
"""Return the log history up to the maximum number of logs."""
|
||||
return "\n".join(self._log)
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -20,7 +20,7 @@ class NotEnoughFreeSpaceError(IOError):
|
|||
|
||||
|
||||
class ScanInfo(BaseModel):
|
||||
"""Summary information about a scan folder"""
|
||||
"""Summary information about a scan folder."""
|
||||
|
||||
name: str
|
||||
created: float
|
||||
|
|
@ -31,7 +31,7 @@ class ScanInfo(BaseModel):
|
|||
|
||||
|
||||
class ScanDirectoryManager:
|
||||
"""A class for managing interactions with scan directories"""
|
||||
"""A class for managing interactions with scan directories."""
|
||||
|
||||
_base_scan_dir: str
|
||||
|
||||
|
|
@ -42,22 +42,22 @@ class ScanDirectoryManager:
|
|||
|
||||
@property
|
||||
def base_dir(self) -> str:
|
||||
"""The base directory scans are saved to"""
|
||||
"""The base directory scans are saved to."""
|
||||
return self._base_scan_dir
|
||||
|
||||
def exists(self, scan_name: str) -> bool:
|
||||
"""Return True if scan of this name exists on disk"""
|
||||
"""Return True if scan of this name exists on disk."""
|
||||
return os.path.isdir(self.path_for(scan_name))
|
||||
|
||||
def path_for(self, scan_name: str) -> str:
|
||||
"""Return the path for a given scan name
|
||||
"""Return the path for a given scan name.
|
||||
|
||||
Returns the path even if it doesn't exist
|
||||
"""
|
||||
return os.path.join(self._base_scan_dir, scan_name)
|
||||
|
||||
def img_dir_for(self, scan_name: str) -> str:
|
||||
"""Return the path for the image dir for a given scan name
|
||||
"""Return the path for the image dir for a given scan name.
|
||||
|
||||
Returns the path even if it doesn't exist
|
||||
"""
|
||||
|
|
@ -66,7 +66,7 @@ class ScanDirectoryManager:
|
|||
def get_file_path_from(
|
||||
self, scan_name: str, filename: str, check_exists: bool = False
|
||||
) -> Optional[str]:
|
||||
"""Return the full file path for the file within a scan directory
|
||||
"""Return the full file path for the file within a scan directory.
|
||||
|
||||
If check_exists is True then None will be returned if the file does
|
||||
not exist.
|
||||
|
|
@ -80,7 +80,7 @@ class ScanDirectoryManager:
|
|||
def get_file_path_from_img_dir(
|
||||
self, scan_name: str, filename: str, check_exists: bool = False
|
||||
) -> Optional[str]:
|
||||
"""Return the full file path for the file within a scan directory
|
||||
"""Return the full file path for the file within a scan directory.
|
||||
|
||||
If check_exists is True, None is returned if the file does not exist. If False
|
||||
then the path is returned anyway
|
||||
|
|
@ -103,18 +103,18 @@ class ScanDirectoryManager:
|
|||
|
||||
@property
|
||||
def all_scans(self) -> list[str]:
|
||||
"""Return a list of the scan names in the base directory"""
|
||||
"""Return a list of the scan names in the base directory."""
|
||||
return [f.name for f in os.scandir(self._base_scan_dir) if f.is_dir()]
|
||||
|
||||
def all_scans_info(self) -> list[ScanInfo]:
|
||||
"""Return a lists of ScanInfo objects for each scan"""
|
||||
"""Return a lists of ScanInfo objects for each scan."""
|
||||
all_info: list[ScanInfo] = []
|
||||
for scan_name in self.all_scans:
|
||||
all_info.append(ScanDirectory(scan_name, self.base_dir).scan_info())
|
||||
return all_info
|
||||
|
||||
def _unique_scan_name(self, scan_name: str) -> str:
|
||||
"""Get the next unique scan name starting with the given name
|
||||
"""Get the next unique scan name starting with the given name.
|
||||
|
||||
For more explanation on the scan naming see `new_scan_dir`
|
||||
"""
|
||||
|
|
@ -143,7 +143,7 @@ class ScanDirectoryManager:
|
|||
return f"{scan_name}_{scan_num:0{SCAN_ZERO_PAD_DIGITS}d}"
|
||||
|
||||
def new_scan_dir(self, scan_name: str) -> "ScanDirectory":
|
||||
"""Get a unique name for this scan and create a directory for it
|
||||
"""Get a unique name for this scan and create a directory for it.
|
||||
|
||||
The scan will be named ``{scan_name}_0001`` where the number is
|
||||
zero-padded to be SCAN_ZERO_PAD_DIGITS digits long (to allow correct sorting if the
|
||||
|
|
@ -165,11 +165,11 @@ class ScanDirectoryManager:
|
|||
return ScanDirectory(full_scan_name, self.base_dir)
|
||||
|
||||
def delete_scan(self, scan_name: str) -> None:
|
||||
"""Delete a scan"""
|
||||
"""Delete a scan."""
|
||||
shutil.rmtree(self.path_for(scan_name))
|
||||
|
||||
def zip_scan(self, scan_name: str, final_version: bool = False) -> "ScanDirectory":
|
||||
"""Zips any images from the scan not yet zipped, return full path to zip
|
||||
"""Zips any images from the scan not yet zipped, return full path to zip.
|
||||
|
||||
``final_version`` Set true to stitch all files not just the scan images
|
||||
this should only be done at the end as it is not possible to update a file
|
||||
|
|
@ -178,7 +178,7 @@ class ScanDirectoryManager:
|
|||
return ScanDirectory(scan_name, self.base_dir).zip_files(final_version)
|
||||
|
||||
def check_free_disk_space(self, min_space: int = 500000000) -> None:
|
||||
"""Raise an exception if there is not enough free disk space to continue scanning
|
||||
"""Raise an exception if there is not enough free disk space to continue scanning.
|
||||
|
||||
:param min_space: the minimum space required in bytes.
|
||||
Default = 500,000,000 (500MB)
|
||||
|
|
@ -194,7 +194,7 @@ class ScanDirectoryManager:
|
|||
|
||||
|
||||
class ScanDirectory:
|
||||
"""A class for handling interactions with scan directories
|
||||
"""A class for handling interactions with scan directories.
|
||||
|
||||
Initalisation parameters:
|
||||
name: the name of the scan (the scan directory basename)
|
||||
|
|
@ -214,12 +214,12 @@ class ScanDirectory:
|
|||
|
||||
@property
|
||||
def name(self) -> str:
|
||||
"""The name of the scan"""
|
||||
"""The name of the scan."""
|
||||
return self._name
|
||||
|
||||
@property
|
||||
def dir_path(self) -> str:
|
||||
"""The full path to the scan directory"""
|
||||
"""The full path to the scan directory."""
|
||||
return os.path.join(self._base_scan_dir, self._name)
|
||||
|
||||
@property
|
||||
|
|
@ -235,17 +235,17 @@ class ScanDirectory:
|
|||
|
||||
@property
|
||||
def created_time(self) -> float:
|
||||
"""The time the directory was created on disk"""
|
||||
"""The time the directory was created on disk."""
|
||||
return os.path.getctime(self.dir_path)
|
||||
|
||||
def get_scan_files(self):
|
||||
"""Return a list of the files in the images dir"""
|
||||
"""Return a list of the files in the images dir."""
|
||||
if self.images_dir is None:
|
||||
return []
|
||||
return os.listdir(self.images_dir)
|
||||
|
||||
def _extract_scan_images(self, file_list: list[str]):
|
||||
"""Extract files which match the naming convention for scan images
|
||||
"""Extract files which match the naming convention for scan images.
|
||||
|
||||
:param file_list: The list of files to search. Normally this would be
|
||||
``self.get_scan_files()``
|
||||
|
|
@ -255,7 +255,7 @@ class ScanDirectory:
|
|||
return [i for i in file_list if IMAGE_REGEX.search(i)]
|
||||
|
||||
def _extract_final_stitches(self, file_list: list[str]):
|
||||
"""Extract files which match the naming convention for final stitches
|
||||
"""Extract files which match the naming convention for final stitches.
|
||||
|
||||
:param file_list: The list of files to search.
|
||||
|
||||
|
|
@ -264,7 +264,7 @@ class ScanDirectory:
|
|||
return [i for i in file_list if STITCH_REGEX.search(i)]
|
||||
|
||||
def _extract_dzi_files(self, file_list: list[str]):
|
||||
"""Extract files which match the naming convention for dzi_files
|
||||
"""Extract files which match the naming convention for dzi_files.
|
||||
|
||||
:param file_list: The list of files to search.
|
||||
|
||||
|
|
@ -273,7 +273,7 @@ class ScanDirectory:
|
|||
return [i for i in file_list if i.endswith("dzi")]
|
||||
|
||||
def get_final_stitch_name(self) -> Optional[str]:
|
||||
"""Return the filename for the final stitch (in the images dir)
|
||||
"""Return the filename for the final stitch (in the images dir).
|
||||
|
||||
If no final stitch is found, return None
|
||||
"""
|
||||
|
|
@ -283,11 +283,11 @@ class ScanDirectory:
|
|||
return stitches[0]
|
||||
|
||||
def get_modified_time(self) -> float:
|
||||
"""Return the modified time of the directory"""
|
||||
"""Return the modified time of the directory."""
|
||||
return max(os.stat(root).st_mtime for root, _, _ in os.walk(self.dir_path))
|
||||
|
||||
def scan_info(self) -> ScanInfo:
|
||||
"""Return the information for the scan directory as a ScanInfo object"""
|
||||
"""Return the information for the scan directory as a ScanInfo object."""
|
||||
scan_files = self.get_scan_files()
|
||||
scan_images = self._extract_scan_images(scan_files)
|
||||
stitches = self._extract_final_stitches(scan_files)
|
||||
|
|
@ -325,7 +325,7 @@ class ScanDirectory:
|
|||
return files
|
||||
|
||||
def zip_files(self, final_version: bool = False) -> str:
|
||||
"""Zips any images from the scan not yet zipped, return full path to zip
|
||||
"""Zips any images from the scan not yet zipped, return full path to zip.
|
||||
|
||||
``final_version`` Set true to stitch all files not just the scan images
|
||||
this should only be done at the end as it is not possible to update a file
|
||||
|
|
@ -357,6 +357,6 @@ class ScanDirectory:
|
|||
|
||||
|
||||
def get_files_in_zip(zip_path):
|
||||
"""List the relative paths of all files and folders in the zip folder specified"""
|
||||
"""List the relative paths of all files and folders in the zip folder specified."""
|
||||
scan_zip = zipfile.ZipFile(zip_path)
|
||||
return [os.path.normpath(i) for i in scan_zip.namelist()]
|
||||
|
|
|
|||
|
|
@ -115,7 +115,7 @@ class ScanPlanner:
|
|||
|
||||
@property
|
||||
def imaged_locations(self) -> XYZPosList:
|
||||
"""Property to access a copy of the imaged_locations"""
|
||||
"""Property to access a copy of the imaged_locations."""
|
||||
return copy(self._imaged_locations)
|
||||
|
||||
@property
|
||||
|
|
@ -145,12 +145,12 @@ class ScanPlanner:
|
|||
raise NotImplementedError("Did you call the ScanPlanner base class?")
|
||||
|
||||
def position_visited(self, position: XYPos) -> bool:
|
||||
"""Return True if input xy position has been visited before"""
|
||||
"""Return True if input xy position has been visited before."""
|
||||
# Ensure tuple for correct matching!
|
||||
return tuple(position) in self._path_history
|
||||
|
||||
def position_planned(self, position: XYPos) -> bool:
|
||||
"""Return True if input xy position is planned"""
|
||||
"""Return True if input xy position is planned."""
|
||||
# Ensure tuple for correct matching!
|
||||
return tuple(position) in self._remaining_locations
|
||||
|
||||
|
|
@ -205,7 +205,7 @@ class ScanPlanner:
|
|||
def mark_location_visited(
|
||||
self, xyz_pos: XYZPos, imaged: bool, focused: bool
|
||||
) -> None:
|
||||
"""Mark the location as visited
|
||||
"""Mark the location as visited.
|
||||
|
||||
Args:
|
||||
xyz_pos: the x_y_z position
|
||||
|
|
@ -282,7 +282,7 @@ class SmartSpiral(ScanPlanner):
|
|||
def mark_location_visited(
|
||||
self, xyz_pos: XYZPos, imaged: bool = True, focused: bool = True
|
||||
) -> None:
|
||||
"""Mark the location as visited. Adjust extra positions accordingly
|
||||
"""Mark the location as visited. Adjust extra positions accordingly.
|
||||
|
||||
Args:
|
||||
xyz_pos: the x_y_z position
|
||||
|
|
@ -327,7 +327,7 @@ class SmartSpiral(ScanPlanner):
|
|||
self._remaining_locations.append(new_pos)
|
||||
|
||||
def _re_sort_remaining_locations(self, current_pos: XYPos) -> None:
|
||||
"""Sort the remaining positions besed on the current location"""
|
||||
"""Sort the remaining positions besed on the current location."""
|
||||
|
||||
# Defined rather than use a lambda for readability
|
||||
def sort_key(pos):
|
||||
|
|
@ -421,7 +421,7 @@ class SmartSpiral(ScanPlanner):
|
|||
def distance_between(
|
||||
current_pos: XYPos | np.ndarray, next_pos: XYPos | np.ndarray
|
||||
) -> float:
|
||||
"""Calculate the distance between the two xy positions
|
||||
"""Calculate the distance between the two xy positions.
|
||||
|
||||
This was previously called ``distance_to_site``
|
||||
"""
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ from ..logging import configure_logging, retrieve_log, retrieve_log_from_file
|
|||
|
||||
|
||||
def set_shutdown_function(shutdown_function: Callable[[], None]):
|
||||
"""Ensure a function is called before the shutdown
|
||||
"""Ensure a function is called before the shutdown.
|
||||
|
||||
This monkey patches the Uvicorn Server's handle_exit. This is needed because
|
||||
the uvicorn ``lifecycle`` events and FastAPI ``shutdown`` events only fire once
|
||||
|
|
@ -65,7 +65,7 @@ def _get_scans_dir(config: dict) -> Optional[str]:
|
|||
|
||||
|
||||
def serve_from_cli(argv: Optional[list[str]] = None):
|
||||
"""Start the server from the command line"""
|
||||
"""Start the server from the command line."""
|
||||
args = lt.cli.parse_args(argv)
|
||||
|
||||
log_config = copy(uvicorn.config.LOGGING_CONFIG)
|
||||
|
|
|
|||
|
|
@ -25,7 +25,7 @@ def add_static_file(app: FastAPI, fname: str, folder: str) -> None:
|
|||
|
||||
|
||||
def add_static_files(app: FastAPI, scans_folder: Optional[str]) -> None:
|
||||
"""Add the static files responsible for the webapp app to the FastAPI app
|
||||
"""Add the static files responsible for the webapp app to the FastAPI app.
|
||||
|
||||
app: The FastAPI app to add to, in this case the OpenFlexure server
|
||||
"""
|
||||
|
|
|
|||
|
|
@ -22,7 +22,7 @@ class RecentringThing(lt.Thing):
|
|||
max_steps=15,
|
||||
lateral_distance=5000,
|
||||
):
|
||||
"""Recentre the stage, based on the focal plane
|
||||
"""Recentre the stage, based on the focal plane.
|
||||
|
||||
Autofocuses at multiple points around the sample to
|
||||
find the overall maximum (or minimum) height, which
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
"""OpenFlexure Microscope autofocus module
|
||||
"""OpenFlexure Microscope autofocus module.
|
||||
|
||||
This module defines a Thing that is responsible for using the stage and
|
||||
camera together to perform an autofocus routine.
|
||||
|
|
@ -26,7 +26,7 @@ from .stage import StageDependency as Stage
|
|||
|
||||
|
||||
class StackParams:
|
||||
"""A class for holding for scan parameters
|
||||
"""A class for holding for scan parameters.
|
||||
|
||||
All arguments are keyword only
|
||||
|
||||
|
|
@ -97,7 +97,7 @@ class StackParams:
|
|||
|
||||
@property
|
||||
def stack_z_range(self) -> int:
|
||||
"""The range of the z stack, in steps
|
||||
"""The range of the z stack, in steps.
|
||||
|
||||
Note that this is the range of the minimum number of images captured,
|
||||
which is also the range of the images stored in memory that can be
|
||||
|
|
@ -107,7 +107,7 @@ class StackParams:
|
|||
|
||||
@property
|
||||
def steps_undershoot(self) -> int:
|
||||
"""The distance to deliberately undershoot the estimated optimal starting point"""
|
||||
"""The distance to deliberately undershoot the estimated optimal starting point."""
|
||||
# Starting too low by "steps_undershoot" makes smart stacking faster.
|
||||
# Starting a stack too high requires it to move to the start,
|
||||
# autofocus and then re-stack. Starting slightly too low only
|
||||
|
|
@ -116,14 +116,14 @@ class StackParams:
|
|||
|
||||
@property
|
||||
def max_images_to_test(self) -> int:
|
||||
"""The maximum number of images that will be captured and tested in a stack
|
||||
"""The maximum number of images that will be captured and tested in a stack.
|
||||
|
||||
This is 15 images more then the minimum number that are captured.
|
||||
"""
|
||||
return self.min_images_to_test + 15
|
||||
|
||||
def slice_to_save(self, sharpest_index):
|
||||
"""Return the slice of images to save given the index of the sharpest image"""
|
||||
"""Return the slice of images to save given the index of the sharpest image."""
|
||||
images_each_side = (self.images_to_save - 1) // 2
|
||||
return slice(
|
||||
max(sharpest_index - images_each_side, 0),
|
||||
|
|
@ -133,7 +133,7 @@ class StackParams:
|
|||
|
||||
@dataclass
|
||||
class CaptureInfo:
|
||||
"""The information from a capture in a z_stack"""
|
||||
"""The information from a capture in a z_stack."""
|
||||
|
||||
buffer_id: int
|
||||
position: dict[str, int]
|
||||
|
|
@ -141,7 +141,7 @@ class CaptureInfo:
|
|||
|
||||
@property
|
||||
def filename(self) -> str:
|
||||
"""The filename for this image generated from the position"""
|
||||
"""The filename for this image generated from the position."""
|
||||
return f"{self.position['x']}_{self.position['y']}_{self.position['z']}.jpeg"
|
||||
|
||||
|
||||
|
|
@ -195,7 +195,7 @@ class JPEGSharpnessMonitor:
|
|||
running = False
|
||||
|
||||
async def monitor_sharpness(self):
|
||||
"""Start monitoring the frame sizes"""
|
||||
"""Start monitoring the frame sizes."""
|
||||
self.running = True
|
||||
async for frame in self.camera.lores_mjpeg_stream.frame_async_generator():
|
||||
self.jpeg_times.append(time.time())
|
||||
|
|
@ -205,7 +205,7 @@ class JPEGSharpnessMonitor:
|
|||
|
||||
@contextmanager
|
||||
def run(self):
|
||||
"""Context manager, during which we will monitor sharpness from the camera"""
|
||||
"""Context manager, during which we will monitor sharpness from the camera."""
|
||||
self.portal.start_task_soon(self.monitor_sharpness)
|
||||
try:
|
||||
yield
|
||||
|
|
@ -233,7 +233,7 @@ class JPEGSharpnessMonitor:
|
|||
def move_data(
|
||||
self, istart: int, istop: Optional[int] = None
|
||||
) -> tuple[np.ndarray, np.ndarray, np.ndarray]:
|
||||
"""Extract sharpness as a function of (interpolated) z"""
|
||||
"""Extract sharpness as a function of (interpolated) z."""
|
||||
if istop is None:
|
||||
istop = istart + 2
|
||||
jpeg_times: np.ndarray = np.array(self.jpeg_times)
|
||||
|
|
@ -261,7 +261,7 @@ class JPEGSharpnessMonitor:
|
|||
return jpeg_times, jpeg_zs, jpeg_sizes[start:stop]
|
||||
|
||||
def sharpest_z_on_move(self, index: int) -> int:
|
||||
"""Return the z position of the sharpest image on a given move"""
|
||||
"""Return the z position of the sharpest image on a given move."""
|
||||
_, jz, js = self.move_data(index)
|
||||
if len(js) == 0:
|
||||
raise ValueError(
|
||||
|
|
@ -270,7 +270,7 @@ class JPEGSharpnessMonitor:
|
|||
return jz[np.argmax(js)]
|
||||
|
||||
def data_dict(self) -> SharpnessDataArrays:
|
||||
"""Return the gathered data as a single convenient dictionary"""
|
||||
"""Return the gathered data as a single convenient dictionary."""
|
||||
data = {}
|
||||
for k in ["jpeg_times", "jpeg_sizes", "stage_times", "stage_positions"]:
|
||||
data[k] = getattr(self, k)
|
||||
|
|
@ -295,7 +295,7 @@ class AutofocusThing(lt.Thing):
|
|||
dz: int = 2000,
|
||||
start: str = "centre",
|
||||
) -> SharpnessDataArrays:
|
||||
"""Sweep the stage up and down, then move to the sharpest point
|
||||
"""Sweep the stage up and down, then move to the sharpest point.
|
||||
|
||||
This method will will move down by dz/2, sweep up by dz, and then evaluate
|
||||
the position where the image was sharpest. We'll then move back down, and
|
||||
|
|
@ -325,7 +325,7 @@ class AutofocusThing(lt.Thing):
|
|||
dz: Sequence[int],
|
||||
wait: float = 0,
|
||||
) -> SharpnessDataArrays:
|
||||
"""Make a move (or a series of moves) and monitor sharpness
|
||||
"""Make a move (or a series of moves) and monitor sharpness.
|
||||
|
||||
This method will will make a series of relative moves in z, and
|
||||
return the sharpness (JPEG size) vs time, along with timestamps
|
||||
|
|
@ -561,7 +561,7 @@ class AutofocusThing(lt.Thing):
|
|||
cam: WrappedCamera,
|
||||
stage: Stage,
|
||||
) -> tuple[bool, list[CaptureInfo], Optional[int]]:
|
||||
"""Capture a series of images checking that sharpest image central
|
||||
"""Capture a series of images checking that sharpest image central.
|
||||
|
||||
The images are seperated in z offset by stack_parameters.stack_dz, as they
|
||||
are captured the last stack_parameters.min_images_to_test images are checked
|
||||
|
|
|
|||
|
|
@ -22,7 +22,7 @@ class BackgroundDetectThing(lt.Thing):
|
|||
|
||||
@lt.thing_setting
|
||||
def background_distributions(self) -> Optional[ChannelDistributions]:
|
||||
"""The statistics of the background image"""
|
||||
"""The statistics of the background image."""
|
||||
bd = self._background_distributions
|
||||
if bd is None:
|
||||
return None
|
||||
|
|
@ -56,7 +56,7 @@ class BackgroundDetectThing(lt.Thing):
|
|||
)
|
||||
|
||||
def background_mask(self, image: np.ndarray) -> np.ndarray:
|
||||
"""Calculate a binary image, showing whether each pixel is background
|
||||
"""Calculate a binary image, showing whether each pixel is background.
|
||||
|
||||
The image should be in LUV format, the ouput will be binary with the
|
||||
same shape in the first two dimensions.
|
||||
|
|
@ -78,7 +78,7 @@ class BackgroundDetectThing(lt.Thing):
|
|||
|
||||
@lt.thing_action
|
||||
def background_fraction(self, cam: CamDep) -> float:
|
||||
"""Determine what fraction of the current image is background
|
||||
"""Determine what fraction of the current image is background.
|
||||
|
||||
This action will acquire a new image from the preview stream, then
|
||||
evaluate whether it is foreground or background, by comparing it
|
||||
|
|
@ -96,7 +96,7 @@ class BackgroundDetectThing(lt.Thing):
|
|||
|
||||
@lt.thing_action
|
||||
def image_is_sample(self, cam: CamDep) -> bool:
|
||||
"""Label the current image as either background or sample"""
|
||||
"""Label the current image as either background or sample."""
|
||||
b_fraction = self.background_fraction(cam)
|
||||
fraction_threshold = self.fraction
|
||||
|
||||
|
|
@ -104,7 +104,7 @@ class BackgroundDetectThing(lt.Thing):
|
|||
|
||||
@lt.thing_action
|
||||
def set_background(self, cam: CamDep):
|
||||
"""Grab an image, and use its statistics to set the background
|
||||
"""Grab an image, and use its statistics to set the background.
|
||||
|
||||
This should be run when the microscope is looking at an empty region,
|
||||
and will calculate the mean and standard deviation of the pixel values
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
"""OpenFlexure Microscope Camera
|
||||
"""OpenFlexure Microscope Camera.
|
||||
|
||||
This module defines the interface for cameras. Any compatible lt.Thing
|
||||
should enabe the server to work.
|
||||
|
|
@ -23,23 +23,23 @@ class JPEGBlob(lt.blob.Blob):
|
|||
|
||||
|
||||
class PNGBlob(lt.blob.Blob):
|
||||
"""A class representing a PNG image as a LabThings FastAPI Blob"""
|
||||
"""A class representing a PNG image as a LabThings FastAPI Blob."""
|
||||
|
||||
media_type: str = "image/png"
|
||||
|
||||
|
||||
class ArrayModel(RootModel):
|
||||
"""A model for an array"""
|
||||
"""A model for an array."""
|
||||
|
||||
root: NDArray
|
||||
|
||||
|
||||
class CaptureError(RuntimeError):
|
||||
"""An error trying to capture from a CameraThing"""
|
||||
"""An error trying to capture from a CameraThing."""
|
||||
|
||||
|
||||
class NoImageInMemoryError(RuntimeError):
|
||||
"""An error called if no image in in memory when an method is called to use that image"""
|
||||
"""An error called if no image in in memory when an method is called to use that image."""
|
||||
|
||||
|
||||
class CameraMemoryBuffer:
|
||||
|
|
@ -61,7 +61,7 @@ class CameraMemoryBuffer:
|
|||
def add_image(
|
||||
self, image: Any, metadata: Optional[dict] = None, buffer_max: int = 1
|
||||
) -> int:
|
||||
"""Add an image to the Memory buffer
|
||||
"""Add an image to the Memory buffer.
|
||||
|
||||
This will add an image to the memory buffer. By default the buffer will
|
||||
be cleared. To allow saving multiple images the buffer_max must be set
|
||||
|
|
@ -114,7 +114,7 @@ class CameraMemoryBuffer:
|
|||
) from e
|
||||
|
||||
def clear(self):
|
||||
"""Clear all images from memory"""
|
||||
"""Clear all images from memory."""
|
||||
self._storage.clear()
|
||||
|
||||
def _create_space(self, buffer_max: int) -> None:
|
||||
|
|
@ -140,7 +140,7 @@ class CameraMemoryBuffer:
|
|||
|
||||
|
||||
class BaseCamera(lt.Thing):
|
||||
"""The base class for all cameras. All cameras must directly inherit from this class"""
|
||||
"""The base class for all cameras. All cameras must directly inherit from this class."""
|
||||
|
||||
mjpeg_stream = lt.outputs.MJPEGStreamDescriptor()
|
||||
lores_mjpeg_stream = lt.outputs.MJPEGStreamDescriptor()
|
||||
|
|
@ -181,7 +181,7 @@ class BaseCamera(lt.Thing):
|
|||
|
||||
@lt.thing_property
|
||||
def stream_active(self) -> bool:
|
||||
"""Whether the MJPEG stream is active"""
|
||||
"""Whether the MJPEG stream is active."""
|
||||
raise NotImplementedError(
|
||||
"CameraThings must define their own stream_active method"
|
||||
)
|
||||
|
|
@ -203,7 +203,7 @@ class BaseCamera(lt.Thing):
|
|||
resolution: Literal["lores", "main", "full"] = "main",
|
||||
wait: Optional[float] = 5,
|
||||
) -> JPEGBlob:
|
||||
"""Acquire one image from the camera and return as a JPEG blob"""
|
||||
"""Acquire one image from the camera and return as a JPEG blob."""
|
||||
raise NotImplementedError(
|
||||
"CameraThings must define their own capture_jpeg method"
|
||||
)
|
||||
|
|
@ -214,7 +214,7 @@ class BaseCamera(lt.Thing):
|
|||
portal: lt.deps.BlockingPortal,
|
||||
stream_name: Literal["main", "lores"] = "main",
|
||||
) -> JPEGBlob:
|
||||
"""Acquire one image from the preview stream and return as an array
|
||||
"""Acquire one image from the preview stream and return as an array.
|
||||
|
||||
This differs from ``capture_jpeg`` in that it does not pause the MJPEG
|
||||
preview stream. Instead, we simply return the next frame from that
|
||||
|
|
@ -233,7 +233,7 @@ class BaseCamera(lt.Thing):
|
|||
portal: lt.deps.BlockingPortal,
|
||||
stream_name: Literal["main", "lores"] = "main",
|
||||
) -> int:
|
||||
"""Acquire one image from the preview stream and return its size"""
|
||||
"""Acquire one image from the preview stream and return its size."""
|
||||
stream = (
|
||||
self.lores_mjpeg_stream if stream_name == "lores" else self.mjpeg_stream
|
||||
)
|
||||
|
|
@ -245,7 +245,7 @@ class BaseCamera(lt.Thing):
|
|||
stream_name: Literal["main", "lores", "raw"],
|
||||
wait: Optional[float],
|
||||
) -> None:
|
||||
"""Capture a PIL image from stream stream_name with timeout wait"""
|
||||
"""Capture a PIL image from stream stream_name with timeout wait."""
|
||||
raise NotImplementedError(
|
||||
"CameraThings must define their own capture_image method"
|
||||
)
|
||||
|
|
@ -258,7 +258,7 @@ class BaseCamera(lt.Thing):
|
|||
metadata_getter: lt.deps.GetThingStates,
|
||||
save_resolution: Optional[Tuple[int, int]] = None,
|
||||
) -> None:
|
||||
"""Capture an image and save it to disk
|
||||
"""Capture an image and save it to disk.
|
||||
|
||||
:param jpeg_path: The path to save the file to
|
||||
:param logger: This should be injected automatically by Labthings FastAPI
|
||||
|
|
@ -288,7 +288,7 @@ class BaseCamera(lt.Thing):
|
|||
metadata_getter: lt.deps.GetThingStates,
|
||||
buffer_max: int = 1,
|
||||
) -> None:
|
||||
"""Capture an image to memory. This can be saved later with ``save_from_memory``
|
||||
"""Capture an image to memory. This can be saved later with ``save_from_memory``.
|
||||
|
||||
Note that only one image is held in memory so this will overwrite any image
|
||||
in memory.
|
||||
|
|
@ -336,7 +336,7 @@ class BaseCamera(lt.Thing):
|
|||
|
||||
@lt.thing_action
|
||||
def clear_buffers(self) -> None:
|
||||
"""Clear all images in memory"""
|
||||
"""Clear all images in memory."""
|
||||
self._memory_buffer.clear()
|
||||
|
||||
def _robust_image_capture(
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
"""OpenFlexure Microscope OpenCV Camera
|
||||
"""OpenFlexure Microscope OpenCV Camera.
|
||||
|
||||
This module defines a camera Thing that uses OpenCV's
|
||||
``VideoCapture``.
|
||||
|
|
@ -23,7 +23,7 @@ from . import BaseCamera, JPEGBlob
|
|||
|
||||
|
||||
class OpenCVCamera(BaseCamera):
|
||||
"""A Thing representing an OpenCV camera"""
|
||||
"""A Thing representing an OpenCV camera."""
|
||||
|
||||
def __init__(self, camera_index: int = 0):
|
||||
self.camera_index = camera_index
|
||||
|
|
@ -45,7 +45,7 @@ class OpenCVCamera(BaseCamera):
|
|||
|
||||
@lt.thing_property
|
||||
def stream_active(self) -> bool:
|
||||
"""Whether the MJPEG stream is active"""
|
||||
"""Whether the MJPEG stream is active."""
|
||||
if self._capture_enabled and self._capture_thread:
|
||||
return self._capture_thread.is_alive()
|
||||
return False
|
||||
|
|
@ -71,7 +71,7 @@ class OpenCVCamera(BaseCamera):
|
|||
self,
|
||||
resolution: Literal["main", "full"] = "full",
|
||||
) -> NDArray:
|
||||
"""Acquire one image from the camera and return as an array
|
||||
"""Acquire one image from the camera and return as an array.
|
||||
|
||||
This function will produce a nested list containing an uncompressed RGB image.
|
||||
It's likely to be highly inefficient - raw and/or uncompressed captures using
|
||||
|
|
@ -91,7 +91,7 @@ class OpenCVCamera(BaseCamera):
|
|||
metadata_getter: lt.deps.GetThingStates,
|
||||
resolution: Literal["main", "full"] = "main",
|
||||
) -> JPEGBlob:
|
||||
"""Acquire one image from the camera and return as a JPEG blob
|
||||
"""Acquire one image from the camera and return as a JPEG blob.
|
||||
|
||||
This function will produce a JPEG image.
|
||||
"""
|
||||
|
|
|
|||
|
|
@ -40,10 +40,10 @@ from . import BaseCamera, JPEGBlob, ArrayModel
|
|||
|
||||
|
||||
class PicameraStreamOutput(Output):
|
||||
"""An Output class that sends frames to a stream"""
|
||||
"""An Output class that sends frames to a stream."""
|
||||
|
||||
def __init__(self, stream: lt.outputs.MJPEGStream, portal: lt.deps.BlockingPortal):
|
||||
"""Create an output that puts frames in an MJPEGStream
|
||||
"""Create an output that puts frames in an MJPEGStream.
|
||||
|
||||
We need to pass the stream object, and also the blocking portal, because
|
||||
new frame notifications happen in the anyio event loop and frames are
|
||||
|
|
@ -57,7 +57,7 @@ class PicameraStreamOutput(Output):
|
|||
def outputframe(
|
||||
self, frame, _keyframe=True, _timestamp=None, _packet=None, _audio=False
|
||||
):
|
||||
"""Add a frame to the stream's ringbuffer"""
|
||||
"""Add a frame to the stream's ringbuffer."""
|
||||
self.stream.add_frame(frame, self.portal)
|
||||
|
||||
|
||||
|
|
@ -104,7 +104,7 @@ class LensShading(BaseModel):
|
|||
|
||||
|
||||
class StreamingPiCamera2(BaseCamera):
|
||||
"""A Thing that represents an OpenCV camera"""
|
||||
"""A Thing that represents an OpenCV camera."""
|
||||
|
||||
def __init__(self, camera_num: int = 0):
|
||||
self._setting_save_in_progress = False
|
||||
|
|
@ -244,7 +244,7 @@ class StreamingPiCamera2(BaseCamera):
|
|||
|
||||
@lt.thing_property
|
||||
def sensor_modes(self) -> list[SensorMode]:
|
||||
"""All the available modes the current sensor supports"""
|
||||
"""All the available modes the current sensor supports."""
|
||||
if not self._sensor_modes:
|
||||
with self._streaming_picamera() as cam:
|
||||
self._sensor_modes = cam.sensor_modes
|
||||
|
|
@ -254,14 +254,14 @@ class StreamingPiCamera2(BaseCamera):
|
|||
|
||||
@lt.thing_property
|
||||
def sensor_mode(self) -> Optional[SensorModeSelector]:
|
||||
"""The intended sensor mode of the camera"""
|
||||
"""The intended sensor mode of the camera."""
|
||||
if self._sensor_mode is None:
|
||||
return None
|
||||
return SensorModeSelector(**self._sensor_mode)
|
||||
|
||||
@sensor_mode.setter
|
||||
def sensor_mode(self, new_mode: Optional[SensorModeSelector | dict]):
|
||||
"""Change the sensor mode used"""
|
||||
"""Change the sensor mode used."""
|
||||
if new_mode is None:
|
||||
self._sensor_mode = None
|
||||
elif isinstance(new_mode, SensorModeSelector):
|
||||
|
|
@ -277,7 +277,7 @@ class StreamingPiCamera2(BaseCamera):
|
|||
|
||||
@lt.thing_property
|
||||
def sensor_resolution(self) -> Optional[tuple[int, int]]:
|
||||
"""The native resolution of the camera's sensor"""
|
||||
"""The native resolution of the camera's sensor."""
|
||||
with self._streaming_picamera() as cam:
|
||||
return cam.sensor_resolution
|
||||
|
||||
|
|
@ -324,7 +324,7 @@ class StreamingPiCamera2(BaseCamera):
|
|||
|
||||
@property
|
||||
def streaming(self) -> bool:
|
||||
"""True if the camera is streaming"""
|
||||
"""True if the camera is streaming."""
|
||||
return self._picamera is not None and self._picamera.started
|
||||
|
||||
@contextmanager
|
||||
|
|
@ -432,7 +432,7 @@ class StreamingPiCamera2(BaseCamera):
|
|||
|
||||
@lt.thing_action
|
||||
def stop_streaming(self, stop_web_stream: bool = True) -> None:
|
||||
"""Stop the MJPEG stream"""
|
||||
"""Stop the MJPEG stream."""
|
||||
with self._streaming_picamera() as picam:
|
||||
try:
|
||||
picam.stop_recording() # This should also stop the extra lores encoder
|
||||
|
|
@ -474,7 +474,7 @@ class StreamingPiCamera2(BaseCamera):
|
|||
stream_name: Literal["main", "lores", "raw", "full"] = "main",
|
||||
wait: Optional[float] = 0.9,
|
||||
) -> ArrayModel:
|
||||
"""Acquire one image from the camera and return as an array
|
||||
"""Acquire one image from the camera and return as an array.
|
||||
|
||||
This function will produce a nested list containing an uncompressed RGB image.
|
||||
It's likely to be highly inefficient - raw and/or uncompressed captures using
|
||||
|
|
@ -496,7 +496,7 @@ class StreamingPiCamera2(BaseCamera):
|
|||
|
||||
@lt.thing_property
|
||||
def camera_configuration(self) -> Mapping:
|
||||
"""The "configuration" dictionary of the picamera2 object
|
||||
"""The "configuration" dictionary of the picamera2 object.
|
||||
|
||||
The "configuration" sets the resolution and format of the camera's streams.
|
||||
Together with the "tuning" it determines how the sensor is configured and
|
||||
|
|
@ -516,7 +516,7 @@ class StreamingPiCamera2(BaseCamera):
|
|||
resolution: Literal["lores", "main", "full"] = "main",
|
||||
wait: Optional[float] = 0.9,
|
||||
) -> JPEGBlob:
|
||||
"""Acquire one image from the camera as a JPEG
|
||||
"""Acquire one image from the camera as a JPEG.
|
||||
|
||||
The JPEG will be acquired using ``Picamera2.capture_file``. If the
|
||||
``resolution`` parameter is ``main`` or ``lores``, it will be captured
|
||||
|
|
@ -567,7 +567,7 @@ class StreamingPiCamera2(BaseCamera):
|
|||
|
||||
@lt.thing_property
|
||||
def capture_metadata(self) -> dict:
|
||||
"""Return the metadata from the camera"""
|
||||
"""Return the metadata from the camera."""
|
||||
with self._streaming_picamera() as cam:
|
||||
return cam.capture_metadata()
|
||||
|
||||
|
|
@ -577,7 +577,7 @@ class StreamingPiCamera2(BaseCamera):
|
|||
target_white_level: int = 700,
|
||||
percentile: float = 99.9,
|
||||
):
|
||||
"""Adjust exposure until a the target white level is reached
|
||||
"""Adjust exposure until a the target white level is reached.
|
||||
|
||||
Starting from the minimum exposure, gradually increase exposure until
|
||||
the image reaches the specified white level.
|
||||
|
|
@ -603,7 +603,7 @@ class StreamingPiCamera2(BaseCamera):
|
|||
method: Literal["percentile", "centre"] = "centre",
|
||||
luminance_power: float = 1.0,
|
||||
):
|
||||
"""Correct the white balance of the image
|
||||
"""Correct the white balance of the image.
|
||||
|
||||
This calibration requires a neutral image, such that the 99th centile
|
||||
of each colour channel should correspond to white. We calculate the
|
||||
|
|
@ -711,7 +711,7 @@ class StreamingPiCamera2(BaseCamera):
|
|||
|
||||
@lt.thing_action
|
||||
def full_auto_calibrate(self) -> None:
|
||||
"""Perform a full auto-calibration
|
||||
"""Perform a full auto-calibration.
|
||||
|
||||
This function will call the other calibration actions in sequence:
|
||||
|
||||
|
|
@ -729,7 +729,7 @@ class StreamingPiCamera2(BaseCamera):
|
|||
|
||||
@lt.thing_action
|
||||
def flat_lens_shading(self) -> None:
|
||||
"""Disable flat-field correction
|
||||
"""Disable flat-field correction.
|
||||
|
||||
This method will set a completely flat lens shading table. It is not the
|
||||
same as the default behaviour, which is to use an adaptive lens shading
|
||||
|
|
@ -748,7 +748,7 @@ class StreamingPiCamera2(BaseCamera):
|
|||
|
||||
@lt.thing_property
|
||||
def lens_shading_tables(self) -> Optional[LensShading]:
|
||||
"""The current lens shading (i.e. flat-field correction)
|
||||
"""The current lens shading (i.e. flat-field correction).
|
||||
|
||||
Return the current lens shading correction, as three 2D lists each with
|
||||
dimensions 16x12, if a static lens shading table is in use.
|
||||
|
|
@ -770,7 +770,7 @@ class StreamingPiCamera2(BaseCamera):
|
|||
return None
|
||||
|
||||
def reshape_lst(lin: list[float]) -> list[list[float]]:
|
||||
"""Reshape the 192 element list into a 2D 16x12 list"""
|
||||
"""Reshape the 192 element list into a 2D 16x12 list."""
|
||||
w, h = 16, 12
|
||||
return [lin[w * i : w * (i + 1)] for i in range(h)]
|
||||
|
||||
|
|
@ -782,7 +782,7 @@ class StreamingPiCamera2(BaseCamera):
|
|||
|
||||
@lens_shading_tables.setter
|
||||
def lens_shading_tables(self, lst: LensShading) -> None:
|
||||
"""Set the lens shading tables"""
|
||||
"""Set the lens shading tables."""
|
||||
with self._streaming_picamera(pause_stream=True):
|
||||
recalibrate_utils.set_static_lst(
|
||||
self.tuning,
|
||||
|
|
@ -795,7 +795,7 @@ class StreamingPiCamera2(BaseCamera):
|
|||
def correct_colour_gains_for_lens_shading(
|
||||
self, colour_gains: tuple[float, float]
|
||||
) -> tuple[float, float]:
|
||||
"""Correct white balance gains for the effect of lens shading
|
||||
"""Correct white balance gains for the effect of lens shading.
|
||||
|
||||
The white balance algorithm we use assumes the brightest pixels
|
||||
should be white, and that the only thing affecting the colour of
|
||||
|
|
@ -825,7 +825,7 @@ class StreamingPiCamera2(BaseCamera):
|
|||
|
||||
@lt.thing_action
|
||||
def flat_lens_shading_chrominance(self) -> None:
|
||||
"""Disable flat-field correction
|
||||
"""Disable flat-field correction.
|
||||
|
||||
This method will set the chrominance of the lens shading table to be
|
||||
flat, i.e. we'll correct vignetting of intensity, but not any change in
|
||||
|
|
@ -840,7 +840,7 @@ class StreamingPiCamera2(BaseCamera):
|
|||
|
||||
@lt.thing_action
|
||||
def reset_lens_shading(self) -> None:
|
||||
"""Revert to default lens shading settings
|
||||
"""Revert to default lens shading settings.
|
||||
|
||||
This method will restore the default "adaptive" lens shading method used
|
||||
by the Raspberry Pi camera.
|
||||
|
|
@ -851,7 +851,7 @@ class StreamingPiCamera2(BaseCamera):
|
|||
|
||||
@lt.thing_property
|
||||
def lens_shading_is_static(self) -> bool:
|
||||
"""Whether the lens shading is static
|
||||
"""Whether the lens shading is static.
|
||||
|
||||
This property is true if the lens shading correction has been set to use
|
||||
a static table (i.e. the number of automatic correction iterations is zero).
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
"""Functions to set up a Raspberry Pi Camera v2 for scientific use
|
||||
"""Functions to set up a Raspberry Pi Camera v2 for scientific use.
|
||||
|
||||
This module provides slower, simpler functions to set the
|
||||
gain, exposure, and white balance of a Raspberry Pi camera, using
|
||||
|
|
@ -52,7 +52,7 @@ LensShadingTables = tuple[np.ndarray, np.ndarray, np.ndarray]
|
|||
|
||||
|
||||
def load_default_tuning(cam: Picamera2) -> dict:
|
||||
"""Load the default tuning file for the camera
|
||||
"""Load the default tuning file for the camera.
|
||||
|
||||
This will open and close the camera to determine its model. If you are
|
||||
using a model that's supported by ``picamera2`` it should have a tuning
|
||||
|
|
@ -76,7 +76,7 @@ def load_default_tuning(cam: Picamera2) -> dict:
|
|||
|
||||
|
||||
def set_minimum_exposure(camera: Picamera2) -> None:
|
||||
"""Enable manual exposure, with low gain and shutter speed
|
||||
"""Enable manual exposure, with low gain and shutter speed.
|
||||
|
||||
We set exposure mode to manual, analog and digital gain
|
||||
to 1, and shutter speed to the minimum (8us for Pi Camera v2)
|
||||
|
|
@ -93,7 +93,7 @@ def set_minimum_exposure(camera: Picamera2) -> None:
|
|||
|
||||
|
||||
class ExposureTest(BaseModel):
|
||||
"""Record the results of testing the camera's current exposure settings"""
|
||||
"""Record the results of testing the camera's current exposure settings."""
|
||||
|
||||
level: int
|
||||
exposure_time: int
|
||||
|
|
@ -101,7 +101,7 @@ class ExposureTest(BaseModel):
|
|||
|
||||
|
||||
def test_exposure_settings(camera: Picamera2, percentile: float) -> ExposureTest:
|
||||
"""Evaluate current exposure settings using a raw image
|
||||
"""Evaluate current exposure settings using a raw image.
|
||||
|
||||
CAMERA SHOULD BE STARTED!
|
||||
|
||||
|
|
@ -136,7 +136,7 @@ def test_exposure_settings(camera: Picamera2, percentile: float) -> ExposureTest
|
|||
|
||||
|
||||
def check_convergence(test: ExposureTest, target: int, tolerance: float) -> bool:
|
||||
"""Check whether the brightness is within the specified target range"""
|
||||
"""Check whether the brightness is within the specified target range."""
|
||||
return abs(test.level - target) < target * tolerance
|
||||
|
||||
|
||||
|
|
@ -321,7 +321,7 @@ def channels_from_bayer_array(bayer_array: np.ndarray) -> np.ndarray:
|
|||
|
||||
|
||||
def get_16x12_grid(chan: np.ndarray, dx: int, dy: int) -> np.ndarray:
|
||||
"""Compresses channel down to a 16x12 grid - from libcamera
|
||||
"""Compresses channel down to a 16x12 grid - from libcamera.
|
||||
|
||||
This is taken from
|
||||
https://git.linuxtv.org/libcamera.git/tree/utils/raspberrypi/ctt/ctt_alsc.py
|
||||
|
|
@ -343,7 +343,7 @@ def get_16x12_grid(chan: np.ndarray, dx: int, dy: int) -> np.ndarray:
|
|||
|
||||
|
||||
def upsample_channels(grids: np.ndarray, shape: tuple[int]) -> np.ndarray:
|
||||
"""Zoom an image in the last two dimensions
|
||||
"""Zoom an image in the last two dimensions.
|
||||
|
||||
This is effectively the inverse operation of ``get_16x12_grid``
|
||||
"""
|
||||
|
|
@ -354,7 +354,7 @@ def upsample_channels(grids: np.ndarray, shape: tuple[int]) -> np.ndarray:
|
|||
|
||||
|
||||
def downsampled_channels(channels: np.ndarray, blacklevel=64) -> list[np.ndarray]:
|
||||
"""Generate a downsampled, un-normalised image from which to calculate the LST
|
||||
"""Generate a downsampled, un-normalised image from which to calculate the LST.
|
||||
|
||||
TODO: blacklevel probably ought to be determined from the camera...
|
||||
"""
|
||||
|
|
@ -382,7 +382,7 @@ def lst_from_channels(channels: np.ndarray) -> LensShadingTables:
|
|||
|
||||
|
||||
def lst_from_grids(grids: np.ndarray) -> LensShadingTables:
|
||||
"""Given 4 downsampled grids, generate the luminance and chrominance tables
|
||||
"""Given 4 downsampled grids, generate the luminance and chrominance tables.
|
||||
|
||||
The grids are the 4 BAYER channels RGGB
|
||||
|
||||
|
|
@ -408,7 +408,7 @@ def lst_from_grids(grids: np.ndarray) -> LensShadingTables:
|
|||
|
||||
|
||||
def grids_from_lst(lum: np.ndarray, Cr: np.ndarray, Cb: np.ndarray) -> np.ndarray:
|
||||
"""Convert form luminance/chrominance dict to four RGGB channels
|
||||
"""Convert form luminance/chrominance dict to four RGGB channels.
|
||||
|
||||
Note that these will be normalised - the maximum green value is always 1.
|
||||
Also, note that the channels are BGGR, to be consistent with the
|
||||
|
|
@ -462,13 +462,13 @@ def set_static_ccm(
|
|||
|
||||
|
||||
def get_static_ccm(tuning: dict) -> None:
|
||||
"""Get the ``rpi.ccm`` section of a camera tuning dict"""
|
||||
"""Get the ``rpi.ccm`` section of a camera tuning dict."""
|
||||
ccm = Picamera2.find_tuning_algo(tuning, "rpi.ccm")
|
||||
return ccm["ccms"]
|
||||
|
||||
|
||||
def lst_is_static(tuning: dict) -> bool:
|
||||
"""Whether the lens shading table is set to static"""
|
||||
"""Whether the lens shading table is set to static."""
|
||||
alsc = Picamera2.find_tuning_algo(tuning, "rpi.alsc")
|
||||
return alsc["n_iter"] == 0
|
||||
|
||||
|
|
@ -493,13 +493,13 @@ def set_static_geq(
|
|||
|
||||
|
||||
def _geq_is_static(tuning: dict) -> bool:
|
||||
"""Whether the green equalisation is set to static"""
|
||||
"""Whether the green equalisation is set to static."""
|
||||
geq = Picamera2.find_tuning_algo(tuning, "rpi.geq")
|
||||
return geq["offset"] == 65535
|
||||
|
||||
|
||||
def index_of_algorithm(algorithms: list[dict], algorithm: str) -> int:
|
||||
"""Find the index of an algorithm's section in the tuning file"""
|
||||
"""Find the index of an algorithm's section in the tuning file."""
|
||||
for i, a in enumerate(algorithms):
|
||||
if algorithm in a:
|
||||
return i
|
||||
|
|
@ -557,5 +557,5 @@ def recreate_camera_manager() -> None:
|
|||
|
||||
|
||||
def as_flat_rounded_list(array: np.ndarray, round_to: int = 3) -> list[float]:
|
||||
"""Flatten array, round, and then convert to list"""
|
||||
"""Flatten array, round, and then convert to list."""
|
||||
return np.reshape(array, -1).round(round_to).tolist()
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
"""OpenFlexure Microscope OpenCV Camera
|
||||
"""OpenFlexure Microscope OpenCV Camera.
|
||||
|
||||
This module defines a Thing that is responsible for using the stage and
|
||||
camera together to perform an autofocus routine.
|
||||
|
|
@ -30,7 +30,7 @@ RATIO = 0.2
|
|||
|
||||
|
||||
class SimulatedCamera(BaseCamera):
|
||||
"""A Thing representing an OpenCV camera"""
|
||||
"""A Thing representing an OpenCV camera."""
|
||||
|
||||
_stage: Optional[BaseStage] = None
|
||||
_server: Optional[lt.ThingServer] = None
|
||||
|
|
@ -53,7 +53,7 @@ class SimulatedCamera(BaseCamera):
|
|||
self.generate_canvas()
|
||||
|
||||
def generate_sprites(self):
|
||||
"""Generate sprites to populate the image"""
|
||||
"""Generate sprites to populate the image."""
|
||||
self.sprites = []
|
||||
black = np.zeros(self.glyph_shape, dtype=np.uint8)
|
||||
x = np.arange(black.shape[0])
|
||||
|
|
@ -65,7 +65,7 @@ class SimulatedCamera(BaseCamera):
|
|||
self.sprites.append(sprite)
|
||||
|
||||
def generate_blobs(self, n_blobs: int = 1000):
|
||||
"""Generate coordinates of blobs
|
||||
"""Generate coordinates of blobs.
|
||||
|
||||
Blobs are characterised by X, Y, sprite
|
||||
We also generate a KD tree to rapidly find blobs in an image
|
||||
|
|
@ -78,7 +78,7 @@ class SimulatedCamera(BaseCamera):
|
|||
self.blobs[:, 2] = rng.choice(len(self.sprites), n_blobs)
|
||||
|
||||
def generate_canvas(self):
|
||||
"""Generate a blank canvas"""
|
||||
"""Generate a blank canvas."""
|
||||
self.canvas = np.zeros(self.canvas_shape, dtype=np.uint8)
|
||||
self.canvas[...] = 255
|
||||
w, h, _ = self.glyph_shape
|
||||
|
|
@ -89,7 +89,7 @@ class SimulatedCamera(BaseCamera):
|
|||
] -= self.sprites[int(sprite)]
|
||||
|
||||
def generate_image(self, pos: tuple[int, int, int]):
|
||||
"""Generate an image with blobs based on supplied coordinates"""
|
||||
"""Generate an image with blobs based on supplied coordinates."""
|
||||
canvas_width, canvas_height, _ = self.canvas_shape
|
||||
image_width, image_height, _ = self.shape
|
||||
pos = tuple(x * RATIO for x in pos)
|
||||
|
|
@ -124,7 +124,7 @@ class SimulatedCamera(BaseCamera):
|
|||
return self._stage.instantaneous_position
|
||||
|
||||
def generate_frame(self):
|
||||
"""Generate a frame with blobs based on the stage coordinates"""
|
||||
"""Generate a frame with blobs based on the stage coordinates."""
|
||||
try:
|
||||
pos = self.get_stage_position()
|
||||
except Exception as e:
|
||||
|
|
@ -145,7 +145,7 @@ class SimulatedCamera(BaseCamera):
|
|||
|
||||
@lt.thing_property
|
||||
def stream_active(self) -> bool:
|
||||
"""Whether the MJPEG stream is active"""
|
||||
"""Whether the MJPEG stream is active."""
|
||||
if self._capture_enabled and self._capture_thread:
|
||||
return self._capture_thread.is_alive()
|
||||
return False
|
||||
|
|
@ -170,7 +170,7 @@ class SimulatedCamera(BaseCamera):
|
|||
self,
|
||||
resolution: Literal["main", "full"] = "full",
|
||||
) -> ArrayModel:
|
||||
"""Acquire one image from the camera and return as an array
|
||||
"""Acquire one image from the camera and return as an array.
|
||||
|
||||
This function will produce a nested list containing an uncompressed RGB image.
|
||||
It's likely to be highly inefficient - raw and/or uncompressed captures using
|
||||
|
|
@ -185,7 +185,7 @@ class SimulatedCamera(BaseCamera):
|
|||
metadata_getter: lt.deps.GetThingStates,
|
||||
resolution: Literal["main", "full"] = "main",
|
||||
) -> JPEGBlob:
|
||||
"""Acquire one image from the camera and return as a JPEG blob
|
||||
"""Acquire one image from the camera and return as a JPEG blob.
|
||||
|
||||
This function will produce a JPEG image.
|
||||
"""
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
"""OpenFlexure Microscope API extension for stage calibration
|
||||
"""OpenFlexure Microscope API extension for stage calibration.
|
||||
|
||||
This file contains the HTTP API for camera/stage calibration. It
|
||||
includes calibration functions that measure the relationship between
|
||||
|
|
@ -53,7 +53,7 @@ class HardwareInterfaceModel(BaseModel):
|
|||
|
||||
|
||||
def downsample(factor: int, image: np.ndarray) -> np.ndarray:
|
||||
"""Downsample an image by taking the mean of each nxn region
|
||||
"""Downsample an image by taking the mean of each nxn region.
|
||||
|
||||
This should be very efficient: we calculate the mean of each
|
||||
``factor * factor`` square, no interpolation. If the image is
|
||||
|
|
@ -79,7 +79,7 @@ DEFAULT_SETTLING_TIME = 0.2
|
|||
def make_hardware_interface(
|
||||
stage: Stage, camera: Camera, downsample_factor: int = 2
|
||||
) -> HardwareInterfaceModel:
|
||||
"""Construct the functions we need to interface with the hardware"""
|
||||
"""Construct the functions we need to interface with the hardware."""
|
||||
axes = stage.axis_names
|
||||
|
||||
def pos2dict(pos: Sequence[float]) -> Mapping[str, float]:
|
||||
|
|
@ -144,7 +144,7 @@ class LoggingMoveWrapper:
|
|||
self.clear_history()
|
||||
|
||||
def __call__(self, new_position: CoordinateType, *args, **kwargs):
|
||||
"""Move to a new position, and record it"""
|
||||
"""Move to a new position, and record it."""
|
||||
self._history.append((time.time(), self._current_position))
|
||||
self._move_function(new_position, *args, **kwargs)
|
||||
self._current_position = new_position
|
||||
|
|
@ -152,13 +152,13 @@ class LoggingMoveWrapper:
|
|||
|
||||
@property
|
||||
def history(self) -> MoveHistory:
|
||||
"""The history, as a numpy array of times and another of positions"""
|
||||
"""The history, as a numpy array of times and another of positions."""
|
||||
times: List[float] = [t for t, p in self._history if p is not None]
|
||||
positions: List[CoordinateType] = [p for t, p in self._history if p is not None]
|
||||
return MoveHistory(times, positions)
|
||||
|
||||
def clear_history(self):
|
||||
"""Reset our history to be an empty list"""
|
||||
"""Reset our history to be an empty list."""
|
||||
self._history: List[Tuple[float, Optional[CoordinateType]]] = []
|
||||
|
||||
|
||||
|
|
@ -175,7 +175,7 @@ class CSMUncalibratedError(HTTPException):
|
|||
|
||||
|
||||
class CameraStageMapper(lt.Thing):
|
||||
"""A Thing to manage mapping between image and stage coordinates"""
|
||||
"""A Thing to manage mapping between image and stage coordinates."""
|
||||
|
||||
@lt.thing_action
|
||||
def calibrate_1d(
|
||||
|
|
@ -185,7 +185,7 @@ class CameraStageMapper(lt.Thing):
|
|||
logger: lt.deps.InvocationLogger,
|
||||
direction: Tuple[float, float, float],
|
||||
) -> DenumpifyingDict:
|
||||
"""Move a microscope's stage in 1D, and figure out the relationship with the camera"""
|
||||
"""Move a microscope's stage in 1D, and figure out the relationship with the camera."""
|
||||
move = LoggingMoveWrapper(
|
||||
hw.move
|
||||
) # log positions and times for stage calibration
|
||||
|
|
@ -214,7 +214,7 @@ class CameraStageMapper(lt.Thing):
|
|||
def calibrate_xy(
|
||||
self, hw: HardwareInterfaceDep, stage: Stage, logger: lt.deps.InvocationLogger
|
||||
) -> DenumpifyingDict:
|
||||
"""Move the microscope's stage in X and Y, to calibrate its relationship to the camera
|
||||
"""Move the microscope's stage in X and Y, to calibrate its relationship to the camera.
|
||||
|
||||
This performs two 1d calibrations in x and y, then combines their results.
|
||||
"""
|
||||
|
|
@ -291,13 +291,13 @@ class CameraStageMapper(lt.Thing):
|
|||
|
||||
@lt.thing_property
|
||||
def image_resolution(self) -> Optional[Tuple[float, float]]:
|
||||
"""The image size used to calibrate the image_to_stage_displacement_matrix"""
|
||||
"""The image size used to calibrate the image_to_stage_displacement_matrix."""
|
||||
if self.last_calibration is None:
|
||||
return None
|
||||
return self.last_calibration["image_resolution"]
|
||||
|
||||
def assert_calibrated(self):
|
||||
"""Raise an exception if the image_to_stage_displacement matrix is not set"""
|
||||
"""Raise an exception if the image_to_stage_displacement matrix is not set."""
|
||||
if self.image_to_stage_displacement_matrix is None:
|
||||
# Disable check of no message in raised exception as the message is explicitly
|
||||
# added by CSMUncalibratedError
|
||||
|
|
@ -310,7 +310,7 @@ class CameraStageMapper(lt.Thing):
|
|||
x: float,
|
||||
y: float,
|
||||
):
|
||||
"""Move by a given number of pixels on the camera
|
||||
"""Move by a given number of pixels on the camera.
|
||||
|
||||
NB x and y here refer to what is usually understood to be the horizontal and
|
||||
vertical axes of the image. In many toolkits, "matrix indices" are used, which
|
||||
|
|
@ -329,7 +329,7 @@ class CameraStageMapper(lt.Thing):
|
|||
|
||||
@lt.thing_property
|
||||
def thing_state(self) -> dict[str, Any]:
|
||||
"""Summary metadata describing the current state of the Thing"""
|
||||
"""Summary metadata describing the current state of the Thing."""
|
||||
return {
|
||||
k: getattr(self, k)
|
||||
for k in ["image_to_stage_displacement_matrix", "image_resolution"]
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
"""OpenFlexure Settings Management
|
||||
"""OpenFlexure Settings Management.
|
||||
|
||||
This module provides some settings management across the other Things, and
|
||||
for code that currently lives in clients but needs to persist settings on
|
||||
|
|
@ -21,7 +21,7 @@ ThingServerDep = Annotated[lt.ThingServer, Depends(thing_server_from_request)]
|
|||
|
||||
|
||||
def recursive_update(old_dict: MutableMapping, update: Mapping):
|
||||
"""Update a dictionary recursively"""
|
||||
"""Update a dictionary recursively."""
|
||||
for k, v in update.items():
|
||||
if isinstance(v, Mapping):
|
||||
if k in old_dict and isinstance(old_dict[k], MutableMapping):
|
||||
|
|
@ -33,7 +33,7 @@ def recursive_update(old_dict: MutableMapping, update: Mapping):
|
|||
|
||||
|
||||
def nested_dict_get(data: dict, key: Sequence[str], create=False) -> Any:
|
||||
"""Index a dict-of-dicts with a sequence of strings"""
|
||||
"""Index a dict-of-dicts with a sequence of strings."""
|
||||
subdict = data
|
||||
for k in key:
|
||||
if k not in subdict and create:
|
||||
|
|
@ -100,7 +100,7 @@ class SettingsManager(lt.Thing):
|
|||
|
||||
@lt.thing_setting
|
||||
def microscope_id(self) -> UUID:
|
||||
"""A unique identifier for this microscope"""
|
||||
"""A unique identifier for this microscope."""
|
||||
if self._microscope_id is None:
|
||||
self._microscope_id = str(uuid4())
|
||||
return UUID(self._microscope_id)
|
||||
|
|
@ -117,7 +117,7 @@ class SettingsManager(lt.Thing):
|
|||
|
||||
@lt.thing_action
|
||||
def get_things_state(self, metadata_getter: lt.deps.GetThingStates) -> Mapping:
|
||||
"""Metadata summarising the current state of all Things in the server"""
|
||||
"""Metadata summarising the current state of all Things in the server."""
|
||||
return metadata_getter()
|
||||
|
||||
@property
|
||||
|
|
|
|||
|
|
@ -42,7 +42,7 @@ STITCHING_RESOLUTION = (820, 616)
|
|||
|
||||
|
||||
class ScanNotRunningError(RuntimeError):
|
||||
"""Exception called when scan not running that requires a scan to be running"""
|
||||
"""Exception called when scan not running that requires a scan to be running."""
|
||||
|
||||
|
||||
def _scan_running(method):
|
||||
|
|
@ -170,7 +170,7 @@ class SmartScanThing(lt.Thing):
|
|||
|
||||
@_scan_running
|
||||
def _check_background_and_csm_set(self):
|
||||
"""Before starting a scan, check that background and camera-stage-mapping are set
|
||||
"""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
|
||||
|
|
@ -229,7 +229,7 @@ class SmartScanThing(lt.Thing):
|
|||
|
||||
@_scan_running
|
||||
def _calc_displacement_from_test_image(self, overlap: int) -> tuple[int, int]:
|
||||
"""Take a test image and use camera stage mapping to calculate x and y displacement
|
||||
"""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%.
|
||||
|
|
@ -520,7 +520,7 @@ class SmartScanThing(lt.Thing):
|
|||
|
||||
@_scan_running
|
||||
def _return_to_starting_position(self):
|
||||
"""Return to the initial scan position, if set"""
|
||||
"""Return to the initial scan position, if set."""
|
||||
self._scan_logger.info("Returning to starting position.")
|
||||
if self._starting_position is not None:
|
||||
self._stage.move_absolute(
|
||||
|
|
@ -529,7 +529,7 @@ class SmartScanThing(lt.Thing):
|
|||
|
||||
@_scan_running
|
||||
def _perform_final_stitch(self):
|
||||
"""Update the scan zip and perform final stitch of the data"""
|
||||
"""Update the scan zip and perform final stitch of the data."""
|
||||
if self._scan_images_taken <= 3:
|
||||
self._scan_logger.info("Not performing a stitch as 3 or fewer images taken")
|
||||
return
|
||||
|
|
@ -626,7 +626,7 @@ class SmartScanThing(lt.Thing):
|
|||
|
||||
@lt.thing_property
|
||||
def scans(self) -> list[scan_directories.ScanInfo]:
|
||||
"""All the available scans
|
||||
"""All the available scans.
|
||||
|
||||
Each scan has a name (which can be used to access it), along with
|
||||
its modified and created times (according to the filesystem) and
|
||||
|
|
@ -687,7 +687,7 @@ class SmartScanThing(lt.Thing):
|
|||
"scans",
|
||||
)
|
||||
def delete_all_scans(self, logger: lt.deps.InvocationLogger) -> None:
|
||||
"""Delete all the scans on the microscope
|
||||
"""Delete all the scans on the microscope.
|
||||
|
||||
**This will irreversibly remove all scanned data from the
|
||||
microscope!**
|
||||
|
|
@ -698,7 +698,7 @@ class SmartScanThing(lt.Thing):
|
|||
|
||||
@lt.thing_action
|
||||
def purge_empty_scans(self, logger: lt.deps.InvocationLogger) -> None:
|
||||
"""Delete all scan folders containing no images at the top level"""
|
||||
"""Delete all scan folders containing no images at the top level."""
|
||||
# JSON is ignored as it's created before any images are captured
|
||||
for scan_info in self._scan_dir_manager.all_scans_info():
|
||||
if scan_info.number_of_images == 0:
|
||||
|
|
@ -722,7 +722,7 @@ class SmartScanThing(lt.Thing):
|
|||
|
||||
@property
|
||||
def latest_preview_stitch_path(self) -> Optional[str]:
|
||||
"""The path of the latest preview stitched image, or None if not available"""
|
||||
"""The path of the latest preview stitched image, or None if not available."""
|
||||
if not self.latest_scan_name:
|
||||
return None
|
||||
|
||||
|
|
@ -732,7 +732,7 @@ class SmartScanThing(lt.Thing):
|
|||
|
||||
@lt.thing_property
|
||||
def latest_preview_stitch_time(self) -> Optional[float]:
|
||||
"""The modification time of the latest preview image, to allow live updating
|
||||
"""The modification time of the latest preview image, to allow live updating.
|
||||
|
||||
This will return None (``null`` to JS) if there is no preview image to return.
|
||||
|
||||
|
|
@ -795,7 +795,7 @@ class SmartScanThing(lt.Thing):
|
|||
|
||||
@_scan_running
|
||||
def _preview_stitch_running(self) -> bool:
|
||||
"""Whether there is a preview stitch running in a subprocess"""
|
||||
"""Whether there is a preview stitch running in a subprocess."""
|
||||
with self._preview_stitch_popen_lock:
|
||||
if self._preview_stitch_popen is None:
|
||||
return False
|
||||
|
|
@ -805,7 +805,7 @@ class SmartScanThing(lt.Thing):
|
|||
|
||||
@_scan_running
|
||||
def _preview_stitch_wait(self):
|
||||
"""Wait for an ongoing preview stitch to return"""
|
||||
"""Wait for an ongoing preview stitch to return."""
|
||||
if self._preview_stitch_running():
|
||||
with self._preview_stitch_popen_lock:
|
||||
self._preview_stitch_popen.wait()
|
||||
|
|
@ -816,7 +816,7 @@ class SmartScanThing(lt.Thing):
|
|||
cancel: lt.deps.CancelHook,
|
||||
cmd: list[str],
|
||||
) -> CompletedProcess:
|
||||
"""Run a subprocess and log any output
|
||||
"""Run a subprocess and log any output.
|
||||
|
||||
Raises:
|
||||
ChildProcessError if exit code is not zero
|
||||
|
|
@ -870,7 +870,7 @@ class SmartScanThing(lt.Thing):
|
|||
stitch_resize: Optional[float] = None,
|
||||
overlap: float = 0.0,
|
||||
) -> None:
|
||||
"""Generate a stitched image based on stage position metadata
|
||||
"""Generate a stitched image based on stage position metadata.
|
||||
|
||||
Note that as this is a lt.thing_action it needs the logger passed as
|
||||
a variable if called from another thing action
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ import labthings_fastapi as lt
|
|||
|
||||
|
||||
class BaseStage(lt.Thing):
|
||||
"""A base stage class for OpenFlexure translation stages
|
||||
"""A base stage class for OpenFlexure translation stages.
|
||||
|
||||
This can't be used directly but should reduce boilerplate code when
|
||||
implementing new stages. A minimal working stage must implement
|
||||
|
|
@ -38,7 +38,7 @@ class BaseStage(lt.Thing):
|
|||
|
||||
@property
|
||||
def thing_state(self):
|
||||
"""Summary metadata describing the current state of the stage"""
|
||||
"""Summary metadata describing the current state of the stage."""
|
||||
return {"position": self.position}
|
||||
|
||||
@lt.thing_action
|
||||
|
|
@ -67,7 +67,7 @@ class BaseStage(lt.Thing):
|
|||
|
||||
@lt.thing_action
|
||||
def set_zero_position(self):
|
||||
"""Make the current position zero in all axes
|
||||
"""Make the current position zero in all axes.
|
||||
|
||||
This action does not move the stage, but resets the position to zero.
|
||||
It is intended for use after manually or automatically recentring the
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ from . import BaseStage
|
|||
|
||||
|
||||
class DummyStage(BaseStage):
|
||||
"""A dummy stage for testing purposes
|
||||
"""A dummy stage for testing purposes.
|
||||
|
||||
This stage should work similarly to a Sangaboard stage, but without any
|
||||
hardware attached.
|
||||
|
|
@ -83,7 +83,7 @@ class DummyStage(BaseStage):
|
|||
|
||||
@lt.thing_action
|
||||
def set_zero_position(self):
|
||||
"""Make the current position zero in all axes
|
||||
"""Make the current position zero in all axes.
|
||||
|
||||
This action does not move the stage, but resets the position to zero.
|
||||
It is intended for use after manually or automatically recentring the
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ from . import BaseStage
|
|||
|
||||
|
||||
class SangaboardThing(BaseStage):
|
||||
"""A Thing to manage a Sangaboard motor controller
|
||||
"""A Thing to manage a Sangaboard motor controller.
|
||||
|
||||
Internally, this uses the ``pysangaboard`` package from PyPi. This imports
|
||||
as ``sangaboard``. As ``pysangaboard`` does not support some features added
|
||||
|
|
@ -115,7 +115,7 @@ class SangaboardThing(BaseStage):
|
|||
|
||||
@lt.thing_action
|
||||
def set_zero_position(self) -> None:
|
||||
"""Make the current position zero in all axes
|
||||
"""Make the current position zero in all axes.
|
||||
|
||||
This action does not move the stage, but resets the position to zero.
|
||||
It is intended for use after manually or automatically recentring the
|
||||
|
|
@ -132,7 +132,7 @@ class SangaboardThing(BaseStage):
|
|||
dt: float = 0.5,
|
||||
led_channel: Literal["cc"] = "cc",
|
||||
) -> None:
|
||||
"""Flash the LED to identify the board
|
||||
"""Flash the LED to identify the board.
|
||||
|
||||
This is intended to be useful in situations where there are multiple
|
||||
Sangaboards in use, and it is necessary to identify which one is
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
"""OpenFlexure Microscope system control Thing
|
||||
"""OpenFlexure Microscope system control Thing.
|
||||
|
||||
This module defines a Thing that can shut down or restart the host computer.
|
||||
"""
|
||||
|
|
@ -15,11 +15,11 @@ class CommandOutput(BaseModel):
|
|||
|
||||
|
||||
class SystemControlThing(lt.Thing):
|
||||
"""Attempt to shutdown the device"""
|
||||
"""Attempt to shutdown the device."""
|
||||
|
||||
@lt.thing_action
|
||||
def shutdown(self) -> CommandOutput:
|
||||
"""Attempt to shutdown the device"""
|
||||
"""Attempt to shutdown the device."""
|
||||
p = subprocess.Popen(
|
||||
["sudo", "shutdown", "-h", "now"],
|
||||
stderr=subprocess.PIPE,
|
||||
|
|
@ -36,7 +36,7 @@ class SystemControlThing(lt.Thing):
|
|||
|
||||
@lt.thing_action
|
||||
def reboot(self) -> CommandOutput:
|
||||
"""Attempt to reboot the device"""
|
||||
"""Attempt to reboot the device."""
|
||||
p = subprocess.Popen(
|
||||
["sudo", "shutdown", "-r", "now"],
|
||||
stderr=subprocess.PIPE,
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@ class ErrorCapturingThread(Thread):
|
|||
"""
|
||||
|
||||
def __init__(self, group=None, target=None, args=None, kwargs=None, daemon=None):
|
||||
"""Initialise with the same arguments as Thread"""
|
||||
"""Initialise with the same arguments as Thread."""
|
||||
# As all inputs are keywords we need to set the default values for args and kwargs:
|
||||
if args is None:
|
||||
args = ()
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue