242 lines
8 KiB
Python
242 lines
8 KiB
Python
import logging
|
|
from contextlib import contextmanager
|
|
from typing import Tuple, Generator
|
|
|
|
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 (
|
|
adjust_shutter_and_gain_from_raw,
|
|
adjust_white_balance_from_raw,
|
|
flat_lens_shading_table,
|
|
get_channel_percentiles,
|
|
lst_from_camera,
|
|
)
|
|
|
|
|
|
@contextmanager
|
|
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
|
|
block has finished.
|
|
"""
|
|
with scamera.lock:
|
|
assert (
|
|
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.stream_resolution
|
|
if streaming:
|
|
logging.info("Stopping stream in pause_stream context manager")
|
|
scamera.stop_stream()
|
|
try:
|
|
yield scamera
|
|
finally:
|
|
scamera.stream_resolution = old_resolution
|
|
if streaming:
|
|
logging.info("Restarting stream in pause_stream context manager")
|
|
scamera.start_stream()
|
|
|
|
|
|
class LSTExtension(BaseExtension):
|
|
def __init__(self) -> None:
|
|
super().__init__(
|
|
"org.openflexure.calibration.picamera",
|
|
version="2.0.0-beta.1",
|
|
description="Routines to perform flat-field correction on the camera.",
|
|
)
|
|
|
|
self.add_view(RecalibrateView, "/recalibrate", endpoint="recalibrate")
|
|
self.add_view(
|
|
FlattenLSTView,
|
|
"/flatten_lens_shading_table",
|
|
endpoint="flatten_lens_shading_table",
|
|
)
|
|
self.add_view(
|
|
DeleteLSTView,
|
|
"/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",
|
|
)
|
|
|
|
|
|
@contextmanager
|
|
def find_picamera() -> Generator[Tuple[PiCamera, BaseCamera, Microscope], None, None]:
|
|
"""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 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 AutoLensShadingTableView(ActionView):
|
|
def post(self):
|
|
"""Perform flat-field correction
|
|
|
|
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):
|
|
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):
|
|
with find_picamera() as (picamera, scamera, microscope):
|
|
with pause_stream(scamera):
|
|
picamera.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"])
|