From c7aa6f376a0de76283003270994c543efff2646a Mon Sep 17 00:00:00 2001 From: Eric Bezzam Date: Wed, 29 Nov 2023 17:17:48 +0100 Subject: [PATCH] Add utility for checking capture config. --- configs/capture.yaml | 3 +- lensless/hardware/utils.py | 59 +++++++++++++++++++++++++++- lensless/utils/image.py | 19 +++++---- scripts/measure/on_device_capture.py | 53 +++---------------------- 4 files changed, 75 insertions(+), 59 deletions(-) diff --git a/configs/capture.yaml b/configs/capture.yaml index e723cc48..6392ebc2 100644 --- a/configs/capture.yaml +++ b/configs/capture.yaml @@ -7,7 +7,8 @@ exp: 0.5 config_pause: 2 sensor_mode: "0" # {'off': 0, 'auto': 1, 'sunlight': 2, 'cloudy': 3, 'shade': 4, 'tungsten': 5, 'fluorescent': 6, 'incandescent': 7, 'flash': 8, 'horizon': 9} iso: 100 -awb_gains: null +# awb_gains: null # to use gains estimte by system +awb_gains: [1.9, 1.2] # -- only one of below should be set down: null # downsample from maximum resolution diff --git a/lensless/hardware/utils.py b/lensless/hardware/utils.py index adadbc1c..c49cce21 100644 --- a/lensless/hardware/utils.py +++ b/lensless/hardware/utils.py @@ -6,7 +6,7 @@ import paramiko from pprint import pprint from paramiko.ssh_exception import AuthenticationException, BadHostKeyException, SSHException -from lensless.hardware.sensor import SensorOptions +from lensless.hardware.sensor import SensorOptions, sensor_dict, SensorParam import cv2 from lensless.utils.image import print_image_info from lensless.utils.io import load_image @@ -17,6 +17,63 @@ logging.getLogger("paramiko").setLevel(logging.WARNING) +def check_capture_config(config): + + sensor = config.sensor + nbits_bayer = config.nbits_bayer + exp = config.exp + rgb = config.rgb + gray = config.gray + legacy = config.legacy + down = config.down + res = config.res + nbits_out = config.nbits_out + awb_gains = config.awb_gains + + assert sensor in SensorOptions.values(), f"Sensor must be one of {SensorOptions.values()}" + + assert res is None or down is None, "Cannot specify both res and down" + if res is not None: + assert len(res) == 2, "res must be a tuple of length 2" + + assert rgb is False or gray is False, "Cannot set both rgb and gray" + + supported_bit_depth = sensor_dict[sensor][SensorParam.BIT_DEPTH] + assert ( + nbits_out in supported_bit_depth + ), f"nbits_out must be one of {supported_bit_depth} for sensor {sensor}" + assert nbits_bayer in [8, 16], "nbits_bayer must be 8 or 16" + if nbits_bayer == 16: + # TODO may not work if there are multiple bit depths above 8 + nbits_measured = max(supported_bit_depth) + assert nbits_measured > 8 + else: + nbits_measured = 8 + + if SensorParam.BLACK_LEVEL in sensor_dict[sensor]: + black_level = sensor_dict[sensor][SensorParam.BLACK_LEVEL] * (2**nbits_measured - 1) + else: + black_level = 0 + if SensorParam.CCM_MATRIX in sensor_dict[sensor]: + ccm = sensor_dict[sensor][SensorParam.CCM_MATRIX] + else: + ccm = None + + # https://www.raspberrypi.com/documentation/accessories/camera.html#hardware-specification + sensor_param = sensor_dict[sensor] + assert exp <= sensor_param[SensorParam.MAX_EXPOSURE] + assert exp >= sensor_param[SensorParam.MIN_EXPOSURE] + + if awb_gains is not None: + assert len(awb_gains) == 2, "awb_gains must be a tuple of length 2" + + if sensor == SensorOptions.RPI_GS.value: + assert not legacy, "Legacy capture software not supported for Global Shutter sensor" + + # return sensor parameters + return black_level, ccm, supported_bit_depth, nbits_measured + + def capture( rpi_username, rpi_hostname, diff --git a/lensless/utils/image.py b/lensless/utils/image.py index 5d21318f..e9be1a57 100644 --- a/lensless/utils/image.py +++ b/lensless/utils/image.py @@ -270,25 +270,24 @@ def bayer2rgb_cc( if ccm is None: ccm = np.eye(3) - ccm = np.array(ccm).astype(np.float32) + ccm = ccm.copy().astype(np.float32) # demosaic Bayer data - img = cv2.cvtColor(img, cv2.COLOR_BayerRG2RGB) + img = cv2.cvtColor(img, cv2.COLOR_BayerRG2RGB).astype(np.float32) # correction - img = img - black_level - img = np.clip(img, 0, 2**nbits - 1).astype(np.float32) - + img -= black_level if red_gain: img[:, :, 0] *= red_gain if blue_gain: img[:, :, 2] *= blue_gain - img = img / (2**nbits - 1 - black_level) + np.clip(img, 0, 2**nbits - 1, out=img) + img /= 2**nbits - 1 - black_level img[img > 1] = 1 - img_re = img.reshape(-1, 3, order="F") - img_re = img_re @ ccm.T - img = img_re.reshape(img.shape, order="F") - # img = (img.reshape(-1, 3, order="F") @ ccm.T).reshape(img.shape, order="F") + # img_re = img.reshape(-1, 3, order="F") + # img_re = img_re @ ccm.T + # img = img_re.reshape(img.shape, order="F") + img = (img.reshape(-1, 3, order="F") @ ccm.T).reshape(img.shape, order="F") img[img < 0] = 0 img[img > 1] = 1 return (img * (2**nbits_out - 1)).astype(dtype) diff --git a/scripts/measure/on_device_capture.py b/scripts/measure/on_device_capture.py index a2cb05d5..8196a79f 100644 --- a/scripts/measure/on_device_capture.py +++ b/scripts/measure/on_device_capture.py @@ -31,9 +31,8 @@ import numpy as np from time import sleep from PIL import Image -from lensless.hardware.utils import get_distro +from lensless.hardware.utils import get_distro, check_capture_config from lensless.utils.image import bayer2rgb_cc, rgb2gray, resize -from lensless.hardware.constants import RPI_HQ_CAMERA_CCM_MATRIX, RPI_HQ_CAMERA_BLACK_LEVEL from lensless.hardware.sensor import SensorOptions, sensor_dict, SensorParam from fractions import Fraction import time @@ -56,13 +55,14 @@ @hydra.main(version_base=None, config_path="../../configs", config_name="capture") def capture(config): - sensor = config.sensor + black_level, ccm, supported_bit_depth, nbits_measured = check_capture_config(config) + bayer = config.bayer nbits_bayer = config.nbits_bayer fn = config.fn exp = config.exp config_pause = config.config_pause - sensor_mode = config.sensor_mode + sensor_mode = int(config.sensor_mode) rgb = config.rgb gray = config.gray iso = config.iso @@ -71,44 +71,9 @@ def capture(config): res = config.res nbits_out = config.nbits_out - assert sensor in SensorOptions.values(), f"Sensor must be one of {SensorOptions.values()}" - - assert res is None or down is None, "Cannot specify both res and down" - assert rgb is False or gray is False, "Cannot set both rgb and gray" - - supported_bit_depth = sensor_dict[sensor][SensorParam.BIT_DEPTH] - assert ( - nbits_out in supported_bit_depth - ), f"nbits_out must be one of {supported_bit_depth} for sensor {sensor}" - assert nbits_bayer in [8, 16], "nbits_bayer must be 8 or 16" - if nbits_bayer == 16: - # TODO may not work if there are multiple bit depths above 8 - nbits_measured = max(supported_bit_depth) - assert nbits_measured > 8 - else: - nbits_measured = 8 - - if SensorParam.BLACK_LEVEL in sensor_dict[sensor]: - black_level = sensor_dict[sensor][SensorParam.BLACK_LEVEL] * (2**nbits_measured - 1) - else: - black_level = 0 - if SensorParam.CCM_MATRIX in sensor_dict[sensor]: - ccm = sensor_dict[sensor][SensorParam.CCM_MATRIX] - else: - ccm = None - - # https://www.raspberrypi.com/documentation/accessories/camera.html#hardware-specification - sensor_param = sensor_dict[sensor] - assert exp <= sensor_param[SensorParam.MAX_EXPOSURE] - assert exp >= sensor_param[SensorParam.MIN_EXPOSURE] - sensor_mode = int(sensor_mode) - distro = get_distro() print("RPi distribution : {}".format(distro)) - if sensor == SensorOptions.RPI_GS.value: - assert not legacy - if "bullseye" in distro and not legacy: # TODO : grayscale and downsample assert not rgb @@ -161,9 +126,7 @@ def capture(config): fn += ".png" max_res = picam2.camera_properties["PixelArraySize"] - if res: - assert len(res) == 2 - else: + if res is None: res = np.array(max_res) if down is not None: res = (np.array(res) / down).astype(int) @@ -184,7 +147,6 @@ def capture(config): "AnalogueGain": 1.0, } if config.awb_gains is not None: - assert len(config.awb_gains) == 2 new_controls["ColourGains"] = tuple(config.awb_gains) picam2.set_controls(new_controls) @@ -281,9 +243,7 @@ def capture(config): from picamerax import PiCamera camera = PiCamera() - if res: - assert len(res) == 2 - else: + if res is None: res = np.array(camera.MAX_RESOLUTION) if down is not None: res = (np.array(res) / down).astype(int) @@ -295,7 +255,6 @@ def capture(config): time.sleep(config.config_pause) if config.awb_gains is not None: - assert len(config.awb_gains) == 2 g = (Fraction(config.awb_gains[0]), Fraction(config.awb_gains[1])) g = tuple(g) camera.awb_mode = "off"