Static type analysis
This commit is contained in:
parent
3aebb8bead
commit
7866ec0f47
63 changed files with 1825 additions and 2722 deletions
|
|
@ -39,36 +39,44 @@ from openflexure_microscope.paths import (
|
|||
logs_file_path,
|
||||
)
|
||||
|
||||
|
||||
# Custom RotatingFileHandler subclass
|
||||
class CustomRotatingFileHandler(logging.handlers.RotatingFileHandler):
|
||||
"""
|
||||
A custom class for a rotating file log handler, with defaults we like.
|
||||
1MB per file, maximum of 5 historic files.
|
||||
Non-propagating logs (so we can separate access logs from error logs)
|
||||
Optional debugging level.
|
||||
"""
|
||||
|
||||
def __init__(self, filename: str, debug: bool = False) -> None:
|
||||
super().__init__(filename, maxBytes=1_000_000, backupCount=5)
|
||||
# Set formatter
|
||||
self.setFormatter(
|
||||
logging.Formatter(
|
||||
"[%(asctime)s] [%(threadName)s] [%(levelname)s] %(message)s"
|
||||
)
|
||||
)
|
||||
# Never propagate
|
||||
self.propagate = False
|
||||
# Conditionally enable debugging
|
||||
if debug:
|
||||
self.setLevel(logging.DEBUG)
|
||||
|
||||
|
||||
# Log files
|
||||
ROOT_LOGFILE = logs_file_path("openflexure_microscope.log")
|
||||
ACCESS_LOGFILE = logs_file_path("openflexure_microscope.access.log")
|
||||
|
||||
# Basic log format
|
||||
formatter = logging.Formatter(
|
||||
"[%(asctime)s] [%(threadName)s] [%(levelname)s] %(message)s"
|
||||
)
|
||||
|
||||
# Our WSGI server uses Werkzeug, so use that for the access log
|
||||
access_log = logging.getLogger("werkzeug")
|
||||
# Block the access logs from propagating up to the root logger
|
||||
access_log.propagate = False
|
||||
|
||||
# Create error log file handler
|
||||
fh = logging.handlers.RotatingFileHandler(
|
||||
ROOT_LOGFILE, maxBytes=1_000_000, backupCount=5
|
||||
)
|
||||
fh.setFormatter(formatter)
|
||||
fh.setLevel(logging.DEBUG)
|
||||
fh.propagate = False
|
||||
|
||||
fh = CustomRotatingFileHandler(ROOT_LOGFILE, debug=debug_app)
|
||||
# Create access log file handler
|
||||
afh = logging.handlers.RotatingFileHandler(
|
||||
ACCESS_LOGFILE, maxBytes=1_000_000, backupCount=5
|
||||
)
|
||||
afh.setFormatter(formatter)
|
||||
afh.setLevel(logging.DEBUG)
|
||||
afh.propagate = False
|
||||
|
||||
afh = CustomRotatingFileHandler(ACCESS_LOGFILE, debug=debug_app)
|
||||
# Add file handler to root logger
|
||||
root_log.addHandler(fh)
|
||||
access_log.addHandler(afh)
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import logging
|
||||
import time
|
||||
from contextlib import contextmanager
|
||||
from typing import Callable, List, Optional, Tuple
|
||||
|
||||
import numpy as np
|
||||
from labthings import current_action, fields, find_component
|
||||
|
|
@ -8,24 +9,27 @@ from labthings.extensions import BaseExtension
|
|||
from labthings.views import ActionView, View
|
||||
from scipy import ndimage
|
||||
|
||||
from openflexure_microscope.camera.base import BaseCamera
|
||||
from openflexure_microscope.devel import abort
|
||||
from openflexure_microscope.microscope import Microscope
|
||||
from openflexure_microscope.stage.base import BaseStage
|
||||
from openflexure_microscope.utilities import set_properties
|
||||
|
||||
### Autofocus utilities
|
||||
|
||||
|
||||
class JPEGSharpnessMonitor:
|
||||
def __init__(self, microscope):
|
||||
self.microscope = microscope
|
||||
self.camera = microscope.camera
|
||||
self.stage = microscope.stage
|
||||
def __init__(self, microscope: Microscope):
|
||||
self.microscope: Microscope = microscope
|
||||
self.camera: BaseCamera = microscope.camera
|
||||
self.stage: BaseStage = microscope.stage
|
||||
|
||||
self.recording_start_time = None
|
||||
self.recording_start_time: Optional[float] = None
|
||||
|
||||
self.stage_positions = []
|
||||
self.stage_times = []
|
||||
self.jpeg_times = []
|
||||
self.jpeg_sizes = []
|
||||
self.stage_positions: List[Tuple[int, int, int]] = []
|
||||
self.stage_times: List[float] = []
|
||||
self.jpeg_times: List[float] = []
|
||||
self.jpeg_sizes: List[int] = []
|
||||
|
||||
def start(self):
|
||||
# Log the recording start time
|
||||
|
|
@ -35,14 +39,14 @@ class JPEGSharpnessMonitor:
|
|||
self.camera.stream.stop_tracking()
|
||||
self.camera.stream.reset_tracking()
|
||||
|
||||
def focus_rel(self, dz, backlash=False, **kwargs):
|
||||
def focus_rel(self, dz: int, backlash: bool = False, **kwargs):
|
||||
# Store the start time and position
|
||||
self.camera.stream.start_tracking()
|
||||
self.stage_times.append(time.time())
|
||||
self.stage_positions.append(self.stage.position)
|
||||
|
||||
# Main move
|
||||
self.stage.move_rel([0, 0, dz], backlash=backlash, **kwargs)
|
||||
self.stage.move_rel((0, 0, dz), backlash=backlash, **kwargs)
|
||||
|
||||
# Store the end time and position
|
||||
self.camera.stream.stop_tracking()
|
||||
|
|
@ -63,7 +67,7 @@ class JPEGSharpnessMonitor:
|
|||
final_z_position = self.stage_positions[-1][2]
|
||||
return data_index, final_z_position
|
||||
|
||||
def move_data(self, istart, istop=None):
|
||||
def move_data(self, istart: int, istop: Optional[int] = None):
|
||||
"""Extract sharpness as a function of (interpolated) z"""
|
||||
if istop is None:
|
||||
istop = istart + 2
|
||||
|
|
@ -88,7 +92,7 @@ class JPEGSharpnessMonitor:
|
|||
jpeg_zs = np.interp(jpeg_times, stage_times, stage_zs)
|
||||
return jpeg_times, jpeg_zs, jpeg_sizes[start:stop]
|
||||
|
||||
def sharpest_z_on_move(self, index):
|
||||
def sharpest_z_on_move(self, index: int):
|
||||
"""Return the z position of the sharpest image on a given move"""
|
||||
_, jz, js = self.move_data(index)
|
||||
if len(js) == 0:
|
||||
|
|
@ -105,23 +109,14 @@ class JPEGSharpnessMonitor:
|
|||
return data
|
||||
|
||||
|
||||
def decimate_to(shape, image):
|
||||
"""Decimate an image to reduce its size if it's too big."""
|
||||
decimation = np.max(
|
||||
np.ceil(np.array(image.shape, dtype=np.float)[: len(shape)] / np.array(shape))
|
||||
)
|
||||
return image[:: int(decimation), :: int(decimation), ...]
|
||||
|
||||
|
||||
def sharpness_sum_lap2(rgb_image):
|
||||
def sharpness_sum_lap2(rgb_image: np.ndarray):
|
||||
"""Return an image sharpness metric: sum(laplacian(image)**")"""
|
||||
# image_bw=np.mean(decimate_to((1000,1000), rgb_image),2)
|
||||
image_bw = np.mean(rgb_image, 2)
|
||||
image_lap = ndimage.filters.laplace(image_bw)
|
||||
return np.mean(image_lap.astype(np.float) ** 4)
|
||||
|
||||
|
||||
def sharpness_edge(image):
|
||||
def sharpness_edge(image: np.ndarray):
|
||||
"""Return a sharpness metric optimised for vertical lines"""
|
||||
gray = np.mean(image.astype(float), 2)
|
||||
n = 20
|
||||
|
|
@ -134,13 +129,23 @@ def sharpness_edge(image):
|
|||
### Autofocus extension
|
||||
|
||||
|
||||
def measure_sharpness(microscope, metric_fn=sharpness_sum_lap2):
|
||||
def measure_sharpness(microscope: Microscope, metric_fn: Callable = sharpness_sum_lap2):
|
||||
"""Measure the sharpness of the camera's current view."""
|
||||
if hasattr(microscope.camera, "array"):
|
||||
return metric_fn(microscope.camera.array(use_video_port=True))
|
||||
|
||||
if hasattr(microscope.camera, "array") and callable(
|
||||
getattr(microscope.camera, "array")
|
||||
):
|
||||
return metric_fn(getattr(microscope.camera, "array")(use_video_port=True))
|
||||
else:
|
||||
raise RuntimeError(f"Object {microscope.camera} has no method `array`")
|
||||
|
||||
|
||||
def autofocus(microscope, dz, settle=0.5, metric_fn=sharpness_sum_lap2):
|
||||
def autofocus(
|
||||
microscope: Microscope,
|
||||
dz: List[int],
|
||||
settle: float = 0.5,
|
||||
metric_fn: Callable = sharpness_sum_lap2,
|
||||
):
|
||||
"""Perform a simple autofocus routine.
|
||||
The stage is moved to z positions (relative to current position) in dz,
|
||||
and at each position an image is captured and the sharpness function
|
||||
|
|
@ -154,7 +159,10 @@ def autofocus(microscope, dz, settle=0.5, metric_fn=sharpness_sum_lap2):
|
|||
with set_properties(stage, backlash=256), stage.lock, camera.lock:
|
||||
sharpnesses = []
|
||||
positions = []
|
||||
camera.annotate_text = ""
|
||||
|
||||
# Some cameras may not have annotate_text. Reset if it does
|
||||
if getattr(camera, "annotate_text"):
|
||||
setattr(camera, "annotate_text", "")
|
||||
|
||||
for _ in stage.scan_z(dz, return_to_start=False):
|
||||
if current_action() and current_action().stopped:
|
||||
|
|
@ -164,13 +172,13 @@ def autofocus(microscope, dz, settle=0.5, metric_fn=sharpness_sum_lap2):
|
|||
sharpnesses.append(measure_sharpness(microscope, metric_fn))
|
||||
|
||||
newposition = positions[np.argmax(sharpnesses)]
|
||||
stage.move_rel([0, 0, newposition - stage.position[2]])
|
||||
stage.move_rel((0, 0, newposition - stage.position[2]))
|
||||
|
||||
return positions, sharpnesses
|
||||
|
||||
|
||||
@contextmanager
|
||||
def monitor_sharpness(microscope):
|
||||
def monitor_sharpness(microscope: Microscope):
|
||||
m = JPEGSharpnessMonitor(microscope)
|
||||
m.start()
|
||||
try:
|
||||
|
|
@ -179,21 +187,21 @@ def monitor_sharpness(microscope):
|
|||
m.stop()
|
||||
|
||||
|
||||
def move_and_find_focus(microscope, dz):
|
||||
def move_and_find_focus(microscope: Microscope, dz: int):
|
||||
"""Make a relative Z move and return the peak sharpness position"""
|
||||
with monitor_sharpness(microscope) as m:
|
||||
m.focus_rel(dz)
|
||||
return m.sharpest_z_on_move(0)
|
||||
|
||||
|
||||
def move_and_measure(microscope, dz):
|
||||
def move_and_measure(microscope: Microscope, dz: int):
|
||||
"""Make a relative Z move and return the sharpness data"""
|
||||
with monitor_sharpness(microscope) as m:
|
||||
m.focus_rel(dz)
|
||||
return m.data_dict()
|
||||
|
||||
|
||||
def fast_autofocus(microscope, dz=2000):
|
||||
def fast_autofocus(microscope: Microscope, dz: int = 2000):
|
||||
"""Perform a down-up-down-up autofocus"""
|
||||
with microscope.camera.lock, microscope.stage.lock:
|
||||
with monitor_sharpness(microscope) as m:
|
||||
|
|
@ -216,7 +224,11 @@ def fast_autofocus(microscope, dz=2000):
|
|||
|
||||
|
||||
def fast_up_down_up_autofocus(
|
||||
microscope, dz=2000, target_z=0, initial_move_up=True, mini_backlash=25
|
||||
microscope: Microscope,
|
||||
dz: int = 2000,
|
||||
target_z: int = 0,
|
||||
initial_move_up: bool = True,
|
||||
mini_backlash: int = 25,
|
||||
):
|
||||
"""Autofocus by measuring on the way down, and moving back up with feedback.
|
||||
|
||||
|
|
@ -256,7 +268,7 @@ def fast_up_down_up_autofocus(
|
|||
with microscope.camera.lock, microscope.stage.lock:
|
||||
with monitor_sharpness(microscope) as m:
|
||||
# Ensure the MJPEG stream has started
|
||||
microscope.camera.start_stream_recording()
|
||||
microscope.camera.start_stream()
|
||||
|
||||
logging.debug("Initial move")
|
||||
if initial_move_up:
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import logging
|
||||
import os
|
||||
from typing import Dict, List, Optional, Tuple
|
||||
|
||||
import psutil
|
||||
from flask import abort
|
||||
|
|
@ -9,32 +10,43 @@ from labthings.marshalling import use_args
|
|||
from labthings.views import PropertyView, View
|
||||
|
||||
from openflexure_microscope.api.utilities.gui import build_gui
|
||||
from openflexure_microscope.captures.capture_manager import BASE_CAPTURE_PATH
|
||||
from openflexure_microscope.captures.capture_manager import (
|
||||
BASE_CAPTURE_PATH,
|
||||
CaptureManager,
|
||||
)
|
||||
from openflexure_microscope.microscope import Microscope
|
||||
from openflexure_microscope.paths import check_rw, settings_file_path
|
||||
|
||||
AS_SETTINGS_PATH = settings_file_path("autostorage_settings.json")
|
||||
|
||||
|
||||
def get_partitions():
|
||||
def get_partitions() -> List[str]:
|
||||
return [disk.mountpoint for disk in psutil.disk_partitions() if "rw" in disk.opts]
|
||||
|
||||
|
||||
def get_permissive_partitions():
|
||||
def get_permissive_partitions() -> List[str]:
|
||||
return [partition for partition in get_partitions() if check_rw(partition)]
|
||||
|
||||
|
||||
def get_permissive_locations():
|
||||
def get_permissive_locations() -> List[Tuple[str, str]]:
|
||||
return [
|
||||
(partition, os.path.join(partition, "openflexure", "data", "micrographs"))
|
||||
for partition in get_permissive_partitions()
|
||||
]
|
||||
|
||||
|
||||
def get_current_location(capture_manager):
|
||||
return capture_manager.paths.get("default")
|
||||
def get_current_location(capture_manager: Optional[CaptureManager]) -> str:
|
||||
if capture_manager:
|
||||
return capture_manager.paths.get("default", BASE_CAPTURE_PATH)
|
||||
return BASE_CAPTURE_PATH
|
||||
|
||||
|
||||
def set_current_location(capture_manager, location: str):
|
||||
def set_current_location(capture_manager: Optional[CaptureManager], location: str):
|
||||
if not capture_manager:
|
||||
logging.warning(
|
||||
"Cannot set_current_location of a missing capture_manager. Skipping."
|
||||
)
|
||||
return
|
||||
if not os.path.isdir(location):
|
||||
os.makedirs(location)
|
||||
logging.debug("Updating location...")
|
||||
|
|
@ -44,12 +56,12 @@ def set_current_location(capture_manager, location: str):
|
|||
logging.debug("Capture location changed successfully.")
|
||||
|
||||
|
||||
def get_default_location():
|
||||
def get_default_location() -> str:
|
||||
return BASE_CAPTURE_PATH
|
||||
|
||||
|
||||
def get_all_locations():
|
||||
locations = {}
|
||||
def get_all_locations() -> Dict[str, str]:
|
||||
locations: Dict[str, str] = {}
|
||||
# If default is not already listed (e.g. if it's currently set)
|
||||
if get_default_location() not in locations.values():
|
||||
locations["Default"] = get_default_location()
|
||||
|
|
@ -71,29 +83,23 @@ def get_all_locations():
|
|||
return {k: v for k, v in locations.items() if v}
|
||||
|
||||
|
||||
class CaptureStorageLocation:
|
||||
def __init__(self, mountpoint):
|
||||
pass
|
||||
|
||||
|
||||
class AutostorageExtension(BaseExtension):
|
||||
def __init__(self):
|
||||
BaseExtension.__init__(
|
||||
self,
|
||||
super().__init__(
|
||||
"org.openflexure.autostorage",
|
||||
version="2.0.0",
|
||||
description="Handle switching capture storage devices",
|
||||
)
|
||||
|
||||
# We'll store a reference to a CaptureManager object, who's capture paths will be modified
|
||||
self.capture_manager = None
|
||||
self.capture_manager: Optional[CaptureManager] = None
|
||||
|
||||
self.initial_location = get_default_location()
|
||||
self.initial_location: str = get_default_location()
|
||||
|
||||
# Register the on_microscope function to run when the microscope is attached
|
||||
self.on_component("org.openflexure.microscope", self.on_microscope)
|
||||
|
||||
def on_microscope(self, microscope_obj):
|
||||
def on_microscope(self, microscope_obj: Microscope):
|
||||
"""Function to automatically call when the parent LabThing has a microscope attached."""
|
||||
logging.debug("Autostorage extension found microscope %s", microscope_obj)
|
||||
if hasattr(microscope_obj, "captures"):
|
||||
|
|
@ -110,10 +116,15 @@ class AutostorageExtension(BaseExtension):
|
|||
self.check_location(self.initial_location)
|
||||
|
||||
logging.debug(self.get_locations())
|
||||
else:
|
||||
raise RuntimeError(
|
||||
"Attached a microscope with no `captures` capture manager. Skipping extension."
|
||||
)
|
||||
|
||||
def check_location(self, location=None):
|
||||
def check_location(self, location: Optional[str] = None) -> bool:
|
||||
location = location or get_current_location(self.capture_manager)
|
||||
if not location:
|
||||
location = get_current_location(self.capture_manager)
|
||||
return False
|
||||
# If preferred path does not exist, or cannot be written to
|
||||
if not (os.path.isdir(location) and check_rw(location)):
|
||||
logging.error(
|
||||
|
|
@ -122,8 +133,9 @@ class AutostorageExtension(BaseExtension):
|
|||
)
|
||||
# Reset the storage location to default
|
||||
set_current_location(self.capture_manager, get_default_location())
|
||||
return True
|
||||
|
||||
def get_locations(self):
|
||||
def get_locations(self) -> Dict[str, str]:
|
||||
if self.capture_manager:
|
||||
locations = get_all_locations()
|
||||
|
||||
|
|
@ -135,7 +147,7 @@ class AutostorageExtension(BaseExtension):
|
|||
else:
|
||||
return {}
|
||||
|
||||
def get_preferred_key(self):
|
||||
def get_preferred_key(self) -> Optional[str]:
|
||||
current = get_current_location(self.capture_manager)
|
||||
locations = self.get_locations()
|
||||
|
||||
|
|
@ -146,22 +158,28 @@ class AutostorageExtension(BaseExtension):
|
|||
"Multiple path matches found. Weird, but carrying on using zeroth."
|
||||
)
|
||||
|
||||
return matches[0]
|
||||
if matches:
|
||||
return matches[0]
|
||||
else:
|
||||
logging.warning("No matches found. Skipping.")
|
||||
return None
|
||||
|
||||
def set_preferred_key(self, new_path_key: str):
|
||||
if not new_path_key in self.get_locations().keys():
|
||||
raise KeyError(f"No location named {new_path_key}")
|
||||
|
||||
location = self.get_locations().get(new_path_key)
|
||||
set_current_location(self.capture_manager, location)
|
||||
if location:
|
||||
set_current_location(self.capture_manager, location)
|
||||
|
||||
def key_to_title(self, path_key: str):
|
||||
def key_to_title(self, path_key: Optional[str]) -> Optional[str]:
|
||||
if not path_key:
|
||||
return None
|
||||
if not path_key in self.get_locations().keys():
|
||||
raise KeyError(f"No location named {path_key}")
|
||||
|
||||
return f"{path_key} ({self.get_locations().get(path_key)})"
|
||||
|
||||
def title_to_key(self, path_title: str):
|
||||
def title_to_key(self, path_title: str) -> str:
|
||||
matches = []
|
||||
for loc_key in self.get_locations().keys():
|
||||
if path_title.startswith(loc_key):
|
||||
|
|
@ -174,10 +192,15 @@ class AutostorageExtension(BaseExtension):
|
|||
|
||||
return matches[0]
|
||||
|
||||
def get_titles(self):
|
||||
return [self.key_to_title(key) for key in self.get_locations().keys()]
|
||||
def get_titles(self) -> List[str]:
|
||||
titles: List[str] = []
|
||||
for key in self.get_locations().keys():
|
||||
title: Optional[str] = self.key_to_title(key)
|
||||
if title:
|
||||
titles.append(title)
|
||||
return titles
|
||||
|
||||
def get_preferred_title(self):
|
||||
def get_preferred_title(self) -> Optional[str]:
|
||||
return self.key_to_title(self.get_preferred_key())
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -1,14 +1,15 @@
|
|||
import logging
|
||||
from contextlib import contextmanager
|
||||
|
||||
# Type hinting
|
||||
from typing import Tuple
|
||||
|
||||
import picamerax
|
||||
from flask import abort
|
||||
from labthings import find_component
|
||||
from labthings.extensions import BaseExtension
|
||||
from labthings.views import ActionView
|
||||
|
||||
from openflexure_microscope.camera.base import BaseCamera
|
||||
from openflexure_microscope.microscope import Microscope
|
||||
|
||||
from .recalibrate_utils import (
|
||||
auto_expose_and_freeze_settings,
|
||||
flat_lens_shading_table,
|
||||
|
|
@ -17,7 +18,7 @@ from .recalibrate_utils import (
|
|||
|
||||
|
||||
@contextmanager
|
||||
def pause_stream(scamera, resolution: Tuple[int, int] = None):
|
||||
def pause_stream(scamera: BaseCamera):
|
||||
"""This context manager locks a streaming camera, and pauses the stream.
|
||||
|
||||
The stream is re-enabled, with the original resolution, once the with
|
||||
|
|
@ -28,20 +29,20 @@ def pause_stream(scamera, resolution: Tuple[int, int] = None):
|
|||
not scamera.record_active
|
||||
), "We can't pause the camera's video stream while a recording is in progress."
|
||||
streaming = scamera.stream_active
|
||||
old_resolution = scamera.camera.resolution
|
||||
old_resolution = scamera.stream_resolution
|
||||
if streaming:
|
||||
logging.info("Stopping stream in pause_stream context manager")
|
||||
scamera.stop_stream_recording(resolution=resolution)
|
||||
scamera.stop_stream()
|
||||
try:
|
||||
yield scamera
|
||||
finally:
|
||||
scamera.camera.resolution = old_resolution
|
||||
scamera.stream_resolution = old_resolution
|
||||
if streaming:
|
||||
logging.info("Restarting stream in pause_stream context manager")
|
||||
scamera.start_stream_recording()
|
||||
scamera.start_stream()
|
||||
|
||||
|
||||
def recalibrate(microscope):
|
||||
def recalibrate(microscope: Microscope):
|
||||
"""Reset the camera's settings.
|
||||
|
||||
This generates new gains, exposure time, and lens shading
|
||||
|
|
@ -49,11 +50,15 @@ def recalibrate(microscope):
|
|||
with a gray level of 230. It takes a little while to run.
|
||||
"""
|
||||
with pause_stream(microscope.camera) as scamera:
|
||||
auto_expose_and_freeze_settings(
|
||||
scamera.camera
|
||||
) # scamera.camera is the PiCamera object
|
||||
recalibrate_camera(scamera.camera)
|
||||
microscope.save_settings()
|
||||
if hasattr(scamera, "picamera"):
|
||||
picamera_obj: picamerax.PiCamera = getattr(scamera, "picamera")
|
||||
auto_expose_and_freeze_settings(picamera_obj)
|
||||
recalibrate_camera(picamera_obj)
|
||||
microscope.save_settings()
|
||||
else:
|
||||
raise RuntimeError(
|
||||
"Recalibrate can only be used with a Raspberry Pi camera"
|
||||
)
|
||||
|
||||
|
||||
class RecalibrateView(ActionView):
|
||||
|
|
|
|||
|
|
@ -1,19 +1,21 @@
|
|||
import logging
|
||||
import time
|
||||
from fractions import Fraction
|
||||
from typing import List, Optional, Tuple
|
||||
|
||||
import numpy as np
|
||||
from picamerax import PiCamera
|
||||
from picamerax.array import PiBayerArray, PiRGBArray
|
||||
|
||||
|
||||
def rgb_image(camera, resize=None, **kwargs):
|
||||
def rgb_image(camera: PiCamera, resize: Optional[Tuple[int, int]] = None, **kwargs):
|
||||
"""Capture an image and return an RGB numpy array"""
|
||||
with PiRGBArray(camera, size=resize) as output:
|
||||
camera.capture(output, format="rgb", resize=resize, **kwargs)
|
||||
return output.array
|
||||
|
||||
|
||||
def flat_lens_shading_table(camera):
|
||||
def flat_lens_shading_table(camera: PiCamera):
|
||||
"""Return a flat (i.e. unity gain) lens shading table.
|
||||
|
||||
This is mostly useful because it makes it easy to get the size
|
||||
|
|
@ -24,14 +26,11 @@ def flat_lens_shading_table(camera):
|
|||
raise ImportError(
|
||||
"This program requires the forked picamera library with lens shading support"
|
||||
)
|
||||
return (
|
||||
# pylint: disable=W0212
|
||||
np.zeros(camera._lens_shading_table_shape(), dtype=np.uint8)
|
||||
+ 32
|
||||
)
|
||||
# pylint: disable=protected-access
|
||||
return np.zeros(camera._lens_shading_table_shape(), dtype=np.uint8) + 32
|
||||
|
||||
|
||||
def adjust_exposure_to_setpoint(camera, setpoint):
|
||||
def adjust_exposure_to_setpoint(camera: PiCamera, setpoint: int):
|
||||
"""Adjust the camera's exposure time until the maximum pixel value is <setpoint>."""
|
||||
print("Adjusting shutter speed to hit setpoint {}".format(setpoint), end="")
|
||||
for _ in range(3):
|
||||
|
|
@ -43,7 +42,7 @@ def adjust_exposure_to_setpoint(camera, setpoint):
|
|||
print("done")
|
||||
|
||||
|
||||
def auto_expose_and_freeze_settings(camera):
|
||||
def auto_expose_and_freeze_settings(camera: PiCamera):
|
||||
"""Freeze the settings after auto-exposing to white illumination"""
|
||||
logging.info("Allowing the camera to auto-expose")
|
||||
if "greyworld" in camera.AWB_MODES:
|
||||
|
|
@ -66,7 +65,7 @@ def auto_expose_and_freeze_settings(camera):
|
|||
logging.info("Shutter speed = %s", (camera.shutter_speed))
|
||||
camera.exposure_mode = "off"
|
||||
logging.info("Auto exposure disabled")
|
||||
g = camera.awb_gains
|
||||
g: Tuple[Fraction, Fraction] = camera.awb_gains
|
||||
camera.awb_mode = "off"
|
||||
camera.awb_gains = g
|
||||
logging.info("Auto white balance disabled, gains are %s", (g))
|
||||
|
|
@ -76,10 +75,10 @@ def auto_expose_and_freeze_settings(camera):
|
|||
adjust_exposure_to_setpoint(camera, 215)
|
||||
|
||||
|
||||
def channels_from_bayer_array(bayer_array):
|
||||
def channels_from_bayer_array(bayer_array: np.ndarray) -> np.ndarray:
|
||||
"""Given the 'array' from a PiBayerArray, return the 4 channels."""
|
||||
bayer_pattern = [(i // 2, i % 2) for i in range(4)]
|
||||
channels = np.zeros(
|
||||
bayer_pattern: List[Tuple[int, int]] = [(0, 0), (0, 1), (1, 0), (1, 1)]
|
||||
channels: np.ndarray = np.zeros(
|
||||
(4, bayer_array.shape[0] // 2, bayer_array.shape[1] // 2),
|
||||
dtype=bayer_array.dtype,
|
||||
)
|
||||
|
|
@ -92,14 +91,19 @@ def channels_from_bayer_array(bayer_array):
|
|||
return channels
|
||||
|
||||
|
||||
def lst_from_channels(channels):
|
||||
def lst_from_channels(channels: np.ndarray) -> np.ndarray:
|
||||
"""Given the 4 Bayer colour channels from a white image, generate a LST."""
|
||||
full_resolution = np.array(channels.shape[1:]) * 2 # channels have been binned
|
||||
# lst_resolution = list(np.ceil(full_resolution / 64.0).astype(int))
|
||||
lst_resolution = [(r // 64) + 1 for r in full_resolution]
|
||||
# NB the size of the LST is 1/64th of the image, but rounded UP.
|
||||
full_resolution: np.ndarray = np.array(
|
||||
channels.shape[1:]
|
||||
) * 2 # channels have been binned
|
||||
|
||||
# NOTE: the size of the LST is 1/64th of the image, but rounded UP.
|
||||
lst_resolution: List[int] = [(r // 64) + 1 for r in full_resolution]
|
||||
|
||||
logging.info("Generating a lens shading table at %sx%s", *lst_resolution)
|
||||
lens_shading = np.zeros([channels.shape[0]] + lst_resolution, dtype=np.float)
|
||||
lens_shading: np.ndarray = np.zeros(
|
||||
[channels.shape[0]] + lst_resolution, dtype=np.float
|
||||
)
|
||||
for i in range(lens_shading.shape[0]):
|
||||
image_channel = channels[i, :, :]
|
||||
iw, ih = image_channel.shape
|
||||
|
|
@ -153,7 +157,7 @@ def lst_from_channels(channels):
|
|||
return lens_shading_table[::-1, :, :].copy()
|
||||
|
||||
|
||||
def recalibrate_camera(camera):
|
||||
def recalibrate_camera(camera: PiCamera):
|
||||
"""Reset the lens shading table and exposure settings.
|
||||
|
||||
This method first resets to a flat lens shading table, then auto-exposes,
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ import logging
|
|||
import time
|
||||
import uuid
|
||||
from functools import reduce
|
||||
from typing import Tuple
|
||||
from typing import Dict, List, Optional, Tuple
|
||||
|
||||
from labthings import (
|
||||
current_action,
|
||||
|
|
@ -14,20 +14,32 @@ from labthings import (
|
|||
)
|
||||
from labthings.extensions import BaseExtension
|
||||
from labthings.views import ActionView
|
||||
from typing_extensions import Literal
|
||||
|
||||
from openflexure_microscope.api.v2.views.actions.camera import FullCaptureArgs
|
||||
from openflexure_microscope.captures.capture_manager import generate_basename
|
||||
from openflexure_microscope.devel import abort
|
||||
from openflexure_microscope.microscope import Microscope
|
||||
|
||||
# Type alias for convenience
|
||||
XyCoordinate = Tuple[int, int]
|
||||
XyzCoordinate = Tuple[int, int, int]
|
||||
|
||||
|
||||
### Grid construction
|
||||
|
||||
|
||||
def construct_grid(initial, step_sizes, n_steps, style="raster"):
|
||||
def construct_grid(
|
||||
initial: XyCoordinate,
|
||||
step_sizes: XyCoordinate,
|
||||
n_steps: XyCoordinate,
|
||||
style: Literal["raster", "snake", "spiral"] = "raster",
|
||||
):
|
||||
"""
|
||||
Given an initial position, step sizes, and number of steps,
|
||||
construct a 2-dimensional list of scan x-y positions.
|
||||
"""
|
||||
arr = []
|
||||
arr: List[List[XyCoordinate]] = [] # 2D array of coordinates
|
||||
|
||||
if style == "spiral":
|
||||
# deal with the centre image immediately
|
||||
|
|
@ -35,31 +47,48 @@ def construct_grid(initial, step_sizes, n_steps, style="raster"):
|
|||
arr.append([initial])
|
||||
# for spiral, n_steps is the number of shells, and so only requires n_steps[0]
|
||||
for i in range(2, n_steps[0] + 1):
|
||||
arr.append([])
|
||||
arr.append([]) # Append new shell holder
|
||||
side_length = (2 * i) - 1
|
||||
|
||||
# iteratively generate the next location to append
|
||||
coord = [coord[ax] + [-1, 1][ax] * step_sizes[ax] for ax in range(2)]
|
||||
# Iteratively generate the next location to append
|
||||
|
||||
# Start coordinate of the shell
|
||||
# We create a copy of coord so that the new value of coord doesn't depend on itself
|
||||
# Otherwise we create a generator, not a tuple, which makes type checking angry
|
||||
last_coordinate: XyCoordinate = coord
|
||||
coord = (
|
||||
last_coordinate[0] + [-1, 1][0] * step_sizes[0],
|
||||
last_coordinate[1] + [-1, 1][1] * step_sizes[1],
|
||||
)
|
||||
for direction in ([1, 0], [0, -1], [-1, 0], [0, 1]):
|
||||
for _ in range(side_length - 1):
|
||||
coord = [
|
||||
coord[ax] + direction[ax] * step_sizes[ax] for ax in range(2)
|
||||
]
|
||||
arr[i - 1].append(tuple(coord))
|
||||
last_coordinate = coord
|
||||
coord = (
|
||||
last_coordinate[0] + direction[0] * step_sizes[0],
|
||||
last_coordinate[1] + direction[1] * step_sizes[1],
|
||||
)
|
||||
arr[i - 1].append(coord)
|
||||
|
||||
# If raster or snake
|
||||
else:
|
||||
for i in range(n_steps[0]): # x axis
|
||||
arr.append([])
|
||||
for j in range(n_steps[1]): # y axis
|
||||
# Create a coordinate array
|
||||
coord = [initial[ax] + [i, j][ax] * step_sizes[ax] for ax in range(2)]
|
||||
# Create a coordinate tuple
|
||||
coord = (
|
||||
initial[0] + [i, j][0] * step_sizes[0],
|
||||
initial[1] + [i, j][1] * step_sizes[1],
|
||||
)
|
||||
# Append coordinate array to position grid
|
||||
arr[i].append(tuple(coord))
|
||||
arr[i].append(coord)
|
||||
|
||||
# Style modifiers
|
||||
if style == "snake":
|
||||
# For each line (row) in the coordinate array
|
||||
for i, line in enumerate(arr):
|
||||
# If it's an odd row
|
||||
if i % 2 != 0:
|
||||
# Reverse the list of coordinates
|
||||
line.reverse()
|
||||
|
||||
return arr
|
||||
|
|
@ -76,16 +105,17 @@ class ScanExtension(BaseExtension):
|
|||
|
||||
def capture(
|
||||
self,
|
||||
microscope,
|
||||
basename,
|
||||
microscope: Microscope,
|
||||
basename: Optional[str],
|
||||
namemode: str = "coordinates",
|
||||
temporary: bool = False,
|
||||
use_video_port: bool = False,
|
||||
resize: Tuple[int, int] = None,
|
||||
resize: Optional[Tuple[int, int]] = None,
|
||||
bayer: bool = False,
|
||||
metadata: dict = None,
|
||||
annotations: dict = None,
|
||||
tags: list = None,
|
||||
metadata: Optional[dict] = None,
|
||||
annotations: Optional[Dict[str, str]] = None,
|
||||
tags: Optional[List[str]] = None,
|
||||
dataset: Optional[Dict[str, str]] = None,
|
||||
):
|
||||
metadata = metadata or {}
|
||||
annotations = annotations or {}
|
||||
|
|
@ -113,6 +143,7 @@ class ScanExtension(BaseExtension):
|
|||
bayer=bayer,
|
||||
annotations=annotations,
|
||||
tags=tags,
|
||||
dataset=dataset,
|
||||
metadata=metadata,
|
||||
cache_key=folder,
|
||||
)
|
||||
|
|
@ -125,21 +156,21 @@ class ScanExtension(BaseExtension):
|
|||
### Scanning
|
||||
def tile(
|
||||
self,
|
||||
microscope,
|
||||
basename: str = None,
|
||||
microscope: Microscope,
|
||||
basename: Optional[str] = None,
|
||||
namemode: str = "coordinates",
|
||||
temporary: bool = False,
|
||||
stride_size: list = (2000, 1500, 100),
|
||||
grid: list = (3, 3, 5),
|
||||
stride_size: XyzCoordinate = (2000, 1500, 100),
|
||||
grid: XyzCoordinate = (3, 3, 5),
|
||||
style="raster",
|
||||
autofocus_dz: int = 50,
|
||||
use_video_port: bool = False,
|
||||
resize: Tuple[int, int] = None,
|
||||
resize: Optional[Tuple[int, int]] = None,
|
||||
bayer: bool = False,
|
||||
fast_autofocus=False,
|
||||
metadata: dict = None,
|
||||
annotations: dict = None,
|
||||
tags: list = None,
|
||||
fast_autofocus: bool = False,
|
||||
metadata: Optional[dict] = None,
|
||||
annotations: Optional[Dict[str, str]] = None,
|
||||
tags: Optional[List[str]] = None,
|
||||
):
|
||||
metadata = metadata or {}
|
||||
annotations = annotations or {}
|
||||
|
|
@ -160,16 +191,14 @@ class ScanExtension(BaseExtension):
|
|||
|
||||
# Add dataset metadata
|
||||
dataset_d = {
|
||||
"dataset": {
|
||||
"id": uuid.uuid4(),
|
||||
"type": "xyzScan",
|
||||
"name": basename,
|
||||
"acquisitionDate": datetime.datetime.now().isoformat(),
|
||||
"strideSize": stride_size,
|
||||
"grid": grid,
|
||||
"style": style,
|
||||
"autofocusDz": autofocus_dz,
|
||||
}
|
||||
"id": uuid.uuid4(),
|
||||
"type": "xyzScan",
|
||||
"name": basename,
|
||||
"acquisitionDate": datetime.datetime.now().isoformat(),
|
||||
"strideSize": stride_size,
|
||||
"grid": grid,
|
||||
"style": style,
|
||||
"autofocusDz": autofocus_dz,
|
||||
}
|
||||
|
||||
# Check if autofocus is enabled
|
||||
|
|
@ -186,7 +215,7 @@ class ScanExtension(BaseExtension):
|
|||
|
||||
# Construct an x-y grid (worry about z later)
|
||||
x_y_grid = construct_grid(
|
||||
initial_position, stride_size[:2], grid[:2], style=style
|
||||
initial_position[:2], stride_size[:2], grid[:2], style=style
|
||||
)
|
||||
|
||||
# Keep the initial Z position the same as our current position
|
||||
|
|
@ -201,13 +230,13 @@ class ScanExtension(BaseExtension):
|
|||
next_z = initial_z # Reset z position at start of each new row
|
||||
logging.debug("Returning to initial z position")
|
||||
microscope.stage.move_abs(
|
||||
[line[0][0], line[0][1], next_z]
|
||||
(line[0][0], line[0][1], next_z)
|
||||
) # RWB: I think this line is redundant
|
||||
|
||||
for x_y in line:
|
||||
# Move to new grid position without changing z
|
||||
logging.debug("Moving to step %s", ([x_y[0], x_y[1], next_z]))
|
||||
microscope.stage.move_abs([x_y[0], x_y[1], next_z])
|
||||
microscope.stage.move_abs((x_y[0], x_y[1], next_z))
|
||||
# Refocus
|
||||
if autofocus_enabled:
|
||||
if fast_autofocus:
|
||||
|
|
@ -232,7 +261,7 @@ class ScanExtension(BaseExtension):
|
|||
use_video_port=use_video_port,
|
||||
resize=resize,
|
||||
bayer=bayer,
|
||||
metadata=dataset_d,
|
||||
dataset=dataset_d,
|
||||
annotations=annotations,
|
||||
tags=tags,
|
||||
)
|
||||
|
|
@ -251,7 +280,7 @@ class ScanExtension(BaseExtension):
|
|||
use_video_port=use_video_port,
|
||||
resize=resize,
|
||||
bayer=bayer,
|
||||
metadata=dataset_d,
|
||||
dataset=dataset_d,
|
||||
annotations=annotations,
|
||||
tags=tags,
|
||||
)
|
||||
|
|
@ -268,19 +297,20 @@ class ScanExtension(BaseExtension):
|
|||
|
||||
def stack(
|
||||
self,
|
||||
microscope,
|
||||
basename: str = None,
|
||||
microscope: Microscope,
|
||||
basename: Optional[str] = None,
|
||||
namemode: str = "coordinates",
|
||||
temporary: bool = False,
|
||||
step_size: int = 100,
|
||||
steps: int = 5,
|
||||
return_to_start: bool = True,
|
||||
use_video_port: bool = False,
|
||||
resize: Tuple[int, int] = None,
|
||||
resize: Optional[Tuple[int, int]] = None,
|
||||
bayer: bool = False,
|
||||
metadata: dict = None,
|
||||
annotations: dict = None,
|
||||
tags: list = None,
|
||||
metadata: Optional[dict] = None,
|
||||
annotations: Optional[Dict[str, str]] = None,
|
||||
dataset: Optional[Dict[str, str]] = None,
|
||||
tags: Optional[List[str]] = None,
|
||||
):
|
||||
metadata = metadata or {}
|
||||
annotations = annotations or {}
|
||||
|
|
@ -293,7 +323,7 @@ class ScanExtension(BaseExtension):
|
|||
with microscope.lock:
|
||||
# Move to center scan
|
||||
logging.debug("Moving to z-stack starting position")
|
||||
microscope.stage.move_rel([0, 0, int((-step_size * steps) / 2)])
|
||||
microscope.stage.move_rel((0, 0, int((-step_size * steps) / 2)))
|
||||
logging.debug("Starting scan from position %s", microscope.stage.position)
|
||||
|
||||
for i in range(steps):
|
||||
|
|
@ -309,6 +339,7 @@ class ScanExtension(BaseExtension):
|
|||
bayer=bayer,
|
||||
metadata=metadata,
|
||||
annotations=annotations,
|
||||
dataset=dataset,
|
||||
tags=tags,
|
||||
)
|
||||
# Update task progress
|
||||
|
|
@ -319,7 +350,7 @@ class ScanExtension(BaseExtension):
|
|||
|
||||
if i != steps - 1:
|
||||
logging.debug("Moving z by %s", (step_size))
|
||||
microscope.stage.move_rel([0, 0, step_size])
|
||||
microscope.stage.move_rel((0, 0, step_size))
|
||||
if return_to_start:
|
||||
logging.debug("Returning to %s", (initial_position))
|
||||
microscope.stage.move_abs(initial_position)
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ import os
|
|||
import tempfile
|
||||
import uuid
|
||||
import zipfile
|
||||
from typing import IO, List, Optional
|
||||
|
||||
from flask import abort, send_file, url_for
|
||||
from labthings import fields, find_component, update_action_progress
|
||||
|
|
@ -11,6 +12,9 @@ from labthings.schema import Schema, pre_dump
|
|||
from labthings.utilities import description_from_view
|
||||
from labthings.views import ActionView, PropertyView, View
|
||||
|
||||
from openflexure_microscope.captures import CaptureObject
|
||||
from openflexure_microscope.microscope import Microscope
|
||||
|
||||
|
||||
class ZipObjectSchema(Schema):
|
||||
id = fields.String()
|
||||
|
|
@ -32,11 +36,13 @@ class ZipObjectSchema(Schema):
|
|||
|
||||
|
||||
class ZipObjectDescription:
|
||||
def __init__(self, id_, file_pointer, data_size=None):
|
||||
self.id = id_
|
||||
self.fp = file_pointer
|
||||
self.data_size = data_size
|
||||
self.zip_size = os.path.getsize(self.fp.name) * 1e-6
|
||||
def __init__(
|
||||
self, id_: str, file_pointer: IO[bytes], data_size: Optional[float] = None
|
||||
):
|
||||
self.id: str = id_
|
||||
self.fp: IO[bytes] = file_pointer
|
||||
self.data_size: Optional[float] = data_size
|
||||
self.zip_size: float = os.path.getsize(self.fp.name) * 1e-6
|
||||
|
||||
def close(self):
|
||||
logging.debug(self.fp.name)
|
||||
|
|
@ -60,22 +66,26 @@ class ZipManager:
|
|||
|
||||
self.session_zips = {}
|
||||
|
||||
def build_zip_from_capture_ids(self, microscope, capture_id_list):
|
||||
def build_zip_from_capture_ids(
|
||||
self, microscope: Microscope, capture_id_list: List[str]
|
||||
):
|
||||
logging.debug(capture_id_list)
|
||||
|
||||
# Get array of captures from IDs
|
||||
capture_list = [
|
||||
optional_capture_list: List[Optional[CaptureObject]] = [
|
||||
microscope.captures.images.get(capture_id) for capture_id in capture_id_list
|
||||
]
|
||||
# Remove Nones from list (missing/invalid captures)
|
||||
capture_list = [capture for capture in capture_list if capture]
|
||||
capture_list: List[CaptureObject] = [
|
||||
capture for capture in optional_capture_list if capture
|
||||
]
|
||||
|
||||
# Get size (in bytes) of each capture
|
||||
capture_sizes = [
|
||||
capture_sizes: List[float] = [
|
||||
os.path.getsize(capture_obj.file) for capture_obj in capture_list
|
||||
]
|
||||
# Calculate size of input data in megabytes
|
||||
data_size_megabytes = sum(capture_sizes) * 1e-6
|
||||
data_size_megabytes: float = sum(capture_sizes) * 1e-6
|
||||
|
||||
# If more than 1GB
|
||||
if data_size_megabytes > 1000:
|
||||
|
|
@ -85,10 +95,10 @@ class ZipManager:
|
|||
)
|
||||
|
||||
# Number of files to add (used for task progress)
|
||||
n_files = len(capture_id_list)
|
||||
n_files: int = len(capture_id_list)
|
||||
|
||||
# Create temporary file
|
||||
fp = tempfile.NamedTemporaryFile(delete=False)
|
||||
fp: IO[bytes] = tempfile.NamedTemporaryFile(delete=False)
|
||||
|
||||
# Open temp file as a ZIP file
|
||||
with zipfile.ZipFile(fp, "w") as zipObj:
|
||||
|
|
@ -102,8 +112,8 @@ class ZipManager:
|
|||
# Update task progress
|
||||
update_action_progress(int((index / n_files) * 100))
|
||||
|
||||
session_id = str(uuid.uuid4())
|
||||
session_description = ZipObjectDescription(
|
||||
session_id: str = str(uuid.uuid4())
|
||||
session_description: ZipObjectDescription = ZipObjectDescription(
|
||||
session_id, fp, data_size=data_size_megabytes
|
||||
)
|
||||
self.session_zips[session_id] = session_description
|
||||
|
|
@ -113,7 +123,7 @@ class ZipManager:
|
|||
def marshaled_build_zip_from_capture_ids(self, *args, **kwargs):
|
||||
return ZipObjectSchema().dump(self.build_zip_from_capture_ids(*args, **kwargs))
|
||||
|
||||
def zip_fp_from_id(self, session_id):
|
||||
def zip_fp_from_id(self, session_id: str):
|
||||
return self.session_zips[session_id].fp
|
||||
|
||||
def __del__(self):
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ class SleepFor(ActionView):
|
|||
args = {"time": fields.Float(description="Time to sleep, in seconds", example=0.5)}
|
||||
|
||||
def post(self, args):
|
||||
sleep_time = args.get("time")
|
||||
sleep_time: int = args.get("time", 0)
|
||||
logging.info("Going to sleep for %s...", sleep_time)
|
||||
start = time.time()
|
||||
time.sleep(sleep_time)
|
||||
|
|
|
|||
196
openflexure_microscope/api/static/package-lock.json
generated
196
openflexure_microscope/api/static/package-lock.json
generated
|
|
@ -2917,6 +2917,16 @@
|
|||
"integrity": "sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg==",
|
||||
"dev": true
|
||||
},
|
||||
"ansi-styles": {
|
||||
"version": "4.3.0",
|
||||
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
|
||||
"integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"color-convert": "^2.0.1"
|
||||
}
|
||||
},
|
||||
"browserslist": {
|
||||
"version": "4.14.7",
|
||||
"resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.14.7.tgz",
|
||||
|
|
@ -2962,12 +2972,36 @@
|
|||
"integrity": "sha512-gOerH9Wz2IRZ2ZPdMfBvyOi3cjaz4O4dgNwPGzx8EhqAs4+2IL/O+fJsbt+znSigujoZG8bVcIAUM/I/E5K3MA==",
|
||||
"dev": true
|
||||
},
|
||||
"color-convert": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
|
||||
"integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"color-name": "~1.1.4"
|
||||
}
|
||||
},
|
||||
"color-name": {
|
||||
"version": "1.1.4",
|
||||
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
|
||||
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"electron-to-chromium": {
|
||||
"version": "1.3.592",
|
||||
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.3.592.tgz",
|
||||
"integrity": "sha512-kGNowksvqQiPb1pUSQKpd8JFoGPLxYOwduNRCqCxGh/2Q1qE2JdmwouCW41lUzDxOb/2RIV4lR0tVIfboWlO9A==",
|
||||
"dev": true
|
||||
},
|
||||
"emojis-list": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/emojis-list/-/emojis-list-3.0.0.tgz",
|
||||
"integrity": "sha512-/kyM18EfinwXZbno9FyUGeFh87KC8HRQBQGildHZbEuRyWFOmv1U10o9BBp8XVZDVNNuQKyIGIu5ZYAAXJ0V2Q==",
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"fast-deep-equal": {
|
||||
"version": "3.1.3",
|
||||
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
|
||||
|
|
@ -3001,6 +3035,35 @@
|
|||
"integrity": "sha512-WjKPNJF79dtJAVniUlGGWHYGz2jWxT6VhN/4m1NdkbZ2nOsEF+cI1Edgql5zCRhs/VsQYRvrXctxktVXZUkixw==",
|
||||
"dev": true
|
||||
},
|
||||
"has-flag": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
|
||||
"integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"json5": {
|
||||
"version": "2.1.3",
|
||||
"resolved": "https://registry.npmjs.org/json5/-/json5-2.1.3.tgz",
|
||||
"integrity": "sha512-KXPvOm8K9IJKFM0bmdn8QXh7udDh1g/giieX0NLCaMnb4hEiVFqnop2ImTXCc5e0/oHz3LTqmHGtExn5hfMkOA==",
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"minimist": "^1.2.5"
|
||||
}
|
||||
},
|
||||
"loader-utils": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-2.0.0.tgz",
|
||||
"integrity": "sha512-rP4F0h2RaWSvPEkD7BLDFQnvSf+nK+wr3ESUjNTyAGobqrijmW92zc+SO6d4p4B1wh7+B/Jg1mkQe5NYUEHtHQ==",
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"big.js": "^5.2.2",
|
||||
"emojis-list": "^3.0.0",
|
||||
"json5": "^2.1.2"
|
||||
}
|
||||
},
|
||||
"locate-path": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz",
|
||||
|
|
@ -3134,6 +3197,16 @@
|
|||
"ansi-regex": "^5.0.0"
|
||||
}
|
||||
},
|
||||
"supports-color": {
|
||||
"version": "7.2.0",
|
||||
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
|
||||
"integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"has-flag": "^4.0.0"
|
||||
}
|
||||
},
|
||||
"terser-webpack-plugin": {
|
||||
"version": "2.3.8",
|
||||
"resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-2.3.8.tgz",
|
||||
|
|
@ -3160,6 +3233,31 @@
|
|||
"psl": "^1.1.28",
|
||||
"punycode": "^2.1.1"
|
||||
}
|
||||
},
|
||||
"vue-loader-v16": {
|
||||
"version": "npm:vue-loader@16.1.0",
|
||||
"resolved": "https://registry.npmjs.org/vue-loader/-/vue-loader-16.1.0.tgz",
|
||||
"integrity": "sha512-fTtCdI7VeyNK0HP4q4y9Z9ts8TUeaF+2/FjKx8CJ/7/Oem1rCX7zIJe+d+jLrVnVNQjENd3gqmANraLcdRWwnQ==",
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"chalk": "^4.1.0",
|
||||
"hash-sum": "^2.0.0",
|
||||
"loader-utils": "^2.0.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"chalk": {
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.0.tgz",
|
||||
"integrity": "sha512-qwx12AxXe2Q5xQ43Ac//I6v5aXTipYrSESdOgzrN+9XjgEpyjpKuvSGaN4qE93f7TQTlerQQ8S+EQ0EyDoVL1A==",
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"ansi-styles": "^4.1.0",
|
||||
"supports-color": "^7.1.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
|
@ -13759,104 +13857,6 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"vue-loader-v16": {
|
||||
"version": "npm:vue-loader@16.0.0-rc.2",
|
||||
"resolved": "https://registry.npmjs.org/vue-loader/-/vue-loader-16.0.0-rc.2.tgz",
|
||||
"integrity": "sha512-cz8GK4dgIf1UTC+do80pGvh8BHcCRHLIQVHV9ONVQ8wtoqS9t/+H02rKcQP+TVNg7khgLyQV2+8eHUq7/AFq3g==",
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"chalk": "^4.1.0",
|
||||
"hash-sum": "^2.0.0",
|
||||
"loader-utils": "^2.0.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"ansi-styles": {
|
||||
"version": "4.3.0",
|
||||
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
|
||||
"integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"color-convert": "^2.0.1"
|
||||
}
|
||||
},
|
||||
"chalk": {
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.0.tgz",
|
||||
"integrity": "sha512-qwx12AxXe2Q5xQ43Ac//I6v5aXTipYrSESdOgzrN+9XjgEpyjpKuvSGaN4qE93f7TQTlerQQ8S+EQ0EyDoVL1A==",
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"ansi-styles": "^4.1.0",
|
||||
"supports-color": "^7.1.0"
|
||||
}
|
||||
},
|
||||
"color-convert": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
|
||||
"integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"color-name": "~1.1.4"
|
||||
}
|
||||
},
|
||||
"color-name": {
|
||||
"version": "1.1.4",
|
||||
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
|
||||
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"emojis-list": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/emojis-list/-/emojis-list-3.0.0.tgz",
|
||||
"integrity": "sha512-/kyM18EfinwXZbno9FyUGeFh87KC8HRQBQGildHZbEuRyWFOmv1U10o9BBp8XVZDVNNuQKyIGIu5ZYAAXJ0V2Q==",
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"has-flag": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
|
||||
"integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"json5": {
|
||||
"version": "2.1.3",
|
||||
"resolved": "https://registry.npmjs.org/json5/-/json5-2.1.3.tgz",
|
||||
"integrity": "sha512-KXPvOm8K9IJKFM0bmdn8QXh7udDh1g/giieX0NLCaMnb4hEiVFqnop2ImTXCc5e0/oHz3LTqmHGtExn5hfMkOA==",
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"minimist": "^1.2.5"
|
||||
}
|
||||
},
|
||||
"loader-utils": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-2.0.0.tgz",
|
||||
"integrity": "sha512-rP4F0h2RaWSvPEkD7BLDFQnvSf+nK+wr3ESUjNTyAGobqrijmW92zc+SO6d4p4B1wh7+B/Jg1mkQe5NYUEHtHQ==",
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"big.js": "^5.2.2",
|
||||
"emojis-list": "^3.0.0",
|
||||
"json5": "^2.1.2"
|
||||
}
|
||||
},
|
||||
"supports-color": {
|
||||
"version": "7.2.0",
|
||||
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
|
||||
"integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"has-flag": "^4.0.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"vue-observe-visibility": {
|
||||
"version": "0.4.6",
|
||||
"resolved": "https://registry.npmjs.org/vue-observe-visibility/-/vue-observe-visibility-0.4.6.tgz",
|
||||
|
|
|
|||
|
|
@ -80,6 +80,10 @@ export default {
|
|||
type: String,
|
||||
required: true
|
||||
},
|
||||
captures: {
|
||||
type: Array,
|
||||
required: true
|
||||
},
|
||||
tags: {
|
||||
type: Array,
|
||||
required: false,
|
||||
|
|
@ -92,7 +96,7 @@ export default {
|
|||
computed: {
|
||||
allURLs: function() {
|
||||
var urls = [];
|
||||
for (var capture of this.scanState.captures) {
|
||||
for (var capture of this.captures) {
|
||||
urls.push(capture.links.self.href);
|
||||
}
|
||||
return urls;
|
||||
|
|
@ -115,6 +119,7 @@ export default {
|
|||
|
||||
deleteAll: function() {
|
||||
axios.all(this.allURLs.map(l => axios.delete(l))).then(() => {
|
||||
console.log("Delete finished")
|
||||
// Emit signal to update capture list
|
||||
this.$root.$emit("globalUpdateCaptures");
|
||||
});
|
||||
|
|
|
|||
|
|
@ -105,6 +105,7 @@
|
|||
:time="item.time"
|
||||
:tags="item.tags"
|
||||
:type="item.type"
|
||||
:captures="item.captures"
|
||||
:thumbnail="item.thumbnail"
|
||||
@selectFolder="selectFolder"
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -1,38 +1,51 @@
|
|||
import errno
|
||||
import logging
|
||||
import os
|
||||
from typing import Union
|
||||
|
||||
from flask import Blueprint, current_app, url_for
|
||||
from werkzeug.exceptions import BadRequest
|
||||
from flask import Blueprint, Flask, current_app, url_for
|
||||
from flask.views import View
|
||||
|
||||
from . import gui
|
||||
|
||||
__all__ = [
|
||||
"gui",
|
||||
"view_class_from_endpoint",
|
||||
"blueprint_for_module",
|
||||
"get_bool",
|
||||
"list_routes",
|
||||
"create_file",
|
||||
"init_default_extensions",
|
||||
]
|
||||
|
||||
def view_class_from_endpoint(endpoint: str):
|
||||
|
||||
def view_class_from_endpoint(endpoint: str) -> View:
|
||||
return current_app.view_functions[endpoint].view_class
|
||||
|
||||
|
||||
def blueprint_for_module(module_name, api_ver=2, suffix=""):
|
||||
def blueprint_for_module(module_name: str, api_ver: int = 2, suffix: str = ""):
|
||||
return Blueprint(
|
||||
blueprint_name_for_module(module_name, api_ver=api_ver, suffix=suffix),
|
||||
module_name,
|
||||
)
|
||||
|
||||
|
||||
def blueprint_name_for_module(module_name, api_ver=2, suffix=""):
|
||||
def blueprint_name_for_module(module_name: str, api_ver: int = 2, suffix: str = ""):
|
||||
bp_name = module_name.split(".")[-1]
|
||||
return f"v{api_ver}_{bp_name}_blueprint{suffix}"
|
||||
|
||||
|
||||
def get_bool(get_arg):
|
||||
def get_bool(get_arg: Union[bool, str]):
|
||||
"""Convert GET request argument string to a Python bool"""
|
||||
if get_arg == "true" or get_arg == "True" or get_arg == "1":
|
||||
if isinstance(get_arg, bool):
|
||||
return get_arg
|
||||
elif get_arg == "true" or get_arg == "True" or get_arg == "1":
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
|
||||
|
||||
def list_routes(app):
|
||||
def list_routes(app: Flask):
|
||||
output = {}
|
||||
for rule in app.url_map.iter_rules():
|
||||
|
||||
|
|
@ -49,7 +62,7 @@ def list_routes(app):
|
|||
return output
|
||||
|
||||
|
||||
def create_file(config_path):
|
||||
def create_file(config_path: str):
|
||||
if not os.path.exists(os.path.dirname(config_path)):
|
||||
try:
|
||||
os.makedirs(os.path.dirname(config_path))
|
||||
|
|
@ -58,7 +71,7 @@ def create_file(config_path):
|
|||
raise
|
||||
|
||||
|
||||
def init_default_extensions(extension_dir):
|
||||
def init_default_extensions(extension_dir: str):
|
||||
os.makedirs(extension_dir, exist_ok=True)
|
||||
|
||||
default_ext_path = os.path.join(extension_dir, "defaults.py")
|
||||
|
|
|
|||
|
|
@ -1,6 +1,9 @@
|
|||
import copy
|
||||
import logging
|
||||
from functools import wraps
|
||||
from typing import Callable, Union
|
||||
|
||||
from labthings.extensions import BaseExtension
|
||||
|
||||
|
||||
def clean_rule(rule: str):
|
||||
|
|
@ -9,13 +12,12 @@ def clean_rule(rule: str):
|
|||
return f"{rule}"
|
||||
|
||||
|
||||
def build_gui_from_dict(gui_description, extension_object):
|
||||
def build_gui_from_dict(gui_description: dict, extension_object: BaseExtension):
|
||||
# Make a working copy of GUI description
|
||||
api_gui = copy.deepcopy(gui_description)
|
||||
# Grab the extensions rules dictionary
|
||||
# NOTE: The LabThings extension object should probably have a non-private
|
||||
# way to get the rules dictionary. For now we need to ignore pylint W0212
|
||||
ext_rules = extension_object._rules # pylint: disable=W0212
|
||||
# TODO: Public property should be added to LabThings
|
||||
ext_rules = extension_object._rules # pylint: disable=protected-access
|
||||
|
||||
# Expand shorthand routes into full relative URLs
|
||||
if "forms" in gui_description and isinstance(api_gui["forms"], list):
|
||||
|
|
@ -35,7 +37,7 @@ def build_gui_from_dict(gui_description, extension_object):
|
|||
return api_gui
|
||||
|
||||
|
||||
def build_gui_from_func(func, extension_object):
|
||||
def build_gui_from_func(func: Callable, extension_object: BaseExtension):
|
||||
@wraps(func)
|
||||
def wrapped(*args, **kwargs):
|
||||
return build_gui_from_dict(func(*args, **kwargs), extension_object)
|
||||
|
|
@ -43,7 +45,7 @@ def build_gui_from_func(func, extension_object):
|
|||
return wrapped
|
||||
|
||||
|
||||
def build_gui(gui_description, extension_object):
|
||||
def build_gui(gui_description: Union[dict, Callable], extension_object: BaseExtension):
|
||||
# If given a function that generates a GUI dictionary
|
||||
if callable(gui_description):
|
||||
# Wrap in the route expander
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import io
|
||||
import logging
|
||||
from typing import Dict, Optional, Tuple
|
||||
|
||||
from flask import send_file
|
||||
from labthings import Schema, fields, find_component
|
||||
|
|
@ -42,12 +43,14 @@ class CaptureAPI(ActionView):
|
|||
"""
|
||||
microscope = find_component("org.openflexure.microscope")
|
||||
|
||||
resize = args.get("resize", None)
|
||||
if resize:
|
||||
resize = (
|
||||
int(resize["width"]),
|
||||
int(resize["height"]),
|
||||
resize_dict: Optional[Dict[str, int]] = args.get("resize", None)
|
||||
if resize_dict:
|
||||
resize: Optional[Tuple[int, int]] = (
|
||||
int(resize_dict["width"]),
|
||||
int(resize_dict["height"]),
|
||||
) # Convert dict to tuple
|
||||
else:
|
||||
resize = None
|
||||
|
||||
# Explicitally acquire lock (prevents empty files being created if lock is unavailable)
|
||||
with microscope.camera.lock:
|
||||
|
|
@ -76,12 +79,14 @@ class RAMCaptureAPI(ActionView):
|
|||
"""
|
||||
microscope = find_component("org.openflexure.microscope")
|
||||
|
||||
resize = args.get("resize", None)
|
||||
if resize:
|
||||
resize = (
|
||||
int(resize["width"]),
|
||||
int(resize["height"]),
|
||||
resize_dict: Optional[Dict[str, int]] = args.get("resize", None)
|
||||
if resize_dict:
|
||||
resize: Optional[Tuple[int, int]] = (
|
||||
int(resize_dict["width"]),
|
||||
int(resize_dict["height"]),
|
||||
) # Convert dict to tuple
|
||||
else:
|
||||
resize = None
|
||||
|
||||
# Open a BytesIO stream to be destroyed once request has returned
|
||||
with microscope.camera.lock, io.BytesIO() as stream:
|
||||
|
|
@ -113,15 +118,18 @@ class GPUPreviewStartAPI(ActionView):
|
|||
"""
|
||||
microscope = find_component("org.openflexure.microscope")
|
||||
|
||||
window = args.get("window")
|
||||
logging.debug(window)
|
||||
# Get window argument from request
|
||||
window_arg = args.get("window")
|
||||
logging.debug(window_arg)
|
||||
|
||||
if len(window) != 4:
|
||||
fullscreen = True
|
||||
window = None
|
||||
else:
|
||||
# Default to no window
|
||||
fullscreen: bool = True
|
||||
window: Optional[Tuple[int, int, int, int]] = None
|
||||
|
||||
# If request argument is well formed, use that
|
||||
if len(window_arg) == 4:
|
||||
fullscreen = False
|
||||
window = [int(w) for w in window]
|
||||
window = (int(w) for w in window_arg)
|
||||
|
||||
microscope.camera.start_preview(fullscreen=fullscreen, window=window)
|
||||
return microscope.state
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import logging
|
||||
from typing import List, Tuple
|
||||
|
||||
from labthings import fields, find_component
|
||||
from labthings.views import ActionView
|
||||
|
|
@ -24,11 +25,11 @@ class MoveStageAPI(ActionView):
|
|||
|
||||
# Handle absolute positioning (calculate a relative move from current position and target)
|
||||
if (args.get("absolute")) and (microscope.stage): # Only if stage exists
|
||||
target_position = axes_to_array(args, ["x", "y", "z"])
|
||||
target_position: List[int] = axes_to_array(args, ["x", "y", "z"])
|
||||
logging.debug("TARGET: %s", (target_position))
|
||||
position = [
|
||||
position: Tuple[int, int, int] = (
|
||||
target_position[i] - microscope.stage.position[i] for i in range(3)
|
||||
]
|
||||
)
|
||||
logging.debug("DELTA: %s", (position))
|
||||
|
||||
else:
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ import subprocess
|
|||
from labthings.views import ActionView
|
||||
|
||||
|
||||
def is_raspberrypi():
|
||||
def is_raspberrypi() -> bool:
|
||||
"""
|
||||
Checks if Raspberry Pi.
|
||||
"""
|
||||
|
|
|
|||
|
|
@ -34,17 +34,18 @@ class LSTImageProperty(PropertyView):
|
|||
logging.warning("PiCamera.lens_shading_table returned as None")
|
||||
return ("", 204)
|
||||
else:
|
||||
# Get the LST Numpy array from the camera's memoryview
|
||||
lst = np.asarray(microscope.camera.camera.lens_shading_table)
|
||||
# R next to G1
|
||||
# Create array of R next to G1
|
||||
top = np.concatenate(lst[:2], axis=1)
|
||||
# G2 next to B
|
||||
# Create array of G2 next to B
|
||||
bottom = np.concatenate(lst[2:], axis=1)
|
||||
# Combined into a 2x2 grid of greyscale images
|
||||
# Create combined array of 2x2 grid of greyscale images
|
||||
all_channels = np.concatenate((top, bottom), axis=0)
|
||||
# Create a PNG
|
||||
img_bytes = BytesIO()
|
||||
img_bytes: BytesIO = BytesIO()
|
||||
Image.fromarray(np.uint8(all_channels), "L").save(img_bytes, "PNG")
|
||||
img_bytes.seek(0)
|
||||
# Return the image
|
||||
fname = f"lst-{datetime.date.today().isoformat()}.png"
|
||||
fname: str = f"lst-{datetime.date.today().isoformat()}.png"
|
||||
return send_file(img_bytes, as_attachment=True, attachment_filename=fname)
|
||||
|
|
|
|||
|
|
@ -1,3 +1,7 @@
|
|||
from io import BytesIO
|
||||
from typing import List, Optional, Union
|
||||
from uuid import UUID
|
||||
|
||||
from flask import abort, redirect, request, send_file, url_for
|
||||
from labthings import Schema, fields, find_component
|
||||
from labthings.marshalling import marshal_with, use_args
|
||||
|
|
@ -6,6 +10,7 @@ from labthings.views import PropertyView, View
|
|||
from marshmallow import pre_dump
|
||||
|
||||
from openflexure_microscope.api.utilities import get_bool
|
||||
from openflexure_microscope.captures import CaptureObject
|
||||
|
||||
# SCHEMAS
|
||||
|
||||
|
|
@ -61,10 +66,10 @@ class CaptureSchema(ImageSchema):
|
|||
links = fields.Dict()
|
||||
|
||||
@pre_dump
|
||||
def generate_links(self, data, **_):
|
||||
def generate_links(self, data: Union[dict, CaptureObject], **_):
|
||||
if isinstance(data, dict):
|
||||
capture_id = data.get("id")
|
||||
capture_name = data.get("name")
|
||||
capture_id: Optional[Union[str, UUID]] = data.get("id")
|
||||
capture_name: Optional[str] = data.get("name")
|
||||
else:
|
||||
capture_id = data.id
|
||||
capture_name = data.name
|
||||
|
|
@ -102,7 +107,7 @@ class CaptureSchema(ImageSchema):
|
|||
if isinstance(data, dict):
|
||||
data["links"] = links
|
||||
else:
|
||||
data.links = links
|
||||
setattr(data, "links", links)
|
||||
|
||||
return data
|
||||
|
||||
|
|
@ -129,7 +134,7 @@ class CaptureList(PropertyView):
|
|||
List all image captures
|
||||
"""
|
||||
microscope = find_component("org.openflexure.microscope")
|
||||
image_list = microscope.captures.images.values()
|
||||
image_list: List[CaptureObject] = microscope.captures.images.values()
|
||||
return image_list
|
||||
|
||||
|
||||
|
|
@ -142,7 +147,7 @@ class CaptureView(View):
|
|||
Description of a single image capture
|
||||
"""
|
||||
microscope = find_component("org.openflexure.microscope")
|
||||
capture_obj = microscope.captures.images.get(id_)
|
||||
capture_obj: Optional[CaptureObject] = microscope.captures.images.get(id_)
|
||||
|
||||
if not capture_obj:
|
||||
return abort(404) # 404 Not Found
|
||||
|
|
@ -154,7 +159,7 @@ class CaptureView(View):
|
|||
Delete a single image capture
|
||||
"""
|
||||
microscope = find_component("org.openflexure.microscope")
|
||||
capture_obj = microscope.captures.images.get(id_)
|
||||
capture_obj: Optional[CaptureObject] = microscope.captures.images.get(id_)
|
||||
|
||||
if not capture_obj:
|
||||
return abort(404) # 404 Not Found
|
||||
|
|
@ -171,17 +176,17 @@ class CaptureDownload(View):
|
|||
tags = ["captures"]
|
||||
responses = {200: {"content_type": "image/jpeg"}}
|
||||
|
||||
def get(self, id_, filename):
|
||||
def get(self, id_, filename: Optional[str]):
|
||||
"""
|
||||
Image data for a single image capture
|
||||
"""
|
||||
microscope = find_component("org.openflexure.microscope")
|
||||
capture_obj = microscope.captures.images.get(id_)
|
||||
capture_obj: Optional[CaptureObject] = microscope.captures.images.get(id_)
|
||||
|
||||
if not capture_obj:
|
||||
return abort(404) # 404 Not Found
|
||||
|
||||
thumbnail = get_bool(request.args.get("thumbnail"))
|
||||
thumbnail: bool = get_bool(request.args.get("thumbnail", ""))
|
||||
|
||||
# If no filename is specified, redirect to the capture's currently set filename
|
||||
if not filename:
|
||||
|
|
@ -189,7 +194,7 @@ class CaptureDownload(View):
|
|||
url_for(
|
||||
"DownloadAPI",
|
||||
id=id_,
|
||||
filename=capture_obj.filename,
|
||||
filename=capture_obj.name,
|
||||
thumbnail=thumbnail,
|
||||
),
|
||||
code=307,
|
||||
|
|
@ -197,10 +202,14 @@ class CaptureDownload(View):
|
|||
|
||||
# Download the image data using the requested filename
|
||||
if thumbnail:
|
||||
img = capture_obj.thumbnail
|
||||
img: Optional[BytesIO] = capture_obj.thumbnail
|
||||
else:
|
||||
img = capture_obj.data
|
||||
|
||||
# If we can't get any data, return 404
|
||||
if not img:
|
||||
return abort(404) # 404 Not Found
|
||||
|
||||
return send_file(img, mimetype="image/jpeg")
|
||||
|
||||
|
||||
|
|
@ -212,7 +221,7 @@ class CaptureTags(View):
|
|||
Get tags associated with a single image capture
|
||||
"""
|
||||
microscope = find_component("org.openflexure.microscope")
|
||||
capture_obj = microscope.captures.images.get(id_)
|
||||
capture_obj: Optional[CaptureObject] = microscope.captures.images.get(id_)
|
||||
|
||||
if not capture_obj:
|
||||
return abort(404) # 404 Not Found
|
||||
|
|
@ -225,7 +234,7 @@ class CaptureTags(View):
|
|||
Add tags to a single image capture
|
||||
"""
|
||||
microscope = find_component("org.openflexure.microscope")
|
||||
capture_obj = microscope.captures.images.get(id_)
|
||||
capture_obj: Optional[CaptureObject] = microscope.captures.images.get(id_)
|
||||
|
||||
if not capture_obj:
|
||||
return abort(404) # 404 Not Found
|
||||
|
|
@ -240,7 +249,7 @@ class CaptureTags(View):
|
|||
Delete tags from a single image capture
|
||||
"""
|
||||
microscope = find_component("org.openflexure.microscope")
|
||||
capture_obj = microscope.captures.images.get(id_)
|
||||
capture_obj: Optional[CaptureObject] = microscope.captures.images.get(id_)
|
||||
|
||||
if not capture_obj:
|
||||
return abort(404) # 404 Not Found
|
||||
|
|
@ -259,7 +268,7 @@ class CaptureAnnotations(View):
|
|||
Get annotations associated with a single image capture
|
||||
"""
|
||||
microscope = find_component("org.openflexure.microscope")
|
||||
capture_obj = microscope.captures.images.get(id_)
|
||||
capture_obj: Optional[CaptureObject] = microscope.captures.images.get(id_)
|
||||
|
||||
if not capture_obj:
|
||||
return abort(404) # 404 Not Found
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import logging
|
||||
from typing import Any, List
|
||||
|
||||
from flask import abort
|
||||
from labthings import fields, find_component
|
||||
|
|
@ -35,29 +36,29 @@ class NestedSettingsProperty(View):
|
|||
tags = ["properties"]
|
||||
responses = {404: {"description": "Settings key cannot be found"}}
|
||||
|
||||
def get(self, route):
|
||||
def get(self, route: str):
|
||||
"""
|
||||
Show a nested section of the current microscope settings
|
||||
"""
|
||||
microscope = find_component("org.openflexure.microscope")
|
||||
keys = route.split("/")
|
||||
keys: List[str] = route.split("/")
|
||||
|
||||
try:
|
||||
value = get_by_path(microscope.read_settings(), keys)
|
||||
value: Any = get_by_path(microscope.read_settings(), keys)
|
||||
except KeyError:
|
||||
return abort(404)
|
||||
|
||||
return value
|
||||
|
||||
@use_args(fields.Dict())
|
||||
def put(self, args, route):
|
||||
def put(self, args: Any, route: str):
|
||||
"""
|
||||
Update a nested section of the current microscope settings
|
||||
"""
|
||||
microscope = find_component("org.openflexure.microscope")
|
||||
keys = route.split("/")
|
||||
keys: List[str] = route.split("/")
|
||||
|
||||
dictionary = create_from_path(keys)
|
||||
dictionary: dict = create_from_path(keys)
|
||||
set_by_path(dictionary, keys, args)
|
||||
|
||||
microscope.update_settings(dictionary)
|
||||
|
|
@ -84,10 +85,10 @@ class NestedStateProperty(View):
|
|||
Show a nested section of the current microscope state
|
||||
"""
|
||||
microscope = find_component("org.openflexure.microscope")
|
||||
keys = route.split("/")
|
||||
keys: List[str] = route.split("/")
|
||||
|
||||
try:
|
||||
value = get_by_path(microscope.state, keys)
|
||||
value: Any = get_by_path(microscope.state, keys)
|
||||
except KeyError:
|
||||
return abort(404)
|
||||
|
||||
|
|
@ -112,10 +113,10 @@ class NestedConfigurationProperty(View):
|
|||
Show a nested section of the current microscope state
|
||||
"""
|
||||
microscope = find_component("org.openflexure.microscope")
|
||||
keys = route.split("/")
|
||||
keys: List[str] = route.split("/")
|
||||
|
||||
try:
|
||||
value = get_by_path(microscope.configuration, keys)
|
||||
value: Any = get_by_path(microscope.configuration, keys)
|
||||
except KeyError:
|
||||
return abort(404)
|
||||
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ def gen(camera):
|
|||
"""Video streaming generator function."""
|
||||
while True:
|
||||
# the obtained frame is a jpeg
|
||||
frame = camera.stream.getframe()
|
||||
frame: bytes = camera.stream.getframe()
|
||||
|
||||
yield (b"--frame\r\n" b"Content-Type: image/jpeg\r\n\r\n" + frame + b"\r\n")
|
||||
|
||||
|
|
|
|||
|
|
@ -4,6 +4,8 @@ import logging
|
|||
import time
|
||||
from abc import ABCMeta, abstractmethod
|
||||
from collections import namedtuple
|
||||
from types import TracebackType
|
||||
from typing import BinaryIO, List, Optional, Tuple, Type, Union
|
||||
|
||||
from labthings import ClientEvent, StrictLock
|
||||
|
||||
|
|
@ -27,16 +29,29 @@ class FrameStream(io.BytesIO):
|
|||
# Array of TrackerFrame objects
|
||||
io.BytesIO.__init__(self, *args, **kwargs)
|
||||
# Array of TrackerFramer objects
|
||||
self.frames = []
|
||||
self.frames: List[TrackerFrame] = []
|
||||
# Last acquired TrackerFramer object
|
||||
self.last = None
|
||||
self.last: Optional[TrackerFrame] = None
|
||||
|
||||
# Are we currently tracking frame sizes?
|
||||
self.tracking = False
|
||||
self.tracking: bool = False
|
||||
|
||||
# Event to track if a new frame is available since the last getvalue() call
|
||||
# We use a ClientEvent so that each thread can call getvalue() independantly
|
||||
self.new_frame = ClientEvent()
|
||||
self.new_frame: ClientEvent = ClientEvent()
|
||||
|
||||
def __enter__(self):
|
||||
self.start_tracking()
|
||||
return super().__enter__()
|
||||
|
||||
def __exit__(
|
||||
self,
|
||||
t: Optional[Type[BaseException]],
|
||||
value: Optional[BaseException],
|
||||
traceback: Optional[TracebackType],
|
||||
) -> Optional[bool]:
|
||||
self.stop_tracking()
|
||||
return super().__exit__(t, value, traceback)
|
||||
|
||||
def start_tracking(self):
|
||||
"""Start tracking frame sizes"""
|
||||
|
|
@ -92,13 +107,16 @@ class BaseCamera(metaclass=ABCMeta):
|
|||
|
||||
def __init__(self):
|
||||
#: :py:class:`labthings.StrictLock`: Access lock for the camera
|
||||
self.lock = StrictLock(name="Camera", timeout=None)
|
||||
self.lock: StrictLock = StrictLock(name="Camera", timeout=None)
|
||||
#: :py:class:`FrameStream`: Streaming and analysis frame buffer
|
||||
self.stream = FrameStream()
|
||||
self.stream: FrameStream = FrameStream()
|
||||
|
||||
self.stream_active = False
|
||||
self.record_active = False
|
||||
self.preview_active = False
|
||||
self.stream_active: bool = False
|
||||
self.record_active: bool = False
|
||||
self.preview_active: bool = False
|
||||
|
||||
self.image_resolution: Tuple[int, int] = (1312, 976)
|
||||
self.stream_resolution: Tuple[int, int] = (640, 480)
|
||||
|
||||
@property
|
||||
@abstractmethod
|
||||
|
|
@ -114,6 +132,14 @@ class BaseCamera(metaclass=ABCMeta):
|
|||
def settings(self):
|
||||
return self.read_settings()
|
||||
|
||||
@abstractmethod
|
||||
def start_stream(self):
|
||||
"""Ensure the frame stream is actively running"""
|
||||
|
||||
@abstractmethod
|
||||
def stop_stream(self):
|
||||
"""Stop the active stream, if possible"""
|
||||
|
||||
@abstractmethod
|
||||
def update_settings(self, config: dict):
|
||||
"""Update settings from a config dictionary"""
|
||||
|
|
@ -122,6 +148,28 @@ class BaseCamera(metaclass=ABCMeta):
|
|||
def read_settings(self) -> dict:
|
||||
"""Return the current settings as a dictionary"""
|
||||
|
||||
@abstractmethod
|
||||
def capture(
|
||||
self,
|
||||
output: Union[str, BinaryIO],
|
||||
fmt: str = "jpeg",
|
||||
use_video_port: bool = False,
|
||||
resize: Optional[Tuple[int, int]] = None,
|
||||
bayer: bool = True,
|
||||
thumbnail: Optional[Tuple[int, int, int]] = None,
|
||||
):
|
||||
"""
|
||||
Perform a basic capture to output
|
||||
|
||||
Args:
|
||||
output: String or file-like object to write capture data to
|
||||
fmt: Format of the capture.
|
||||
use_video_port: Capture from the video port used for streaming. Lower resolution, faster.
|
||||
resize: Resize the captured image.
|
||||
bayer: Store raw bayer data in capture
|
||||
thumbnail: Dimensions and quality (x, y, quality) of a thumbnail to generate, if supported
|
||||
"""
|
||||
|
||||
def __enter__(self):
|
||||
"""Create camera on context enter."""
|
||||
return self
|
||||
|
|
|
|||
|
|
@ -8,6 +8,8 @@ import time
|
|||
from datetime import datetime
|
||||
|
||||
# Type hinting
|
||||
from typing import BinaryIO, Optional, Tuple, Union
|
||||
|
||||
from PIL import Image, ImageDraw
|
||||
|
||||
from openflexure_microscope.camera.base import BaseCamera
|
||||
|
|
@ -26,17 +28,17 @@ class MissingCamera(BaseCamera):
|
|||
BaseCamera.__init__(self)
|
||||
|
||||
# Update config properties
|
||||
self.image_resolution = (1312, 976)
|
||||
self.stream_resolution = (640, 480)
|
||||
self.numpy_resolution = (1312, 976)
|
||||
self.jpeg_quality = 75
|
||||
self.framerate = 10
|
||||
self.image_resolution: Tuple[int, int] = (1312, 976)
|
||||
self.stream_resolution: Tuple[int, int] = (640, 480)
|
||||
self.numpy_resolution: Tuple[int, int] = (1312, 976)
|
||||
self.jpeg_quality: int = 75
|
||||
self.framerate: int = 10
|
||||
|
||||
# Generate an initial dummy image
|
||||
self.generate_new_dummy_image()
|
||||
|
||||
# Start streaming
|
||||
self.stop = False # Used to indicate that the stream loop should break
|
||||
self.stop: bool = False # Used to indicate that the stream loop should break
|
||||
self.start_worker()
|
||||
# Wait until frames are available
|
||||
logging.info("Waiting for frames")
|
||||
|
|
@ -178,7 +180,11 @@ class MissingCamera(BaseCamera):
|
|||
"""
|
||||
logging.warning("Zoom not implemented in mock camera")
|
||||
|
||||
# LAUNCH ACTIONS
|
||||
def start_stream(self):
|
||||
pass
|
||||
|
||||
def stop_stream(self):
|
||||
pass
|
||||
|
||||
def start_preview(self, *_, **__):
|
||||
"""Start the on board GPU camera preview."""
|
||||
|
|
@ -211,7 +217,15 @@ class MissingCamera(BaseCamera):
|
|||
with self.lock:
|
||||
logging.warning("Recording not implemented in mock camera")
|
||||
|
||||
def capture(self, output, *_, **__):
|
||||
def capture(
|
||||
self,
|
||||
output: Union[str, BinaryIO],
|
||||
fmt: str = "jpeg",
|
||||
use_video_port: bool = False,
|
||||
resize: Optional[Tuple[int, int]] = None,
|
||||
bayer: bool = True,
|
||||
thumbnail: Optional[Tuple[int, int, int]] = None,
|
||||
):
|
||||
"""
|
||||
Capture a still image to a StreamObject.
|
||||
|
||||
|
|
|
|||
|
|
@ -30,7 +30,7 @@ import logging
|
|||
import time
|
||||
|
||||
# Type hinting
|
||||
from typing import Tuple
|
||||
from typing import BinaryIO, Tuple, Union
|
||||
|
||||
import numpy as np
|
||||
|
||||
|
|
@ -77,57 +77,64 @@ class PiCameraStreamer(BaseCamera):
|
|||
BaseCamera.__init__(self)
|
||||
|
||||
#: :py:class:`picamerax.PiCamera`: Attached Picamera object
|
||||
self.camera = picamerax.PiCamera()
|
||||
self.picamera: picamerax.PiCamera = picamerax.PiCamera()
|
||||
|
||||
# Store state of PiCameraStreamer
|
||||
self.preview_active = False
|
||||
self.preview_active: bool = False
|
||||
|
||||
# Reset variable states
|
||||
self.set_zoom(1.0)
|
||||
|
||||
#: tuple: Resolution for image captures
|
||||
self.image_resolution = tuple(self.camera.MAX_RESOLUTION)
|
||||
self.image_resolution: Tuple[int, int] = tuple(self.picamera.MAX_RESOLUTION)
|
||||
#: tuple: Resolution for stream and video captures
|
||||
self.stream_resolution = (832, 624)
|
||||
self.stream_resolution: Tuple[int, int] = (832, 624)
|
||||
#: tuple: Resolution for numpy array captures
|
||||
self.numpy_resolution = (1312, 976)
|
||||
self.numpy_resolution: Tuple[int, int] = (1312, 976)
|
||||
|
||||
self.jpeg_quality = 100 #: int: JPEG quality
|
||||
self.mjpeg_quality = 75 #: int: MJPEG quality
|
||||
self.jpeg_quality: int = 100 #: int: JPEG quality
|
||||
self.mjpeg_quality: int = 75 #: int: MJPEG quality
|
||||
|
||||
# Start stream recording (and set resolution)
|
||||
self.start_stream_recording()
|
||||
self.start_stream()
|
||||
# Wait until frames are available
|
||||
logging.debug("Waiting for frames...")
|
||||
self.stream.new_frame.wait()
|
||||
logging.debug("Camera initialised")
|
||||
|
||||
@property
|
||||
def configuration(self):
|
||||
"""The current camera configuration."""
|
||||
return {"board": self.camera.revision}
|
||||
def camera(self):
|
||||
logging.warning(
|
||||
"PiCameraStreamer.camera is deprecated. Replace with PiCameraStreamer.picamera"
|
||||
)
|
||||
return self.picamera
|
||||
|
||||
@property
|
||||
def state(self):
|
||||
def configuration(self) -> dict:
|
||||
"""The current camera configuration."""
|
||||
return {"board": self.picamera.revision}
|
||||
|
||||
@property
|
||||
def state(self) -> dict:
|
||||
"""The current read-only camera state."""
|
||||
return {}
|
||||
|
||||
def close(self):
|
||||
"""Close the Raspberry Pi PiCameraStreamer."""
|
||||
# Stop stream recording
|
||||
self.stop_stream_recording()
|
||||
self.stop_stream()
|
||||
# Run BaseCamera close method
|
||||
super().close()
|
||||
# Detach Pi camera
|
||||
if self.camera:
|
||||
self.camera.close()
|
||||
if self.picamera:
|
||||
self.picamera.close()
|
||||
|
||||
# HANDLE SETTINGS
|
||||
def read_settings(self) -> dict:
|
||||
"""
|
||||
Return config dictionary of the PiCameraStreamer.
|
||||
"""
|
||||
conf_dict = {
|
||||
conf_dict: dict = {
|
||||
"stream_resolution": self.stream_resolution,
|
||||
"image_resolution": self.image_resolution,
|
||||
"numpy_resolution": self.numpy_resolution,
|
||||
|
|
@ -139,7 +146,7 @@ class PiCameraStreamer(BaseCamera):
|
|||
# Include a subset of picamera properties. Excludes lens shading table
|
||||
for key in PiCameraStreamer.picamera_settings_keys:
|
||||
try:
|
||||
value = getattr(self.camera, key)
|
||||
value = getattr(self.picamera, key)
|
||||
logging.debug("Reading PiCamera().%s: %s", key, value)
|
||||
conf_dict["picamera"][key] = value
|
||||
except AttributeError:
|
||||
|
|
@ -147,11 +154,11 @@ class PiCameraStreamer(BaseCamera):
|
|||
|
||||
# Include a serialised lens shading table
|
||||
if (
|
||||
hasattr(self.camera, "lens_shading_table")
|
||||
and getattr(self.camera, "lens_shading_table") is not None
|
||||
hasattr(self.picamera, "lens_shading_table")
|
||||
and getattr(self.picamera, "lens_shading_table") is not None
|
||||
):
|
||||
conf_dict["picamera"]["lens_shading_table"] = ndarray_to_json(
|
||||
getattr(self.camera, "lens_shading_table")
|
||||
getattr(self.picamera, "lens_shading_table")
|
||||
)
|
||||
|
||||
return conf_dict
|
||||
|
|
@ -179,7 +186,7 @@ class PiCameraStreamer(BaseCamera):
|
|||
# Pause stream while changing settings
|
||||
if self.stream_active: # If stream is active
|
||||
logging.info("Pausing stream to update config.")
|
||||
self.stop_stream_recording() # Pause stream
|
||||
self.stop_stream() # Pause stream
|
||||
paused_stream = True # Remember to unpause stream when done
|
||||
|
||||
# PiCamera parameters
|
||||
|
|
@ -190,11 +197,11 @@ class PiCameraStreamer(BaseCamera):
|
|||
|
||||
# Handle lens shading if camera supports it
|
||||
if (
|
||||
hasattr(self.camera, "lens_shading_table")
|
||||
hasattr(self.picamera, "lens_shading_table")
|
||||
and "lens_shading_table" in config["picamera"]
|
||||
):
|
||||
try:
|
||||
self.camera.lens_shading_table = json_to_ndarray(
|
||||
self.picamera.lens_shading_table = json_to_ndarray(
|
||||
config["picamera"].get("lens_shading_table")
|
||||
)
|
||||
except KeyError as e:
|
||||
|
|
@ -208,7 +215,7 @@ class PiCameraStreamer(BaseCamera):
|
|||
# If stream was paused to update config, unpause
|
||||
if paused_stream:
|
||||
logging.info("Resuming stream.")
|
||||
self.start_stream_recording()
|
||||
self.start_stream()
|
||||
|
||||
else:
|
||||
raise Exception(
|
||||
|
|
@ -229,47 +236,47 @@ class PiCameraStreamer(BaseCamera):
|
|||
logging.debug(
|
||||
"Applying exposure_mode: %s", (settings_dict["exposure_mode"])
|
||||
)
|
||||
self.camera.exposure_mode = settings_dict["exposure_mode"]
|
||||
self.picamera.exposure_mode = settings_dict["exposure_mode"]
|
||||
|
||||
# Apply gains and let them settle
|
||||
if "analog_gain" in settings_dict:
|
||||
logging.debug("Applying analog_gain: %s", (settings_dict["analog_gain"]))
|
||||
set_analog_gain(self.camera, float(settings_dict["analog_gain"]))
|
||||
set_analog_gain(self.picamera, float(settings_dict["analog_gain"]))
|
||||
if "digital_gain" in settings_dict:
|
||||
logging.debug("Applying digital_gain: %s", (settings_dict["digital_gain"]))
|
||||
set_digital_gain(self.camera, float(settings_dict["digital_gain"]))
|
||||
set_digital_gain(self.picamera, float(settings_dict["digital_gain"]))
|
||||
|
||||
# Apply shutter speed
|
||||
if "shutter_speed" in settings_dict:
|
||||
logging.debug(
|
||||
"Applying shutter_speed: %s", (settings_dict["shutter_speed"])
|
||||
)
|
||||
self.camera.shutter_speed = int(settings_dict["shutter_speed"])
|
||||
self.picamera.shutter_speed = int(settings_dict["shutter_speed"])
|
||||
|
||||
time.sleep(0.2) # Let gains settle
|
||||
|
||||
# Handle AWB in a half-smart way
|
||||
if "awb_gains" in settings_dict:
|
||||
logging.debug("Applying awb_mode: off")
|
||||
self.camera.awb_mode = "off"
|
||||
self.picamera.awb_mode = "off"
|
||||
logging.debug("Applying awb_gains: %s", (settings_dict["awb_gains"]))
|
||||
self.camera.awb_gains = settings_dict["awb_gains"]
|
||||
self.picamera.awb_gains = settings_dict["awb_gains"]
|
||||
elif "awb_mode" in settings_dict:
|
||||
logging.debug("Applying awb_mode: %s", (settings_dict["awb_mode"]))
|
||||
self.camera.awb_mode = settings_dict["awb_mode"]
|
||||
self.picamera.awb_mode = settings_dict["awb_mode"]
|
||||
|
||||
# Handle some properties that can be quickly applied
|
||||
batched_keys = ["framerate", "saturation"]
|
||||
for key in batched_keys:
|
||||
if (key in settings_dict) and hasattr(self.camera, key):
|
||||
if (key in settings_dict) and hasattr(self.picamera, key):
|
||||
logging.debug("Applying %s: %s", key, settings_dict[key])
|
||||
setattr(self.camera, key, settings_dict[key])
|
||||
setattr(self.picamera, key, settings_dict[key])
|
||||
|
||||
# Final optional pause to settle
|
||||
if pause_for_effect:
|
||||
time.sleep(0.2)
|
||||
|
||||
def set_zoom(self, zoom_value: float = 1.0) -> None:
|
||||
def set_zoom(self, zoom_value: Union[float, int] = 1.0) -> None:
|
||||
"""
|
||||
Change the camera zoom, handling re-centering and scaling.
|
||||
"""
|
||||
|
|
@ -278,7 +285,7 @@ class PiCameraStreamer(BaseCamera):
|
|||
if self.zoom_value < 1:
|
||||
self.zoom_value = 1
|
||||
# Richard's code for zooming !
|
||||
fov = self.camera.zoom
|
||||
fov = self.picamera.zoom
|
||||
centre = np.array([fov[0] + fov[2] / 2.0, fov[1] + fov[3] / 2.0])
|
||||
size = 1.0 / self.zoom_value
|
||||
# If the new zoom value would be invalid, move the centre to
|
||||
|
|
@ -289,23 +296,23 @@ class PiCameraStreamer(BaseCamera):
|
|||
centre[i] = 0.5 + (1.0 - size) / 2 * np.sign(centre[i] - 0.5)
|
||||
logging.info("setting zoom, centre %s, size %s", centre, size)
|
||||
new_fov = (centre[0] - size / 2, centre[1] - size / 2, size, size)
|
||||
self.camera.zoom = new_fov
|
||||
self.picamera.zoom = new_fov
|
||||
|
||||
# LAUNCH ACTIONS
|
||||
|
||||
def start_preview(self, fullscreen=True, window=None):
|
||||
def start_preview(
|
||||
self, fullscreen: bool = True, window: Tuple[int, int, int, int] = None
|
||||
):
|
||||
"""Start the on board GPU camera preview."""
|
||||
with self.lock(timeout=1):
|
||||
try:
|
||||
if not self.camera.preview:
|
||||
if not self.picamera.preview:
|
||||
logging.debug("Starting preview")
|
||||
self.camera.start_preview(fullscreen=fullscreen, window=window)
|
||||
self.picamera.start_preview(fullscreen=fullscreen, window=window)
|
||||
else:
|
||||
logging.debug("Resizing preview")
|
||||
if window:
|
||||
self.camera.preview.window = window
|
||||
self.picamera.preview.window = window
|
||||
if fullscreen:
|
||||
self.camera.preview.fullscreen = fullscreen
|
||||
self.picamera.preview.fullscreen = fullscreen
|
||||
self.preview_active = True
|
||||
except picamerax.exc.PiCameraMMALError as e:
|
||||
logging.error(
|
||||
|
|
@ -320,11 +327,13 @@ class PiCameraStreamer(BaseCamera):
|
|||
def stop_preview(self):
|
||||
"""Stop the on board GPU camera preview."""
|
||||
with self.lock(timeout=1):
|
||||
if self.camera.preview:
|
||||
self.camera.stop_preview()
|
||||
if self.picamera.preview:
|
||||
self.picamera.stop_preview()
|
||||
self.preview_active = False
|
||||
|
||||
def start_recording(self, output, fmt: str = "h264", quality: int = 15):
|
||||
def start_recording(
|
||||
self, output: Union[str, BinaryIO], fmt: str = "h264", quality: int = 15
|
||||
):
|
||||
"""Start recording.
|
||||
|
||||
Start a new video recording, writing to a output object.
|
||||
|
|
@ -345,7 +354,7 @@ class PiCameraStreamer(BaseCamera):
|
|||
# Start the camera video recording on port 2
|
||||
logging.info("Recording to %s", (output))
|
||||
|
||||
self.camera.start_recording(
|
||||
self.picamera.start_recording(
|
||||
output,
|
||||
format=fmt,
|
||||
splitter_port=2,
|
||||
|
|
@ -370,80 +379,67 @@ class PiCameraStreamer(BaseCamera):
|
|||
with self.lock(timeout=5):
|
||||
# Stop the camera video recording on port 2
|
||||
logging.info("Stopping recording")
|
||||
self.camera.stop_recording(splitter_port=2)
|
||||
self.picamera.stop_recording(splitter_port=2)
|
||||
logging.info("Recording stopped")
|
||||
|
||||
# Update state
|
||||
self.record_active = False
|
||||
|
||||
def start_stream_recording(self, splitter_port: int = 1, **kwargs) -> None:
|
||||
def start_stream(self) -> None:
|
||||
"""
|
||||
Sets the camera resolution to the video/stream resolution, and starts recording if the stream should be active.
|
||||
|
||||
Args:
|
||||
splitter_port (int): Splitter port to start recording on
|
||||
"""
|
||||
for k in kwargs.keys():
|
||||
logging.warning(
|
||||
"Warning, kwarg %s is invalid for stop_stream_recording.", k
|
||||
)
|
||||
with self.lock(timeout=None):
|
||||
# Reduce the resolution for video streaming
|
||||
try:
|
||||
self.camera._check_recording_stopped() # pylint: disable=W0212
|
||||
self.picamera._check_recording_stopped() # pylint: disable=W0212
|
||||
except picamerax.exc.PiCameraRuntimeError:
|
||||
logging.info(
|
||||
"Error while changing resolution: Recording already running."
|
||||
)
|
||||
else:
|
||||
self.camera.resolution = self.stream_resolution
|
||||
self.picamera.resolution = self.stream_resolution
|
||||
# Sprinkled a sleep to prevent camera getting confused by rapid commands
|
||||
time.sleep(0.2)
|
||||
|
||||
# If the stream should be active
|
||||
try:
|
||||
# Start recording on stream port
|
||||
self.camera.start_recording(
|
||||
self.picamera.start_recording(
|
||||
self.stream,
|
||||
format="mjpeg",
|
||||
quality=self.mjpeg_quality,
|
||||
bitrate=-1, # RWB: disable bitrate control
|
||||
# (bitrate control makes JPEG size less good as a focus
|
||||
# metric)
|
||||
splitter_port=splitter_port,
|
||||
splitter_port=1,
|
||||
)
|
||||
except picamerax.exc.PiCameraAlreadyRecording:
|
||||
logging.info("Error while starting preview: Recording already running.")
|
||||
else:
|
||||
self.stream_active = True
|
||||
logging.debug(
|
||||
"Started MJPEG stream at %s on port %s",
|
||||
self.stream_resolution,
|
||||
splitter_port,
|
||||
"Started MJPEG stream at %s on port %s", self.stream_resolution, 1
|
||||
)
|
||||
|
||||
def stop_stream_recording(self, splitter_port: int = 1, **kwargs) -> None:
|
||||
def stop_stream(self) -> None:
|
||||
"""
|
||||
Sets the camera resolution to the still-image resolution, and stops recording if the stream is active.
|
||||
|
||||
Args:
|
||||
splitter_port (int): Splitter port to stop recording on
|
||||
"""
|
||||
for k in kwargs.keys():
|
||||
logging.warning(
|
||||
"Warning, kwarg %s is invalid for stop_stream_recording.", k
|
||||
)
|
||||
with self.lock:
|
||||
# Stop the camera video recording on port 1
|
||||
try:
|
||||
self.camera.stop_recording(splitter_port=splitter_port)
|
||||
self.picamera.stop_recording(splitter_port=1)
|
||||
except picamerax.exc.PiCameraNotRecording:
|
||||
logging.info("Not recording on splitter_port %s", (splitter_port))
|
||||
logging.info("Not recording on splitter_port %s", (1))
|
||||
else:
|
||||
self.stream_active = False
|
||||
logging.info(
|
||||
"Stopped MJPEG stream on port %s. Switching to %s.",
|
||||
splitter_port,
|
||||
1,
|
||||
self.image_resolution,
|
||||
)
|
||||
|
||||
|
|
@ -451,16 +447,16 @@ class PiCameraStreamer(BaseCamera):
|
|||
time.sleep(
|
||||
0.2
|
||||
) # Sprinkled a sleep to prevent camera getting confused by rapid commands
|
||||
self.camera.resolution = self.image_resolution
|
||||
self.picamera.resolution = self.image_resolution
|
||||
|
||||
def capture(
|
||||
self,
|
||||
output,
|
||||
output: Union[str, BinaryIO],
|
||||
fmt: str = "jpeg",
|
||||
use_video_port: bool = False,
|
||||
resize: Tuple[int, int] = None,
|
||||
bayer: bool = True,
|
||||
thumbnail: tuple = None,
|
||||
thumbnail: Tuple[int, int, int] = None,
|
||||
):
|
||||
"""
|
||||
Capture a still image to a StreamObject.
|
||||
|
|
@ -470,10 +466,11 @@ class PiCameraStreamer(BaseCamera):
|
|||
|
||||
Args:
|
||||
output: String or file-like object to write capture data to
|
||||
fmt (str): Format of the capture.
|
||||
use_video_port (bool): Capture from the video port used for streaming. Lower resolution, faster.
|
||||
resize ((int, int)): Resize the captured image.
|
||||
bayer (bool): Store raw bayer data in capture
|
||||
fmt: Format of the capture.
|
||||
use_video_port: Capture from the video port used for streaming. Lower resolution, faster.
|
||||
resize: Resize the captured image.
|
||||
bayer: Store raw bayer data in capture
|
||||
thumbnail: Dimensions and quality (x, y, quality) of a thumbnail to generate, if supported
|
||||
|
||||
Returns:
|
||||
output_object (str/BytesIO): Target object.
|
||||
|
|
@ -483,9 +480,9 @@ class PiCameraStreamer(BaseCamera):
|
|||
|
||||
# Set resolution and stop stream recording if necessary
|
||||
if not use_video_port:
|
||||
self.stop_stream_recording()
|
||||
self.stop_stream()
|
||||
|
||||
self.camera.capture(
|
||||
self.picamera.capture(
|
||||
output,
|
||||
format=fmt,
|
||||
quality=self.jpeg_quality,
|
||||
|
|
@ -497,11 +494,11 @@ class PiCameraStreamer(BaseCamera):
|
|||
|
||||
# Set resolution and start stream recording if necessary
|
||||
if not use_video_port:
|
||||
self.start_stream_recording()
|
||||
self.start_stream()
|
||||
|
||||
return output
|
||||
|
||||
def array(self, use_video_port=True) -> np.ndarray:
|
||||
def array(self, use_video_port: bool = True) -> np.ndarray:
|
||||
"""Capture an uncompressed still RGB image to a Numpy array.
|
||||
|
||||
Args:
|
||||
|
|
@ -513,7 +510,9 @@ class PiCameraStreamer(BaseCamera):
|
|||
"""
|
||||
with self.lock:
|
||||
logging.debug("Creating PiRGBArray")
|
||||
with picamerax.array.PiRGBArray(self.camera) as output:
|
||||
with picamerax.array.PiRGBArray(self.picamera) as output:
|
||||
logging.info("Capturing to %s", (output))
|
||||
self.camera.capture(output, format="rgb", use_video_port=use_video_port)
|
||||
self.picamera.capture(
|
||||
output, format="rgb", use_video_port=use_video_port
|
||||
)
|
||||
return output.array
|
||||
|
|
|
|||
|
|
@ -2,16 +2,17 @@ from __future__ import print_function
|
|||
|
||||
import logging
|
||||
import time
|
||||
from typing import Union
|
||||
|
||||
import picamerax
|
||||
from picamerax import exc, mmal
|
||||
from picamerax.mmalobj import to_rational
|
||||
|
||||
MMAL_PARAMETER_ANALOG_GAIN = mmal.MMAL_PARAMETER_GROUP_CAMERA + 0x59
|
||||
MMAL_PARAMETER_DIGITAL_GAIN = mmal.MMAL_PARAMETER_GROUP_CAMERA + 0x5A
|
||||
MMAL_PARAMETER_ANALOG_GAIN: int = mmal.MMAL_PARAMETER_GROUP_CAMERA + 0x59
|
||||
MMAL_PARAMETER_DIGITAL_GAIN: int = mmal.MMAL_PARAMETER_GROUP_CAMERA + 0x5A
|
||||
|
||||
|
||||
def set_gain(camera, gain, value):
|
||||
def set_gain(camera: picamerax.PiCamera, gain: int, value: Union[int, float]):
|
||||
"""Set the analog gain of a PiCamera.
|
||||
|
||||
camera: the picamerax.PiCamera() instance you are configuring
|
||||
|
|
@ -32,12 +33,12 @@ def set_gain(camera, gain, value):
|
|||
raise exc.PiCameraMMALError(ret)
|
||||
|
||||
|
||||
def set_analog_gain(camera, value):
|
||||
def set_analog_gain(camera: picamerax.PiCamera, value: Union[int, float]):
|
||||
"""Set the gain of a PiCamera object to a given value."""
|
||||
set_gain(camera, MMAL_PARAMETER_ANALOG_GAIN, value)
|
||||
|
||||
|
||||
def set_digital_gain(camera, value):
|
||||
def set_digital_gain(camera: picamerax.PiCamera, value: Union[int, float]):
|
||||
"""Set the digital gain of a PiCamera object to a given value."""
|
||||
set_gain(camera, MMAL_PARAMETER_DIGITAL_GAIN, value)
|
||||
|
||||
|
|
|
|||
|
|
@ -1,3 +1,11 @@
|
|||
from . import capture, capture_manager
|
||||
from .capture import THUMBNAIL_SIZE, CaptureObject
|
||||
from .capture_manager import CaptureManager
|
||||
|
||||
__all__ = [
|
||||
"capture",
|
||||
"capture_manager",
|
||||
"THUMBNAIL_SIZE",
|
||||
"CaptureObject",
|
||||
"CaptureManager",
|
||||
]
|
||||
|
|
|
|||
|
|
@ -6,12 +6,13 @@ import logging
|
|||
import os
|
||||
import uuid
|
||||
from collections import OrderedDict
|
||||
from typing import Callable, Dict, List, Optional
|
||||
|
||||
import dateutil.parser
|
||||
import piexif
|
||||
from piexif import InvalidImageDataError
|
||||
from PIL import Image
|
||||
|
||||
from openflexure_microscope.captures import piexif
|
||||
from openflexure_microscope.captures.piexif import InvalidImageDataError
|
||||
from openflexure_microscope.json import JSONEncoder
|
||||
|
||||
EXIF_FORMATS = ["JPG", "JPEG", "TIF", "TIFF"]
|
||||
|
|
@ -39,7 +40,7 @@ def build_captures_from_exif(capture_path):
|
|||
logging.debug("Reloading capture %s...", (f))
|
||||
capture = capture_from_path(f)
|
||||
if capture:
|
||||
captures[capture.id] = capture
|
||||
captures[str(capture.id)] = capture
|
||||
|
||||
logging.info("%s capture files successfully reloaded", (len(captures)))
|
||||
|
||||
|
|
@ -68,33 +69,42 @@ class CaptureObject(object):
|
|||
Serves to simplify modifying properties of on-disk capture data.
|
||||
"""
|
||||
|
||||
def __init__(self, filepath) -> None:
|
||||
def __init__(self, filepath: str) -> None:
|
||||
"""Create a new StreamObject, to manage capture data."""
|
||||
# Stream for buffering capture data
|
||||
self.stream = io.BytesIO()
|
||||
self.stream: io.BytesIO = io.BytesIO()
|
||||
|
||||
# Store a nice ID
|
||||
self.id = uuid.uuid4() #: str: Unique capture ID
|
||||
self.id: uuid.UUID = uuid.uuid4() #: str: Unique capture ID
|
||||
logging.debug("Created CaptureObject %s", (self.id))
|
||||
|
||||
self.time = datetime.datetime.now()
|
||||
self.time: datetime.datetime = datetime.datetime.now()
|
||||
|
||||
# Create file name. Default to UUID
|
||||
self.format = None
|
||||
self.file = filepath
|
||||
self.file: str = filepath
|
||||
# Placeholders for split file info
|
||||
self.format: str = ""
|
||||
self.basename: str = ""
|
||||
self.filefolder: str = ""
|
||||
self.name: str = ""
|
||||
# Replace placeholders with actual split file info
|
||||
self.split_file_path(self.file)
|
||||
|
||||
if not os.path.exists(self.filefolder):
|
||||
os.makedirs(self.filefolder)
|
||||
|
||||
# Dataset information
|
||||
self._dataset = None
|
||||
self.dataset: Dict[str, str] = {}
|
||||
|
||||
# Dictionary for storing custom annotations
|
||||
# Can be modified via the web API
|
||||
self.annotations = {}
|
||||
self.annotations: Dict[str, str] = {}
|
||||
# List for storing tags
|
||||
# Can be modified via the web API
|
||||
self.tags = []
|
||||
self.tags: List[str] = []
|
||||
|
||||
# On-delete callback
|
||||
self.on_delete: Optional[Callable] = None
|
||||
|
||||
def write(self, s):
|
||||
logging.debug("Writing to %s", self)
|
||||
|
|
@ -112,7 +122,7 @@ class CaptureObject(object):
|
|||
def open(self, mode):
|
||||
return open(self.file, mode)
|
||||
|
||||
def split_file_path(self, filepath):
|
||||
def split_file_path(self, filepath: str):
|
||||
"""
|
||||
Take a full file path, and split it into separated class properties.
|
||||
|
||||
|
|
@ -125,10 +135,10 @@ class CaptureObject(object):
|
|||
self.basename = os.path.splitext(self.name)[0]
|
||||
self.format = self.name.split(".")[-1]
|
||||
|
||||
def _read_exif(self):
|
||||
def _read_exif(self) -> dict:
|
||||
return piexif.load(self.file)
|
||||
|
||||
def _decode_usercomment(self, exif_dict: dict):
|
||||
def _decode_usercomment(self, exif_dict: dict) -> dict:
|
||||
if "Exif" not in exif_dict:
|
||||
raise InvalidImageDataError
|
||||
if piexif.ExifIFD.UserComment not in exif_dict["Exif"]:
|
||||
|
|
@ -150,15 +160,17 @@ class CaptureObject(object):
|
|||
)
|
||||
|
||||
def sync_basic_metadata(self):
|
||||
exif_dict = self._read_exif()
|
||||
exif_dict: dict = self._read_exif()
|
||||
|
||||
metadata_dict = self._decode_usercomment(exif_dict) or {}
|
||||
image_metadata = metadata_dict.get("image")
|
||||
metadata_dict: dict = self._decode_usercomment(exif_dict) or {}
|
||||
self.dataset = metadata_dict.get("dataset")
|
||||
|
||||
image_metadata: Optional[dict] = metadata_dict.get("image")
|
||||
|
||||
if not image_metadata:
|
||||
raise InvalidImageDataError("No capture metadata found")
|
||||
|
||||
self.id = image_metadata.get("id")
|
||||
self.id = uuid.UUID(image_metadata.get("id"))
|
||||
self.format = image_metadata.get("format")
|
||||
self.time = dateutil.parser.isoparse(
|
||||
image_metadata.get("time") or image_metadata.get("acquisitionDate")
|
||||
|
|
@ -166,24 +178,11 @@ class CaptureObject(object):
|
|||
self.tags = image_metadata.get("tags")
|
||||
self.annotations = image_metadata.get("annotations")
|
||||
|
||||
dataset = metadata_dict.get("dataset")
|
||||
if dataset:
|
||||
self._dataset = {
|
||||
"id": dataset.get("id"),
|
||||
"name": dataset.get("name"),
|
||||
"type": dataset.get("type"),
|
||||
}
|
||||
|
||||
def read_full_metadata(self):
|
||||
def read_full_metadata(self) -> dict:
|
||||
logging.info("Reading full capture metadata from %s...", self.file)
|
||||
exif_dict = self._read_exif()
|
||||
return self._decode_usercomment(exif_dict)
|
||||
|
||||
@property
|
||||
def dataset(self):
|
||||
"""Read-only dataset property"""
|
||||
return self._dataset
|
||||
|
||||
@property
|
||||
def exists(self) -> bool:
|
||||
"""Check if capture data file exists on disk."""
|
||||
|
|
@ -193,7 +192,7 @@ class CaptureObject(object):
|
|||
return False
|
||||
|
||||
# HANDLE TAGS
|
||||
def put_tags(self, tags: list):
|
||||
def put_tags(self, tags: List[str]):
|
||||
"""
|
||||
Add a new tag to the ``tags`` list attribute.
|
||||
|
||||
|
|
@ -218,7 +217,7 @@ class CaptureObject(object):
|
|||
|
||||
# HANDLE ANNOTATIONS
|
||||
|
||||
def put_annotations(self, data: dict) -> None:
|
||||
def put_annotations(self, data: Dict[str, str]):
|
||||
"""
|
||||
Merge annotations from a passed dictionary into the capture metadata, and saves.
|
||||
|
||||
|
|
@ -227,7 +226,7 @@ class CaptureObject(object):
|
|||
"""
|
||||
self.put_and_save(annotations=data)
|
||||
|
||||
def delete_annotation(self, key: str) -> None:
|
||||
def delete_annotation(self, key: str):
|
||||
# Update in-memory annotations list
|
||||
if key in self.annotations:
|
||||
del self.annotations[key]
|
||||
|
|
@ -237,7 +236,7 @@ class CaptureObject(object):
|
|||
|
||||
# HANDLE METADATA
|
||||
|
||||
def put_metadata(self, data: dict) -> None:
|
||||
def put_metadata(self, data: dict):
|
||||
"""
|
||||
Merge root metadata from a passed dictionary into the capture metadata, and saves.
|
||||
|
||||
|
|
@ -246,10 +245,19 @@ class CaptureObject(object):
|
|||
"""
|
||||
self.put_and_save(metadata=data)
|
||||
|
||||
# HANDLE DATASET
|
||||
|
||||
def put_dataset(self, data: dict):
|
||||
self.put_and_save(dataset=data)
|
||||
|
||||
# BULK OPERATIONS
|
||||
|
||||
def put_and_save(
|
||||
self, tags: list = None, annotations: dict = None, metadata: dict = None
|
||||
self,
|
||||
tags: Optional[List[str]] = None,
|
||||
annotations: Optional[Dict[str, str]] = None,
|
||||
dataset: Optional[Dict[str, str]] = None,
|
||||
metadata: Optional[dict] = None,
|
||||
):
|
||||
"""
|
||||
Batch-write tags, metadata, and annotations in a single disk operation
|
||||
|
|
@ -258,6 +266,8 @@ class CaptureObject(object):
|
|||
tags = []
|
||||
if not annotations:
|
||||
annotations = {}
|
||||
if not dataset:
|
||||
dataset = {}
|
||||
if not metadata:
|
||||
metadata = {}
|
||||
|
||||
|
|
@ -269,6 +279,9 @@ class CaptureObject(object):
|
|||
# Update in-memory annotations dictionary
|
||||
self.annotations.update(annotations)
|
||||
|
||||
# Update in-memory dataset dictionary
|
||||
self.dataset.update(dataset)
|
||||
|
||||
# Write new data to file EXIF, if supported
|
||||
if self.format.upper() in EXIF_FORMATS and self.exists:
|
||||
logging.info("Writing Exif data to %s", self.file)
|
||||
|
|
@ -281,6 +294,8 @@ class CaptureObject(object):
|
|||
metadata_dict.get("image", {})["tags"] = self.tags
|
||||
# Add new annotations to exif dictionary
|
||||
metadata_dict.get("image", {})["annotations"] = self.annotations
|
||||
# Set new dataset info in exif dictionary
|
||||
metadata_dict["dataset"] = self.dataset
|
||||
# Add new custom metadata to exif dictionary
|
||||
metadata_dict.update(metadata)
|
||||
|
||||
|
|
@ -310,7 +325,7 @@ class CaptureObject(object):
|
|||
return self.read_full_metadata()
|
||||
|
||||
@property
|
||||
def data(self) -> io.BytesIO:
|
||||
def data(self) -> Optional[io.BytesIO]:
|
||||
"""
|
||||
Return a BytesIO object of the capture data.
|
||||
"""
|
||||
|
|
@ -321,16 +336,17 @@ class CaptureObject(object):
|
|||
d = io.BytesIO(f.read()) # Load bytes from file
|
||||
d.seek(0) # Rewind loaded bytestream
|
||||
# Create a copy of the bytestream bytes
|
||||
data = io.BytesIO(d.getbuffer())
|
||||
return io.BytesIO(d.getbuffer())
|
||||
else:
|
||||
data = None
|
||||
|
||||
return data # Read and return bytes data
|
||||
return None
|
||||
|
||||
@property
|
||||
def binary(self) -> bytes:
|
||||
def binary(self) -> Optional[bytes]:
|
||||
"""Return a byte string of the capture data."""
|
||||
return self.data.getvalue()
|
||||
if self.data:
|
||||
return self.data.getvalue()
|
||||
else:
|
||||
return None
|
||||
|
||||
@property
|
||||
def thumbnail(self) -> io.BytesIO:
|
||||
|
|
@ -354,7 +370,7 @@ class CaptureObject(object):
|
|||
|
||||
# FILE MANAGEMENT
|
||||
|
||||
def save(self) -> None:
|
||||
def save(self):
|
||||
"""Write stream to file, and save/update metadata file"""
|
||||
# If a stream OR file exists, save the metadata file
|
||||
if self.exists:
|
||||
|
|
@ -367,6 +383,9 @@ class CaptureObject(object):
|
|||
logging.info("Deleting file %s", (self.file))
|
||||
os.remove(self.file)
|
||||
|
||||
if self.on_delete:
|
||||
self.on_delete(self, self.id)
|
||||
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
|
|
|
|||
|
|
@ -3,11 +3,12 @@ import logging
|
|||
import os
|
||||
import shutil
|
||||
from collections import OrderedDict
|
||||
from typing import Dict, List, MutableMapping, Optional, Union, ValuesView
|
||||
from uuid import UUID
|
||||
|
||||
from labthings import StrictLock
|
||||
|
||||
from openflexure_microscope.paths import data_file_path
|
||||
from openflexure_microscope.utilities import entry_by_uuid
|
||||
|
||||
from .capture import CaptureObject, build_captures_from_exif
|
||||
|
||||
|
|
@ -15,40 +16,18 @@ BASE_CAPTURE_PATH = data_file_path("micrographs")
|
|||
TEMP_CAPTURE_PATH = os.path.join(BASE_CAPTURE_PATH, "tmp")
|
||||
|
||||
|
||||
def last_entry(object_list: list):
|
||||
"""Return the last entry of a list, if the list contains items."""
|
||||
if object_list: # If any images have been captured
|
||||
return object_list[-1] # Return the latest captured image
|
||||
else:
|
||||
return None
|
||||
|
||||
|
||||
def generate_basename():
|
||||
"""Return a default filename based on the capture datetime"""
|
||||
return datetime.datetime.now().strftime("%Y-%m-%d_%H-%M-%S")
|
||||
|
||||
|
||||
def generate_numbered_basename(obj_list: list) -> str:
|
||||
initial_basename = generate_basename()
|
||||
basename = initial_basename
|
||||
# Handle clashing
|
||||
iterator = 1
|
||||
while basename in [obj.basename for obj in obj_list]:
|
||||
basename = initial_basename + "_{}".format(iterator)
|
||||
iterator += 1
|
||||
|
||||
return basename
|
||||
|
||||
|
||||
class CaptureManager:
|
||||
def __init__(self):
|
||||
self.paths = {"default": BASE_CAPTURE_PATH, "temp": TEMP_CAPTURE_PATH}
|
||||
self.paths: Dict[str, str] = {
|
||||
"default": BASE_CAPTURE_PATH,
|
||||
"temp": TEMP_CAPTURE_PATH,
|
||||
}
|
||||
|
||||
self.lock = StrictLock(timeout=1, name="Captures")
|
||||
self.lock: StrictLock = StrictLock(timeout=1, name="Captures")
|
||||
|
||||
# Capture data
|
||||
self.images = OrderedDict()
|
||||
self.videos = OrderedDict()
|
||||
self.images: MutableMapping[str, CaptureObject] = OrderedDict()
|
||||
self.videos: MutableMapping[str, CaptureObject] = OrderedDict()
|
||||
|
||||
# FILE MANAGEMENT
|
||||
|
||||
|
|
@ -96,41 +75,28 @@ class CaptureManager:
|
|||
|
||||
# RETURNING CAPTURES
|
||||
|
||||
@property
|
||||
def image(self):
|
||||
"""Return the latest captured image."""
|
||||
return last_entry(self.images.values())
|
||||
|
||||
@property
|
||||
def video(self):
|
||||
"""Return the latest recorded video."""
|
||||
return last_entry(self.videos.values())
|
||||
|
||||
def image_from_id(self, image_id):
|
||||
def image_from_id(self, image_id: Union[str, int, UUID]) -> Optional[CaptureObject]:
|
||||
"""Return an image StreamObject with a matching ID."""
|
||||
logging.warning("image_from_id is deprecated. Access captures as a dictionary.")
|
||||
return entry_by_uuid(image_id, self.images.values())
|
||||
return entry_by_uuid(image_id, self.images)
|
||||
|
||||
def video_from_id(self, video_id):
|
||||
def video_from_id(self, video_id: Union[str, int, UUID]) -> Optional[CaptureObject]:
|
||||
"""Return a video StreamObject with a matching ID."""
|
||||
logging.warning("video_from_id is deprecated. Access captures as a dictionary.")
|
||||
return entry_by_uuid(video_id, self.videos.values())
|
||||
return entry_by_uuid(video_id, self.videos)
|
||||
|
||||
# CREATING NEW CAPTURES
|
||||
|
||||
def _new_output(self, temporary, filename, folder, fmt):
|
||||
# Generate file name
|
||||
if not filename:
|
||||
filename = generate_numbered_basename(self.images.values())
|
||||
logging.debug(filename)
|
||||
def _new_output(self, temporary: bool, filename: str, folder: str, fmt: str):
|
||||
|
||||
filename = "{}.{}".format(filename, fmt)
|
||||
|
||||
# Generate folder
|
||||
base_folder = self.paths["temp"] if temporary else self.paths["default"]
|
||||
base_folder: str = self.paths["temp"] if temporary else self.paths["default"]
|
||||
folder = os.path.join(base_folder, folder)
|
||||
|
||||
# Generate file path
|
||||
filepath = os.path.join(folder, filename)
|
||||
filepath: str = os.path.join(folder, filename)
|
||||
|
||||
# Create capture object
|
||||
output = CaptureObject(filepath=filepath)
|
||||
|
|
@ -143,7 +109,7 @@ class CaptureManager:
|
|||
def new_image(
|
||||
self,
|
||||
temporary: bool = True,
|
||||
filename: str = None,
|
||||
filename: Optional[str] = None,
|
||||
folder: str = "",
|
||||
fmt: str = "jpeg",
|
||||
):
|
||||
|
|
@ -158,23 +124,28 @@ class CaptureManager:
|
|||
folder (str): Name of the folder in which to store the capture.
|
||||
fmt (str): Format of the capture.
|
||||
"""
|
||||
# Generate file name
|
||||
if not filename:
|
||||
filename = generate_numbered_basename(self.images.values())
|
||||
|
||||
# Create a new output object
|
||||
output = self._new_output(temporary, filename, folder, fmt)
|
||||
# Add an on-delete callback
|
||||
output.on_delete = self.remove_image
|
||||
|
||||
# Update capture list
|
||||
capture_key = str(output.id)
|
||||
logging.debug("Adding image %s with key %s", output, capture_key)
|
||||
self.images[capture_key] = output
|
||||
logging.debug("Adding image %s with key %s", output, output.id)
|
||||
self.images[str(output.id)] = output
|
||||
|
||||
return output
|
||||
|
||||
def new_video(
|
||||
self,
|
||||
temporary: bool = False,
|
||||
filename: str = None,
|
||||
filename: Optional[str] = None,
|
||||
folder: str = "",
|
||||
fmt: str = "h264",
|
||||
):
|
||||
) -> CaptureObject:
|
||||
|
||||
"""
|
||||
Create a new video capture object.
|
||||
|
|
@ -186,12 +157,72 @@ class CaptureManager:
|
|||
folder (str): Name of the folder in which to store the capture.
|
||||
fmt (str): Format of the capture.
|
||||
"""
|
||||
# Generate file name
|
||||
if not filename:
|
||||
filename = generate_numbered_basename(self.videos.values())
|
||||
|
||||
# Create a new output object
|
||||
output = self._new_output(temporary, filename, folder, fmt)
|
||||
# Add an on-delete callback
|
||||
output.on_delete = self.remove_video
|
||||
|
||||
# Update capture list
|
||||
capture_key = str(output.id)
|
||||
logging.debug("Adding video %s with key %s", output, capture_key)
|
||||
self.videos[capture_key] = output
|
||||
logging.debug("Adding video %s with key %s", output, output.id)
|
||||
self.videos[str(output.id)] = output
|
||||
|
||||
return output
|
||||
|
||||
def remove_image(self, capture_obj: CaptureObject, capture_id: str):
|
||||
logging.info("Deleting capture %s", capture_id)
|
||||
if capture_id in self.images:
|
||||
logging.info("Deleting capture object %s", capture_obj)
|
||||
del self.images[capture_id]
|
||||
|
||||
def remove_video(self, capture_obj: CaptureObject, capture_id: str):
|
||||
logging.info("Deleting capture %s", capture_id)
|
||||
if capture_id in self.images:
|
||||
logging.info("Deleting capture object %s", capture_obj)
|
||||
del self.videos[capture_id]
|
||||
|
||||
|
||||
def entry_by_uuid(
|
||||
entry_id: Union[str, int, UUID], object_dict: MutableMapping[str, CaptureObject]
|
||||
) -> Optional[CaptureObject]:
|
||||
"""Return an object from a list, if <object>.id matches id argument."""
|
||||
if isinstance(entry_id, str):
|
||||
key: str = entry_id
|
||||
elif isinstance(entry_id, UUID):
|
||||
key = str(entry_id)
|
||||
elif isinstance(entry_id, int):
|
||||
key = str(UUID(int=entry_id))
|
||||
else:
|
||||
raise TypeError("Argument entry_id must be a string, integer, or UUID object.")
|
||||
|
||||
return object_dict.get(key)
|
||||
|
||||
|
||||
def generate_basename() -> str:
|
||||
"""Return a default filename based on the capture datetime"""
|
||||
return datetime.datetime.now().strftime("%Y-%m-%d_%H-%M-%S")
|
||||
|
||||
|
||||
def generate_numbered_basename(
|
||||
obj_list: Union[ValuesView[CaptureObject], List[CaptureObject]]
|
||||
) -> str:
|
||||
"""
|
||||
This function prevents rapid captures from having clashing generated names.
|
||||
|
||||
Our generated names are a datetime string going as far as seconds,
|
||||
so if you create 2 captures within 1 second then they would have a name
|
||||
clash. This method handles appending sequential integers to names
|
||||
that would clash.
|
||||
"""
|
||||
initial_basename = generate_basename()
|
||||
basename = initial_basename
|
||||
# Handle clashing
|
||||
iterator = 1
|
||||
while basename in [obj.basename for obj in obj_list]:
|
||||
basename = initial_basename + "_{}".format(iterator)
|
||||
iterator += 1
|
||||
|
||||
return basename
|
||||
|
|
|
|||
|
|
@ -1,21 +0,0 @@
|
|||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2014, 2015 hMatoba
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
|
|
@ -1,9 +0,0 @@
|
|||
from ._dump import dump
|
||||
from ._exceptions import *
|
||||
from ._exif import *
|
||||
from ._insert import insert
|
||||
from ._load import load
|
||||
from ._remove import remove
|
||||
from ._transplant import transplant
|
||||
|
||||
VERSION = "1.1.2"
|
||||
|
|
@ -1,97 +0,0 @@
|
|||
import struct
|
||||
|
||||
from ._exceptions import InvalidImageDataError
|
||||
|
||||
|
||||
def split_into_segments(data):
|
||||
"""Slices JPEG meta data into a list from JPEG binary data.
|
||||
"""
|
||||
if data[0:2] != b"\xff\xd8":
|
||||
raise InvalidImageDataError("Given data isn't JPEG.")
|
||||
|
||||
head = 2
|
||||
segments = [b"\xff\xd8"]
|
||||
while 1:
|
||||
if data[head : head + 2] == b"\xff\xda":
|
||||
segments.append(data[head:])
|
||||
break
|
||||
else:
|
||||
length = struct.unpack(">H", data[head + 2 : head + 4])[0]
|
||||
endPoint = head + length + 2
|
||||
seg = data[head:endPoint]
|
||||
segments.append(seg)
|
||||
head = endPoint
|
||||
|
||||
if head >= len(data):
|
||||
raise InvalidImageDataError("Wrong JPEG data.")
|
||||
return segments
|
||||
|
||||
|
||||
def read_exif_from_file(filename):
|
||||
"""Slices JPEG meta data into a list from JPEG binary data.
|
||||
"""
|
||||
f = open(filename, "rb")
|
||||
data = f.read(6)
|
||||
|
||||
if data[0:2] != b"\xff\xd8":
|
||||
raise InvalidImageDataError("Given data isn't JPEG.")
|
||||
|
||||
head = data[2:6]
|
||||
HEAD_LENGTH = 4
|
||||
exif = None
|
||||
while len(head) == HEAD_LENGTH:
|
||||
length = struct.unpack(">H", head[2:4])[0]
|
||||
|
||||
if head[:2] == b"\xff\xe1":
|
||||
segment_data = f.read(length - 2)
|
||||
if segment_data[:4] != b"Exif":
|
||||
head = f.read(HEAD_LENGTH)
|
||||
continue
|
||||
exif = head + segment_data
|
||||
break
|
||||
elif head[0:1] == b"\xff":
|
||||
f.read(length - 2)
|
||||
head = f.read(HEAD_LENGTH)
|
||||
else:
|
||||
break
|
||||
|
||||
f.close()
|
||||
return exif
|
||||
|
||||
|
||||
def get_exif_seg(segments):
|
||||
"""Returns Exif from JPEG meta data list
|
||||
"""
|
||||
for seg in segments:
|
||||
if seg[0:2] == b"\xff\xe1" and seg[4:10] == b"Exif\x00\x00":
|
||||
return seg
|
||||
return None
|
||||
|
||||
|
||||
def merge_segments(segments, exif=b""):
|
||||
"""Merges Exif with APP0 and APP1 manipulations.
|
||||
"""
|
||||
if (
|
||||
segments[1][0:2] == b"\xff\xe0"
|
||||
and segments[2][0:2] == b"\xff\xe1"
|
||||
and segments[2][4:10] == b"Exif\x00\x00"
|
||||
):
|
||||
if exif:
|
||||
segments[2] = exif
|
||||
segments.pop(1)
|
||||
elif exif is None:
|
||||
segments.pop(2)
|
||||
else:
|
||||
segments.pop(1)
|
||||
elif segments[1][0:2] == b"\xff\xe0":
|
||||
if exif:
|
||||
segments[1] = exif
|
||||
elif segments[1][0:2] == b"\xff\xe1" and segments[1][4:10] == b"Exif\x00\x00":
|
||||
if exif:
|
||||
segments[1] = exif
|
||||
elif exif is None:
|
||||
segments.pop(1)
|
||||
else:
|
||||
if exif:
|
||||
segments.insert(1, exif)
|
||||
return b"".join(segments)
|
||||
|
|
@ -1,369 +0,0 @@
|
|||
import copy
|
||||
import numbers
|
||||
import struct
|
||||
|
||||
from ._common import split_into_segments
|
||||
from ._exif import TAGS, TYPES, ExifIFD, ImageIFD
|
||||
|
||||
TIFF_HEADER_LENGTH = 8
|
||||
|
||||
|
||||
def dump(exif_dict_original):
|
||||
"""
|
||||
py:function:: piexif.load(data)
|
||||
|
||||
Return exif as bytes.
|
||||
|
||||
:param dict exif: Exif data({"0th":dict, "Exif":dict, "GPS":dict, "Interop":dict, "1st":dict, "thumbnail":bytes})
|
||||
:return: Exif
|
||||
:rtype: bytes
|
||||
"""
|
||||
exif_dict = copy.deepcopy(exif_dict_original)
|
||||
header = b"Exif\x00\x00\x4d\x4d\x00\x2a\x00\x00\x00\x08"
|
||||
exif_is = False
|
||||
gps_is = False
|
||||
interop_is = False
|
||||
first_is = False
|
||||
|
||||
if "0th" in exif_dict:
|
||||
zeroth_ifd = exif_dict["0th"]
|
||||
else:
|
||||
zeroth_ifd = {}
|
||||
|
||||
if (
|
||||
("Exif" in exif_dict)
|
||||
and len(exif_dict["Exif"])
|
||||
or ("Interop" in exif_dict)
|
||||
and len(exif_dict["Interop"])
|
||||
):
|
||||
zeroth_ifd[ImageIFD.ExifTag] = 1
|
||||
exif_is = True
|
||||
exif_ifd = exif_dict["Exif"]
|
||||
if ("Interop" in exif_dict) and len(exif_dict["Interop"]):
|
||||
exif_ifd[ExifIFD.InteroperabilityTag] = 1
|
||||
interop_is = True
|
||||
interop_ifd = exif_dict["Interop"]
|
||||
elif ExifIFD.InteroperabilityTag in exif_ifd:
|
||||
exif_ifd.pop(ExifIFD.InteroperabilityTag)
|
||||
elif ImageIFD.ExifTag in zeroth_ifd:
|
||||
zeroth_ifd.pop(ImageIFD.ExifTag)
|
||||
|
||||
if ("GPS" in exif_dict) and len(exif_dict["GPS"]):
|
||||
zeroth_ifd[ImageIFD.GPSTag] = 1
|
||||
gps_is = True
|
||||
gps_ifd = exif_dict["GPS"]
|
||||
elif ImageIFD.GPSTag in zeroth_ifd:
|
||||
zeroth_ifd.pop(ImageIFD.GPSTag)
|
||||
|
||||
if (
|
||||
("1st" in exif_dict)
|
||||
and ("thumbnail" in exif_dict)
|
||||
and (exif_dict["thumbnail"] is not None)
|
||||
):
|
||||
first_is = True
|
||||
exif_dict["1st"][ImageIFD.JPEGInterchangeFormat] = 1
|
||||
exif_dict["1st"][ImageIFD.JPEGInterchangeFormatLength] = 1
|
||||
first_ifd = exif_dict["1st"]
|
||||
|
||||
zeroth_set = _dict_to_bytes(zeroth_ifd, "0th", 0)
|
||||
zeroth_length = (
|
||||
len(zeroth_set[0]) + exif_is * 12 + gps_is * 12 + 4 + len(zeroth_set[1])
|
||||
)
|
||||
|
||||
if exif_is:
|
||||
exif_set = _dict_to_bytes(exif_ifd, "Exif", zeroth_length)
|
||||
exif_length = len(exif_set[0]) + interop_is * 12 + len(exif_set[1])
|
||||
else:
|
||||
exif_bytes = b""
|
||||
exif_length = 0
|
||||
if gps_is:
|
||||
gps_set = _dict_to_bytes(gps_ifd, "GPS", zeroth_length + exif_length)
|
||||
gps_bytes = b"".join(gps_set)
|
||||
gps_length = len(gps_bytes)
|
||||
else:
|
||||
gps_bytes = b""
|
||||
gps_length = 0
|
||||
if interop_is:
|
||||
offset = zeroth_length + exif_length + gps_length
|
||||
interop_set = _dict_to_bytes(interop_ifd, "Interop", offset)
|
||||
interop_bytes = b"".join(interop_set)
|
||||
interop_length = len(interop_bytes)
|
||||
else:
|
||||
interop_bytes = b""
|
||||
interop_length = 0
|
||||
if first_is:
|
||||
offset = zeroth_length + exif_length + gps_length + interop_length
|
||||
first_set = _dict_to_bytes(first_ifd, "1st", offset)
|
||||
thumbnail = _get_thumbnail(exif_dict["thumbnail"])
|
||||
thumbnail_max_size = 64000
|
||||
if len(thumbnail) > thumbnail_max_size:
|
||||
raise ValueError("Given thumbnail is too large. max 64kB")
|
||||
else:
|
||||
first_bytes = b""
|
||||
if exif_is:
|
||||
pointer_value = TIFF_HEADER_LENGTH + zeroth_length
|
||||
pointer_str = struct.pack(">I", pointer_value)
|
||||
key = ImageIFD.ExifTag
|
||||
key_str = struct.pack(">H", key)
|
||||
type_str = struct.pack(">H", TYPES.Long)
|
||||
length_str = struct.pack(">I", 1)
|
||||
exif_pointer = key_str + type_str + length_str + pointer_str
|
||||
else:
|
||||
exif_pointer = b""
|
||||
if gps_is:
|
||||
pointer_value = TIFF_HEADER_LENGTH + zeroth_length + exif_length
|
||||
pointer_str = struct.pack(">I", pointer_value)
|
||||
key = ImageIFD.GPSTag
|
||||
key_str = struct.pack(">H", key)
|
||||
type_str = struct.pack(">H", TYPES.Long)
|
||||
length_str = struct.pack(">I", 1)
|
||||
gps_pointer = key_str + type_str + length_str + pointer_str
|
||||
else:
|
||||
gps_pointer = b""
|
||||
if interop_is:
|
||||
pointer_value = TIFF_HEADER_LENGTH + zeroth_length + exif_length + gps_length
|
||||
pointer_str = struct.pack(">I", pointer_value)
|
||||
key = ExifIFD.InteroperabilityTag
|
||||
key_str = struct.pack(">H", key)
|
||||
type_str = struct.pack(">H", TYPES.Long)
|
||||
length_str = struct.pack(">I", 1)
|
||||
interop_pointer = key_str + type_str + length_str + pointer_str
|
||||
else:
|
||||
interop_pointer = b""
|
||||
if first_is:
|
||||
pointer_value = (
|
||||
TIFF_HEADER_LENGTH
|
||||
+ zeroth_length
|
||||
+ exif_length
|
||||
+ gps_length
|
||||
+ interop_length
|
||||
)
|
||||
first_ifd_pointer = struct.pack(">L", pointer_value)
|
||||
thumbnail_pointer = (
|
||||
pointer_value + len(first_set[0]) + 24 + 4 + len(first_set[1])
|
||||
)
|
||||
thumbnail_p_bytes = b"\x02\x01\x00\x04\x00\x00\x00\x01" + struct.pack(
|
||||
">L", thumbnail_pointer
|
||||
)
|
||||
thumbnail_length_bytes = b"\x02\x02\x00\x04\x00\x00\x00\x01" + struct.pack(
|
||||
">L", len(thumbnail)
|
||||
)
|
||||
first_bytes = (
|
||||
first_set[0]
|
||||
+ thumbnail_p_bytes
|
||||
+ thumbnail_length_bytes
|
||||
+ b"\x00\x00\x00\x00"
|
||||
+ first_set[1]
|
||||
+ thumbnail
|
||||
)
|
||||
else:
|
||||
first_ifd_pointer = b"\x00\x00\x00\x00"
|
||||
|
||||
zeroth_bytes = (
|
||||
zeroth_set[0] + exif_pointer + gps_pointer + first_ifd_pointer + zeroth_set[1]
|
||||
)
|
||||
if exif_is:
|
||||
exif_bytes = exif_set[0] + interop_pointer + exif_set[1]
|
||||
|
||||
return header + zeroth_bytes + exif_bytes + gps_bytes + interop_bytes + first_bytes
|
||||
|
||||
|
||||
def _get_thumbnail(jpeg):
|
||||
segments = split_into_segments(jpeg)
|
||||
while b"\xff\xe0" <= segments[1][0:2] <= b"\xff\xef":
|
||||
segments.pop(1)
|
||||
thumbnail = b"".join(segments)
|
||||
return thumbnail
|
||||
|
||||
|
||||
def _pack_byte(*args):
|
||||
return struct.pack("B" * len(args), *args)
|
||||
|
||||
|
||||
def _pack_signed_byte(*args):
|
||||
return struct.pack("b" * len(args), *args)
|
||||
|
||||
|
||||
def _pack_short(*args):
|
||||
return struct.pack(">" + "H" * len(args), *args)
|
||||
|
||||
|
||||
def _pack_signed_short(*args):
|
||||
return struct.pack(">" + "h" * len(args), *args)
|
||||
|
||||
|
||||
def _pack_long(*args):
|
||||
return struct.pack(">" + "L" * len(args), *args)
|
||||
|
||||
|
||||
def _pack_slong(*args):
|
||||
return struct.pack(">" + "l" * len(args), *args)
|
||||
|
||||
|
||||
def _pack_float(*args):
|
||||
return struct.pack(">" + "f" * len(args), *args)
|
||||
|
||||
|
||||
def _pack_double(*args):
|
||||
return struct.pack(">" + "d" * len(args), *args)
|
||||
|
||||
|
||||
def _value_to_bytes(raw_value, value_type, offset):
|
||||
four_bytes_over = b""
|
||||
value_str = b""
|
||||
|
||||
if value_type == TYPES.Byte:
|
||||
length = len(raw_value)
|
||||
if length <= 4:
|
||||
value_str = _pack_byte(*raw_value) + b"\x00" * (4 - length)
|
||||
else:
|
||||
value_str = struct.pack(">I", offset)
|
||||
four_bytes_over = _pack_byte(*raw_value)
|
||||
elif value_type == TYPES.Short:
|
||||
length = len(raw_value)
|
||||
if length <= 2:
|
||||
value_str = _pack_short(*raw_value) + b"\x00\x00" * (2 - length)
|
||||
else:
|
||||
value_str = struct.pack(">I", offset)
|
||||
four_bytes_over = _pack_short(*raw_value)
|
||||
elif value_type == TYPES.Long:
|
||||
length = len(raw_value)
|
||||
if length <= 1:
|
||||
value_str = _pack_long(*raw_value)
|
||||
else:
|
||||
value_str = struct.pack(">I", offset)
|
||||
four_bytes_over = _pack_long(*raw_value)
|
||||
elif value_type == TYPES.SLong:
|
||||
length = len(raw_value)
|
||||
if length <= 1:
|
||||
value_str = _pack_slong(*raw_value)
|
||||
else:
|
||||
value_str = struct.pack(">I", offset)
|
||||
four_bytes_over = _pack_slong(*raw_value)
|
||||
elif value_type == TYPES.Ascii:
|
||||
try:
|
||||
new_value = raw_value.encode("latin1") + b"\x00"
|
||||
except: # pylint: disable=W0702
|
||||
try:
|
||||
new_value = raw_value + b"\x00"
|
||||
except TypeError as e:
|
||||
raise ValueError("Got invalid type to convert.") from e
|
||||
length = len(new_value)
|
||||
if length > 4:
|
||||
value_str = struct.pack(">I", offset)
|
||||
four_bytes_over = new_value
|
||||
else:
|
||||
value_str = new_value + b"\x00" * (4 - length)
|
||||
elif value_type == TYPES.Rational:
|
||||
if isinstance(raw_value[0], numbers.Integral):
|
||||
length = 1
|
||||
num, den = raw_value
|
||||
new_value = struct.pack(">L", num) + struct.pack(">L", den)
|
||||
elif isinstance(raw_value[0], tuple):
|
||||
length = len(raw_value)
|
||||
new_value = b""
|
||||
for _, val in enumerate(raw_value):
|
||||
num, den = val
|
||||
new_value += struct.pack(">L", num) + struct.pack(">L", den)
|
||||
value_str = struct.pack(">I", offset)
|
||||
four_bytes_over = new_value
|
||||
elif value_type == TYPES.SRational:
|
||||
if isinstance(raw_value[0], numbers.Integral):
|
||||
length = 1
|
||||
num, den = raw_value
|
||||
new_value = struct.pack(">l", num) + struct.pack(">l", den)
|
||||
elif isinstance(raw_value[0], tuple):
|
||||
length = len(raw_value)
|
||||
new_value = b""
|
||||
for _, val in enumerate(raw_value):
|
||||
num, den = val
|
||||
new_value += struct.pack(">l", num) + struct.pack(">l", den)
|
||||
value_str = struct.pack(">I", offset)
|
||||
four_bytes_over = new_value
|
||||
elif value_type == TYPES.Undefined:
|
||||
length = len(raw_value)
|
||||
if length > 4:
|
||||
value_str = struct.pack(">I", offset)
|
||||
try:
|
||||
four_bytes_over = b"" + raw_value
|
||||
except TypeError as e:
|
||||
raise ValueError("Got invalid type to convert.") from e
|
||||
else:
|
||||
try:
|
||||
value_str = raw_value + b"\x00" * (4 - length)
|
||||
except TypeError as e:
|
||||
raise ValueError("Got invalid type to convert.") from e
|
||||
elif value_type == TYPES.SByte: # Signed Byte
|
||||
length = len(raw_value)
|
||||
if length <= 4:
|
||||
value_str = _pack_signed_byte(*raw_value) + b"\x00" * (4 - length)
|
||||
else:
|
||||
value_str = struct.pack(">I", offset)
|
||||
four_bytes_over = _pack_signed_byte(*raw_value)
|
||||
elif value_type == TYPES.SShort: # Signed Short
|
||||
length = len(raw_value)
|
||||
if length <= 2:
|
||||
value_str = _pack_signed_short(*raw_value) + b"\x00\x00" * (2 - length)
|
||||
else:
|
||||
value_str = struct.pack(">I", offset)
|
||||
four_bytes_over = _pack_signed_short(*raw_value)
|
||||
elif value_type == TYPES.Float:
|
||||
length = len(raw_value)
|
||||
if length <= 1:
|
||||
value_str = _pack_float(*raw_value)
|
||||
else:
|
||||
value_str = struct.pack(">I", offset)
|
||||
four_bytes_over = _pack_float(*raw_value)
|
||||
elif value_type == TYPES.DFloat: # Double
|
||||
length = len(raw_value)
|
||||
value_str = struct.pack(">I", offset)
|
||||
four_bytes_over = _pack_double(*raw_value)
|
||||
|
||||
length_str = struct.pack(">I", length)
|
||||
return length_str, value_str, four_bytes_over
|
||||
|
||||
|
||||
def _dict_to_bytes(ifd_dict, ifd, ifd_offset):
|
||||
tag_count = len(ifd_dict)
|
||||
entry_header = struct.pack(">H", tag_count)
|
||||
if ifd in ("0th", "1st"):
|
||||
entries_length = 2 + tag_count * 12 + 4
|
||||
else:
|
||||
entries_length = 2 + tag_count * 12
|
||||
entries = b""
|
||||
values = b""
|
||||
|
||||
for _, key in enumerate(sorted(ifd_dict)):
|
||||
if (ifd == "0th") and (key in (ImageIFD.ExifTag, ImageIFD.GPSTag)):
|
||||
continue
|
||||
elif (ifd == "Exif") and (key == ExifIFD.InteroperabilityTag):
|
||||
continue
|
||||
elif (ifd == "1st") and (
|
||||
key
|
||||
in (ImageIFD.JPEGInterchangeFormat, ImageIFD.JPEGInterchangeFormatLength)
|
||||
):
|
||||
continue
|
||||
|
||||
raw_value = ifd_dict[key]
|
||||
key_str = struct.pack(">H", key)
|
||||
value_type = TAGS[ifd][key]["type"]
|
||||
type_str = struct.pack(">H", value_type)
|
||||
four_bytes_over = b""
|
||||
|
||||
if isinstance(raw_value, numbers.Integral) or isinstance(raw_value, float):
|
||||
raw_value = (raw_value,)
|
||||
offset = TIFF_HEADER_LENGTH + entries_length + ifd_offset + len(values)
|
||||
|
||||
try:
|
||||
length_str, value_str, four_bytes_over = _value_to_bytes(
|
||||
raw_value, value_type, offset
|
||||
)
|
||||
except ValueError as e:
|
||||
raise ValueError(
|
||||
'"dump" got wrong type of exif value.\n'
|
||||
+ "{0} in {1} IFD. Got as {2}.".format(key, ifd, type(ifd_dict[key]))
|
||||
) from e
|
||||
|
||||
entries += key_str + type_str + length_str + value_str
|
||||
values += four_bytes_over
|
||||
return (entry_header + entries, values)
|
||||
|
|
@ -1,2 +0,0 @@
|
|||
class InvalidImageDataError(ValueError):
|
||||
pass
|
||||
|
|
@ -1,649 +0,0 @@
|
|||
class TYPES:
|
||||
Byte = 1
|
||||
Ascii = 2
|
||||
Short = 3
|
||||
Long = 4
|
||||
Rational = 5
|
||||
SByte = 6
|
||||
Undefined = 7
|
||||
SShort = 8
|
||||
SLong = 9
|
||||
SRational = 10
|
||||
Float = 11
|
||||
DFloat = 12
|
||||
|
||||
|
||||
TAGS = {
|
||||
"Image": {
|
||||
11: {"name": "ProcessingSoftware", "type": TYPES.Ascii},
|
||||
254: {"name": "NewSubfileType", "type": TYPES.Long},
|
||||
255: {"name": "SubfileType", "type": TYPES.Short},
|
||||
256: {"name": "ImageWidth", "type": TYPES.Long},
|
||||
257: {"name": "ImageLength", "type": TYPES.Long},
|
||||
258: {"name": "BitsPerSample", "type": TYPES.Short},
|
||||
259: {"name": "Compression", "type": TYPES.Short},
|
||||
262: {"name": "PhotometricInterpretation", "type": TYPES.Short},
|
||||
263: {"name": "Threshholding", "type": TYPES.Short},
|
||||
264: {"name": "CellWidth", "type": TYPES.Short},
|
||||
265: {"name": "CellLength", "type": TYPES.Short},
|
||||
266: {"name": "FillOrder", "type": TYPES.Short},
|
||||
269: {"name": "DocumentName", "type": TYPES.Ascii},
|
||||
270: {"name": "ImageDescription", "type": TYPES.Ascii},
|
||||
271: {"name": "Make", "type": TYPES.Ascii},
|
||||
272: {"name": "Model", "type": TYPES.Ascii},
|
||||
273: {"name": "StripOffsets", "type": TYPES.Long},
|
||||
274: {"name": "Orientation", "type": TYPES.Short},
|
||||
277: {"name": "SamplesPerPixel", "type": TYPES.Short},
|
||||
278: {"name": "RowsPerStrip", "type": TYPES.Long},
|
||||
279: {"name": "StripByteCounts", "type": TYPES.Long},
|
||||
282: {"name": "XResolution", "type": TYPES.Rational},
|
||||
283: {"name": "YResolution", "type": TYPES.Rational},
|
||||
284: {"name": "PlanarConfiguration", "type": TYPES.Short},
|
||||
290: {"name": "GrayResponseUnit", "type": TYPES.Short},
|
||||
291: {"name": "GrayResponseCurve", "type": TYPES.Short},
|
||||
292: {"name": "T4Options", "type": TYPES.Long},
|
||||
293: {"name": "T6Options", "type": TYPES.Long},
|
||||
296: {"name": "ResolutionUnit", "type": TYPES.Short},
|
||||
301: {"name": "TransferFunction", "type": TYPES.Short},
|
||||
305: {"name": "Software", "type": TYPES.Ascii},
|
||||
306: {"name": "DateTime", "type": TYPES.Ascii},
|
||||
315: {"name": "Artist", "type": TYPES.Ascii},
|
||||
316: {"name": "HostComputer", "type": TYPES.Ascii},
|
||||
317: {"name": "Predictor", "type": TYPES.Short},
|
||||
318: {"name": "WhitePoint", "type": TYPES.Rational},
|
||||
319: {"name": "PrimaryChromaticities", "type": TYPES.Rational},
|
||||
320: {"name": "ColorMap", "type": TYPES.Short},
|
||||
321: {"name": "HalftoneHints", "type": TYPES.Short},
|
||||
322: {"name": "TileWidth", "type": TYPES.Short},
|
||||
323: {"name": "TileLength", "type": TYPES.Short},
|
||||
324: {"name": "TileOffsets", "type": TYPES.Short},
|
||||
325: {"name": "TileByteCounts", "type": TYPES.Short},
|
||||
330: {"name": "SubIFDs", "type": TYPES.Long},
|
||||
332: {"name": "InkSet", "type": TYPES.Short},
|
||||
333: {"name": "InkNames", "type": TYPES.Ascii},
|
||||
334: {"name": "NumberOfInks", "type": TYPES.Short},
|
||||
336: {"name": "DotRange", "type": TYPES.Byte},
|
||||
337: {"name": "TargetPrinter", "type": TYPES.Ascii},
|
||||
338: {"name": "ExtraSamples", "type": TYPES.Short},
|
||||
339: {"name": "SampleFormat", "type": TYPES.Short},
|
||||
340: {"name": "SMinSampleValue", "type": TYPES.Short},
|
||||
341: {"name": "SMaxSampleValue", "type": TYPES.Short},
|
||||
342: {"name": "TransferRange", "type": TYPES.Short},
|
||||
343: {"name": "ClipPath", "type": TYPES.Byte},
|
||||
344: {"name": "XClipPathUnits", "type": TYPES.Long},
|
||||
345: {"name": "YClipPathUnits", "type": TYPES.Long},
|
||||
346: {"name": "Indexed", "type": TYPES.Short},
|
||||
347: {"name": "JPEGTables", "type": TYPES.Undefined},
|
||||
351: {"name": "OPIProxy", "type": TYPES.Short},
|
||||
512: {"name": "JPEGProc", "type": TYPES.Long},
|
||||
513: {"name": "JPEGInterchangeFormat", "type": TYPES.Long},
|
||||
514: {"name": "JPEGInterchangeFormatLength", "type": TYPES.Long},
|
||||
515: {"name": "JPEGRestartInterval", "type": TYPES.Short},
|
||||
517: {"name": "JPEGLosslessPredictors", "type": TYPES.Short},
|
||||
518: {"name": "JPEGPointTransforms", "type": TYPES.Short},
|
||||
519: {"name": "JPEGQTables", "type": TYPES.Long},
|
||||
520: {"name": "JPEGDCTables", "type": TYPES.Long},
|
||||
521: {"name": "JPEGACTables", "type": TYPES.Long},
|
||||
529: {"name": "YCbCrCoefficients", "type": TYPES.Rational},
|
||||
530: {"name": "YCbCrSubSampling", "type": TYPES.Short},
|
||||
531: {"name": "YCbCrPositioning", "type": TYPES.Short},
|
||||
532: {"name": "ReferenceBlackWhite", "type": TYPES.Rational},
|
||||
700: {"name": "XMLPacket", "type": TYPES.Byte},
|
||||
18246: {"name": "Rating", "type": TYPES.Short},
|
||||
18249: {"name": "RatingPercent", "type": TYPES.Short},
|
||||
32781: {"name": "ImageID", "type": TYPES.Ascii},
|
||||
33421: {"name": "CFARepeatPatternDim", "type": TYPES.Short},
|
||||
33422: {"name": "CFAPattern", "type": TYPES.Byte},
|
||||
33423: {"name": "BatteryLevel", "type": TYPES.Rational},
|
||||
33432: {"name": "Copyright", "type": TYPES.Ascii},
|
||||
33434: {"name": "ExposureTime", "type": TYPES.Rational},
|
||||
34377: {"name": "ImageResources", "type": TYPES.Byte},
|
||||
34665: {"name": "ExifTag", "type": TYPES.Long},
|
||||
34675: {"name": "InterColorProfile", "type": TYPES.Undefined},
|
||||
34853: {"name": "GPSTag", "type": TYPES.Long},
|
||||
34857: {"name": "Interlace", "type": TYPES.Short},
|
||||
34858: {"name": "TimeZoneOffset", "type": TYPES.Long},
|
||||
34859: {"name": "SelfTimerMode", "type": TYPES.Short},
|
||||
37387: {"name": "FlashEnergy", "type": TYPES.Rational},
|
||||
37388: {"name": "SpatialFrequencyResponse", "type": TYPES.Undefined},
|
||||
37389: {"name": "Noise", "type": TYPES.Undefined},
|
||||
37390: {"name": "FocalPlaneXResolution", "type": TYPES.Rational},
|
||||
37391: {"name": "FocalPlaneYResolution", "type": TYPES.Rational},
|
||||
37392: {"name": "FocalPlaneResolutionUnit", "type": TYPES.Short},
|
||||
37393: {"name": "ImageNumber", "type": TYPES.Long},
|
||||
37394: {"name": "SecurityClassification", "type": TYPES.Ascii},
|
||||
37395: {"name": "ImageHistory", "type": TYPES.Ascii},
|
||||
37397: {"name": "ExposureIndex", "type": TYPES.Rational},
|
||||
37398: {"name": "TIFFEPStandardID", "type": TYPES.Byte},
|
||||
37399: {"name": "SensingMethod", "type": TYPES.Short},
|
||||
40091: {"name": "XPTitle", "type": TYPES.Byte},
|
||||
40092: {"name": "XPComment", "type": TYPES.Byte},
|
||||
40093: {"name": "XPAuthor", "type": TYPES.Byte},
|
||||
40094: {"name": "XPKeywords", "type": TYPES.Byte},
|
||||
40095: {"name": "XPSubject", "type": TYPES.Byte},
|
||||
50341: {"name": "PrintImageMatching", "type": TYPES.Undefined},
|
||||
50706: {"name": "DNGVersion", "type": TYPES.Byte},
|
||||
50707: {"name": "DNGBackwardVersion", "type": TYPES.Byte},
|
||||
50708: {"name": "UniqueCameraModel", "type": TYPES.Ascii},
|
||||
50709: {"name": "LocalizedCameraModel", "type": TYPES.Byte},
|
||||
50710: {"name": "CFAPlaneColor", "type": TYPES.Byte},
|
||||
50711: {"name": "CFALayout", "type": TYPES.Short},
|
||||
50712: {"name": "LinearizationTable", "type": TYPES.Short},
|
||||
50713: {"name": "BlackLevelRepeatDim", "type": TYPES.Short},
|
||||
50714: {"name": "BlackLevel", "type": TYPES.Rational},
|
||||
50715: {"name": "BlackLevelDeltaH", "type": TYPES.SRational},
|
||||
50716: {"name": "BlackLevelDeltaV", "type": TYPES.SRational},
|
||||
50717: {"name": "WhiteLevel", "type": TYPES.Short},
|
||||
50718: {"name": "DefaultScale", "type": TYPES.Rational},
|
||||
50719: {"name": "DefaultCropOrigin", "type": TYPES.Short},
|
||||
50720: {"name": "DefaultCropSize", "type": TYPES.Short},
|
||||
50721: {"name": "ColorMatrix1", "type": TYPES.SRational},
|
||||
50722: {"name": "ColorMatrix2", "type": TYPES.SRational},
|
||||
50723: {"name": "CameraCalibration1", "type": TYPES.SRational},
|
||||
50724: {"name": "CameraCalibration2", "type": TYPES.SRational},
|
||||
50725: {"name": "ReductionMatrix1", "type": TYPES.SRational},
|
||||
50726: {"name": "ReductionMatrix2", "type": TYPES.SRational},
|
||||
50727: {"name": "AnalogBalance", "type": TYPES.Rational},
|
||||
50728: {"name": "AsShotNeutral", "type": TYPES.Short},
|
||||
50729: {"name": "AsShotWhiteXY", "type": TYPES.Rational},
|
||||
50730: {"name": "BaselineExposure", "type": TYPES.SRational},
|
||||
50731: {"name": "BaselineNoise", "type": TYPES.Rational},
|
||||
50732: {"name": "BaselineSharpness", "type": TYPES.Rational},
|
||||
50733: {"name": "BayerGreenSplit", "type": TYPES.Long},
|
||||
50734: {"name": "LinearResponseLimit", "type": TYPES.Rational},
|
||||
50735: {"name": "CameraSerialNumber", "type": TYPES.Ascii},
|
||||
50736: {"name": "LensInfo", "type": TYPES.Rational},
|
||||
50737: {"name": "ChromaBlurRadius", "type": TYPES.Rational},
|
||||
50738: {"name": "AntiAliasStrength", "type": TYPES.Rational},
|
||||
50739: {"name": "ShadowScale", "type": TYPES.SRational},
|
||||
50740: {"name": "DNGPrivateData", "type": TYPES.Byte},
|
||||
50741: {"name": "MakerNoteSafety", "type": TYPES.Short},
|
||||
50778: {"name": "CalibrationIlluminant1", "type": TYPES.Short},
|
||||
50779: {"name": "CalibrationIlluminant2", "type": TYPES.Short},
|
||||
50780: {"name": "BestQualityScale", "type": TYPES.Rational},
|
||||
50781: {"name": "RawDataUniqueID", "type": TYPES.Byte},
|
||||
50827: {"name": "OriginalRawFileName", "type": TYPES.Byte},
|
||||
50828: {"name": "OriginalRawFileData", "type": TYPES.Undefined},
|
||||
50829: {"name": "ActiveArea", "type": TYPES.Short},
|
||||
50830: {"name": "MaskedAreas", "type": TYPES.Short},
|
||||
50831: {"name": "AsShotICCProfile", "type": TYPES.Undefined},
|
||||
50832: {"name": "AsShotPreProfileMatrix", "type": TYPES.SRational},
|
||||
50833: {"name": "CurrentICCProfile", "type": TYPES.Undefined},
|
||||
50834: {"name": "CurrentPreProfileMatrix", "type": TYPES.SRational},
|
||||
50879: {"name": "ColorimetricReference", "type": TYPES.Short},
|
||||
50931: {"name": "CameraCalibrationSignature", "type": TYPES.Byte},
|
||||
50932: {"name": "ProfileCalibrationSignature", "type": TYPES.Byte},
|
||||
50934: {"name": "AsShotProfileName", "type": TYPES.Byte},
|
||||
50935: {"name": "NoiseReductionApplied", "type": TYPES.Rational},
|
||||
50936: {"name": "ProfileName", "type": TYPES.Byte},
|
||||
50937: {"name": "ProfileHueSatMapDims", "type": TYPES.Long},
|
||||
50938: {"name": "ProfileHueSatMapData1", "type": TYPES.Float},
|
||||
50939: {"name": "ProfileHueSatMapData2", "type": TYPES.Float},
|
||||
50940: {"name": "ProfileToneCurve", "type": TYPES.Float},
|
||||
50941: {"name": "ProfileEmbedPolicy", "type": TYPES.Long},
|
||||
50942: {"name": "ProfileCopyright", "type": TYPES.Byte},
|
||||
50964: {"name": "ForwardMatrix1", "type": TYPES.SRational},
|
||||
50965: {"name": "ForwardMatrix2", "type": TYPES.SRational},
|
||||
50966: {"name": "PreviewApplicationName", "type": TYPES.Byte},
|
||||
50967: {"name": "PreviewApplicationVersion", "type": TYPES.Byte},
|
||||
50968: {"name": "PreviewSettingsName", "type": TYPES.Byte},
|
||||
50969: {"name": "PreviewSettingsDigest", "type": TYPES.Byte},
|
||||
50970: {"name": "PreviewColorSpace", "type": TYPES.Long},
|
||||
50971: {"name": "PreviewDateTime", "type": TYPES.Ascii},
|
||||
50972: {"name": "RawImageDigest", "type": TYPES.Undefined},
|
||||
50973: {"name": "OriginalRawFileDigest", "type": TYPES.Undefined},
|
||||
50974: {"name": "SubTileBlockSize", "type": TYPES.Long},
|
||||
50975: {"name": "RowInterleaveFactor", "type": TYPES.Long},
|
||||
50981: {"name": "ProfileLookTableDims", "type": TYPES.Long},
|
||||
50982: {"name": "ProfileLookTableData", "type": TYPES.Float},
|
||||
51008: {"name": "OpcodeList1", "type": TYPES.Undefined},
|
||||
51009: {"name": "OpcodeList2", "type": TYPES.Undefined},
|
||||
51022: {"name": "OpcodeList3", "type": TYPES.Undefined},
|
||||
60606: {"name": "ZZZTestSlong1", "type": TYPES.SLong},
|
||||
60607: {"name": "ZZZTestSlong2", "type": TYPES.SLong},
|
||||
60608: {"name": "ZZZTestSByte", "type": TYPES.SByte},
|
||||
60609: {"name": "ZZZTestSShort", "type": TYPES.SShort},
|
||||
60610: {"name": "ZZZTestDFloat", "type": TYPES.DFloat},
|
||||
},
|
||||
"Exif": {
|
||||
33434: {"name": "ExposureTime", "type": TYPES.Rational},
|
||||
33437: {"name": "FNumber", "type": TYPES.Rational},
|
||||
34850: {"name": "ExposureProgram", "type": TYPES.Short},
|
||||
34852: {"name": "SpectralSensitivity", "type": TYPES.Ascii},
|
||||
34855: {"name": "ISOSpeedRatings", "type": TYPES.Short},
|
||||
34856: {"name": "OECF", "type": TYPES.Undefined},
|
||||
34864: {"name": "SensitivityType", "type": TYPES.Short},
|
||||
34865: {"name": "StandardOutputSensitivity", "type": TYPES.Long},
|
||||
34866: {"name": "RecommendedExposureIndex", "type": TYPES.Long},
|
||||
34867: {"name": "ISOSpeed", "type": TYPES.Long},
|
||||
34868: {"name": "ISOSpeedLatitudeyyy", "type": TYPES.Long},
|
||||
34869: {"name": "ISOSpeedLatitudezzz", "type": TYPES.Long},
|
||||
36864: {"name": "ExifVersion", "type": TYPES.Undefined},
|
||||
36867: {"name": "DateTimeOriginal", "type": TYPES.Ascii},
|
||||
36868: {"name": "DateTimeDigitized", "type": TYPES.Ascii},
|
||||
36880: {"name": "OffsetTime", "type": TYPES.Ascii},
|
||||
36881: {"name": "OffsetTimeOriginal", "type": TYPES.Ascii},
|
||||
36882: {"name": "OffsetTimeDigitized", "type": TYPES.Ascii},
|
||||
37121: {"name": "ComponentsConfiguration", "type": TYPES.Undefined},
|
||||
37122: {"name": "CompressedBitsPerPixel", "type": TYPES.Rational},
|
||||
37377: {"name": "ShutterSpeedValue", "type": TYPES.SRational},
|
||||
37378: {"name": "ApertureValue", "type": TYPES.Rational},
|
||||
37379: {"name": "BrightnessValue", "type": TYPES.SRational},
|
||||
37380: {"name": "ExposureBiasValue", "type": TYPES.SRational},
|
||||
37381: {"name": "MaxApertureValue", "type": TYPES.Rational},
|
||||
37382: {"name": "SubjectDistance", "type": TYPES.Rational},
|
||||
37383: {"name": "MeteringMode", "type": TYPES.Short},
|
||||
37384: {"name": "LightSource", "type": TYPES.Short},
|
||||
37385: {"name": "Flash", "type": TYPES.Short},
|
||||
37386: {"name": "FocalLength", "type": TYPES.Rational},
|
||||
37396: {"name": "SubjectArea", "type": TYPES.Short},
|
||||
37500: {"name": "MakerNote", "type": TYPES.Undefined},
|
||||
37510: {"name": "UserComment", "type": TYPES.Undefined},
|
||||
37520: {"name": "SubSecTime", "type": TYPES.Ascii},
|
||||
37521: {"name": "SubSecTimeOriginal", "type": TYPES.Ascii},
|
||||
37522: {"name": "SubSecTimeDigitized", "type": TYPES.Ascii},
|
||||
37888: {"name": "Temperature", "type": TYPES.SRational},
|
||||
37889: {"name": "Humidity", "type": TYPES.Rational},
|
||||
37890: {"name": "Pressure", "type": TYPES.Rational},
|
||||
37891: {"name": "WaterDepth", "type": TYPES.SRational},
|
||||
37892: {"name": "Acceleration", "type": TYPES.Rational},
|
||||
37893: {"name": "CameraElevationAngle", "type": TYPES.SRational},
|
||||
40960: {"name": "FlashpixVersion", "type": TYPES.Undefined},
|
||||
40961: {"name": "ColorSpace", "type": TYPES.Short},
|
||||
40962: {"name": "PixelXDimension", "type": TYPES.Long},
|
||||
40963: {"name": "PixelYDimension", "type": TYPES.Long},
|
||||
40964: {"name": "RelatedSoundFile", "type": TYPES.Ascii},
|
||||
40965: {"name": "InteroperabilityTag", "type": TYPES.Long},
|
||||
41483: {"name": "FlashEnergy", "type": TYPES.Rational},
|
||||
41484: {"name": "SpatialFrequencyResponse", "type": TYPES.Undefined},
|
||||
41486: {"name": "FocalPlaneXResolution", "type": TYPES.Rational},
|
||||
41487: {"name": "FocalPlaneYResolution", "type": TYPES.Rational},
|
||||
41488: {"name": "FocalPlaneResolutionUnit", "type": TYPES.Short},
|
||||
41492: {"name": "SubjectLocation", "type": TYPES.Short},
|
||||
41493: {"name": "ExposureIndex", "type": TYPES.Rational},
|
||||
41495: {"name": "SensingMethod", "type": TYPES.Short},
|
||||
41728: {"name": "FileSource", "type": TYPES.Undefined},
|
||||
41729: {"name": "SceneType", "type": TYPES.Undefined},
|
||||
41730: {"name": "CFAPattern", "type": TYPES.Undefined},
|
||||
41985: {"name": "CustomRendered", "type": TYPES.Short},
|
||||
41986: {"name": "ExposureMode", "type": TYPES.Short},
|
||||
41987: {"name": "WhiteBalance", "type": TYPES.Short},
|
||||
41988: {"name": "DigitalZoomRatio", "type": TYPES.Rational},
|
||||
41989: {"name": "FocalLengthIn35mmFilm", "type": TYPES.Short},
|
||||
41990: {"name": "SceneCaptureType", "type": TYPES.Short},
|
||||
41991: {"name": "GainControl", "type": TYPES.Short},
|
||||
41992: {"name": "Contrast", "type": TYPES.Short},
|
||||
41993: {"name": "Saturation", "type": TYPES.Short},
|
||||
41994: {"name": "Sharpness", "type": TYPES.Short},
|
||||
41995: {"name": "DeviceSettingDescription", "type": TYPES.Undefined},
|
||||
41996: {"name": "SubjectDistanceRange", "type": TYPES.Short},
|
||||
42016: {"name": "ImageUniqueID", "type": TYPES.Ascii},
|
||||
42032: {"name": "CameraOwnerName", "type": TYPES.Ascii},
|
||||
42033: {"name": "BodySerialNumber", "type": TYPES.Ascii},
|
||||
42034: {"name": "LensSpecification", "type": TYPES.Rational},
|
||||
42035: {"name": "LensMake", "type": TYPES.Ascii},
|
||||
42036: {"name": "LensModel", "type": TYPES.Ascii},
|
||||
42037: {"name": "LensSerialNumber", "type": TYPES.Ascii},
|
||||
42240: {"name": "Gamma", "type": TYPES.Rational},
|
||||
},
|
||||
"GPS": {
|
||||
0: {"name": "GPSVersionID", "type": TYPES.Byte},
|
||||
1: {"name": "GPSLatitudeRef", "type": TYPES.Ascii},
|
||||
2: {"name": "GPSLatitude", "type": TYPES.Rational},
|
||||
3: {"name": "GPSLongitudeRef", "type": TYPES.Ascii},
|
||||
4: {"name": "GPSLongitude", "type": TYPES.Rational},
|
||||
5: {"name": "GPSAltitudeRef", "type": TYPES.Byte},
|
||||
6: {"name": "GPSAltitude", "type": TYPES.Rational},
|
||||
7: {"name": "GPSTimeStamp", "type": TYPES.Rational},
|
||||
8: {"name": "GPSSatellites", "type": TYPES.Ascii},
|
||||
9: {"name": "GPSStatus", "type": TYPES.Ascii},
|
||||
10: {"name": "GPSMeasureMode", "type": TYPES.Ascii},
|
||||
11: {"name": "GPSDOP", "type": TYPES.Rational},
|
||||
12: {"name": "GPSSpeedRef", "type": TYPES.Ascii},
|
||||
13: {"name": "GPSSpeed", "type": TYPES.Rational},
|
||||
14: {"name": "GPSTrackRef", "type": TYPES.Ascii},
|
||||
15: {"name": "GPSTrack", "type": TYPES.Rational},
|
||||
16: {"name": "GPSImgDirectionRef", "type": TYPES.Ascii},
|
||||
17: {"name": "GPSImgDirection", "type": TYPES.Rational},
|
||||
18: {"name": "GPSMapDatum", "type": TYPES.Ascii},
|
||||
19: {"name": "GPSDestLatitudeRef", "type": TYPES.Ascii},
|
||||
20: {"name": "GPSDestLatitude", "type": TYPES.Rational},
|
||||
21: {"name": "GPSDestLongitudeRef", "type": TYPES.Ascii},
|
||||
22: {"name": "GPSDestLongitude", "type": TYPES.Rational},
|
||||
23: {"name": "GPSDestBearingRef", "type": TYPES.Ascii},
|
||||
24: {"name": "GPSDestBearing", "type": TYPES.Rational},
|
||||
25: {"name": "GPSDestDistanceRef", "type": TYPES.Ascii},
|
||||
26: {"name": "GPSDestDistance", "type": TYPES.Rational},
|
||||
27: {"name": "GPSProcessingMethod", "type": TYPES.Undefined},
|
||||
28: {"name": "GPSAreaInformation", "type": TYPES.Undefined},
|
||||
29: {"name": "GPSDateStamp", "type": TYPES.Ascii},
|
||||
30: {"name": "GPSDifferential", "type": TYPES.Short},
|
||||
31: {"name": "GPSHPositioningError", "type": TYPES.Rational},
|
||||
},
|
||||
"Interop": {1: {"name": "InteroperabilityIndex", "type": TYPES.Ascii}},
|
||||
}
|
||||
|
||||
TAGS["0th"] = TAGS["Image"]
|
||||
TAGS["1st"] = TAGS["Image"]
|
||||
|
||||
|
||||
class ImageIFD:
|
||||
"""Exif tag number reference - 0th IFD"""
|
||||
|
||||
ProcessingSoftware = 11
|
||||
NewSubfileType = 254
|
||||
SubfileType = 255
|
||||
ImageWidth = 256
|
||||
ImageLength = 257
|
||||
BitsPerSample = 258
|
||||
Compression = 259
|
||||
PhotometricInterpretation = 262
|
||||
Threshholding = 263
|
||||
CellWidth = 264
|
||||
CellLength = 265
|
||||
FillOrder = 266
|
||||
DocumentName = 269
|
||||
ImageDescription = 270
|
||||
Make = 271
|
||||
Model = 272
|
||||
StripOffsets = 273
|
||||
Orientation = 274
|
||||
SamplesPerPixel = 277
|
||||
RowsPerStrip = 278
|
||||
StripByteCounts = 279
|
||||
XResolution = 282
|
||||
YResolution = 283
|
||||
PlanarConfiguration = 284
|
||||
GrayResponseUnit = 290
|
||||
GrayResponseCurve = 291
|
||||
T4Options = 292
|
||||
T6Options = 293
|
||||
ResolutionUnit = 296
|
||||
TransferFunction = 301
|
||||
Software = 305
|
||||
DateTime = 306
|
||||
Artist = 315
|
||||
HostComputer = 316
|
||||
Predictor = 317
|
||||
WhitePoint = 318
|
||||
PrimaryChromaticities = 319
|
||||
ColorMap = 320
|
||||
HalftoneHints = 321
|
||||
TileWidth = 322
|
||||
TileLength = 323
|
||||
TileOffsets = 324
|
||||
TileByteCounts = 325
|
||||
SubIFDs = 330
|
||||
InkSet = 332
|
||||
InkNames = 333
|
||||
NumberOfInks = 334
|
||||
DotRange = 336
|
||||
TargetPrinter = 337
|
||||
ExtraSamples = 338
|
||||
SampleFormat = 339
|
||||
SMinSampleValue = 340
|
||||
SMaxSampleValue = 341
|
||||
TransferRange = 342
|
||||
ClipPath = 343
|
||||
XClipPathUnits = 344
|
||||
YClipPathUnits = 345
|
||||
Indexed = 346
|
||||
JPEGTables = 347
|
||||
OPIProxy = 351
|
||||
JPEGProc = 512
|
||||
JPEGInterchangeFormat = 513
|
||||
JPEGInterchangeFormatLength = 514
|
||||
JPEGRestartInterval = 515
|
||||
JPEGLosslessPredictors = 517
|
||||
JPEGPointTransforms = 518
|
||||
JPEGQTables = 519
|
||||
JPEGDCTables = 520
|
||||
JPEGACTables = 521
|
||||
YCbCrCoefficients = 529
|
||||
YCbCrSubSampling = 530
|
||||
YCbCrPositioning = 531
|
||||
ReferenceBlackWhite = 532
|
||||
XMLPacket = 700
|
||||
Rating = 18246
|
||||
RatingPercent = 18249
|
||||
ImageID = 32781
|
||||
CFARepeatPatternDim = 33421
|
||||
CFAPattern = 33422
|
||||
BatteryLevel = 33423
|
||||
Copyright = 33432
|
||||
ExposureTime = 33434
|
||||
ImageResources = 34377
|
||||
ExifTag = 34665
|
||||
InterColorProfile = 34675
|
||||
GPSTag = 34853
|
||||
Interlace = 34857
|
||||
TimeZoneOffset = 34858
|
||||
SelfTimerMode = 34859
|
||||
FlashEnergy = 37387
|
||||
SpatialFrequencyResponse = 37388
|
||||
Noise = 37389
|
||||
FocalPlaneXResolution = 37390
|
||||
FocalPlaneYResolution = 37391
|
||||
FocalPlaneResolutionUnit = 37392
|
||||
ImageNumber = 37393
|
||||
SecurityClassification = 37394
|
||||
ImageHistory = 37395
|
||||
ExposureIndex = 37397
|
||||
TIFFEPStandardID = 37398
|
||||
SensingMethod = 37399
|
||||
XPTitle = 40091
|
||||
XPComment = 40092
|
||||
XPAuthor = 40093
|
||||
XPKeywords = 40094
|
||||
XPSubject = 40095
|
||||
PrintImageMatching = 50341
|
||||
DNGVersion = 50706
|
||||
DNGBackwardVersion = 50707
|
||||
UniqueCameraModel = 50708
|
||||
LocalizedCameraModel = 50709
|
||||
CFAPlaneColor = 50710
|
||||
CFALayout = 50711
|
||||
LinearizationTable = 50712
|
||||
BlackLevelRepeatDim = 50713
|
||||
BlackLevel = 50714
|
||||
BlackLevelDeltaH = 50715
|
||||
BlackLevelDeltaV = 50716
|
||||
WhiteLevel = 50717
|
||||
DefaultScale = 50718
|
||||
DefaultCropOrigin = 50719
|
||||
DefaultCropSize = 50720
|
||||
ColorMatrix1 = 50721
|
||||
ColorMatrix2 = 50722
|
||||
CameraCalibration1 = 50723
|
||||
CameraCalibration2 = 50724
|
||||
ReductionMatrix1 = 50725
|
||||
ReductionMatrix2 = 50726
|
||||
AnalogBalance = 50727
|
||||
AsShotNeutral = 50728
|
||||
AsShotWhiteXY = 50729
|
||||
BaselineExposure = 50730
|
||||
BaselineNoise = 50731
|
||||
BaselineSharpness = 50732
|
||||
BayerGreenSplit = 50733
|
||||
LinearResponseLimit = 50734
|
||||
CameraSerialNumber = 50735
|
||||
LensInfo = 50736
|
||||
ChromaBlurRadius = 50737
|
||||
AntiAliasStrength = 50738
|
||||
ShadowScale = 50739
|
||||
DNGPrivateData = 50740
|
||||
MakerNoteSafety = 50741
|
||||
CalibrationIlluminant1 = 50778
|
||||
CalibrationIlluminant2 = 50779
|
||||
BestQualityScale = 50780
|
||||
RawDataUniqueID = 50781
|
||||
OriginalRawFileName = 50827
|
||||
OriginalRawFileData = 50828
|
||||
ActiveArea = 50829
|
||||
MaskedAreas = 50830
|
||||
AsShotICCProfile = 50831
|
||||
AsShotPreProfileMatrix = 50832
|
||||
CurrentICCProfile = 50833
|
||||
CurrentPreProfileMatrix = 50834
|
||||
ColorimetricReference = 50879
|
||||
CameraCalibrationSignature = 50931
|
||||
ProfileCalibrationSignature = 50932
|
||||
AsShotProfileName = 50934
|
||||
NoiseReductionApplied = 50935
|
||||
ProfileName = 50936
|
||||
ProfileHueSatMapDims = 50937
|
||||
ProfileHueSatMapData1 = 50938
|
||||
ProfileHueSatMapData2 = 50939
|
||||
ProfileToneCurve = 50940
|
||||
ProfileEmbedPolicy = 50941
|
||||
ProfileCopyright = 50942
|
||||
ForwardMatrix1 = 50964
|
||||
ForwardMatrix2 = 50965
|
||||
PreviewApplicationName = 50966
|
||||
PreviewApplicationVersion = 50967
|
||||
PreviewSettingsName = 50968
|
||||
PreviewSettingsDigest = 50969
|
||||
PreviewColorSpace = 50970
|
||||
PreviewDateTime = 50971
|
||||
RawImageDigest = 50972
|
||||
OriginalRawFileDigest = 50973
|
||||
SubTileBlockSize = 50974
|
||||
RowInterleaveFactor = 50975
|
||||
ProfileLookTableDims = 50981
|
||||
ProfileLookTableData = 50982
|
||||
OpcodeList1 = 51008
|
||||
OpcodeList2 = 51009
|
||||
OpcodeList3 = 51022
|
||||
NoiseProfile = 51041
|
||||
ZZZTestSlong1 = 60606
|
||||
ZZZTestSlong2 = 60607
|
||||
ZZZTestSByte = 60608
|
||||
ZZZTestSShort = 60609
|
||||
ZZZTestDFloat = 60610
|
||||
|
||||
|
||||
class ExifIFD:
|
||||
"""Exif tag number reference - Exif IFD"""
|
||||
|
||||
ExposureTime = 33434
|
||||
FNumber = 33437
|
||||
ExposureProgram = 34850
|
||||
SpectralSensitivity = 34852
|
||||
ISOSpeedRatings = 34855
|
||||
OECF = 34856
|
||||
SensitivityType = 34864
|
||||
StandardOutputSensitivity = 34865
|
||||
RecommendedExposureIndex = 34866
|
||||
ISOSpeed = 34867
|
||||
ISOSpeedLatitudeyyy = 34868
|
||||
ISOSpeedLatitudezzz = 34869
|
||||
ExifVersion = 36864
|
||||
DateTimeOriginal = 36867
|
||||
DateTimeDigitized = 36868
|
||||
OffsetTime = 36880
|
||||
OffsetTimeOriginal = 36881
|
||||
OffsetTimeDigitized = 36882
|
||||
ComponentsConfiguration = 37121
|
||||
CompressedBitsPerPixel = 37122
|
||||
ShutterSpeedValue = 37377
|
||||
ApertureValue = 37378
|
||||
BrightnessValue = 37379
|
||||
ExposureBiasValue = 37380
|
||||
MaxApertureValue = 37381
|
||||
SubjectDistance = 37382
|
||||
MeteringMode = 37383
|
||||
LightSource = 37384
|
||||
Flash = 37385
|
||||
FocalLength = 37386
|
||||
Temperature = 37888
|
||||
Humidity = 37889
|
||||
Pressure = 37890
|
||||
WaterDepth = 37891
|
||||
Acceleration = 37892
|
||||
CameraElevationAngle = 37893
|
||||
SubjectArea = 37396
|
||||
MakerNote = 37500
|
||||
UserComment = 37510
|
||||
SubSecTime = 37520
|
||||
SubSecTimeOriginal = 37521
|
||||
SubSecTimeDigitized = 37522
|
||||
FlashpixVersion = 40960
|
||||
ColorSpace = 40961
|
||||
PixelXDimension = 40962
|
||||
PixelYDimension = 40963
|
||||
RelatedSoundFile = 40964
|
||||
InteroperabilityTag = 40965
|
||||
FlashEnergy = 41483
|
||||
SpatialFrequencyResponse = 41484
|
||||
FocalPlaneXResolution = 41486
|
||||
FocalPlaneYResolution = 41487
|
||||
FocalPlaneResolutionUnit = 41488
|
||||
SubjectLocation = 41492
|
||||
ExposureIndex = 41493
|
||||
SensingMethod = 41495
|
||||
FileSource = 41728
|
||||
SceneType = 41729
|
||||
CFAPattern = 41730
|
||||
CustomRendered = 41985
|
||||
ExposureMode = 41986
|
||||
WhiteBalance = 41987
|
||||
DigitalZoomRatio = 41988
|
||||
FocalLengthIn35mmFilm = 41989
|
||||
SceneCaptureType = 41990
|
||||
GainControl = 41991
|
||||
Contrast = 41992
|
||||
Saturation = 41993
|
||||
Sharpness = 41994
|
||||
DeviceSettingDescription = 41995
|
||||
SubjectDistanceRange = 41996
|
||||
ImageUniqueID = 42016
|
||||
CameraOwnerName = 42032
|
||||
BodySerialNumber = 42033
|
||||
LensSpecification = 42034
|
||||
LensMake = 42035
|
||||
LensModel = 42036
|
||||
LensSerialNumber = 42037
|
||||
Gamma = 42240
|
||||
|
||||
|
||||
class GPSIFD:
|
||||
"""Exif tag number reference - GPS IFD"""
|
||||
|
||||
GPSVersionID = 0
|
||||
GPSLatitudeRef = 1
|
||||
GPSLatitude = 2
|
||||
GPSLongitudeRef = 3
|
||||
GPSLongitude = 4
|
||||
GPSAltitudeRef = 5
|
||||
GPSAltitude = 6
|
||||
GPSTimeStamp = 7
|
||||
GPSSatellites = 8
|
||||
GPSStatus = 9
|
||||
GPSMeasureMode = 10
|
||||
GPSDOP = 11
|
||||
GPSSpeedRef = 12
|
||||
GPSSpeed = 13
|
||||
GPSTrackRef = 14
|
||||
GPSTrack = 15
|
||||
GPSImgDirectionRef = 16
|
||||
GPSImgDirection = 17
|
||||
GPSMapDatum = 18
|
||||
GPSDestLatitudeRef = 19
|
||||
GPSDestLatitude = 20
|
||||
GPSDestLongitudeRef = 21
|
||||
GPSDestLongitude = 22
|
||||
GPSDestBearingRef = 23
|
||||
GPSDestBearing = 24
|
||||
GPSDestDistanceRef = 25
|
||||
GPSDestDistance = 26
|
||||
GPSProcessingMethod = 27
|
||||
GPSAreaInformation = 28
|
||||
GPSDateStamp = 29
|
||||
GPSDifferential = 30
|
||||
GPSHPositioningError = 31
|
||||
|
||||
|
||||
class InteropIFD:
|
||||
"""Exif tag number reference - Interoperability IFD"""
|
||||
|
||||
InteroperabilityIndex = 1
|
||||
|
|
@ -1,61 +0,0 @@
|
|||
import io
|
||||
import struct
|
||||
import sys
|
||||
|
||||
from . import _webp
|
||||
from ._common import merge_segments, split_into_segments
|
||||
from ._exceptions import InvalidImageDataError
|
||||
|
||||
|
||||
def insert(exif, image, new_file=None):
|
||||
"""
|
||||
py:function:: piexif.insert(exif_bytes, filename)
|
||||
|
||||
Insert exif into JPEG.
|
||||
|
||||
:param bytes exif_bytes: Exif as bytes
|
||||
:param str filename: JPEG
|
||||
"""
|
||||
if exif[0:6] != b"\x45\x78\x69\x66\x00\x00":
|
||||
raise ValueError("Given data is not exif data")
|
||||
|
||||
output_file = False
|
||||
# Prevents "UnicodeWarning: Unicode equal comparison failed" warnings on Python 2
|
||||
maybe_image = sys.version_info >= (3, 0, 0) or isinstance(image, str)
|
||||
|
||||
if maybe_image and image[0:2] == b"\xff\xd8":
|
||||
image_data = image
|
||||
file_type = "jpeg"
|
||||
elif maybe_image and image[0:4] == b"RIFF" and image[8:12] == b"WEBP":
|
||||
image_data = image
|
||||
file_type = "webp"
|
||||
else:
|
||||
with open(image, "rb") as f:
|
||||
image_data = f.read()
|
||||
if image_data[0:2] == b"\xff\xd8":
|
||||
file_type = "jpeg"
|
||||
elif image_data[0:4] == b"RIFF" and image_data[8:12] == b"WEBP":
|
||||
file_type = "webp"
|
||||
else:
|
||||
raise InvalidImageDataError
|
||||
output_file = True
|
||||
|
||||
if file_type == "jpeg":
|
||||
exif = b"\xff\xe1" + struct.pack(">H", len(exif) + 2) + exif
|
||||
segments = split_into_segments(image_data)
|
||||
new_data = merge_segments(segments, exif)
|
||||
elif file_type == "webp":
|
||||
exif = exif[6:]
|
||||
new_data = _webp.insert(image_data, exif)
|
||||
|
||||
if isinstance(new_file, io.BytesIO):
|
||||
new_file.write(new_data)
|
||||
new_file.seek(0)
|
||||
elif new_file:
|
||||
with open(new_file, "wb+") as f:
|
||||
f.write(new_data)
|
||||
elif output_file:
|
||||
with open(image, "wb+") as f:
|
||||
f.write(new_data)
|
||||
else:
|
||||
raise ValueError("Give a 3rd argument to 'insert' to output file")
|
||||
|
|
@ -1,324 +0,0 @@
|
|||
import struct
|
||||
import sys
|
||||
|
||||
from . import _webp
|
||||
from ._common import get_exif_seg, read_exif_from_file, split_into_segments
|
||||
from ._exceptions import InvalidImageDataError
|
||||
from ._exif import TAGS, TYPES, ExifIFD, ImageIFD
|
||||
|
||||
LITTLE_ENDIAN = b"\x49\x49"
|
||||
|
||||
|
||||
def load(input_data, key_is_name=False):
|
||||
"""
|
||||
py:function:: piexif.load(filename)
|
||||
|
||||
Return exif data as dict. Keys(IFD name), be contained, are "0th", "Exif", "GPS", "Interop", "1st", and "thumbnail". Without "thumbnail", the value is dict(tag name/tag value). "thumbnail" value is JPEG as bytes.
|
||||
|
||||
:param str filename: JPEG or TIFF
|
||||
:return: Exif data({"0th":dict, "Exif":dict, "GPS":dict, "Interop":dict, "1st":dict, "thumbnail":bytes})
|
||||
:rtype: dict
|
||||
"""
|
||||
exif_dict = {
|
||||
"0th": {},
|
||||
"Exif": {},
|
||||
"GPS": {},
|
||||
"Interop": {},
|
||||
"1st": {},
|
||||
"thumbnail": None,
|
||||
}
|
||||
exifReader = _ExifReader(input_data)
|
||||
if exifReader.tiftag is None:
|
||||
return exif_dict
|
||||
|
||||
if exifReader.tiftag[0:2] == LITTLE_ENDIAN:
|
||||
exifReader.endian_mark = "<"
|
||||
else:
|
||||
exifReader.endian_mark = ">"
|
||||
|
||||
pointer = struct.unpack(exifReader.endian_mark + "L", exifReader.tiftag[4:8])[0]
|
||||
exif_dict["0th"] = exifReader.get_ifd_dict(pointer, "0th")
|
||||
first_ifd_pointer = exif_dict["0th"].pop("first_ifd_pointer")
|
||||
if ImageIFD.ExifTag in exif_dict["0th"]:
|
||||
pointer = exif_dict["0th"][ImageIFD.ExifTag]
|
||||
exif_dict["Exif"] = exifReader.get_ifd_dict(pointer, "Exif")
|
||||
if ImageIFD.GPSTag in exif_dict["0th"]:
|
||||
pointer = exif_dict["0th"][ImageIFD.GPSTag]
|
||||
exif_dict["GPS"] = exifReader.get_ifd_dict(pointer, "GPS")
|
||||
if ExifIFD.InteroperabilityTag in exif_dict["Exif"]:
|
||||
pointer = exif_dict["Exif"][ExifIFD.InteroperabilityTag]
|
||||
exif_dict["Interop"] = exifReader.get_ifd_dict(pointer, "Interop")
|
||||
if first_ifd_pointer != b"\x00\x00\x00\x00":
|
||||
pointer = struct.unpack(exifReader.endian_mark + "L", first_ifd_pointer)[0]
|
||||
exif_dict["1st"] = exifReader.get_ifd_dict(pointer, "1st")
|
||||
if (
|
||||
ImageIFD.JPEGInterchangeFormat in exif_dict["1st"]
|
||||
and ImageIFD.JPEGInterchangeFormatLength in exif_dict["1st"]
|
||||
):
|
||||
end = (
|
||||
exif_dict["1st"][ImageIFD.JPEGInterchangeFormat]
|
||||
+ exif_dict["1st"][ImageIFD.JPEGInterchangeFormatLength]
|
||||
)
|
||||
thumb = exifReader.tiftag[
|
||||
exif_dict["1st"][ImageIFD.JPEGInterchangeFormat] : end
|
||||
]
|
||||
exif_dict["thumbnail"] = thumb
|
||||
|
||||
if key_is_name:
|
||||
exif_dict = _get_key_name_dict(exif_dict)
|
||||
return exif_dict
|
||||
|
||||
|
||||
class _ExifReader(object):
|
||||
def __init__(self, data):
|
||||
# Prevents "UnicodeWarning: Unicode equal comparison failed" warnings on Python 2
|
||||
maybe_image = sys.version_info >= (3, 0, 0) or isinstance(data, str)
|
||||
|
||||
if maybe_image and data[0:2] == b"\xff\xd8": # JPEG
|
||||
segments = split_into_segments(data)
|
||||
app1 = get_exif_seg(segments)
|
||||
if app1:
|
||||
self.tiftag = app1[10:]
|
||||
else:
|
||||
self.tiftag = None
|
||||
elif maybe_image and data[0:2] in (b"\x49\x49", b"\x4d\x4d"): # TIFF
|
||||
self.tiftag = data
|
||||
elif maybe_image and data[0:4] == b"RIFF" and data[8:12] == b"WEBP":
|
||||
self.tiftag = _webp.get_exif(data)
|
||||
elif maybe_image and data[0:4] == b"Exif": # Exif
|
||||
self.tiftag = data[6:]
|
||||
else:
|
||||
with open(data, "rb") as f:
|
||||
magic_number = f.read(2)
|
||||
if magic_number == b"\xff\xd8": # JPEG
|
||||
app1 = read_exif_from_file(data)
|
||||
if app1:
|
||||
self.tiftag = app1[10:]
|
||||
else:
|
||||
self.tiftag = None
|
||||
elif magic_number in (b"\x49\x49", b"\x4d\x4d"): # TIFF
|
||||
with open(data, "rb") as f:
|
||||
self.tiftag = f.read()
|
||||
else:
|
||||
with open(data, "rb") as f:
|
||||
header = f.read(12)
|
||||
if header[0:4] == b"RIFF" and header[8:12] == b"WEBP":
|
||||
with open(data, "rb") as f:
|
||||
file_data = f.read()
|
||||
self.tiftag = _webp.get_exif(file_data)
|
||||
else:
|
||||
raise InvalidImageDataError("Given file is neither JPEG nor TIFF.")
|
||||
|
||||
self.endian_mark = None
|
||||
|
||||
def get_ifd_dict(self, pointer, ifd_name, read_unknown=False):
|
||||
ifd_dict = {}
|
||||
tag_count = struct.unpack(
|
||||
self.endian_mark + "H", self.tiftag[pointer : pointer + 2]
|
||||
)[0]
|
||||
offset = pointer + 2
|
||||
if ifd_name in ["0th", "1st"]:
|
||||
t = "Image"
|
||||
else:
|
||||
t = ifd_name
|
||||
p_and_value = []
|
||||
for x in range(tag_count):
|
||||
pointer = offset + 12 * x
|
||||
tag = struct.unpack(
|
||||
self.endian_mark + "H", self.tiftag[pointer : pointer + 2]
|
||||
)[0]
|
||||
value_type = struct.unpack(
|
||||
self.endian_mark + "H", self.tiftag[pointer + 2 : pointer + 4]
|
||||
)[0]
|
||||
value_num = struct.unpack(
|
||||
self.endian_mark + "L", self.tiftag[pointer + 4 : pointer + 8]
|
||||
)[0]
|
||||
value = self.tiftag[pointer + 8 : pointer + 12]
|
||||
p_and_value.append((pointer, value_type, value_num, value))
|
||||
v_set = (value_type, value_num, value, tag)
|
||||
if tag in TAGS[t]:
|
||||
ifd_dict[tag] = self.convert_value(v_set)
|
||||
elif read_unknown:
|
||||
ifd_dict[tag] = (v_set[0], v_set[1], v_set[2], self.tiftag)
|
||||
# else:
|
||||
# pass
|
||||
|
||||
if ifd_name == "0th":
|
||||
pointer = offset + 12 * tag_count
|
||||
ifd_dict["first_ifd_pointer"] = self.tiftag[pointer : pointer + 4]
|
||||
return ifd_dict
|
||||
|
||||
def convert_value(self, val):
|
||||
data = None
|
||||
t = val[0]
|
||||
length = val[1]
|
||||
value = val[2]
|
||||
|
||||
if t == TYPES.Byte: # BYTE
|
||||
if length > 4:
|
||||
pointer = struct.unpack(self.endian_mark + "L", value)[0]
|
||||
data = struct.unpack(
|
||||
"B" * length, self.tiftag[pointer : pointer + length]
|
||||
)
|
||||
else:
|
||||
data = struct.unpack("B" * length, value[0:length])
|
||||
elif t == TYPES.Ascii: # ASCII
|
||||
if length > 4:
|
||||
pointer = struct.unpack(self.endian_mark + "L", value)[0]
|
||||
data = self.tiftag[pointer : pointer + length - 1]
|
||||
else:
|
||||
data = value[0 : length - 1]
|
||||
elif t == TYPES.Short: # SHORT
|
||||
if length > 2:
|
||||
pointer = struct.unpack(self.endian_mark + "L", value)[0]
|
||||
data = struct.unpack(
|
||||
self.endian_mark + "H" * length,
|
||||
self.tiftag[pointer : pointer + length * 2],
|
||||
)
|
||||
else:
|
||||
data = struct.unpack(
|
||||
self.endian_mark + "H" * length, value[0 : length * 2]
|
||||
)
|
||||
elif t == TYPES.Long: # LONG
|
||||
if length > 1:
|
||||
pointer = struct.unpack(self.endian_mark + "L", value)[0]
|
||||
data = struct.unpack(
|
||||
self.endian_mark + "L" * length,
|
||||
self.tiftag[pointer : pointer + length * 4],
|
||||
)
|
||||
else:
|
||||
data = struct.unpack(self.endian_mark + "L" * length, value)
|
||||
elif t == TYPES.Rational: # RATIONAL
|
||||
pointer = struct.unpack(self.endian_mark + "L", value)[0]
|
||||
if length > 1:
|
||||
data = tuple(
|
||||
(
|
||||
struct.unpack(
|
||||
self.endian_mark + "L",
|
||||
self.tiftag[pointer + x * 8 : pointer + 4 + x * 8],
|
||||
)[0],
|
||||
struct.unpack(
|
||||
self.endian_mark + "L",
|
||||
self.tiftag[pointer + 4 + x * 8 : pointer + 8 + x * 8],
|
||||
)[0],
|
||||
)
|
||||
for x in range(length)
|
||||
)
|
||||
else:
|
||||
data = (
|
||||
struct.unpack(
|
||||
self.endian_mark + "L", self.tiftag[pointer : pointer + 4]
|
||||
)[0],
|
||||
struct.unpack(
|
||||
self.endian_mark + "L", self.tiftag[pointer + 4 : pointer + 8]
|
||||
)[0],
|
||||
)
|
||||
elif t == TYPES.SByte: # SIGNED BYTES
|
||||
if length > 4:
|
||||
pointer = struct.unpack(self.endian_mark + "L", value)[0]
|
||||
data = struct.unpack(
|
||||
"b" * length, self.tiftag[pointer : pointer + length]
|
||||
)
|
||||
else:
|
||||
data = struct.unpack("b" * length, value[0:length])
|
||||
elif t == TYPES.Undefined: # UNDEFINED BYTES
|
||||
if length > 4:
|
||||
pointer = struct.unpack(self.endian_mark + "L", value)[0]
|
||||
data = self.tiftag[pointer : pointer + length]
|
||||
else:
|
||||
data = value[0:length]
|
||||
elif t == TYPES.SShort: # SIGNED SHORT
|
||||
if length > 2:
|
||||
pointer = struct.unpack(self.endian_mark + "L", value)[0]
|
||||
data = struct.unpack(
|
||||
self.endian_mark + "h" * length,
|
||||
self.tiftag[pointer : pointer + length * 2],
|
||||
)
|
||||
else:
|
||||
data = struct.unpack(
|
||||
self.endian_mark + "h" * length, value[0 : length * 2]
|
||||
)
|
||||
elif t == TYPES.SLong: # SLONG
|
||||
if length > 1:
|
||||
pointer = struct.unpack(self.endian_mark + "L", value)[0]
|
||||
data = struct.unpack(
|
||||
self.endian_mark + "l" * length,
|
||||
self.tiftag[pointer : pointer + length * 4],
|
||||
)
|
||||
else:
|
||||
data = struct.unpack(self.endian_mark + "l" * length, value)
|
||||
elif t == TYPES.SRational: # SRATIONAL
|
||||
pointer = struct.unpack(self.endian_mark + "L", value)[0]
|
||||
if length > 1:
|
||||
data = tuple(
|
||||
(
|
||||
struct.unpack(
|
||||
self.endian_mark + "l",
|
||||
self.tiftag[pointer + x * 8 : pointer + 4 + x * 8],
|
||||
)[0],
|
||||
struct.unpack(
|
||||
self.endian_mark + "l",
|
||||
self.tiftag[pointer + 4 + x * 8 : pointer + 8 + x * 8],
|
||||
)[0],
|
||||
)
|
||||
for x in range(length)
|
||||
)
|
||||
else:
|
||||
data = (
|
||||
struct.unpack(
|
||||
self.endian_mark + "l", self.tiftag[pointer : pointer + 4]
|
||||
)[0],
|
||||
struct.unpack(
|
||||
self.endian_mark + "l", self.tiftag[pointer + 4 : pointer + 8]
|
||||
)[0],
|
||||
)
|
||||
elif t == TYPES.Float: # FLOAT
|
||||
if length > 1:
|
||||
pointer = struct.unpack(self.endian_mark + "L", value)[0]
|
||||
data = struct.unpack(
|
||||
self.endian_mark + "f" * length,
|
||||
self.tiftag[pointer : pointer + length * 4],
|
||||
)
|
||||
else:
|
||||
data = struct.unpack(self.endian_mark + "f" * length, value)
|
||||
elif t == TYPES.DFloat: # DOUBLE
|
||||
pointer = struct.unpack(self.endian_mark + "L", value)[0]
|
||||
data = struct.unpack(
|
||||
self.endian_mark + "d" * length,
|
||||
self.tiftag[pointer : pointer + length * 8],
|
||||
)
|
||||
else:
|
||||
raise ValueError(
|
||||
"Exif might be wrong. Got incorrect value "
|
||||
+ "type to decode.\n"
|
||||
+ "tag: "
|
||||
+ str(val[3])
|
||||
+ "\ntype: "
|
||||
+ str(t)
|
||||
)
|
||||
|
||||
if isinstance(data, tuple) and (len(data) == 1):
|
||||
return data[0]
|
||||
else:
|
||||
return data
|
||||
|
||||
|
||||
def _get_key_name_dict(exif_dict):
|
||||
new_dict = {
|
||||
"0th": {
|
||||
TAGS["Image"][n]["name"]: value for n, value in exif_dict["0th"].items()
|
||||
},
|
||||
"Exif": {
|
||||
TAGS["Exif"][n]["name"]: value for n, value in exif_dict["Exif"].items()
|
||||
},
|
||||
"1st": {
|
||||
TAGS["Image"][n]["name"]: value for n, value in exif_dict["1st"].items()
|
||||
},
|
||||
"GPS": {TAGS["GPS"][n]["name"]: value for n, value in exif_dict["GPS"].items()},
|
||||
"Interop": {
|
||||
TAGS["Interop"][n]["name"]: value
|
||||
for n, value in exif_dict["Interop"].items()
|
||||
},
|
||||
"thumbnail": exif_dict["thumbnail"],
|
||||
}
|
||||
return new_dict
|
||||
|
|
@ -1,57 +0,0 @@
|
|||
import io
|
||||
|
||||
from . import _webp
|
||||
from ._common import get_exif_seg, split_into_segments
|
||||
|
||||
|
||||
def remove(src, new_file=None):
|
||||
"""
|
||||
py:function:: piexif.remove(filename)
|
||||
|
||||
Remove exif from JPEG.
|
||||
|
||||
:param str filename: JPEG
|
||||
"""
|
||||
output_is_file = False
|
||||
if src[0:2] == b"\xff\xd8":
|
||||
src_data = src
|
||||
file_type = "jpeg"
|
||||
elif src[0:4] == b"RIFF" and src[8:12] == b"WEBP":
|
||||
src_data = src
|
||||
file_type = "webp"
|
||||
else:
|
||||
with open(src, "rb") as f:
|
||||
src_data = f.read()
|
||||
output_is_file = True
|
||||
if src_data[0:2] == b"\xff\xd8":
|
||||
file_type = "jpeg"
|
||||
elif src_data[0:4] == b"RIFF" and src_data[8:12] == b"WEBP":
|
||||
file_type = "webp"
|
||||
|
||||
if file_type == "jpeg":
|
||||
segments = split_into_segments(src_data)
|
||||
exif = get_exif_seg(segments)
|
||||
if exif:
|
||||
new_data = src_data.replace(exif, b"")
|
||||
else:
|
||||
new_data = src_data
|
||||
elif file_type == "webp":
|
||||
try:
|
||||
new_data = _webp.remove(src_data)
|
||||
except ValueError:
|
||||
new_data = src_data
|
||||
except Exception as e:
|
||||
print(e)
|
||||
raise ValueError("Error occurred.") from e
|
||||
|
||||
if isinstance(new_file, io.BytesIO):
|
||||
new_file.write(new_data)
|
||||
new_file.seek(0)
|
||||
elif new_file:
|
||||
with open(new_file, "wb+") as f:
|
||||
f.write(new_data)
|
||||
elif output_is_file:
|
||||
with open(src, "wb+") as f:
|
||||
f.write(new_data)
|
||||
else:
|
||||
raise ValueError("Give a second argument to 'remove' to output file")
|
||||
|
|
@ -1,45 +0,0 @@
|
|||
import io
|
||||
|
||||
from ._common import get_exif_seg, merge_segments, split_into_segments
|
||||
|
||||
|
||||
def transplant(exif_src, image, new_file=None):
|
||||
"""
|
||||
py:function:: piexif.transplant(filename1, filename2)
|
||||
|
||||
Transplant exif from filename1 to filename2.
|
||||
|
||||
:param str filename1: JPEG
|
||||
:param str filename2: JPEG
|
||||
"""
|
||||
if exif_src[0:2] == b"\xff\xd8":
|
||||
src_data = exif_src
|
||||
else:
|
||||
with open(exif_src, "rb") as f:
|
||||
src_data = f.read()
|
||||
segments = split_into_segments(src_data)
|
||||
exif = get_exif_seg(segments)
|
||||
if exif is None:
|
||||
raise ValueError("not found exif in input")
|
||||
|
||||
output_file = False
|
||||
if image[0:2] == b"\xff\xd8":
|
||||
image_data = image
|
||||
else:
|
||||
with open(image, "rb") as f:
|
||||
image_data = f.read()
|
||||
output_file = True
|
||||
segments = split_into_segments(image_data)
|
||||
new_data = merge_segments(segments, exif)
|
||||
|
||||
if isinstance(new_file, io.BytesIO):
|
||||
new_file.write(new_data)
|
||||
new_file.seek(0)
|
||||
elif new_file:
|
||||
with open(new_file, "wb+") as f:
|
||||
f.write(new_data)
|
||||
elif output_file:
|
||||
with open(image, "wb+") as f:
|
||||
f.write(new_data)
|
||||
else:
|
||||
raise ValueError("Give a 3rd argument to 'transplant' to output file")
|
||||
|
|
@ -1,270 +0,0 @@
|
|||
import struct
|
||||
|
||||
|
||||
def split(data):
|
||||
if data[0:4] != b"RIFF" or data[8:12] != b"WEBP":
|
||||
raise ValueError("Not WebP")
|
||||
|
||||
webp_length_bytes = data[4:8]
|
||||
webp_length = struct.unpack("<L", webp_length_bytes)[0]
|
||||
RIFF_HEADER_SIZE = 8
|
||||
file_size = RIFF_HEADER_SIZE + webp_length
|
||||
|
||||
start = 12
|
||||
pointer = start
|
||||
CHUNK_FOURCC_LENGTH = 4
|
||||
LENGTH_BYTES_LENGTH = 4
|
||||
|
||||
chunks = []
|
||||
while pointer + CHUNK_FOURCC_LENGTH + LENGTH_BYTES_LENGTH < file_size:
|
||||
fourcc = data[pointer : pointer + CHUNK_FOURCC_LENGTH]
|
||||
pointer += CHUNK_FOURCC_LENGTH
|
||||
chunk_length_bytes = data[pointer : pointer + LENGTH_BYTES_LENGTH]
|
||||
chunk_length = struct.unpack("<L", chunk_length_bytes)[0]
|
||||
pointer += LENGTH_BYTES_LENGTH
|
||||
|
||||
chunk_data = data[pointer : pointer + chunk_length]
|
||||
chunks.append(
|
||||
{"fourcc": fourcc, "length_bytes": chunk_length_bytes, "data": chunk_data}
|
||||
)
|
||||
|
||||
padding = 1 if chunk_length % 2 else 0
|
||||
|
||||
pointer += chunk_length + padding
|
||||
return chunks
|
||||
|
||||
|
||||
def merge_chunks(chunks):
|
||||
merged = b"".join(
|
||||
[
|
||||
chunk["fourcc"]
|
||||
+ chunk["length_bytes"]
|
||||
+ chunk["data"]
|
||||
+ (len(chunk["data"]) % 2) * b"\x00"
|
||||
for chunk in chunks
|
||||
]
|
||||
)
|
||||
return merged
|
||||
|
||||
|
||||
def _get_size_from_vp8x(chunk):
|
||||
width_minus_one_bytes = chunk["data"][-6:-3] + b"\x00"
|
||||
width_minus_one = struct.unpack("<L", width_minus_one_bytes)[0]
|
||||
width = width_minus_one + 1
|
||||
height_minus_one_bytes = chunk["data"][-3:] + b"\x00"
|
||||
height_minus_one = struct.unpack("<L", height_minus_one_bytes)[0]
|
||||
height = height_minus_one + 1
|
||||
return (width, height)
|
||||
|
||||
|
||||
def _get_size_from_vp8(chunk):
|
||||
BEGIN_CODE = b"\x9d\x01\x2a"
|
||||
begin_index = chunk["data"].find(BEGIN_CODE)
|
||||
if begin_index == -1:
|
||||
ValueError("wrong VP8")
|
||||
else:
|
||||
BEGIN_CODE_LENGTH = len(BEGIN_CODE)
|
||||
LENGTH_BYTES_LENGTH = 2
|
||||
length_start = begin_index + BEGIN_CODE_LENGTH
|
||||
width_bytes = chunk["data"][length_start : length_start + LENGTH_BYTES_LENGTH]
|
||||
width = struct.unpack("<H", width_bytes)[0]
|
||||
height_bytes = chunk["data"][
|
||||
length_start + LENGTH_BYTES_LENGTH : length_start + 2 * LENGTH_BYTES_LENGTH
|
||||
]
|
||||
height = struct.unpack("<H", height_bytes)[0]
|
||||
return (width, height)
|
||||
|
||||
|
||||
def _vp8L_contains_alpha(chunk_data):
|
||||
flag = ord(chunk_data[4:5]) >> 5 - 1 & ord(b"\x01")
|
||||
contains = 1 * flag
|
||||
return contains
|
||||
|
||||
|
||||
def _get_size_from_vp8L(chunk):
|
||||
b1 = chunk["data"][1:2]
|
||||
b2 = chunk["data"][2:3]
|
||||
b3 = chunk["data"][3:4]
|
||||
b4 = chunk["data"][4:5]
|
||||
|
||||
width_minus_one = (ord(b2) & ord(b"\x3F")) << 8 | ord(b1)
|
||||
width = width_minus_one + 1
|
||||
|
||||
height_minus_one = (
|
||||
(ord(b4) & ord(b"\x0F")) << 10 | ord(b3) << 2 | (ord(b2) & ord(b"\xC0")) >> 6
|
||||
)
|
||||
height = height_minus_one + 1
|
||||
|
||||
return (width, height)
|
||||
|
||||
|
||||
def _get_size_from_anmf(chunk):
|
||||
width_minus_one_bytes = chunk["data"][6:9] + b"\x00"
|
||||
width_minus_one = struct.unpack("<L", width_minus_one_bytes)[0]
|
||||
width = width_minus_one + 1
|
||||
height_minus_one_bytes = chunk["data"][9:12] + b"\x00"
|
||||
height_minus_one = struct.unpack("<L", height_minus_one_bytes)[0]
|
||||
height = height_minus_one + 1
|
||||
return (width, height)
|
||||
|
||||
|
||||
def set_vp8x(chunks):
|
||||
|
||||
width = None
|
||||
height = None
|
||||
flags = [
|
||||
b"0",
|
||||
b"0",
|
||||
b"0",
|
||||
b"0",
|
||||
b"0",
|
||||
b"0",
|
||||
b"0",
|
||||
b"0",
|
||||
] # [0, 0, ICC, Alpha, EXIF, XMP, Anim, 0]
|
||||
|
||||
for chunk in chunks:
|
||||
if chunk["fourcc"] == b"VP8X":
|
||||
width, height = _get_size_from_vp8x(chunk)
|
||||
elif chunk["fourcc"] == b"VP8 ":
|
||||
width, height = _get_size_from_vp8(chunk)
|
||||
elif chunk["fourcc"] == b"VP8L":
|
||||
is_rgba = _vp8L_contains_alpha(chunk["data"])
|
||||
if is_rgba:
|
||||
flags[3] = b"1"
|
||||
width, height = _get_size_from_vp8L(chunk)
|
||||
elif chunk["fourcc"] == b"ANMF":
|
||||
width, height = _get_size_from_anmf(chunk)
|
||||
elif chunk["fourcc"] == b"ICCP":
|
||||
flags[2] = b"1"
|
||||
elif chunk["fourcc"] == b"ALPH":
|
||||
flags[3] = b"1"
|
||||
elif chunk["fourcc"] == b"EXIF":
|
||||
flags[4] = b"1"
|
||||
elif chunk["fourcc"] == b"XMP ":
|
||||
flags[5] = b"1"
|
||||
elif chunk["fourcc"] == b"ANIM":
|
||||
flags[6] = b"1"
|
||||
width_minus_one = width - 1
|
||||
height_minus_one = height - 1
|
||||
|
||||
if chunks[0]["fourcc"] == b"VP8X":
|
||||
chunks.pop(0)
|
||||
|
||||
header_bytes = b"VP8X"
|
||||
length_bytes = b"\x0a\x00\x00\x00"
|
||||
flags_bytes = struct.pack("B", int(b"".join(flags), 2))
|
||||
padding_bytes = b"\x00\x00\x00"
|
||||
width_bytes = struct.pack("<L", width_minus_one)[:3]
|
||||
height_bytes = struct.pack("<L", height_minus_one)[:3]
|
||||
|
||||
data_bytes = flags_bytes + padding_bytes + width_bytes + height_bytes
|
||||
|
||||
vp8x_chunk = {
|
||||
"fourcc": header_bytes,
|
||||
"length_bytes": length_bytes,
|
||||
"data": data_bytes,
|
||||
}
|
||||
chunks.insert(0, vp8x_chunk)
|
||||
|
||||
return chunks
|
||||
|
||||
|
||||
def get_file_header(chunks):
|
||||
WEBP_HEADER_LENGTH = 4
|
||||
FOURCC_LENGTH = 4
|
||||
LENGTH_BYTES_LENGTH = 4
|
||||
|
||||
length = WEBP_HEADER_LENGTH
|
||||
for chunk in chunks:
|
||||
data_length = struct.unpack("<L", chunk["length_bytes"])[0]
|
||||
data_length += 1 if data_length % 2 else 0
|
||||
length += FOURCC_LENGTH + LENGTH_BYTES_LENGTH + data_length
|
||||
length_bytes = struct.pack("<L", length)
|
||||
riff = b"RIFF"
|
||||
webp_header = b"WEBP"
|
||||
file_header = riff + length_bytes + webp_header
|
||||
return file_header
|
||||
|
||||
|
||||
def get_exif(data):
|
||||
if data[0:4] != b"RIFF" or data[8:12] != b"WEBP":
|
||||
raise ValueError("Not WebP")
|
||||
|
||||
if data[12:16] != b"VP8X":
|
||||
raise ValueError("doesnot have exif")
|
||||
|
||||
webp_length_bytes = data[4:8]
|
||||
webp_length = struct.unpack("<L", webp_length_bytes)[0]
|
||||
RIFF_HEADER_SIZE = 8
|
||||
file_size = RIFF_HEADER_SIZE + webp_length
|
||||
|
||||
start = 12
|
||||
pointer = start
|
||||
CHUNK_FOURCC_LENGTH = 4
|
||||
LENGTH_BYTES_LENGTH = 4
|
||||
|
||||
while pointer < file_size:
|
||||
fourcc = data[pointer : pointer + CHUNK_FOURCC_LENGTH]
|
||||
pointer += CHUNK_FOURCC_LENGTH
|
||||
chunk_length_bytes = data[pointer : pointer + LENGTH_BYTES_LENGTH]
|
||||
chunk_length = struct.unpack("<L", chunk_length_bytes)[0]
|
||||
if chunk_length % 2:
|
||||
chunk_length += 1
|
||||
pointer += LENGTH_BYTES_LENGTH
|
||||
if fourcc == b"EXIF":
|
||||
return data[pointer : pointer + chunk_length]
|
||||
pointer += chunk_length
|
||||
return None # if there isn't exif, return None.
|
||||
|
||||
|
||||
def insert_exif_into_chunks(chunks, exif_bytes):
|
||||
EXIF_HEADER = b"EXIF"
|
||||
exif_length_bytes = struct.pack("<L", len(exif_bytes))
|
||||
exif_chunk = {
|
||||
"fourcc": EXIF_HEADER,
|
||||
"length_bytes": exif_length_bytes,
|
||||
"data": exif_bytes,
|
||||
}
|
||||
|
||||
xmp_index = None
|
||||
animation_index = None
|
||||
|
||||
for index, chunk in enumerate(chunks):
|
||||
if chunk["fourcc"] == b"EXIF":
|
||||
chunks.pop(index)
|
||||
|
||||
for index, chunk in enumerate(chunks):
|
||||
if chunk["fourcc"] == b"XMP ":
|
||||
xmp_index = index
|
||||
elif chunk["fourcc"] == b"ANIM":
|
||||
animation_index = index
|
||||
if xmp_index is not None:
|
||||
chunks.insert(xmp_index, exif_chunk)
|
||||
elif animation_index is not None:
|
||||
chunks.insert(animation_index, exif_chunk)
|
||||
else:
|
||||
chunks.append(exif_chunk)
|
||||
return chunks
|
||||
|
||||
|
||||
def insert(webp_bytes, exif_bytes):
|
||||
chunks = split(webp_bytes)
|
||||
chunks = insert_exif_into_chunks(chunks, exif_bytes)
|
||||
chunks = set_vp8x(chunks)
|
||||
file_header = get_file_header(chunks)
|
||||
merged = merge_chunks(chunks)
|
||||
new_webp_bytes = file_header + merged
|
||||
return new_webp_bytes
|
||||
|
||||
|
||||
def remove(webp_bytes):
|
||||
chunks = split(webp_bytes)
|
||||
for index, chunk in enumerate(chunks):
|
||||
if chunk["fourcc"] == b"EXIF":
|
||||
chunks.pop(index)
|
||||
chunks = set_vp8x(chunks)
|
||||
file_header = get_file_header(chunks)
|
||||
merged = merge_chunks(chunks)
|
||||
new_webp_bytes = file_header + merged
|
||||
return new_webp_bytes
|
||||
|
|
@ -1,76 +0,0 @@
|
|||
class UserComment:
|
||||
#
|
||||
# Names of encodings that we publicly support.
|
||||
#
|
||||
ASCII = "ascii"
|
||||
JIS = "jis"
|
||||
UNICODE = "unicode"
|
||||
ENCODINGS = (ASCII, JIS, UNICODE)
|
||||
|
||||
#
|
||||
# The actual encodings accepted by the standard library differ slightly from
|
||||
# the above.
|
||||
#
|
||||
_JIS = "shift_jis"
|
||||
_UNICODE = "utf_16_be"
|
||||
|
||||
_PREFIX_SIZE = 8
|
||||
#
|
||||
# From Table 9: Character Codes and their Designation
|
||||
#
|
||||
_ASCII_PREFIX = b"\x41\x53\x43\x49\x49\x00\x00\x00"
|
||||
_JIS_PREFIX = b"\x4a\x49\x53\x00\x00\x00\x00\x00"
|
||||
_UNICODE_PREFIX = b"\x55\x4e\x49\x43\x4f\x44\x45\x00"
|
||||
_UNDEFINED_PREFIX = b"\x00\x00\x00\x00\x00\x00\x00\x00"
|
||||
|
||||
@classmethod
|
||||
def load(cls, data):
|
||||
"""
|
||||
Convert "UserComment" value in exif format to str.
|
||||
|
||||
:param bytes data: "UserComment" value from exif
|
||||
:return: u"foobar"
|
||||
:rtype: str(Unicode)
|
||||
:raises: ValueError if the data does not conform to the EXIF specification,
|
||||
or the encoding is unsupported.
|
||||
"""
|
||||
if len(data) < cls._PREFIX_SIZE:
|
||||
raise ValueError("not enough data to decode UserComment")
|
||||
prefix = data[: cls._PREFIX_SIZE]
|
||||
body = data[cls._PREFIX_SIZE :]
|
||||
if prefix == cls._UNDEFINED_PREFIX:
|
||||
raise ValueError("prefix is UNDEFINED, unable to decode UserComment")
|
||||
try:
|
||||
encoding = {
|
||||
cls._ASCII_PREFIX: cls.ASCII,
|
||||
cls._JIS_PREFIX: cls._JIS,
|
||||
cls._UNICODE_PREFIX: cls._UNICODE,
|
||||
}[prefix]
|
||||
except KeyError as e:
|
||||
raise ValueError("unable to determine appropriate encoding") from e
|
||||
return body.decode(encoding, errors="replace")
|
||||
|
||||
@classmethod
|
||||
def dump(cls, data, encoding="ascii"):
|
||||
"""
|
||||
Convert str to appropriate format for "UserComment".
|
||||
|
||||
:param data: Like u"foobar"
|
||||
:param str encoding: "ascii", "jis", or "unicode"
|
||||
:return: b"ASCII\x00\x00\x00foobar"
|
||||
:rtype: bytes
|
||||
:raises: ValueError if the encoding is unsupported.
|
||||
"""
|
||||
if encoding not in cls.ENCODINGS:
|
||||
raise ValueError(
|
||||
"encoding %r must be one of %r" % (encoding, cls.ENCODINGS)
|
||||
)
|
||||
prefix = {
|
||||
cls.ASCII: cls._ASCII_PREFIX,
|
||||
cls.JIS: cls._JIS_PREFIX,
|
||||
cls.UNICODE: cls._UNICODE_PREFIX,
|
||||
}[encoding]
|
||||
internal_encoding = {cls.UNICODE: cls._UNICODE, cls.JIS: cls._JIS}.get(
|
||||
encoding, encoding
|
||||
)
|
||||
return prefix + data.encode(internal_encoding, errors="replace")
|
||||
|
|
@ -4,6 +4,8 @@ from uuid import UUID
|
|||
import numpy as np
|
||||
from labthings.json import LabThingsJSONEncoder
|
||||
|
||||
__all__ = ["JSONEncoder", "LabThingsJSONEncoder"]
|
||||
|
||||
|
||||
class JSONEncoder(LabThingsJSONEncoder):
|
||||
"""
|
||||
|
|
@ -19,12 +21,12 @@ class JSONEncoder(LabThingsJSONEncoder):
|
|||
# Numpy integers
|
||||
elif isinstance(o, np.integer):
|
||||
return int(o)
|
||||
# Numpy floats
|
||||
elif isinstance(o, np.float):
|
||||
return float(o)
|
||||
# Numpy arrays
|
||||
elif isinstance(o, np.ndarray):
|
||||
return o.tolist()
|
||||
# UUIDs
|
||||
elif isinstance(o, UUID):
|
||||
return str(o)
|
||||
else:
|
||||
# call base class implementation which takes care of
|
||||
# raising exceptions for unsupported types
|
||||
|
|
|
|||
|
|
@ -4,13 +4,16 @@ Defines a microscope object, binding a camera and stage with basic functionality
|
|||
"""
|
||||
import logging
|
||||
import uuid
|
||||
from typing import Tuple
|
||||
from typing import Dict, List, Optional, Tuple, Union
|
||||
|
||||
import pkg_resources
|
||||
from expiringdict import ExpiringDict
|
||||
|
||||
from openflexure_microscope.camera.base import BaseCamera
|
||||
from openflexure_microscope.camera.mock import MissingCamera
|
||||
from openflexure_microscope.captures import THUMBNAIL_SIZE, CaptureManager
|
||||
from openflexure_microscope.config import OpenflexureSettingsFile
|
||||
from openflexure_microscope.stage.base import BaseStage
|
||||
from openflexure_microscope.stage.mock import MissingStage
|
||||
from openflexure_microscope.stage.sanga import SangaDeltaStage, SangaStage
|
||||
|
||||
|
|
@ -19,7 +22,7 @@ try:
|
|||
except Exception as e: # pylint: disable=W0703
|
||||
logging.error(e)
|
||||
logging.warning("Unable to import PiCameraStreamer")
|
||||
from labthings import CompositeLock
|
||||
from labthings import CompositeLock, StrictLock
|
||||
|
||||
from openflexure_microscope.config import user_configuration, user_settings
|
||||
|
||||
|
|
@ -32,23 +35,23 @@ class Microscope:
|
|||
"""
|
||||
|
||||
def __init__(self, settings=user_settings, configuration=user_configuration):
|
||||
self.id = f"openflexure:microscope:{uuid.uuid4()}"
|
||||
self.name = self.id
|
||||
self.id: str = f"openflexure:microscope:{uuid.uuid4()}"
|
||||
self.name: str = self.id
|
||||
|
||||
self.captures = CaptureManager()
|
||||
self.captures: CaptureManager = CaptureManager()
|
||||
|
||||
# Store settings and configuration files
|
||||
self.settings_file = settings
|
||||
self.configuration_file = configuration
|
||||
self.settings_file: OpenflexureSettingsFile = settings
|
||||
self.configuration_file: OpenflexureSettingsFile = configuration
|
||||
|
||||
self.extension_settings = {}
|
||||
self.extension_settings: dict = {}
|
||||
|
||||
# Initialise with an empty composite lock
|
||||
#: :py:class:`labthings.CompositeLock`: Composite lock for locking both camera and stage
|
||||
self.lock = CompositeLock([])
|
||||
self.lock: Union[CompositeLock, StrictLock] = CompositeLock([])
|
||||
|
||||
self.camera = None #: Currently connected camera object
|
||||
self.stage = None #: Currently connected stage object
|
||||
self.camera: BaseCamera = None #: Currently connected camera object
|
||||
self.stage: BaseStage = None #: Currently connected stage object
|
||||
|
||||
self.setup(self.configuration_file.load()) # Attach components
|
||||
|
||||
|
|
@ -59,8 +62,12 @@ class Microscope:
|
|||
# Sometimes, like when doing large scans, we don't need to read the hardware
|
||||
# state for every single capture. We can get the data once at the start,
|
||||
# cache it, and reuse that to save on IO. Cached data is stored here.
|
||||
self.configuration_cache = ExpiringDict(max_len=100, max_age_seconds=3600)
|
||||
self.metadata_cache = ExpiringDict(max_len=100, max_age_seconds=3600)
|
||||
self.configuration_cache: Union[dict, ExpiringDict] = ExpiringDict(
|
||||
max_len=100, max_age_seconds=3600
|
||||
)
|
||||
self.metadata_cache: Union[dict, ExpiringDict] = ExpiringDict(
|
||||
max_len=100, max_age_seconds=3600
|
||||
)
|
||||
|
||||
def __enter__(self):
|
||||
"""Create microscope on context enter."""
|
||||
|
|
@ -86,7 +93,7 @@ class Microscope:
|
|||
self.captures.close()
|
||||
logging.info("Closed %s", (self))
|
||||
|
||||
def setup(self, configuration):
|
||||
def setup(self, configuration: dict):
|
||||
"""
|
||||
Attach microscope components based on initially passed configuration file
|
||||
"""
|
||||
|
|
@ -119,12 +126,13 @@ class Microscope:
|
|||
if hasattr(self.stage, "lock"):
|
||||
self.lock.locks.append(self.stage.lock)
|
||||
|
||||
def set_stage(self, configuration=None, stage_type=None):
|
||||
def set_stage(
|
||||
self, configuration: Optional[dict] = None, stage_type: Optional[str] = None
|
||||
):
|
||||
"""
|
||||
Set or change the stage geometry
|
||||
"""
|
||||
if not configuration:
|
||||
configuration = self.configuration
|
||||
configuration = configuration or self.configuration
|
||||
|
||||
if stage_type:
|
||||
if stage_type == configuration["stage"].get("type"):
|
||||
|
|
@ -135,7 +143,7 @@ class Microscope:
|
|||
|
||||
### Close any existing stages
|
||||
if self.stage:
|
||||
stage_port = self.stage.port
|
||||
stage_port = getattr(self.stage, "port")
|
||||
self.stage.close()
|
||||
|
||||
logging.info("Setting stage")
|
||||
|
|
@ -151,7 +159,7 @@ class Microscope:
|
|||
except Exception as e: # pylint: disable=W0703
|
||||
logging.error(e)
|
||||
logging.warning("No compatible Sangaboard hardware found.")
|
||||
elif stage_type in ("SangaDeltaStage"):
|
||||
elif stage_type in ("SangaDeltaStage",):
|
||||
try:
|
||||
logging.info("Trying SangaDeltaStage")
|
||||
self.stage = SangaDeltaStage(port=stage_port)
|
||||
|
|
@ -173,7 +181,7 @@ class Microscope:
|
|||
else:
|
||||
return False
|
||||
|
||||
def has_real_camera(self):
|
||||
def has_real_camera(self) -> bool:
|
||||
"""
|
||||
Check if a real (non-mock) camera is currently attached.
|
||||
"""
|
||||
|
|
@ -184,7 +192,7 @@ class Microscope:
|
|||
|
||||
# Create unified state
|
||||
@property
|
||||
def state(self):
|
||||
def state(self) -> dict:
|
||||
"""Dictionary of the basic microscope state.
|
||||
|
||||
Return:
|
||||
|
|
@ -224,7 +232,7 @@ class Microscope:
|
|||
for key in settings.keys():
|
||||
logging.warning("Key %s is unused and was ignored", key)
|
||||
|
||||
def read_settings(self, full: bool = True):
|
||||
def read_settings(self, full: bool = True) -> dict:
|
||||
"""
|
||||
Get an updated settings dictionary.
|
||||
|
||||
|
|
@ -270,7 +278,7 @@ class Microscope:
|
|||
# Save config to file
|
||||
self.settings_file.save(current_config, backup=True)
|
||||
|
||||
def force_get_configuration(self):
|
||||
def force_get_configuration(self) -> dict:
|
||||
initial_configuration = self.configuration_file.load()
|
||||
|
||||
current_configuration = {
|
||||
|
|
@ -293,7 +301,7 @@ class Microscope:
|
|||
initial_configuration.update(current_configuration)
|
||||
return initial_configuration
|
||||
|
||||
def get_configuration(self, cache_key=None):
|
||||
def get_configuration(self, cache_key: Optional[str] = None) -> dict:
|
||||
if cache_key:
|
||||
cached_config = self.configuration_cache.get(cache_key, None)
|
||||
if cached_config:
|
||||
|
|
@ -308,7 +316,7 @@ class Microscope:
|
|||
def configuration(self):
|
||||
return self.get_configuration()
|
||||
|
||||
def force_get_metadata(self):
|
||||
def force_get_metadata(self) -> dict:
|
||||
"""
|
||||
Read cachable bits of microscope metadata.
|
||||
Currently ID, settings, and configuration can be cached
|
||||
|
|
@ -321,7 +329,7 @@ class Microscope:
|
|||
|
||||
return system_metadata
|
||||
|
||||
def get_metadata(self, cache_key=None):
|
||||
def get_metadata(self, cache_key: Optional[str] = None):
|
||||
"""
|
||||
Read microscope metadata, with partial caching
|
||||
"""
|
||||
|
|
@ -345,22 +353,23 @@ class Microscope:
|
|||
return metadata
|
||||
|
||||
@property
|
||||
def metadata(self):
|
||||
def metadata(self) -> dict:
|
||||
return self.get_metadata()
|
||||
|
||||
def capture(
|
||||
self,
|
||||
filename: str = None,
|
||||
filename: Optional[str] = None,
|
||||
folder: str = "",
|
||||
temporary: bool = False,
|
||||
use_video_port: bool = False,
|
||||
resize: Tuple[int, int] = None,
|
||||
bayer: bool = True,
|
||||
fmt: str = "jpeg",
|
||||
annotations: dict = None,
|
||||
tags: list = None,
|
||||
metadata: dict = None,
|
||||
cache_key: str = None,
|
||||
annotations: Optional[Dict[str, str]] = None,
|
||||
tags: Optional[List[str]] = None,
|
||||
dataset: Optional[Dict[str, str]] = None,
|
||||
metadata: Optional[dict] = None,
|
||||
cache_key: Optional[str] = None,
|
||||
):
|
||||
logging.debug("Microscope capturing to %s", filename)
|
||||
if not annotations:
|
||||
|
|
@ -381,9 +390,6 @@ class Microscope:
|
|||
)
|
||||
|
||||
# Capture to output object
|
||||
extras = {}
|
||||
if fmt == "jpeg":
|
||||
extras["thumbnail"] = (*THUMBNAIL_SIZE, 85)
|
||||
logging.info("Starting microscope capture %s", output.file)
|
||||
self.camera.capture(
|
||||
output,
|
||||
|
|
@ -391,10 +397,10 @@ class Microscope:
|
|||
resize=resize,
|
||||
bayer=bayer,
|
||||
fmt=fmt,
|
||||
**extras,
|
||||
thumbnail=(*THUMBNAIL_SIZE, 85),
|
||||
)
|
||||
|
||||
output.put_and_save(tags, annotations, full_metadata)
|
||||
output.put_and_save(tags, annotations, dataset, full_metadata)
|
||||
logging.debug("Finished capture to %s", output.file)
|
||||
|
||||
return output
|
||||
|
|
|
|||
|
|
@ -3,11 +3,11 @@ import os
|
|||
# UTILITIES
|
||||
|
||||
|
||||
def check_rw(path):
|
||||
def check_rw(path: str) -> bool:
|
||||
return os.access(path, os.W_OK) and os.access(path, os.R_OK)
|
||||
|
||||
|
||||
def settings_file_path(filename: str):
|
||||
def settings_file_path(filename: str) -> str:
|
||||
"""Generate a full file path for a filename to be stored in server settings folder"""
|
||||
settings_dir = os.path.join(OPENFLEXURE_VAR_PATH, "settings")
|
||||
if not os.path.exists(settings_dir):
|
||||
|
|
@ -15,7 +15,7 @@ def settings_file_path(filename: str):
|
|||
return os.path.join(settings_dir, filename)
|
||||
|
||||
|
||||
def data_file_path(filename: str):
|
||||
def data_file_path(filename: str) -> str:
|
||||
"""Generate a full file path for a filename to be stored in server data folder"""
|
||||
data_dir = os.path.join(OPENFLEXURE_VAR_PATH, "data")
|
||||
if not os.path.exists(data_dir):
|
||||
|
|
@ -23,7 +23,7 @@ def data_file_path(filename: str):
|
|||
return os.path.join(data_dir, filename)
|
||||
|
||||
|
||||
def extensions_file_path(filename: str):
|
||||
def extensions_file_path(filename: str) -> str:
|
||||
"""Generate a full file path for a folder to be stored in server extensions"""
|
||||
ext_dir = os.path.join(OPENFLEXURE_VAR_PATH, "extensions")
|
||||
if not os.path.exists(ext_dir):
|
||||
|
|
@ -31,7 +31,7 @@ def extensions_file_path(filename: str):
|
|||
return os.path.join(ext_dir, filename)
|
||||
|
||||
|
||||
def logs_file_path(filename: str):
|
||||
def logs_file_path(filename: str) -> str:
|
||||
"""Generate a full file path for a filename to be stored in server logs"""
|
||||
logs_dir = os.path.join(OPENFLEXURE_VAR_PATH, "logs")
|
||||
if not os.path.exists(logs_dir):
|
||||
|
|
@ -42,14 +42,14 @@ def logs_file_path(filename: str):
|
|||
# BASE PATHS
|
||||
|
||||
if os.name == "nt":
|
||||
PREFERRED_VAR_PATH = os.getenv("PROGRAMDATA") or "C:\\ProgramData"
|
||||
FALLBACK_VAR_PATH = os.path.expanduser("~")
|
||||
PREFERRED_VAR_PATH: str = os.getenv("PROGRAMDATA") or "C:\\ProgramData"
|
||||
FALLBACK_VAR_PATH: str = os.path.expanduser("~")
|
||||
else:
|
||||
PREFERRED_VAR_PATH = "/var"
|
||||
FALLBACK_VAR_PATH = os.path.expanduser("~")
|
||||
|
||||
PREFERRED_OPENFLEXURE_VAR_PATH = os.path.join(PREFERRED_VAR_PATH, "openflexure")
|
||||
FALLBACK_OPENFLEXURE_VAR_PATH = os.path.join(FALLBACK_VAR_PATH, "openflexure")
|
||||
PREFERRED_OPENFLEXURE_VAR_PATH: str = os.path.join(PREFERRED_VAR_PATH, "openflexure")
|
||||
FALLBACK_OPENFLEXURE_VAR_PATH: str = os.path.join(FALLBACK_VAR_PATH, "openflexure")
|
||||
|
||||
if not os.path.exists(PREFERRED_OPENFLEXURE_VAR_PATH) and check_rw(PREFERRED_VAR_PATH):
|
||||
os.makedirs(PREFERRED_OPENFLEXURE_VAR_PATH)
|
||||
|
|
@ -65,8 +65,8 @@ else:
|
|||
# SERVER PATHS
|
||||
|
||||
#: Path of microscope settings file
|
||||
SETTINGS_FILE_PATH = settings_file_path("microscope_settings.json")
|
||||
SETTINGS_FILE_PATH: str = settings_file_path("microscope_settings.json")
|
||||
#: Path of microscope configuration file
|
||||
CONFIGURATION_FILE_PATH = settings_file_path("microscope_configuration.json")
|
||||
CONFIGURATION_FILE_PATH: str = settings_file_path("microscope_configuration.json")
|
||||
#: Path of microscope extensions directory
|
||||
OPENFLEXURE_EXTENSIONS_PATH = extensions_file_path("microscope_extensions")
|
||||
OPENFLEXURE_EXTENSIONS_PATH: str = extensions_file_path("microscope_extensions")
|
||||
|
|
|
|||
|
|
@ -1,9 +1,10 @@
|
|||
import json
|
||||
import logging
|
||||
from typing import List
|
||||
|
||||
from .error_sources import ErrorSource
|
||||
|
||||
ERROR_SOURCES = []
|
||||
ERROR_SOURCES: List[ErrorSource] = []
|
||||
|
||||
|
||||
def trace_config_exceptions():
|
||||
|
|
|
|||
|
|
@ -1,7 +1,9 @@
|
|||
from abc import ABCMeta, abstractmethod
|
||||
from typing import Dict, List, Optional, Tuple, Union
|
||||
|
||||
import numpy as np
|
||||
from labthings import StrictLock
|
||||
from typing_extensions import Literal
|
||||
|
||||
|
||||
class BaseStage(metaclass=ABCMeta):
|
||||
|
|
@ -39,11 +41,11 @@ class BaseStage(metaclass=ABCMeta):
|
|||
|
||||
@property
|
||||
@abstractmethod
|
||||
def position(self):
|
||||
def position(self) -> Tuple[int, int, int]:
|
||||
"""The current position, as a list"""
|
||||
|
||||
@property
|
||||
def position_map(self):
|
||||
def position_map(self) -> Dict[str, int]:
|
||||
return {"x": self.position[0], "y": self.position[1], "z": self.position[2]}
|
||||
|
||||
@property
|
||||
|
|
@ -52,19 +54,27 @@ class BaseStage(metaclass=ABCMeta):
|
|||
"""Get the distance used for backlash compensation."""
|
||||
|
||||
@backlash.setter
|
||||
@abstractmethod
|
||||
def backlash(self):
|
||||
"""Set the distance used for backlash compensation."""
|
||||
# See: https://github.com/python/mypy/issues/4165
|
||||
# Since we can't also decorate this with abstract method we want to be
|
||||
# sure that the setter doesn't actually get used as a noop.
|
||||
raise NotImplementedError
|
||||
|
||||
@abstractmethod
|
||||
def move_rel(self, displacement: list, axis=None, backlash=True):
|
||||
def move_rel(
|
||||
self,
|
||||
displacement: Union[int, Tuple[int, int, int]],
|
||||
axis: Optional[Literal["x", "y", "z"]] = None,
|
||||
backlash: bool = True,
|
||||
):
|
||||
"""Make a relative move, optionally correcting for backlash.
|
||||
displacement: integer or array/list of 3 integers
|
||||
backlash: (default: True) whether to correct for backlash.
|
||||
"""
|
||||
|
||||
@abstractmethod
|
||||
def move_abs(self, final, **kwargs):
|
||||
def move_abs(self, final: Tuple[int, int, int], **kwargs):
|
||||
"""Make an absolute move to a position"""
|
||||
|
||||
@abstractmethod
|
||||
|
|
@ -75,7 +85,12 @@ class BaseStage(metaclass=ABCMeta):
|
|||
def close(self):
|
||||
"""Cleanly close communication with the stage"""
|
||||
|
||||
def scan_linear(self, rel_positions, backlash=True, return_to_start=True):
|
||||
def scan_linear(
|
||||
self,
|
||||
rel_positions: List[Tuple[int, int, int]],
|
||||
backlash: bool = True,
|
||||
return_to_start: bool = True,
|
||||
):
|
||||
"""
|
||||
Scan through a list of (relative) positions (generator fn)
|
||||
rel_positions should be an nx3-element array (or list of 3 element arrays).
|
||||
|
|
@ -86,15 +101,15 @@ class BaseStage(metaclass=ABCMeta):
|
|||
exception occurs during the scan..
|
||||
"""
|
||||
starting_position = self.position
|
||||
rel_positions = np.array(rel_positions)
|
||||
assert rel_positions.shape[1] == 3, ValueError(
|
||||
rel_positions_array: np.ndarray = np.array(rel_positions)
|
||||
assert rel_positions_array.shape[1] == 3, ValueError(
|
||||
"Positions should be 3 elements long."
|
||||
)
|
||||
try:
|
||||
self.move_rel(rel_positions[0], backlash=backlash)
|
||||
self.move_rel(rel_positions_array[0], backlash=backlash)
|
||||
yield 0
|
||||
|
||||
for i, step in enumerate(np.diff(rel_positions, axis=0)):
|
||||
for i, step in enumerate(np.diff(rel_positions_array, axis=0)):
|
||||
self.move_rel(step, backlash=backlash)
|
||||
yield i + 1
|
||||
except Exception as e:
|
||||
|
|
@ -104,11 +119,11 @@ class BaseStage(metaclass=ABCMeta):
|
|||
if return_to_start:
|
||||
self.move_abs(starting_position, backlash=backlash)
|
||||
|
||||
def scan_z(self, dz, **kwargs):
|
||||
def scan_z(self, dz: List[int], **kwargs):
|
||||
"""Scan through a list of (relative) z positions (generator fn)
|
||||
This function takes a 1D numpy array of Z positions, relative to
|
||||
the position at the start of the scan, and converts it into an
|
||||
array of 3D positions with x=y=0. This, along with all the
|
||||
keyword arguments, is then passed to ``scan_linear``.
|
||||
"""
|
||||
return self.scan_linear([[0, 0, z] for z in dz], **kwargs)
|
||||
return self.scan_linear([(0, 0, z) for z in dz], **kwargs)
|
||||
|
|
|
|||
|
|
@ -1,8 +1,10 @@
|
|||
import logging
|
||||
import time
|
||||
from collections.abc import Iterable
|
||||
from typing import Optional, Tuple, Union
|
||||
|
||||
import numpy as np
|
||||
from typing_extensions import Literal
|
||||
|
||||
from openflexure_microscope.stage.base import BaseStage
|
||||
from openflexure_microscope.utilities import axes_to_array
|
||||
|
|
@ -15,8 +17,6 @@ class MissingStage(BaseStage):
|
|||
self._n_axis = 3
|
||||
self._backlash = None
|
||||
|
||||
self.axis_names = ["x", "y", "z"] # Assume all sangaboards are 3 axis
|
||||
|
||||
@property
|
||||
def state(self):
|
||||
"""The general state dictionary of the board."""
|
||||
|
|
@ -66,14 +66,27 @@ class MissingStage(BaseStage):
|
|||
else:
|
||||
self._backlash = np.array([int(blsh)] * self.n_axes, dtype=np.int)
|
||||
|
||||
def move_rel(self, displacement: list, axis=None, backlash=True):
|
||||
def move_rel(
|
||||
self,
|
||||
displacement: Union[int, Tuple[int, int, int]],
|
||||
axis: Optional[Literal["x", "y", "z"]] = None,
|
||||
backlash: bool = True,
|
||||
):
|
||||
time.sleep(0.5)
|
||||
if axis is not None:
|
||||
assert axis in self.axis_names, "axis must be one of {}".format(
|
||||
self.axis_names
|
||||
if axis:
|
||||
# Displacement MUST be an integer if axis name is specified
|
||||
if not isinstance(displacement, int):
|
||||
raise TypeError(
|
||||
"Displacement must be an integer when axis is specified"
|
||||
)
|
||||
# Axis name MUST be x, y, or z
|
||||
if axis not in ("x", "y", "z"):
|
||||
raise ValueError("axis must be one of x, y, or z")
|
||||
move = (
|
||||
displacement if axis == "x" else 0,
|
||||
displacement if axis == "y" else 0,
|
||||
displacement if axis == "z" else 0,
|
||||
)
|
||||
move = np.zeros(self.n_axes, dtype=np.int)
|
||||
move[np.argmax(np.array(self.axis_names) == axis)] = int(displacement)
|
||||
displacement = move
|
||||
|
||||
initial_move = np.array(displacement, dtype=np.int)
|
||||
|
|
|
|||
|
|
@ -1,9 +1,11 @@
|
|||
import logging
|
||||
import time
|
||||
from collections.abc import Iterable
|
||||
from typing import Optional, Tuple, Union
|
||||
|
||||
import numpy as np
|
||||
from sangaboard import Sangaboard
|
||||
from typing_extensions import Literal
|
||||
|
||||
from openflexure_microscope.stage.base import BaseStage
|
||||
from openflexure_microscope.utilities import axes_to_array
|
||||
|
|
@ -32,7 +34,6 @@ class SangaStage(BaseStage):
|
|||
None # Initialise backlash storage, used by property setter/getter
|
||||
)
|
||||
self.settle_time = 0.2 # Default move settle time
|
||||
self.axis_names = ["x", "y", "z"] # Assume all sangaboards are 3 axis
|
||||
|
||||
self._position_on_enter = None
|
||||
|
||||
|
|
@ -52,7 +53,7 @@ class SangaStage(BaseStage):
|
|||
@property
|
||||
def n_axes(self):
|
||||
"""The number of axes this stage has."""
|
||||
return len(self.board.axis_names)
|
||||
return 3
|
||||
|
||||
@property
|
||||
def position(self):
|
||||
|
|
@ -111,7 +112,12 @@ class SangaStage(BaseStage):
|
|||
|
||||
return config
|
||||
|
||||
def move_rel(self, displacement: list, axis=None, backlash=True):
|
||||
def move_rel(
|
||||
self,
|
||||
displacement: Union[int, Tuple[int, int, int]],
|
||||
axis: Optional[Literal["x", "y", "z"]] = None,
|
||||
backlash: bool = True,
|
||||
):
|
||||
"""Make a relative move, optionally correcting for backlash.
|
||||
displacement: integer or array/list of 3 integers
|
||||
axis: None (for 3-axis moves) or one of 'x','y','z'
|
||||
|
|
@ -121,14 +127,21 @@ class SangaStage(BaseStage):
|
|||
logging.debug("Moving sangaboard by %s", displacement)
|
||||
if not backlash or self.backlash is None:
|
||||
return self.board.move_rel(displacement, axis=axis)
|
||||
if axis is not None:
|
||||
# backlash correction is easier if we're always in 3D
|
||||
# so this code just converts single-axis moves into all-axis moves.
|
||||
assert axis in self.axis_names, "axis must be one of {}".format(
|
||||
self.axis_names
|
||||
# If we specify an axis name and a displacement int, convert to a displacement tuple
|
||||
if axis:
|
||||
# Displacement MUST be an integer if axis name is specified
|
||||
if not isinstance(displacement, int):
|
||||
raise TypeError(
|
||||
"Displacement must be an integer when axis is specified"
|
||||
)
|
||||
# Axis name MUST be x, y, or z
|
||||
if axis not in ("x", "y", "z"):
|
||||
raise ValueError("axis must be one of x, y, or z")
|
||||
move = (
|
||||
displacement if axis == "x" else 0,
|
||||
displacement if axis == "y" else 0,
|
||||
displacement if axis == "z" else 0,
|
||||
)
|
||||
move = np.zeros(self.n_axes, dtype=np.int)
|
||||
move[np.argmax(np.array(self.axis_names) == axis)] = int(displacement)
|
||||
displacement = move
|
||||
|
||||
initial_move = np.array(displacement, dtype=np.int)
|
||||
|
|
@ -154,7 +167,7 @@ class SangaStage(BaseStage):
|
|||
# can just take over before settling
|
||||
time.sleep(self.settle_time)
|
||||
|
||||
def move_abs(self, final, **kwargs):
|
||||
def move_abs(self, final: Tuple[int, int, int], **kwargs):
|
||||
"""Make an absolute move to a position
|
||||
"""
|
||||
with self.lock:
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ import copy
|
|||
import logging
|
||||
import time
|
||||
from contextlib import contextmanager
|
||||
from uuid import UUID
|
||||
from typing import Dict, List, Optional
|
||||
|
||||
import numpy as np
|
||||
|
||||
|
|
@ -22,12 +22,12 @@ class Timer(object):
|
|||
logging.debug("%s time: %s", self.name, self.end - self.start)
|
||||
|
||||
|
||||
def deserialise_array_b64(b64_string, dtype, shape):
|
||||
def deserialise_array_b64(b64_string: str, dtype: str, shape: List[int]):
|
||||
flat_arr = np.frombuffer(base64.b64decode(b64_string), dtype)
|
||||
return flat_arr.reshape(shape)
|
||||
|
||||
|
||||
def serialise_array_b64(npy_arr):
|
||||
def serialise_array_b64(npy_arr: np.ndarray):
|
||||
b64_string = base64.b64encode(npy_arr).decode("ascii")
|
||||
dtype = str(npy_arr.dtype)
|
||||
shape = npy_arr.shape
|
||||
|
|
@ -50,9 +50,14 @@ def json_to_ndarray(json_dict: dict):
|
|||
if not json_dict.get(required_param):
|
||||
raise KeyError(f"Missing required key {required_param}")
|
||||
|
||||
return deserialise_array_b64(
|
||||
json_dict.get("base64"), json_dict.get("dtype"), json_dict.get("shape")
|
||||
)
|
||||
b64_string: Optional[str] = json_dict.get("base64")
|
||||
dtype: Optional[str] = json_dict.get("dtype")
|
||||
shape: Optional[List[int]] = json_dict.get("shape")
|
||||
|
||||
if b64_string and dtype and shape:
|
||||
return deserialise_array_b64(b64_string, dtype, shape)
|
||||
else:
|
||||
raise ValueError("Required parameters for decoding are missing")
|
||||
|
||||
|
||||
@contextmanager
|
||||
|
|
@ -82,8 +87,11 @@ def set_properties(obj, **kwargs):
|
|||
|
||||
|
||||
def axes_to_array(
|
||||
coordinate_dictionary, axis_keys=("x", "y", "z"), base_array=None, asint=True
|
||||
):
|
||||
coordinate_dictionary: Dict[str, int],
|
||||
axis_keys=("x", "y", "z"),
|
||||
base_array: Optional[List[int]] = None,
|
||||
asint: bool = True,
|
||||
) -> List[int]:
|
||||
"""Takes key-value pairs of a JSON value, and maps onto an array"""
|
||||
# If no base array is given
|
||||
if not base_array:
|
||||
|
|
@ -101,21 +109,3 @@ def axes_to_array(
|
|||
)
|
||||
|
||||
return base_array
|
||||
|
||||
|
||||
def entry_by_uuid(entry_id: str, object_list: list):
|
||||
"""Return an object from a list, if <object>.id matches id argument."""
|
||||
found = None
|
||||
if type(entry_id) == str:
|
||||
converter = str
|
||||
elif type(entry_id) == int:
|
||||
converter = int
|
||||
elif isinstance(entry_id, UUID):
|
||||
converter = int
|
||||
else:
|
||||
raise TypeError("Argument entry_id must be a string, integer, or UUID object.")
|
||||
for o in object_list:
|
||||
# Convert to strings (in case of UUID objects, for example)
|
||||
if converter(o.id) == converter(entry_id):
|
||||
found = o
|
||||
return found
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue