Merge branch 'master' into imjoy-support
This commit is contained in:
commit
dac1c9a748
21 changed files with 994 additions and 2122 deletions
|
|
@ -23,7 +23,7 @@ import os
|
|||
from datetime import datetime
|
||||
|
||||
import pkg_resources
|
||||
from flask import abort, send_file
|
||||
from flask import abort, send_file, jsonify
|
||||
from flask_cors import CORS, cross_origin
|
||||
from labthings import create_app
|
||||
from labthings.extensions import find_extensions
|
||||
|
|
@ -105,6 +105,20 @@ app, labthing = create_app(
|
|||
# Enable CORS for some routes outside of LabThings
|
||||
cors: CORS = CORS(app)
|
||||
|
||||
# Enable correct handling of Marshmallow/Webargs validation errors
|
||||
# Return validation errors as JSON
|
||||
# (see https://webargs.readthedocs.io/en/latest/framework_support.html)
|
||||
@app.errorhandler(422)
|
||||
@app.errorhandler(400)
|
||||
def handle_error(err):
|
||||
headers = err.data.get("headers", None)
|
||||
messages = err.data.get("messages", ["Invalid request."])
|
||||
if headers:
|
||||
return jsonify({"errors": messages}), err.code, headers
|
||||
else:
|
||||
return jsonify({"errors": messages}), err.code
|
||||
|
||||
|
||||
# Use custom JSON encoder
|
||||
labthing.json_encoder = JSONEncoder
|
||||
app.json_encoder = JSONEncoder
|
||||
|
|
|
|||
|
|
@ -83,6 +83,15 @@ class LoggingMoveWrapper:
|
|||
self._history: List[Tuple[float, Optional[CoordinateType]]] = []
|
||||
|
||||
|
||||
class CSMUncalibratedError(RuntimeError):
|
||||
"""A calibrated camera stage mapper is required, but this one is not calibrated.
|
||||
|
||||
The camera stage mapper requires calibration information to relate image pixels
|
||||
to stage coordinates. If a method attempts to retrieve this calibration before
|
||||
it exists, we raise this exception.
|
||||
"""
|
||||
|
||||
|
||||
class CSMExtension(BaseExtension):
|
||||
"""
|
||||
Use the camera as an encoder, so we can relate camera and stage coordinates
|
||||
|
|
@ -133,7 +142,12 @@ class CSMExtension(BaseExtension):
|
|||
def get_settings(self) -> Dict[str, Any]:
|
||||
"""Retrieve the settings for this extension"""
|
||||
keys: List[str] = ["extensions", self.name]
|
||||
return get_by_path(self.microscope.read_settings(), keys)
|
||||
try:
|
||||
return get_by_path(self.microscope.read_settings(), keys)
|
||||
except KeyError as exc:
|
||||
raise CSMUncalibratedError(
|
||||
"Camera stage mapping calibration data is missing"
|
||||
) from exc
|
||||
|
||||
def camera_stage_functions(self) -> Tuple[Callable, Callable, Callable, Callable]:
|
||||
"""Return functions that allow us to interface with the microscope"""
|
||||
|
|
@ -194,10 +208,13 @@ class CSMExtension(BaseExtension):
|
|||
@property
|
||||
def image_to_stage_displacement_matrix(self) -> np.ndarray: # 2x2 integer array
|
||||
"""A 2x2 matrix that converts displacement in image coordinates to stage coordinates."""
|
||||
displacement_matrix = self.get_settings().get("image_to_stage_displacement")
|
||||
if not displacement_matrix:
|
||||
raise ValueError("The microscope has not yet been calibrated.")
|
||||
return np.array(displacement_matrix)
|
||||
settings = self.get_settings()
|
||||
try:
|
||||
return np.array(settings["image_to_stage_displacement"])
|
||||
except KeyError as exc:
|
||||
raise CSMUncalibratedError(
|
||||
"The microscope has not yet been calibrated."
|
||||
) from exc
|
||||
|
||||
def move_in_image_coordinates(self, displacement_in_pixels: XYCoordinateType):
|
||||
"""Move by a given number of pixels on the camera"""
|
||||
|
|
|
|||
|
|
@ -1,19 +1,23 @@
|
|||
import logging
|
||||
from contextlib import contextmanager
|
||||
from typing import Iterator, Tuple
|
||||
|
||||
import picamerax
|
||||
import labthings.fields as fields
|
||||
from flask import abort
|
||||
from labthings import find_component
|
||||
from labthings.extensions import BaseExtension
|
||||
from labthings.views import ActionView
|
||||
from picamerax import PiCamera
|
||||
|
||||
from openflexure_microscope.camera.base import BaseCamera
|
||||
from openflexure_microscope.microscope import Microscope
|
||||
|
||||
from .recalibrate_utils import (
|
||||
auto_expose_and_freeze_settings,
|
||||
adjust_shutter_and_gain_from_raw,
|
||||
adjust_white_balance_from_raw,
|
||||
flat_lens_shading_table,
|
||||
recalibrate_camera,
|
||||
get_channel_percentiles,
|
||||
lst_from_camera,
|
||||
)
|
||||
|
||||
|
||||
|
|
@ -61,64 +65,178 @@ class LSTExtension(BaseExtension):
|
|||
"/delete_lens_shading_table",
|
||||
endpoint="delete_lens_shading_table",
|
||||
)
|
||||
self.add_view(
|
||||
GetRawChannelPercentilesView,
|
||||
"/get_raw_channel_percentiles",
|
||||
endpoint="get_raw_channel_percentiles",
|
||||
)
|
||||
self.add_view(
|
||||
AutoExposureFromRawView,
|
||||
"/auto_exposure_from_raw",
|
||||
endpoint="auto_exposure_from_raw",
|
||||
)
|
||||
self.add_view(
|
||||
AutoWhiteBalanceFromRawView,
|
||||
"/auto_white_balance_from_raw",
|
||||
endpoint="auto_white_balance_from_raw",
|
||||
)
|
||||
self.add_view(
|
||||
AutoLensShadingTableView,
|
||||
"/auto_lens_shading_table",
|
||||
endpoint="auto_lens_shading_table",
|
||||
)
|
||||
|
||||
def recalibrate(self, microscope: Microscope):
|
||||
|
||||
@contextmanager
|
||||
def find_picamera() -> Iterator[Tuple[PiCamera, BaseCamera, Microscope]]:
|
||||
"""Locate the microscope and raise a sensible error if it's missing."""
|
||||
microscope = find_component("org.openflexure.microscope")
|
||||
|
||||
if not microscope:
|
||||
abort(
|
||||
503, "No microscope connected. Unable to use camera calibration functions."
|
||||
)
|
||||
|
||||
scamera = microscope.camera
|
||||
|
||||
if not hasattr(scamera, "picamera"):
|
||||
abort(503, "The PiCamera calibration plugin requires a Raspberry Pi camera.")
|
||||
|
||||
with scamera.lock:
|
||||
yield scamera.picamera, scamera, microscope
|
||||
|
||||
|
||||
class RecalibrateView(ActionView):
|
||||
def post(self):
|
||||
"""Reset the camera's settings.
|
||||
|
||||
This generates new gains, exposure time, and lens shading
|
||||
table such that the background is as uniform as possible
|
||||
with a gray level of 230. It takes a little while to run.
|
||||
|
||||
This consists of three steps:
|
||||
|
||||
* Reset gain and exposure time, then increase them until
|
||||
we have a suitably bright image
|
||||
* Set the auto white balance based on a raw image
|
||||
* Set the lens shading table based on a raw image
|
||||
|
||||
Each of these steps has its own endpoint if you want to
|
||||
perform them separately.
|
||||
"""
|
||||
with pause_stream(microscope.camera) as scamera:
|
||||
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"
|
||||
)
|
||||
with find_picamera() as (picamera, scamera, microscope):
|
||||
logging.info("Starting microscope recalibration...")
|
||||
adjust_shutter_and_gain_from_raw(picamera)
|
||||
adjust_white_balance_from_raw(picamera)
|
||||
lst = lst_from_camera(picamera)
|
||||
with pause_stream(scamera):
|
||||
picamera.lens_shading_table = lst
|
||||
microscope.save_settings()
|
||||
|
||||
|
||||
class RecalibrateView(ActionView):
|
||||
class AutoLensShadingTableView(ActionView):
|
||||
def post(self):
|
||||
microscope = find_component("org.openflexure.microscope")
|
||||
"""Perform flat-field correction
|
||||
|
||||
if not microscope:
|
||||
abort(503, "No microscope connected. Unable to recalibrate.")
|
||||
|
||||
logging.info("Starting microscope recalibration...")
|
||||
|
||||
return self.extension.recalibrate(microscope)
|
||||
This routine acquires a new image (which should be an
|
||||
empty field of view, i.e. a perfect microscope would
|
||||
record a uniform white image), and then uses it to set
|
||||
the "lens shading table" such that future images will
|
||||
be corrected for vignetting.
|
||||
"""
|
||||
with find_picamera() as (picamera, scamera, microscope):
|
||||
logging.info("Generating lens shading table")
|
||||
lst = lst_from_camera(picamera)
|
||||
with pause_stream(scamera):
|
||||
picamera.lens_shading_table = lst
|
||||
microscope.save_settings()
|
||||
|
||||
|
||||
class FlattenLSTView(ActionView):
|
||||
def post(self):
|
||||
microscope = find_component("org.openflexure.microscope")
|
||||
|
||||
if not microscope:
|
||||
abort(
|
||||
503,
|
||||
"No microscope connected. Unable to flatten the lens shading table.",
|
||||
)
|
||||
|
||||
with pause_stream(microscope.camera) as scamera:
|
||||
flat_lst = flat_lens_shading_table(scamera.camera)
|
||||
scamera.camera.lens_shading_table = flat_lst
|
||||
microscope.save_settings()
|
||||
with find_picamera() as (picamera, scamera, microscope):
|
||||
with pause_stream(scamera):
|
||||
flat_lst = flat_lens_shading_table(picamera)
|
||||
picamera.lens_shading_table = flat_lst
|
||||
microscope.save_settings()
|
||||
|
||||
|
||||
class DeleteLSTView(ActionView):
|
||||
def post(self):
|
||||
microscope = find_component("org.openflexure.microscope")
|
||||
with find_picamera() as (picamera, scamera, microscope):
|
||||
with pause_stream(scamera):
|
||||
picamera.lens_shading_table = None
|
||||
microscope.save_settings()
|
||||
|
||||
if not microscope:
|
||||
abort(
|
||||
503,
|
||||
"No microscope connected. Unable to flatten the lens shading table.",
|
||||
)
|
||||
|
||||
with pause_stream(microscope.camera) as scamera:
|
||||
scamera.camera.lens_shading_table = None
|
||||
microscope.save_settings()
|
||||
class AutoExposureFromRawView(ActionView):
|
||||
args = {
|
||||
"target_white_level": fields.Int(
|
||||
missing=700,
|
||||
example=700,
|
||||
description=(
|
||||
"The pixel value (10-bit format) that we aim for when adjusting shutter/gain."
|
||||
),
|
||||
),
|
||||
"max_iterations": fields.Int(
|
||||
missing=20,
|
||||
description=(
|
||||
"The number of adjustments to the camera's settings to make before giving up."
|
||||
),
|
||||
),
|
||||
"tolerance": fields.Float(
|
||||
missing=0.05,
|
||||
example=0.05,
|
||||
description=(
|
||||
"We stop adjusting when we get within this fraction of the target "
|
||||
"value. It is a number between 0 and 1, usually 0.01--0.1."
|
||||
),
|
||||
),
|
||||
"percentile": fields.Float(
|
||||
missing=99.9,
|
||||
example=99.9,
|
||||
description=(
|
||||
"A float between 0 and 100 setting the centile to use "
|
||||
"to measure the white point of the image. A value "
|
||||
"of 99.9 allows 0.1% of the pixels to be erroneously "
|
||||
"bright - this helps stability in low light."
|
||||
),
|
||||
),
|
||||
}
|
||||
|
||||
def post(self, args):
|
||||
with find_picamera() as (picamera, _, _):
|
||||
adjust_shutter_and_gain_from_raw(picamera, **args)
|
||||
|
||||
|
||||
class AutoWhiteBalanceFromRawView(ActionView):
|
||||
args = {
|
||||
"percentile": fields.Float(
|
||||
missing=99.9,
|
||||
example=99.9,
|
||||
description=(
|
||||
"A float between 0 and 100 setting the centile to use "
|
||||
"to measure the white point of the image. A value "
|
||||
"of 99.9 allows 0.1% of the pixels to be erroneously "
|
||||
"bright - this helps stability in low light."
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
def post(self, args):
|
||||
with find_picamera() as (picamera, _, _):
|
||||
adjust_white_balance_from_raw(picamera, **args)
|
||||
|
||||
|
||||
class GetRawChannelPercentilesView(ActionView):
|
||||
args = {
|
||||
"percentile": fields.Float(
|
||||
example=99.9,
|
||||
description="A float between 0 and 100 setting the centile to calculate",
|
||||
)
|
||||
}
|
||||
schema = fields.List(fields.Integer)
|
||||
|
||||
def post(self, args):
|
||||
with find_picamera() as (picamera, _, _):
|
||||
return get_channel_percentiles(picamera, args["percentile"])
|
||||
|
|
|
|||
|
|
@ -1,7 +1,39 @@
|
|||
"""
|
||||
Functions to set up a Raspberry Pi Camera v2 for scientific use
|
||||
|
||||
This module provides slower, simpler functions to set the
|
||||
gain, exposure, and white balance of a Raspberry Pi camera, using
|
||||
`picamerax` (a fork of `picamera`) to get as-manual-as-possible
|
||||
control over the camera. It's mostly used by the OpenFlexure
|
||||
Microscope, though it deliberately has no hard dependencies on
|
||||
said software, so that it's useful on its own.
|
||||
|
||||
There are three main calibration steps:
|
||||
|
||||
* Setting exposure time and gain to get a reasonably bright
|
||||
image.
|
||||
* Fixing the white balance to get a neutral image
|
||||
* Taking a uniform white image and using it to calibrate
|
||||
the Lens Shading Table
|
||||
|
||||
The most reliable way to do this, avoiding any issues relating
|
||||
to "memory" or nonlinearities in the camera's image processing
|
||||
pipeline, is to use raw images. This is quite slow, but very
|
||||
reliable. The three steps above can be accomplished by:
|
||||
|
||||
```
|
||||
picamera = picamerax.PiCamera()
|
||||
|
||||
adjust_shutter_and_gain_from_raw(picamera)
|
||||
adjust_white_balance_from_raw(picamera)
|
||||
lst = lst_from_camera(picamera)
|
||||
picamera.lens_shading_table = lst
|
||||
```
|
||||
"""
|
||||
|
||||
import logging
|
||||
import time
|
||||
from fractions import Fraction
|
||||
from typing import List, Optional, Tuple
|
||||
from typing import List, NamedTuple, Optional, Tuple
|
||||
|
||||
import numpy as np
|
||||
from picamerax import PiCamera
|
||||
|
|
@ -33,48 +65,190 @@ def flat_lens_shading_table(camera: PiCamera) -> np.ndarray:
|
|||
|
||||
|
||||
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="")
|
||||
"""Adjust the camera's exposure time until the maximum pixel value is <setpoint>.
|
||||
|
||||
NB this method uses RGB images (i.e. processed ones) not raw images.
|
||||
"""
|
||||
logging.info(f"Adjusting shutter speed to hit setpoint {setpoint}")
|
||||
for _ in range(3):
|
||||
print(".", end="")
|
||||
camera.shutter_speed = int(
|
||||
camera.shutter_speed * setpoint / np.max(rgb_image(camera))
|
||||
)
|
||||
time.sleep(1)
|
||||
print("done")
|
||||
|
||||
|
||||
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:
|
||||
print("Calibrating with greyworld AWB")
|
||||
camera.awb_mode = "greyworld"
|
||||
else:
|
||||
print("Calibrating with auto AWB")
|
||||
camera.awb_mode = "auto"
|
||||
camera.exposure_mode = "auto"
|
||||
camera.iso = (
|
||||
0 # This is important, if it's on a fixed ISO, gain might not set properly.
|
||||
)
|
||||
for _ in range(6):
|
||||
print(".", end="")
|
||||
time.sleep(0.5)
|
||||
logging.info("done")
|
||||
|
||||
logging.info("Freezing the camera settings...")
|
||||
camera.shutter_speed = camera.exposure_speed
|
||||
logging.info("Shutter speed = %s", (camera.shutter_speed))
|
||||
def set_minimum_exposure(camera: PiCamera):
|
||||
"""Enable manual exposure, with low gain and shutter speed
|
||||
|
||||
We set exposure mode to manual, analog and digital gain
|
||||
to 1, and shutter speed to the minimum (8us for Pi Camera v2)
|
||||
NB ISO is left at auto, because this is needed for the gains
|
||||
to be set correctly.
|
||||
"""
|
||||
camera.exposure_mode = "off"
|
||||
logging.info("Auto exposure disabled")
|
||||
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))
|
||||
camera.iso = 0 # We must set ISO=0 (auto) or we can't set gain
|
||||
camera.analog_gain = 1
|
||||
camera.digital_gain = 1
|
||||
# Setting the shutter speed to 1us will result in it being set
|
||||
# to the minimum possible, which is probably 8us for PiCamera v2
|
||||
camera.shutter_speed = 1
|
||||
time.sleep(0.5)
|
||||
|
||||
|
||||
class ExposureTest(NamedTuple):
|
||||
"""Record the results of testing the camera's current exposure settings"""
|
||||
|
||||
level: int
|
||||
shutter_speed: int
|
||||
analog_gain: float
|
||||
|
||||
|
||||
def test_exposure_settings(camera: PiCamera, percentile: float) -> ExposureTest:
|
||||
"""Evaluate current exposure settings using a raw image
|
||||
|
||||
We will acquire a raw image and calculate the given percentile
|
||||
of the pixel values. We return a dictionary containing the
|
||||
percentile (which will be compared to the target), as well as
|
||||
the camera's shutter and gain values.
|
||||
"""
|
||||
max_brightness = np.max(get_channel_percentiles(camera, percentile))
|
||||
# The reported brightness can, theoretically, be negative or zero
|
||||
# because of black level compensation. The line below forces a
|
||||
# minimum value of 1 which will keep things well-behaved!
|
||||
if max_brightness < 1:
|
||||
logging.warning(
|
||||
f"Measured brightness of {max_brightness}. "
|
||||
"This should normally be >= 1, and may indicate the "
|
||||
"camera's black level compensation has gone wrong."
|
||||
)
|
||||
max_brightness = 1
|
||||
shutter_speed = int(camera.shutter_speed)
|
||||
analog_gain = float(camera.analog_gain)
|
||||
logging.info(
|
||||
"Analogue gain: %s, Digital gain: %s", camera.analog_gain, camera.digital_gain
|
||||
f"Brightness: {max_brightness: >5.0f}, "
|
||||
f"Gain: {analog_gain: >4.1f}, "
|
||||
f"Shutter: {shutter_speed: >7.0f}"
|
||||
)
|
||||
adjust_exposure_to_setpoint(camera, 215)
|
||||
return ExposureTest(max_brightness, shutter_speed, analog_gain)
|
||||
|
||||
|
||||
def check_convergence(test: ExposureTest, target: int, tolerance: float):
|
||||
"""Check whether the brightness is within the specified target range"""
|
||||
converged = abs(test.level - target) < target * tolerance
|
||||
return converged
|
||||
|
||||
|
||||
def adjust_shutter_and_gain_from_raw(
|
||||
camera: PiCamera,
|
||||
target_white_level: int = 700,
|
||||
max_iterations: int = 20,
|
||||
tolerance: float = 0.05,
|
||||
percentile: float = 99.9,
|
||||
) -> float:
|
||||
"""Adjust exposure and analog gain based on raw images.
|
||||
|
||||
This routine is slow but effective. It uses raw images, so we
|
||||
are not affected by white balance or digital gain.
|
||||
|
||||
|
||||
Arguments:
|
||||
target_white_level:
|
||||
The raw, 10-bit value we aim for. The brightest pixels
|
||||
should be approximately this bright. Maximum possible
|
||||
is about 900, 700 is reasonable.
|
||||
max_iterations:
|
||||
We will terminate once we perform this many iterations,
|
||||
whether or not we converge. More than 10 shouldn't happen.
|
||||
tolerance:
|
||||
How close to the target value we consider "done". Expressed
|
||||
as a fraction of the ``target_white_level`` so 0.05 means
|
||||
+/- 5%
|
||||
percentile:
|
||||
Rather then use the maximum value for each channel, we
|
||||
calculate a percentile. This makes us robust to single
|
||||
pixels that are bright/noisy. 99.9% still picks the top
|
||||
of the brightness range, but seems much more reliable
|
||||
than just ``np.max()``.
|
||||
"""
|
||||
if target_white_level * (tolerance + 1) >= 959:
|
||||
raise ValueError(
|
||||
"The target level is too high - a saturated image would be "
|
||||
"considered successful. target_white_level * (tolerance + 1) "
|
||||
"must be less than 959."
|
||||
)
|
||||
|
||||
set_minimum_exposure(camera)
|
||||
|
||||
# We start with very low exposure settings and work up
|
||||
# until either the brightness is high enough, or we can't increase the
|
||||
# shutter speed any more.
|
||||
iterations = 0
|
||||
while iterations < max_iterations:
|
||||
test = test_exposure_settings(camera, percentile)
|
||||
|
||||
if check_convergence(test, target_white_level, tolerance):
|
||||
break
|
||||
iterations += 1
|
||||
|
||||
# Adjust shutter speed so that the brightness approximates the target
|
||||
# NB we put a maximum of 32 on this, to stop it increasing too quickly.
|
||||
camera.shutter_speed = int(
|
||||
test.shutter_speed * min(target_white_level / test.level, 32)
|
||||
)
|
||||
time.sleep(0.5)
|
||||
|
||||
# Check whether the shutter speed is still going up - if not, we've hit a maximum
|
||||
if camera.shutter_speed == test.shutter_speed:
|
||||
logging.info("Shutter speed has maxed out.")
|
||||
break
|
||||
|
||||
# Now, if we've not converged, increase gain until we converge or run out of options.
|
||||
while iterations < max_iterations:
|
||||
test = test_exposure_settings(camera, percentile)
|
||||
if check_convergence(test, target_white_level, tolerance):
|
||||
break
|
||||
iterations += 1
|
||||
|
||||
# Adjust gain to make the white level hit the target, again with a maximum
|
||||
camera.analog_gain *= min(target_white_level / test.level, 2)
|
||||
time.sleep(0.5)
|
||||
|
||||
# Check the gain is still changing - if not, we have probably hit the maximum
|
||||
if camera.analog_gain == test.analog_gain:
|
||||
logging.info("Gain has maxed out.")
|
||||
break
|
||||
|
||||
if check_convergence(test, target_white_level, tolerance):
|
||||
logging.info(f"Brightness has converged to within {tolerance * 100 :.0f}%.")
|
||||
else:
|
||||
logging.warning(
|
||||
f"Failed to reach target brightness of {target_white_level}."
|
||||
f"Brightness reached {test.level} after {iterations} iterations."
|
||||
)
|
||||
|
||||
return test.level
|
||||
|
||||
|
||||
def adjust_white_balance_from_raw(
|
||||
camera: PiCamera, percentile: float = 99
|
||||
) -> Tuple[float, float]:
|
||||
"""Adjust the white balance in a single shot, based on the raw image.
|
||||
|
||||
NB if ``channels_from_raw_image`` is broken, this will go haywire.
|
||||
We should probably have better logic to verify the channels really
|
||||
are BGGR...
|
||||
"""
|
||||
blue, g1, g2, red = get_channel_percentiles(camera, percentile)
|
||||
green = (g1 + g2) / 2.0
|
||||
new_awb_gains = (green / red, green / blue)
|
||||
logging.info(
|
||||
f"Raw white point is R: {red} G: {green} B: {blue}, "
|
||||
f"setting AWB gains to ({new_awb_gains[0]:.2f}, "
|
||||
f"{new_awb_gains[1]:.2f})."
|
||||
)
|
||||
camera.awb_mode = "off"
|
||||
camera.awb_gains = new_awb_gains
|
||||
return new_awb_gains
|
||||
|
||||
|
||||
def channels_from_bayer_array(bayer_array: np.ndarray) -> np.ndarray:
|
||||
|
|
@ -95,6 +269,21 @@ def channels_from_bayer_array(bayer_array: np.ndarray) -> np.ndarray:
|
|||
return channels
|
||||
|
||||
|
||||
def get_channel_percentiles(camera: PiCamera, percentile: float) -> np.ndarray:
|
||||
"""Calculate the brightness percentile of the pixels in each channel
|
||||
|
||||
This is a number between -64 and 959 for each channel, because the
|
||||
camera takes 10-bit images (maximum=1023) and its zero level is set
|
||||
at 64 for denoising purposes (there's black level compensation built
|
||||
in, and to avoid skewing the noise, the black level is set as 64 to
|
||||
leave some room for negative values.
|
||||
"""
|
||||
with PiBayerArray(camera) as output:
|
||||
camera.capture(output, format="jpeg", bayer=True)
|
||||
channels = channels_from_bayer_array(output.array)
|
||||
return np.percentile(channels, percentile, axis=(1, 2)) - 64
|
||||
|
||||
|
||||
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.ndarray = np.array(
|
||||
|
|
@ -165,6 +354,20 @@ def lst_from_channels(channels: np.ndarray) -> np.ndarray:
|
|||
return lens_shading_table[::-1, :, :].copy()
|
||||
|
||||
|
||||
def lst_from_camera(camera: PiCamera) -> np.ndarray:
|
||||
"""Acquire a raw image and use it to calculate a lens shading table."""
|
||||
with PiBayerArray(camera) as a:
|
||||
camera.capture(a, format="jpeg", bayer=True)
|
||||
raw_image = a.array.copy()
|
||||
|
||||
# Now we need to calculate a lens shading table that would make this flat.
|
||||
# raw_image is a 3D array, with full resolution and 3 colour channels. No
|
||||
# de-mosaicing has been done, so 2/3 of the values are zero (3/4 for R and B
|
||||
# channels, 1/2 for green because there's twice as many green pixels).
|
||||
channels = channels_from_bayer_array(raw_image)
|
||||
return lst_from_channels(channels)
|
||||
|
||||
|
||||
def recalibrate_camera(camera: PiCamera):
|
||||
"""Reset the lens shading table and exposure settings.
|
||||
|
||||
|
|
@ -178,16 +381,7 @@ def recalibrate_camera(camera: PiCamera):
|
|||
camera.lens_shading_table = flat_lens_shading_table(camera)
|
||||
_ = rgb_image(camera) # for some reason the camera won't work unless I do this!
|
||||
|
||||
with PiBayerArray(camera) as a:
|
||||
camera.capture(a, format="jpeg", bayer=True)
|
||||
raw_image = a.array.copy()
|
||||
|
||||
# Now we need to calculate a lens shading table that would make this flat.
|
||||
# raw_image is a 3D array, with full resolution and 3 colour channels. No
|
||||
# de-mosaicing has been done, so 2/3 of the values are zero (3/4 for R and B
|
||||
# channels, 1/2 for green because there's twice as many green pixels).
|
||||
channels = channels_from_bayer_array(raw_image)
|
||||
lens_shading_table = lst_from_channels(channels)
|
||||
lens_shading_table = lst_from_camera(camera)
|
||||
|
||||
camera.lens_shading_table = lens_shading_table
|
||||
_ = rgb_image(camera)
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ import datetime
|
|||
import logging
|
||||
import time
|
||||
import uuid
|
||||
import marshmallow
|
||||
from functools import reduce
|
||||
from typing import Dict, List, Optional, Tuple
|
||||
|
||||
|
|
@ -361,7 +362,11 @@ class ScanExtension(BaseExtension):
|
|||
|
||||
class TileScanArgs(FullCaptureArgs):
|
||||
namemode = fields.String(missing="coordinates", example="coordinates")
|
||||
grid = fields.List(fields.Integer, missing=[3, 3, 3], example=[3, 3, 3])
|
||||
grid = fields.List(
|
||||
fields.Integer(validate=marshmallow.validate.Range(min=1)),
|
||||
missing=[3, 3, 3],
|
||||
example=[3, 3, 3],
|
||||
)
|
||||
style = fields.String(missing="raster")
|
||||
autofocus_dz = fields.Integer(missing=50)
|
||||
fast_autofocus = fields.Boolean(missing=False)
|
||||
|
|
|
|||
34
openflexure_microscope/api/static/package-lock.json
generated
34
openflexure_microscope/api/static/package-lock.json
generated
|
|
@ -3935,38 +3935,12 @@
|
|||
"dev": true
|
||||
},
|
||||
"axios": {
|
||||
"version": "0.19.2",
|
||||
"resolved": "https://registry.npmjs.org/axios/-/axios-0.19.2.tgz",
|
||||
"integrity": "sha512-fjgm5MvRHLhx+osE2xoekY70AhARk3a6hkN+3Io1jc00jtquGvxYlKlsFUhmUET0V5te6CcZI7lcv2Ym61mjHA==",
|
||||
"version": "0.21.1",
|
||||
"resolved": "https://registry.npmjs.org/axios/-/axios-0.21.1.tgz",
|
||||
"integrity": "sha512-dKQiRHxGD9PPRIUNIWvZhPTPpl1rf/OxTYKsqKUDjBwYylTvV7SjSHJb9ratfyzM6wCdLCOYLzs73qpg5c4iGA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"follow-redirects": "1.5.10"
|
||||
},
|
||||
"dependencies": {
|
||||
"debug": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz",
|
||||
"integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"ms": "2.0.0"
|
||||
}
|
||||
},
|
||||
"follow-redirects": {
|
||||
"version": "1.5.10",
|
||||
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.5.10.tgz",
|
||||
"integrity": "sha512-0V5l4Cizzvqt5D44aTXbFZz+FtyXV1vrDN6qrelxtfYQKW0KO0W2T/hkE8xvGa/540LkZlkaUjO4ailYTFtHVQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"debug": "=3.1.0"
|
||||
}
|
||||
},
|
||||
"ms": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
|
||||
"integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=",
|
||||
"dev": true
|
||||
}
|
||||
"follow-redirects": "^1.10.0"
|
||||
}
|
||||
},
|
||||
"babel-eslint": {
|
||||
|
|
|
|||
|
|
@ -23,7 +23,7 @@
|
|||
"@vue/cli-plugin-eslint": "^4.5.9",
|
||||
"@vue/cli-service": "^4.5.9",
|
||||
"@vue/eslint-config-prettier": "^6.0.0",
|
||||
"axios": "^0.19.2",
|
||||
"axios": "^0.21.1",
|
||||
"babel-eslint": "^10.1.0",
|
||||
"css-loader": "^3.6.0",
|
||||
"eslint": "^6.8.0",
|
||||
|
|
|
|||
|
|
@ -194,6 +194,8 @@ import loggingContent from "./tabContentComponents/loggingContent.vue";
|
|||
import calibrationModal from "./modalComponents/calibrationModal.vue";
|
||||
import TabIcon from "./genericComponents/tabIcon.vue";
|
||||
|
||||
import { mapState } from "vuex";
|
||||
|
||||
// Export main app
|
||||
export default {
|
||||
name: "AppContent",
|
||||
|
|
@ -302,21 +304,22 @@ export default {
|
|||
return process.env.VUE_APP_ENABLE_IMJOY === "true";
|
||||
},
|
||||
|
||||
...mapState("imjoy", { imjoyTabs: "tabs" })
|
||||
// Map the tabs from ImJoy's store module so we can display them
|
||||
...mapState("imjoy", { imjoyTabs: "tabs" }),
|
||||
|
||||
// Map the setting for IHI's interface so we can watch it
|
||||
...mapState(["IHIEnabled"])
|
||||
},
|
||||
|
||||
watch: {
|
||||
// Update the interface when the IHI interface is enabled/disabled
|
||||
IHIEnabled: function(newValue) {
|
||||
this.updateTopTabs(newValue);
|
||||
}
|
||||
},
|
||||
|
||||
created: function() {
|
||||
if (this.$store.getters.ready) {
|
||||
// Detect local connection
|
||||
if (
|
||||
["localhost", "0.0.0.0", "127.0.0.1", "[::1]"].includes(
|
||||
window.location.hostname
|
||||
)
|
||||
) {
|
||||
this.$store.commit("changeDisableStream", true);
|
||||
this.$store.commit("changeAutoGpuPreview", true);
|
||||
this.$store.commit("changeTrackWindow", true);
|
||||
}
|
||||
// Update top tabs
|
||||
this.updateTopTabs(this.$store.state.IHIEnabled);
|
||||
// Update plugins
|
||||
|
|
@ -325,16 +328,6 @@ export default {
|
|||
this.startModals();
|
||||
});
|
||||
}
|
||||
|
||||
// Watch for host 'ready', then update status
|
||||
this.unwatchStoreFunction = this.$store.watch(
|
||||
state => {
|
||||
return state.IHIEnabled;
|
||||
},
|
||||
IHIEnabled => {
|
||||
this.updateTopTabs(IHIEnabled);
|
||||
}
|
||||
);
|
||||
},
|
||||
|
||||
mounted() {
|
||||
|
|
@ -352,14 +345,6 @@ export default {
|
|||
});
|
||||
},
|
||||
|
||||
beforeDestroy() {
|
||||
// Then we call that function here to unwatch
|
||||
if (this.unwatchStoreFunction) {
|
||||
this.unwatchStoreFunction();
|
||||
this.unwatchStoreFunction = null;
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
updateTopTabs: function(IHIEnabled) {
|
||||
if (IHIEnabled) {
|
||||
|
|
|
|||
|
|
@ -1,12 +1,36 @@
|
|||
<template>
|
||||
<div>
|
||||
<form class="uk-form-stacked" @submit.prevent="overrideAPIHost">
|
||||
<form
|
||||
class="uk-form-stacked"
|
||||
action=""
|
||||
method="GET"
|
||||
@submit="overrideAPIHost"
|
||||
>
|
||||
<label class="uk-form-label">Override API origin</label>
|
||||
<input v-model="newOrigin" class="uk-input" type="text" />
|
||||
<input
|
||||
v-model="newOrigin"
|
||||
name="overrideOrigin"
|
||||
class="uk-input"
|
||||
type="text"
|
||||
/>
|
||||
<label class="uk-form-label">
|
||||
<input
|
||||
v-model="reloadWhenOverridingOrigin"
|
||||
class="uk-input uk-checkbox"
|
||||
type="checkbox"
|
||||
/>
|
||||
Reload web app with new origin
|
||||
</label>
|
||||
<button class="uk-button uk-button-default uk-margin-small">
|
||||
Apply
|
||||
</button>
|
||||
</form>
|
||||
<form class="uk-form-stacked" @submit.prevent="resetTour">
|
||||
<label class="uk-form-label">Re-run tour on next load</label>
|
||||
<button class="uk-button uk-button-default uk-margin-small">
|
||||
Reset
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
|
@ -19,22 +43,35 @@ export default {
|
|||
|
||||
data: function() {
|
||||
return {
|
||||
newOrigin: this.$store.state.origin
|
||||
newOrigin: this.$store.state.origin,
|
||||
reloadWhenOverridingOrigin: true
|
||||
};
|
||||
},
|
||||
|
||||
mounted() {
|
||||
if (localStorage.overrideOrigin){
|
||||
if (localStorage.overrideOrigin) {
|
||||
this.newOrigin = localStorage.overrideOrigin;
|
||||
}else{
|
||||
} else {
|
||||
this.newOrigin = "http://microscope.local:5000";
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
overrideAPIHost: function() {
|
||||
this.$store.commit("changeOrigin", this.newOrigin);
|
||||
localStorage.overrideOrigin = this.newOrigin
|
||||
overrideAPIHost: function(event) {
|
||||
// Save the origin override, so that if we reload the web app, you can easily
|
||||
localStorage.overrideOrigin = this.newOrigin;
|
||||
|
||||
// If we have elected not to reload the interface, just update the origin
|
||||
// in the store. Otherwise, the form's default action will do the job for us.
|
||||
// TODO: preserve other query parameters when reloading
|
||||
if (!this.reloadWhenOverridingOrigin) {
|
||||
this.$store.commit("changeOrigin", this.newOrigin);
|
||||
event.preventDefault();
|
||||
}
|
||||
},
|
||||
resetTour: function() {
|
||||
// Make the introduction tour run next time the app loads
|
||||
this.setLocalStorageObj("completedTour", false);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
|
|
|||
|
|
@ -155,7 +155,7 @@
|
|||
v-model="scanStepSize.z"
|
||||
class="uk-input uk-form-small"
|
||||
type="number"
|
||||
name="inputPositionZx"
|
||||
name="inputPositionZ"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -172,6 +172,7 @@
|
|||
class="uk-input uk-form-small"
|
||||
type="number"
|
||||
name="inputPositionX"
|
||||
min="1"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -186,6 +187,7 @@
|
|||
class="uk-input uk-form-small"
|
||||
type="number"
|
||||
name="inputPositionY"
|
||||
min="1"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -199,7 +201,8 @@
|
|||
v-model="scanSteps.z"
|
||||
class="uk-input uk-form-small"
|
||||
type="number"
|
||||
name="inputPositionZx"
|
||||
name="inputPositionZ"
|
||||
min="1"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
<div class="uk-grid uk-grid-divider uk-child-width-expand" uk-grid>
|
||||
<div class="uk-width-large">
|
||||
<h3>Manual camera settings</h3>
|
||||
<form @submit.prevent="applySettingsRequest">
|
||||
<form @submit.prevent="applySettingsRequest" >
|
||||
<div class="uk-margin-small-bottom">
|
||||
<ul uk-accordion="multiple: true">
|
||||
<li class="uk-open">
|
||||
|
|
@ -31,7 +31,7 @@
|
|||
v-model="picamera.analog_gain"
|
||||
class="uk-input uk-form-small"
|
||||
type="number"
|
||||
step="0.000001"
|
||||
step="0.0000000000000001"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -45,7 +45,29 @@
|
|||
v-model="picamera.digital_gain"
|
||||
class="uk-input uk-form-small"
|
||||
type="number"
|
||||
step="0.000001"
|
||||
step="0.0000000000000001"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="picamera.awb_gains !== undefined">
|
||||
<label class="uk-form-label" for="form-stacked-text" >
|
||||
White Balance gains
|
||||
</label>
|
||||
<div class="uk-form-controls">
|
||||
<label class="uk-form-label" >R:</label>
|
||||
<input
|
||||
v-model="picamera.awb_gains[0]"
|
||||
class="uk-input uk-form-small"
|
||||
type="number"
|
||||
step="0.0000000000000001"
|
||||
/>
|
||||
<label class="uk-form-label" >B:</label>
|
||||
<input
|
||||
v-model="picamera.awb_gains[1]"
|
||||
class="uk-input uk-form-small"
|
||||
type="number"
|
||||
step="0.0000000000000001"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -194,7 +216,8 @@ export default {
|
|||
shutter_speed: undefined,
|
||||
analog_gain: undefined,
|
||||
digital_gain: undefined,
|
||||
framerate: undefined
|
||||
framerate: undefined,
|
||||
awb_gains: undefined
|
||||
},
|
||||
mjpeg_bitrate: undefined,
|
||||
stream_resolution: undefined,
|
||||
|
|
@ -244,6 +267,7 @@ export default {
|
|||
this.picamera.digital_gain = cameraSettings.picamera.digital_gain;
|
||||
this.picamera.shutter_speed = cameraSettings.picamera.shutter_speed;
|
||||
this.picamera.framerate = cameraSettings.picamera.framerate;
|
||||
this.picamera.awb_gains = cameraSettings.picamera.awb_gains;
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
|
|
@ -263,7 +287,11 @@ export default {
|
|||
shutter_speed: parseFloat(this.picamera.shutter_speed),
|
||||
analog_gain: parseFloat(this.picamera.analog_gain),
|
||||
digital_gain: parseFloat(this.picamera.digital_gain),
|
||||
framerate: parseInt(this.picamera.framerate)
|
||||
framerate: parseInt(this.picamera.framerate),
|
||||
awb_gains: [
|
||||
parseFloat(this.picamera.awb_gains[0]),
|
||||
parseFloat(this.picamera.awb_gains[1]),
|
||||
],
|
||||
}
|
||||
}
|
||||
};
|
||||
|
|
|
|||
|
|
@ -9,7 +9,44 @@
|
|||
'Start recalibration? This may take a while, and the microscope will be locked during this time.'
|
||||
"
|
||||
:submit-url="recalibrationLinks.recalibrate.href"
|
||||
:submit-label="'Auto-Calibrate'"
|
||||
:submit-label="'Full Auto-Calibrate'"
|
||||
@response="onRecalibrateResponse"
|
||||
@error="modalError"
|
||||
>
|
||||
</taskSubmitter>
|
||||
</div>
|
||||
<div v-if="'auto_exposure_from_raw' in recalibrationLinks" class="uk-margin-small">
|
||||
<taskSubmitter
|
||||
:can-terminate="false"
|
||||
:requires-confirmation="false"
|
||||
:submit-url="recalibrationLinks.auto_exposure_from_raw.href"
|
||||
:submit-label="'Auto gain & shutter speed'"
|
||||
@response="onRecalibrateResponse"
|
||||
@error="modalError"
|
||||
>
|
||||
</taskSubmitter>
|
||||
</div>
|
||||
<div v-if="'auto_white_balance_from_raw' in recalibrationLinks" class="uk-margin-small">
|
||||
<taskSubmitter
|
||||
:can-terminate="false"
|
||||
:requires-confirmation="false"
|
||||
:submit-url="recalibrationLinks.auto_white_balance_from_raw.href"
|
||||
:submit-label="'Auto white balance'"
|
||||
@response="onRecalibrateResponse"
|
||||
@error="modalError"
|
||||
>
|
||||
</taskSubmitter>
|
||||
</div>
|
||||
<div v-if="'auto_lens_shading_table' in recalibrationLinks" class="uk-margin-small">
|
||||
<taskSubmitter
|
||||
:can-terminate="false"
|
||||
:requires-confirmation="true"
|
||||
:confirmation-message="
|
||||
'Is the microscope looking at an evenly illuminated, empty field of view? ' +
|
||||
'If not, the current image will show through in any images captured afterwards.'
|
||||
"
|
||||
:submit-url="recalibrationLinks.auto_lens_shading_table.href"
|
||||
:submit-label="'Auto flat field correction'"
|
||||
@response="onRecalibrateResponse"
|
||||
@error="modalError"
|
||||
>
|
||||
|
|
@ -22,15 +59,7 @@
|
|||
class="uk-button uk-button-danger uk-width-1-1"
|
||||
@click="flattenLensShadingTableRequest"
|
||||
>
|
||||
Disable flat-field correction
|
||||
</button>
|
||||
|
||||
<button
|
||||
v-if="'delete_lens_shading_table' in recalibrationLinks"
|
||||
class="uk-button uk-button-danger uk-margin-small-top uk-width-1-1"
|
||||
@click="deleteLensShadingTableRequest"
|
||||
>
|
||||
Adaptive flat-field correction
|
||||
Disable flat field correction
|
||||
</button>
|
||||
</div>
|
||||
|
||||
|
|
|
|||
|
|
@ -33,7 +33,6 @@ export default {
|
|||
},
|
||||
set(value) {
|
||||
this.$store.commit("changeIHIEnabled", value);
|
||||
this.$root.$emit("globalSafeTogglePreview", value);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
|
|
|||
|
|
@ -73,8 +73,11 @@ export default {
|
|||
return this.$store.state.autoGpuPreview;
|
||||
},
|
||||
set(value) {
|
||||
// NB the stream viewer watches the store, and is
|
||||
// responsible for making the request that switches
|
||||
// GPU preview on/off
|
||||
// see streamContent.vue
|
||||
this.$store.commit("changeAutoGpuPreview", value);
|
||||
this.$root.$emit("globalSafeTogglePreview", value);
|
||||
}
|
||||
},
|
||||
|
||||
|
|
@ -86,6 +89,51 @@ export default {
|
|||
this.$store.commit("changeTrackWindow", value);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
watch: {
|
||||
// Cache the stream settings to local storage for persistence
|
||||
// (the next 3 functions all relate to this)
|
||||
disableStream: function(newValue) {
|
||||
console.log(
|
||||
`disableStream updated to ${newValue} and saved in local storage`
|
||||
);
|
||||
this.setLocalStorageObj("disableStream", newValue);
|
||||
},
|
||||
autoGpuPreview: function(newValue) {
|
||||
console.log(
|
||||
`GPU preview updated to ${newValue} and saved in local storage`
|
||||
);
|
||||
this.setLocalStorageObj("autoGpuPreview", newValue);
|
||||
},
|
||||
trackWindow: function(newValue) {
|
||||
console.log(
|
||||
`trackWindow updated to ${newValue} and saved in local storage`
|
||||
);
|
||||
this.setLocalStorageObj("trackWindow", newValue);
|
||||
}
|
||||
},
|
||||
|
||||
created() {
|
||||
// Apply sensible defaults for stream settings, depending on
|
||||
// whether we're connecting locally or remotely, respecting
|
||||
// the settings that were cached previously.
|
||||
const localMode = ["localhost", "0.0.0.0", "127.0.0.1", "[::1]"].includes(
|
||||
window.location.hostname
|
||||
);
|
||||
const localDefaults = {
|
||||
disableStream: true,
|
||||
autoGpuPreview: true,
|
||||
trackWindow: true
|
||||
};
|
||||
for (let k in localDefaults) {
|
||||
if (localStorage.getItem(k) !== null) {
|
||||
this[k] = this.getLocalStorageObj(k);
|
||||
} else if (localMode) {
|
||||
console.log(`${k} set to default value for a local connection`);
|
||||
this[k] = localDefaults[k];
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
|
|
|||
|
|
@ -69,6 +69,16 @@ export default {
|
|||
},
|
||||
settingsUri: function() {
|
||||
return `${this.$store.getters.baseUri}/api/v2/instrument/settings`;
|
||||
},
|
||||
autoGpuPreview: function() {
|
||||
return this.$store.state.autoGpuPreview;
|
||||
}
|
||||
},
|
||||
|
||||
watch: {
|
||||
autoGpuPreview: function(newValue) {
|
||||
// When the GPU preview setting in the store changes, update the server
|
||||
this.safePreviewRequest(newValue);
|
||||
}
|
||||
},
|
||||
|
||||
|
|
@ -77,9 +87,6 @@ export default {
|
|||
this.$root.$on("globalTogglePreview", state => {
|
||||
this.previewRequest(state);
|
||||
});
|
||||
this.$root.$on("globalSafeTogglePreview", state => {
|
||||
this.safePreviewRequest(state);
|
||||
});
|
||||
// A global signal listener to flash the stream element
|
||||
this.$root.$on("globalFlashStream", () => {
|
||||
this.flashStream();
|
||||
|
|
@ -97,7 +104,7 @@ export default {
|
|||
|
||||
created: function() {
|
||||
// Send a request to start/stop GPU preview based on global setting
|
||||
this.safePreviewRequest(this.$store.state.autoGpuPreview);
|
||||
this.safePreviewRequest(this.autoGpuPreview);
|
||||
},
|
||||
|
||||
beforeDestroy: function() {
|
||||
|
|
|
|||
|
|
@ -61,12 +61,25 @@ const moduleImjoy = {
|
|||
getters: {}
|
||||
};
|
||||
|
||||
function getOriginFromLocation() {
|
||||
// This will default to the same origin that's serving
|
||||
// the web app - but can be overridden by the URL.
|
||||
// See also devTools.vue which can change the origin.
|
||||
let url = new URL(window.location.href);
|
||||
let origin = url.searchParams.get("overrideOrigin");
|
||||
if (origin) {
|
||||
return origin;
|
||||
} else {
|
||||
return url.origin;
|
||||
}
|
||||
}
|
||||
|
||||
export default new Vuex.Store({
|
||||
modules: {
|
||||
imjoy: moduleImjoy
|
||||
},
|
||||
state: {
|
||||
origin: window.location.origin,
|
||||
origin: getOriginFromLocation(),
|
||||
available: false,
|
||||
waiting: false,
|
||||
error: "",
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue