Fix auto-exposure and AWB
The previous "auto calibrate" button for the camera relied on a combination of the built-in autoexposure/AWB and some JPEG-based tweaks to get the exposure settings right. This often led to confusing results, e.g. oscillating between green and pink images. I have rewritten the auto-exposure code to adjust gain and shutter speed based on the raw image. This seems to be very reliable, at the expense of being quite slow. That's a price I'm happy to pay. I have replaced both the camera's auto white balance and my JPEG-based AWB hack with a single-shot AWB method that looks at the raw image.
This commit is contained in:
parent
864e4c9c41
commit
5527c4eb7b
2 changed files with 320 additions and 50 deletions
|
|
@ -1,6 +1,8 @@
|
|||
import logging
|
||||
from contextlib import contextmanager
|
||||
|
||||
import labthings.fields as fields
|
||||
import numpy as np
|
||||
import picamerax
|
||||
from flask import abort
|
||||
from labthings import find_component
|
||||
|
|
@ -13,7 +15,11 @@ from openflexure_microscope.microscope import Microscope
|
|||
from .recalibrate_utils import (
|
||||
auto_expose_and_freeze_settings,
|
||||
flat_lens_shading_table,
|
||||
get_channel_percentiles,
|
||||
recalibrate_camera,
|
||||
adjust_shutter_and_gain_from_raw,
|
||||
adjust_white_balance_from_raw,
|
||||
lst_from_camera,
|
||||
)
|
||||
|
||||
|
||||
|
|
@ -61,48 +67,71 @@ 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",
|
||||
)
|
||||
|
||||
|
||||
|
||||
def recalibrate(self, microscope: Microscope):
|
||||
|
||||
def find_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."
|
||||
)
|
||||
|
||||
if not hasattr(microscope.camera, "picamera"):
|
||||
abort(
|
||||
503, "The PiCamera calibration plugin requires a Raspberry Pi camera."
|
||||
)
|
||||
|
||||
return microscope
|
||||
|
||||
|
||||
class RecalibrateView(ActionView):
|
||||
args = {
|
||||
"skip_auto_exposure": fields.Bool(
|
||||
missing=False,
|
||||
description="Whether to "
|
||||
)
|
||||
}
|
||||
def post(self, args):
|
||||
"""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.
|
||||
"""
|
||||
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"
|
||||
)
|
||||
|
||||
|
||||
class RecalibrateView(ActionView):
|
||||
def post(self):
|
||||
microscope = find_component("org.openflexure.microscope")
|
||||
|
||||
if not microscope:
|
||||
abort(503, "No microscope connected. Unable to recalibrate.")
|
||||
|
||||
microscope = find_microscope()
|
||||
logging.info("Starting microscope recalibration...")
|
||||
|
||||
return self.extension.recalibrate(microscope)
|
||||
picamera = microscope.camera.picamera
|
||||
if not args.get("skip_auto_exposure"):
|
||||
adjust_shutter_and_gain_from_raw(picamera)
|
||||
adjust_white_balance_from_raw(picamera)
|
||||
lst = lst_from_camera(picamera)
|
||||
with pause_stream(microscope.camera) as scamera:
|
||||
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.",
|
||||
)
|
||||
|
||||
microscope = find_microscope()
|
||||
with pause_stream(microscope.camera) as scamera:
|
||||
flat_lst = flat_lens_shading_table(scamera.camera)
|
||||
scamera.camera.lens_shading_table = flat_lst
|
||||
|
|
@ -111,14 +140,86 @@ class FlattenLSTView(ActionView):
|
|||
|
||||
class DeleteLSTView(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.",
|
||||
)
|
||||
|
||||
microscope = find_microscope()
|
||||
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):
|
||||
camera = find_component("org.openflexure.microscope").camera.picamera
|
||||
adjust_shutter_and_gain_from_raw(
|
||||
camera,
|
||||
**args # I'm relying on the schema to fill in missing values
|
||||
)
|
||||
|
||||
|
||||
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):
|
||||
camera = find_component("org.openflexure.microscope").camera.picamera
|
||||
adjust_white_balance_from_raw(
|
||||
camera,
|
||||
**args # I'm relying on the schema to fill in missing values
|
||||
)
|
||||
|
||||
|
||||
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):
|
||||
camera = find_component("org.openflexure.microscope").camera.picamera
|
||||
return get_channel_percentiles(camera, args["percentile"])
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue