Added a Thing to run stitching as Actions

This commit is contained in:
Richard Bowman 2024-01-10 20:51:49 +00:00
parent b6b1fd06bf
commit 9ce52c2845
3 changed files with 91 additions and 8 deletions

View file

@ -11,6 +11,7 @@ from .things.system_control import SystemControlThing
from .things.settings_manager import SettingsManager
from .things.auto_recentre_stage import RecentringThing
from .things.smart_scan import SmartScanThing, BackgroundDetectThing
from .things.stitching import Stitcher
from .things.test import APITestThing
from .serve_static_files import add_static_files
from .logging import configure_logging, retrieve_log
@ -26,6 +27,7 @@ thing_server.add_thing(CameraStageMapper(), "/camera_stage_mapping/")
thing_server.add_thing(SystemControlThing(), "/system_control/")
thing_server.add_thing(SettingsManager(), "/settings/")
thing_server.add_thing(SmartScanThing(), "/smart_scan/")
thing_server.add_thing(Stitcher("application/openflexure-stitching/.venv/bin/openflexure-stitch"), "/stitching/")
thing_server.add_thing(BackgroundDetectThing(), "/background_detect/")
thing_server.add_thing(APITestThing(), "/api_test/")
try:

View file

@ -301,21 +301,30 @@ DOWNLOADABLE_SCAN_FILES = (
class SmartScanThing(Thing):
@property
def scan_folder_path(self) -> str:
def scans_folder_path(self) -> str:
"""This folder will hold all the scans we do."""
# TODO: This should be determined using sensible configuration.
# If the working directory is `/var/openflexure` this will result
# in scans being saved at `/var/openflexure/scans/`
return "scans"
def scan_folder_path(self, scan_name: Optional[str]=None):
"""The path to the scan folder with a given name"""
if not scan_name:
if not self.latest_scan_name:
raise IOError("There is no latest scan to return")
scan_name = self.latest_scan_name
return os.path.join(self.scans_folder_path, scan_name)
def new_scan_folder(self) -> str:
"""Create a new empty folder, into which we can save scan images"""
if not os.path.exists(self.scan_folder_path):
os.makedirs(self.scan_folder_path)
if not os.path.exists(self.scans_folder_path):
os.makedirs(self.scans_folder_path)
for j in range(999999):
folder_path = os.path.join(self.scan_folder_path, f"scan_{j:06}")
folder_path = os.path.join(self.scans_folder_path, f"scan_{j:06}")
if not os.path.exists(folder_path):
os.makedirs(folder_path)
self.latest_scan_name = os.path.basename(folder_path)
return folder_path
raise FileExistsError("Could not create a new scan folder: all names in use!")
@ -583,8 +592,8 @@ class SmartScanThing(Thing):
in the `images` folder.
"""
scans: list[ScanInfo] = []
for f in os.listdir(self.scan_folder_path):
path = os.path.join(self.scan_folder_path, f)
for f in os.listdir(self.scans_folder_path):
path = os.path.join(self.scans_folder_path, f)
if os.path.isdir(path):
images_folder = os.path.join(path, "images")
if os.path.isdir(images_folder):
@ -624,7 +633,7 @@ class SmartScanThing(Thing):
raise HTTPException(
403, f"You may only download files named {DOWNLOADABLE_SCAN_FILES}"
)
path = os.path.join(self.scan_folder_path, scan_name, file)
path = os.path.join(self.scans_folder_path, scan_name, file)
if not os.path.isfile(path):
raise HTTPException(404, "File not found")
return FileResponse(path)
@ -642,7 +651,7 @@ class SmartScanThing(Thing):
This endpoint allows scans to be deleted from disk.
"""
path = os.path.join(self.scan_folder_path, scan_name)
path = os.path.join(self.scans_folder_path, scan_name)
if not os.path.isdir(path):
print(f"can't find {path}")
raise HTTPException(404, "Scan not found")

View file

@ -0,0 +1,72 @@
import shutil
from typing import Mapping, Optional
import cv2
from fastapi import HTTPException
from fastapi.responses import FileResponse
import numpy as np
import os
import time
from PIL import Image
from pydantic import BaseModel
from scipy.stats import norm
import logging
from copy import deepcopy
from datetime import datetime
from subprocess import CompletedProcess, run, PIPE
from labthings_fastapi.thing import Thing
from labthings_fastapi.dependencies.raw_thing import raw_thing_dependency
from labthings_fastapi.dependencies.invocation import CancelHook, InvocationLogger, InvocationCancelledError
from labthings_fastapi.decorators import thing_action
from labthings_fastapi.outputs.blob import BlobOutput
from openflexure_microscope_server.things.autofocus import AutofocusThing
from openflexure_microscope_server.things.camera_stage_mapping import CameraStageMapper
from openflexure_microscope_server.things.auto_recentre_stage import RecentringThing
from .smart_scan import SmartScanThing
SmartScanDep = raw_thing_dependency(SmartScanThing)
class JPEGBlob(BlobOutput):
media_type = "image/jpeg"
class Stitcher(Thing):
def __init__(self, path_to_openflexure_stitch: str):
self._script = path_to_openflexure_stitch
def run_subprocess(
self, logger: InvocationLogger, cmd: list[str],
) -> CompletedProcess:
"""Run a subprocess and log any output"""
logger.info(f"Running command in subprocess: `{' '.join(cmd)}")
output = run(cmd, stdout=PIPE, stderr=PIPE, universal_newlines=True)
for pipe in [output.stdout, output.stderr]:
if pipe:
logger.info(pipe)
output.check_returncode()
return output
def images_folder(self, smart_scan: SmartScanDep, scan_name: Optional[str]=None) -> str:
scan_folder = smart_scan.scan_folder_path(scan_name=scan_name)
return os.path.join(scan_folder, "images")
@thing_action
def stitch_scan_from_stage(self, logger: InvocationLogger, smart_scan: SmartScanDep, scan_name: Optional[str]=None, downsample: float=1.0) -> JPEGBlob:
"""Generate a stitched image based on stage position metadata"""
images_folder = self.images_folder(smart_scan=smart_scan, scan_name=scan_name)
self.run_subprocess(logger, [self._script, "--stitching_mode", "only_stage_stitch", images_folder])
return JPEGBlob.from_file(os.path.join(images_folder, "stitched_from_stage.jpg"))
@thing_action
def update_scan_correlations(self, logger: InvocationLogger, smart_scan: SmartScanDep, scan_name: Optional[str]=None) -> None:
"""Generate a stitched image based on stage position metadata"""
images_folder = self.images_folder(smart_scan=smart_scan, scan_name=scan_name)
self.run_subprocess(logger, [self._script, "--stitching_mode", "only_correlate", images_folder])
@thing_action
def stitch_scan(self, logger: InvocationLogger, smart_scan: SmartScanDep, scan_name: Optional[str]=None, downsample: float=1.0) -> None:
"""Generate a stitched image based on stage position metadata"""
images_folder = self.images_folder(smart_scan=smart_scan, scan_name=scan_name)
self.run_subprocess(logger, [self._script, "--stitching_mode", "all", images_folder])