From b5fbcc85b5b2df36cb005b4773f9a1ba08748d82 Mon Sep 17 00:00:00 2001 From: Richard Bowman Date: Wed, 10 Jan 2024 23:17:16 +0000 Subject: [PATCH] Succeed immediately if the stitched image is up to date To save some CPU cycles, we will return immediately if the images don't look newer than the stitch. NB this does mean that the stitched image will be repeatedly transferred, as its URL will change each time it's re-requested. Alternatively, we could fail with an error, which may be more efficient, at the expense of being uglier. --- .../things/stitching.py | 51 ++++++++++++------- 1 file changed, 32 insertions(+), 19 deletions(-) diff --git a/src/openflexure_microscope_server/things/stitching.py b/src/openflexure_microscope_server/things/stitching.py index 9cb4d82c..b2911a2d 100644 --- a/src/openflexure_microscope_server/things/stitching.py +++ b/src/openflexure_microscope_server/things/stitching.py @@ -1,27 +1,12 @@ -import shutil -from typing import Mapping, Optional -import cv2 -from fastapi import HTTPException -from fastapi.responses import FileResponse -import numpy as np +from typing import Optional 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.dependencies.invocation import InvocationLogger 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 @@ -51,13 +36,41 @@ class Stitcher(Thing): 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") + + @staticmethod + def output_up_to_date(folder: str, output_filename: str, image_prefix: str="image", image_suffix: str=".jpg") -> bool: + """Check if any of the images in a folder are newer than a file + + If there are no images (files with the prefix and suffix) newer than the + `output_filename`, we return `True`, i.e. the output is up to date. If + any image in the folder is newer, we return `False`. + + This is not flawless logic - if an update process is slow, images might be + saved between starting that process and saving the output. Consequently, + a `True` from this function does not guarantee the output is up to date. + + If the output file is missing, we also return `False`. + """ + output_path = os.path.join(folder, output_filename) + if not os.path.exists(output_path): + return False + mtime = os.path.getmtime(output_path) + for fname in os.listdir(folder): + if fname.startswith(image_prefix) and fname.endswith(image_suffix): + if os.path.getmtime(os.path.join(folder, fname)) > mtime: + return False + return True @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""" + output_fname = "stitched_from_stage.jpg" 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")) + if self.output_up_to_date(images_folder, output_fname): + logger.info(f"No images are newer than {output_fname}, skipping.") + else: + self.run_subprocess(logger, [self._script, "--stitching_mode", "only_stage_stitch", images_folder]) + return JPEGBlob.from_file(os.path.join(images_folder, output_fname)) @thing_action def update_scan_correlations(self, logger: InvocationLogger, smart_scan: SmartScanDep, scan_name: Optional[str]=None) -> None: