Added a Thing to run stitching as Actions
This commit is contained in:
parent
b6b1fd06bf
commit
9ce52c2845
3 changed files with 91 additions and 8 deletions
|
|
@ -11,6 +11,7 @@ from .things.system_control import SystemControlThing
|
||||||
from .things.settings_manager import SettingsManager
|
from .things.settings_manager import SettingsManager
|
||||||
from .things.auto_recentre_stage import RecentringThing
|
from .things.auto_recentre_stage import RecentringThing
|
||||||
from .things.smart_scan import SmartScanThing, BackgroundDetectThing
|
from .things.smart_scan import SmartScanThing, BackgroundDetectThing
|
||||||
|
from .things.stitching import Stitcher
|
||||||
from .things.test import APITestThing
|
from .things.test import APITestThing
|
||||||
from .serve_static_files import add_static_files
|
from .serve_static_files import add_static_files
|
||||||
from .logging import configure_logging, retrieve_log
|
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(SystemControlThing(), "/system_control/")
|
||||||
thing_server.add_thing(SettingsManager(), "/settings/")
|
thing_server.add_thing(SettingsManager(), "/settings/")
|
||||||
thing_server.add_thing(SmartScanThing(), "/smart_scan/")
|
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(BackgroundDetectThing(), "/background_detect/")
|
||||||
thing_server.add_thing(APITestThing(), "/api_test/")
|
thing_server.add_thing(APITestThing(), "/api_test/")
|
||||||
try:
|
try:
|
||||||
|
|
|
||||||
|
|
@ -301,21 +301,30 @@ DOWNLOADABLE_SCAN_FILES = (
|
||||||
|
|
||||||
class SmartScanThing(Thing):
|
class SmartScanThing(Thing):
|
||||||
@property
|
@property
|
||||||
def scan_folder_path(self) -> str:
|
def scans_folder_path(self) -> str:
|
||||||
"""This folder will hold all the scans we do."""
|
"""This folder will hold all the scans we do."""
|
||||||
# TODO: This should be determined using sensible configuration.
|
# TODO: This should be determined using sensible configuration.
|
||||||
# If the working directory is `/var/openflexure` this will result
|
# If the working directory is `/var/openflexure` this will result
|
||||||
# in scans being saved at `/var/openflexure/scans/`
|
# in scans being saved at `/var/openflexure/scans/`
|
||||||
return "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:
|
def new_scan_folder(self) -> str:
|
||||||
"""Create a new empty folder, into which we can save scan images"""
|
"""Create a new empty folder, into which we can save scan images"""
|
||||||
if not os.path.exists(self.scan_folder_path):
|
if not os.path.exists(self.scans_folder_path):
|
||||||
os.makedirs(self.scan_folder_path)
|
os.makedirs(self.scans_folder_path)
|
||||||
for j in range(999999):
|
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):
|
if not os.path.exists(folder_path):
|
||||||
os.makedirs(folder_path)
|
os.makedirs(folder_path)
|
||||||
|
self.latest_scan_name = os.path.basename(folder_path)
|
||||||
return folder_path
|
return folder_path
|
||||||
raise FileExistsError("Could not create a new scan folder: all names in use!")
|
raise FileExistsError("Could not create a new scan folder: all names in use!")
|
||||||
|
|
||||||
|
|
@ -583,8 +592,8 @@ class SmartScanThing(Thing):
|
||||||
in the `images` folder.
|
in the `images` folder.
|
||||||
"""
|
"""
|
||||||
scans: list[ScanInfo] = []
|
scans: list[ScanInfo] = []
|
||||||
for f in os.listdir(self.scan_folder_path):
|
for f in os.listdir(self.scans_folder_path):
|
||||||
path = os.path.join(self.scan_folder_path, f)
|
path = os.path.join(self.scans_folder_path, f)
|
||||||
if os.path.isdir(path):
|
if os.path.isdir(path):
|
||||||
images_folder = os.path.join(path, "images")
|
images_folder = os.path.join(path, "images")
|
||||||
if os.path.isdir(images_folder):
|
if os.path.isdir(images_folder):
|
||||||
|
|
@ -624,7 +633,7 @@ class SmartScanThing(Thing):
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
403, f"You may only download files named {DOWNLOADABLE_SCAN_FILES}"
|
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):
|
if not os.path.isfile(path):
|
||||||
raise HTTPException(404, "File not found")
|
raise HTTPException(404, "File not found")
|
||||||
return FileResponse(path)
|
return FileResponse(path)
|
||||||
|
|
@ -642,7 +651,7 @@ class SmartScanThing(Thing):
|
||||||
|
|
||||||
This endpoint allows scans to be deleted from disk.
|
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):
|
if not os.path.isdir(path):
|
||||||
print(f"can't find {path}")
|
print(f"can't find {path}")
|
||||||
raise HTTPException(404, "Scan not found")
|
raise HTTPException(404, "Scan not found")
|
||||||
|
|
|
||||||
72
src/openflexure_microscope_server/things/stitching.py
Normal file
72
src/openflexure_microscope_server/things/stitching.py
Normal 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])
|
||||||
Loading…
Add table
Add a link
Reference in a new issue