From 79c94c96edef88e204e5d3403714518a655bdf0c Mon Sep 17 00:00:00 2001 From: Julian Stirling Date: Mon, 17 Nov 2025 14:55:45 +0000 Subject: [PATCH] Set channel minima and error if any LUV channels have std==0 --- .../background_detect.py | 17 +++++++++++++++++ .../things/camera/picamera.py | 14 +++++++++++--- 2 files changed, 28 insertions(+), 3 deletions(-) diff --git a/src/openflexure_microscope_server/background_detect.py b/src/openflexure_microscope_server/background_detect.py index ece3fd6e..d31afc9a 100644 --- a/src/openflexure_microscope_server/background_detect.py +++ b/src/openflexure_microscope_server/background_detect.py @@ -18,6 +18,14 @@ class MissingBackgroundDataError(RuntimeError): """An error raised if checking for sample without background data set.""" +class ChannelBlankError(RuntimeError): + """An error raised if a channel has no measured standard deviation. + + This is not physical and usually means the camera has not yet booted or changed + mode fully. + """ + + class BackgroundDetectorStatus(BaseModel): """The status information about a background detector instance needed for the GUI. @@ -277,6 +285,12 @@ class ChannelDeviationLUV(BackgroundDetectAlgorithm): background_data_model: BaseModel = ChannelDistributions settings_data_model: BaseModel = ColourChannelDetectSettings + # Empirically, 0.5 seems to be approximate the standard deviation for a good image + # in L and V. U appears to be about 60% of this value. U is about 65% of V when + # converting white-noise in RGB into LUV (note that we are using cv2's internal + # LUV colour space not converting to the CIELUV numbers) + min_stds = [0.5, 0.3, 0.5] + def get_sample_coverage(self, image: np.ndarray) -> float: """Return the percentage of the input image that is background. @@ -323,6 +337,9 @@ class ChannelDeviationLUV(BackgroundDetectAlgorithm): mu = np.zeros(3) std = np.median(_chunked_stds(image_luv, 8, 8), axis=(0, 1)) + if np.any(std == 0): + raise ChannelBlankError("Some LUV channels have no standard devaition.") + std = np.maximum(std, self.min_stds) self.background_data = ChannelDistributions( means=mu.tolist(), standard_deviations=std.tolist() diff --git a/src/openflexure_microscope_server/things/camera/picamera.py b/src/openflexure_microscope_server/things/camera/picamera.py index ff7c8867..918c28f5 100644 --- a/src/openflexure_microscope_server/things/camera/picamera.py +++ b/src/openflexure_microscope_server/things/camera/picamera.py @@ -42,6 +42,7 @@ from openflexure_microscope_server.ui import ( action_button_for, property_control_for, ) +from openflexure_microscope_server.background_detect import ChannelBlankError from . import picamera_recalibrate_utils as recalibrate_utils from . import picamera_tuning_file_utils as tf_utils @@ -767,7 +768,6 @@ class StreamingPiCamera2(BaseCamera): * ``auto_expose_from_minimum`` * ``set_static_green_equalisation`` to set geq offset to max * ``calibrate_lens_shading`` (also sets colour gains for white balance) - * ``set_background`` """ self.flat_lens_shading() @@ -775,8 +775,16 @@ class StreamingPiCamera2(BaseCamera): self.set_static_green_equalisation() self.set_ce_enable_to_off() self.calibrate_lens_shading() - time.sleep(0.5) - self.set_background(portal) + for _i in range(3): + try: + time.sleep(1) + self.set_background(portal) + # Return if background is set + return + except ChannelBlankError: + # If channel is blank, sleep a second and try again. + pass + raise RuntimeError("Couldn't set background") @lt.thing_property def primary_calibration_actions(self) -> list[ActionButton]: