diff --git a/configs/capture.yaml b/configs/capture.yaml index 18b6c030..680edca5 100644 --- a/configs/capture.yaml +++ b/configs/capture.yaml @@ -16,4 +16,6 @@ down: null res: null nbits_out: 8 awb_gains: null +auto_exp_psf: false +auto_exp_img: false # awb_gains: [1.9, 1.2] # red, blue \ No newline at end of file diff --git a/scripts/measure/on_device_capture.py b/scripts/measure/on_device_capture.py index 00c8c470..039c2a51 100644 --- a/scripts/measure/on_device_capture.py +++ b/scripts/measure/on_device_capture.py @@ -37,7 +37,8 @@ from lensless.hardware.sensor import SensorOptions, sensor_dict, SensorParam from fractions import Fraction import time - +import copy +from lensless.utils.io import load_image SENSOR_MODES = [ "off", @@ -53,7 +54,6 @@ ] -@hydra.main(version_base=None, config_path="../../configs", config_name="capture") def capture(config): sensor = config.sensor @@ -183,78 +183,80 @@ def capture(config): if bayer: camera = picamerax.PiCamera(framerate=1 / exp, sensor_mode=sensor_mode, resolution=res) - - # camera settings, as little processing as possible - camera.iso = iso - camera.shutter_speed = int(exp * 1e6) - camera.exposure_mode = "off" - camera.drc_strength = "off" - camera.image_denoise = False - camera.image_effect = "none" - camera.still_stats = False - - sleep(config_pause) - awb_gains = camera.awb_gains - camera.awb_mode = "off" - camera.awb_gains = awb_gains - - print("Resolution : {}".format(camera.resolution)) - print("Shutter speed : {}".format(camera.shutter_speed)) - print("ISO : {}".format(camera.iso)) - print("Frame rate : {}".format(camera.framerate)) - print("Sensor mode : {}".format(SENSOR_MODES[sensor_mode])) - # keep this as it needs to be parsed from remote script! - red_gain = float(awb_gains[0]) - blue_gain = float(awb_gains[1]) - print("Red gain : {}".format(red_gain)) - print("Blue gain : {}".format(blue_gain)) - - # capture data - stream = picamerax.array.PiBayerArray(camera) - camera.capture(stream, "jpeg", bayer=True) - - # get bayer data - if sixteen: - output = np.sum(stream.array, axis=2).astype(np.uint16) - else: - output = (np.sum(stream.array, axis=2) >> 2).astype(np.uint8) - - # returning non-bayer data - if rgb or gray: + try: + # camera settings, as little processing as possible + camera.iso = iso + camera.shutter_speed = int(exp * 1e6) + camera.exposure_mode = "off" + camera.drc_strength = "off" + camera.image_denoise = False + camera.image_effect = "none" + camera.still_stats = False + + sleep(config_pause) + awb_gains = camera.awb_gains + camera.awb_mode = "off" + camera.awb_gains = awb_gains + + print("Resolution : {}".format(camera.resolution)) + print("Shutter speed : {}".format(camera.shutter_speed)) + print("ISO : {}".format(camera.iso)) + print("Frame rate : {}".format(camera.framerate)) + print("Sensor mode : {}".format(SENSOR_MODES[sensor_mode])) + # keep this as it needs to be parsed from remote script! + red_gain = float(awb_gains[0]) + blue_gain = float(awb_gains[1]) + print("Red gain : {}".format(red_gain)) + print("Blue gain : {}".format(blue_gain)) + + # capture data + stream = picamerax.array.PiBayerArray(camera) + camera.capture(stream, "jpeg", bayer=True) + + # get bayer data if sixteen: - n_bits = 12 # assuming Raspberry Pi HQ + output = np.sum(stream.array, axis=2).astype(np.uint16) else: - n_bits = 8 - - if config.awb_gains is not None: - red_gain = config.awb_gains[0] - blue_gain = config.awb_gains[1] - - output_rgb = bayer2rgb_cc( - output, - nbits=n_bits, - blue_gain=blue_gain, - red_gain=red_gain, - black_level=RPI_HQ_CAMERA_BLACK_LEVEL, - ccm=RPI_HQ_CAMERA_CCM_MATRIX, - nbits_out=nbits_out, - ) - - if down: - output_rgb = resize( - output_rgb[None, ...], 1 / down, interpolation=cv2.INTER_CUBIC - )[0] - - # need OpenCV to save 16-bit RGB image - if gray: - output_gray = rgb2gray(output_rgb[None, ...]) - output_gray = output_gray.astype(output_rgb.dtype).squeeze() - cv2.imwrite(fn, output_gray) + output = (np.sum(stream.array, axis=2) >> 2).astype(np.uint8) + + # returning non-bayer data + if rgb or gray: + if sixteen: + n_bits = 12 # assuming Raspberry Pi HQ + else: + n_bits = 8 + + if config.awb_gains is not None: + red_gain = config.awb_gains[0] + blue_gain = config.awb_gains[1] + + output_rgb = bayer2rgb_cc( + output, + nbits=n_bits, + blue_gain=blue_gain, + red_gain=red_gain, + black_level=RPI_HQ_CAMERA_BLACK_LEVEL, + ccm=RPI_HQ_CAMERA_CCM_MATRIX, + nbits_out=nbits_out, + ) + + if down: + output_rgb = resize( + output_rgb[None, ...], 1 / down, interpolation=cv2.INTER_CUBIC + )[0] + + # need OpenCV to save 16-bit RGB image + if gray: + output_gray = rgb2gray(output_rgb[None, ...]) + output_gray = output_gray.astype(output_rgb.dtype).squeeze() + cv2.imwrite(fn, output_gray) + else: + cv2.imwrite(fn, cv2.cvtColor(output_rgb, cv2.COLOR_RGB2BGR)) else: - cv2.imwrite(fn, cv2.cvtColor(output_rgb, cv2.COLOR_RGB2BGR)) - else: - img = Image.fromarray(output) - img.save(fn) + img = Image.fromarray(output) + img.save(fn) + finally: + camera.close() else: @@ -271,31 +273,176 @@ def capture(config): # -- now set up camera with desired settings camera = PiCamera(framerate=1 / exp, sensor_mode=sensor_mode, resolution=tuple(res)) + try: + # Wait for the automatic gain control to settle + time.sleep(config.config_pause) - # Wait for the automatic gain control to settle - 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" + camera.awb_gains = g + time.sleep(0.1) + + print("Capturing at resolution: ", res) + print("AWB gains: ", float(camera.awb_gains[0]), float(camera.awb_gains[1])) + + try: + camera.resolution = tuple(res) + camera.capture(fn) + except ValueError: + raise ValueError( + "Out of resources! Use bayer for higher resolution, or increase `gpu_mem` in `/boot/config.txt`." + ) + finally: + camera.close() + - 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" - camera.awb_gains = g - time.sleep(0.1) + print("Image saved to : {}".format(fn)) - print("Capturing at resolution: ", res) - print("AWB gains: ", float(camera.awb_gains[0]), float(camera.awb_gains[1])) +def auto_expose_psf_locally(config): + import copy + import numpy as np - try: - camera.resolution = tuple(res) - camera.capture(fn) - except ValueError: - raise ValueError( - "Out of resources! Use bayer for higher resolution, or increase `gpu_mem` in `/boot/config.txt`." - ) + max_iter = 10 + exp = config.exp + tested_exposures = [] + + min_exp = sensor_dict[config.sensor][SensorParam.MIN_EXPOSURE] + max_exp = sensor_dict[config.sensor][SensorParam.MAX_EXPOSURE] + + i = 0 + while i < max_iter: + print(f"\\n[Auto Exposure] Attempt {i+1} | exp = {exp:.6f}s") + tested_exposures.append(exp) + + # Clone config and update exposure + config_local = copy.deepcopy(config) + config_local.exp = exp + + # Capture image + capture(config_local) + fn = config.fn + ".png" + img = load_image(fn, verbose=False, bayer=config.bayer, + blue_gain=None, red_gain=None, nbits_out=config.nbits_out) + + if img is None: + print(f" Failed to load image from {fn}") + break + + # Histogram analysis + max_val = img.max() + hist, _ = np.histogram(img, bins=range(4097)) + val_4095 = hist[4095] + val_3000_4000 = next((hist[v] for v in range(3000, 4000) if hist[v] > 0), 0) + + print(f" Max: {max_val} | hist[4095]: {val_4095} | first non-zero hist[3000–4000]: {val_3000_4000}") + + # --- Stopping condition --- + if max_val == 4095 and val_4095 <= 3 * val_3000_4000: + print(f" Good exposure at exp = {exp:.6f}s") + break + + # --- Stop if at min exposure and still saturated --- + if exp <= min_exp and max_val == 4095: + print(f" Reached minimum exposure ({exp:.6f}s) and still saturated. Stopping.") + break + + # --- Fine-tuning adjustment --- + if max_val == 4095 and val_4095 <= 20: + print("Fine-tuning: small saturation, slightly reducing exposure") + exp *= 0.95 + # --- General adjustment --- + elif max_val < 4095: + exp *= 1.4 + else: + exp *= 0.8 + + # Clamp to sensor limits + exp = min(max(exp, min_exp), max_exp) + + i += 1 + + print(f"Final exposure used: {exp:.6f}s") + +def auto_expose_image_locally(config): + import copy + import numpy as np + + max_iter = 10 + exp = config.exp + tested_exposures = [] + + min_exp = sensor_dict[config.sensor][SensorParam.MIN_EXPOSURE] + max_exp = sensor_dict[config.sensor][SensorParam.MAX_EXPOSURE] + + i = 0 + while i < max_iter: + print(f"\n[Auto Exposure - Image] Attempt {i+1} | exp = {exp:.6f}s") + tested_exposures.append(exp) + + config_local = copy.deepcopy(config) + config_local.exp = exp + + capture(config_local) + fn = config.fn + ".png" + img = load_image(fn, verbose=False, bayer=config.bayer, + blue_gain=None, red_gain=None, nbits_out=config.nbits_out) + + if img is None: + print(f" Failed to load image from {fn}") + break + + max_val = img.max() + hist, _ = np.histogram(img, bins=range(4097)) + print(f" Max: {max_val} | hist[4095]: {hist[4095]}") + + # Condition 1: max between 3900–4080 + if 3900 <= max_val <= 4080: + print(f" Accepted exposure at exp = {exp:.6f}s (max in range)") + break + + # Condition 2: max == 4095 and contrast is good + if max_val == 4095: + for v in range(1000, 2001): + if hist[v] > 0 and hist[4095] <= 3 * hist[v]: + print(f" Accepted: hist[4095] = {hist[4095]} <= 3 × hist[{v}] = {hist[v]}") + break + else: + # condition not satisfied → decrease exposure + print(" Max = 4095 but low contrast → decreasing exposure") + exp *= 0.85 + exp = max(exp, min_exp) + i += 1 + continue + break + + # 🔼 Increase if too dark + if max_val < 3900: + print(" Too dark → increasing exposure") + exp *= 1.4 + else: + print(" Too bright → decreasing exposure") + exp *= 0.8 + + # Clamp to bounds + exp = min(max(exp, min_exp), max_exp) + i += 1 + + print(f"[Auto Exposure - Image] Final exposure used: {exp:.6f}s") - print("Image saved to : {}".format(fn)) +@hydra.main(version_base=None, config_path="../../configs", config_name="capture") +def main(config): + if getattr(config, "auto_exp_psf", False): + auto_expose_psf_locally(config) + elif getattr(config, "auto_exp_img", False): + auto_expose_image_locally(config) + else: + capture(config) + if __name__ == "__main__": - capture() + main() + diff --git a/scripts/measure/remote_capture.py b/scripts/measure/remote_capture.py index a38f722a..a9b0b60c 100644 --- a/scripts/measure/remote_capture.py +++ b/scripts/measure/remote_capture.py @@ -263,4 +263,4 @@ def liveview(config): if __name__ == "__main__": - liveview() + liveview() \ No newline at end of file