From a7c7959b9f17bfe52440fb3cbda4953edfb42bcb Mon Sep 17 00:00:00 2001 From: mimanerhn <116755181+mimanerhn@users.noreply.github.com> Date: Mon, 26 May 2025 13:26:37 +0200 Subject: [PATCH 1/5] implement auto-exp --- scripts/measure/remote_capture.py | 117 ++++++++++++++++++++++-------- 1 file changed, 86 insertions(+), 31 deletions(-) diff --git a/scripts/measure/remote_capture.py b/scripts/measure/remote_capture.py index a38f722a..36a76180 100644 --- a/scripts/measure/remote_capture.py +++ b/scripts/measure/remote_capture.py @@ -30,6 +30,7 @@ import os import subprocess import cv2 +import numpy as np from pprint import pprint import matplotlib.pyplot as plt from lensless.hardware.utils import check_username_hostname @@ -79,11 +80,15 @@ def liveview(config): # take picture remote_fn = "remote_capture" - print("\nTaking picture...") + print("\n Running auto-exposure...") + + # 1. Build base command pic_command = ( - f"{config.rpi.python} {config.capture.script} sensor={sensor} bayer={bayer} fn={remote_fn} exp={config.capture.exp} iso={config.capture.iso} " - f"config_pause={config.capture.config_pause} sensor_mode={config.capture.sensor_mode} nbits_out={config.capture.nbits_out} " - f"legacy={config.capture.legacy} rgb={config.capture.rgb} gray={config.capture.gray} " + f"{config.rpi.python} {config.capture.script} " + f"sensor={sensor} bayer={bayer} fn={remote_fn} exp={config.capture.exp} iso={config.capture.iso} " + f"config_pause={config.capture.config_pause} sensor_mode={config.capture.sensor_mode} " + f"nbits_out={config.capture.nbits_out} legacy={config.capture.legacy} " + f"rgb={config.capture.rgb} gray={config.capture.gray} " ) if config.capture.nbits > 8: pic_command += " sixteen=True" @@ -92,34 +97,22 @@ def liveview(config): if config.capture.awb_gains: pic_command += f" awb_gains=[{config.capture.awb_gains[0]},{config.capture.awb_gains[1]}]" - print(f"COMMAND : {pic_command}") - ssh = subprocess.Popen( - ["ssh", "%s@%s" % (username, hostname), pic_command], - shell=False, - stdout=subprocess.PIPE, - stderr=subprocess.PIPE, - ) - result = ssh.stdout.readlines() - error = ssh.stderr.readlines() + # 2. Define expected output path + output_path = os.path.join(config.output or os.getcwd(), f"{config.capture.raw_data_fn}.png") - if error != [] and legacy: # new camera software seems to return error even if it works - print("ERROR: %s" % error) - return - if result == []: - error = ssh.stderr.readlines() - print("ERROR: %s" % error) - return - else: - result = [res.decode("UTF-8") for res in result] - result = [res for res in result if len(res) > 3] - result_dict = dict() - for res in result: - _key = res.split(":")[0].strip() - _val = "".join(res.split(":")[1:]).strip() - result_dict[_key] = _val - # result_dict = dict(map(lambda s: map(str.strip, s.split(":")), result)) - print("COMMAND OUTPUT : ") - pprint(result_dict) + # 3. Run auto-exposure loop + result = auto_expose_via_remote(config, pic_command, output_path) + + result = [res.decode("UTF-8") for res in result] + result = [res for res in result if len(res) > 3] + result_dict = dict() + for res in result: + _key = res.split(":")[0].strip() + _val = "".join(res.split(":")[1:]).strip() + result_dict[_key] = _val + # result_dict = dict(map(lambda s: map(str.strip, s.split(":")), result)) + print("COMMAND OUTPUT : ") + pprint(result_dict) if ( "RPi distribution" in result_dict.keys() @@ -262,5 +255,67 @@ def liveview(config): print(f"\nSaved plots to: {save}") +# Auto-exposure logic injected before running SSH command + +def auto_expose_via_remote(config, base_command, output_path): + final_result = [] + exp = config.capture.exp + max_iter = 10 + target_max = 4095 + max_saturation_ratio = 0.001 + + for i in range(max_iter): + print(f"\n Attempt {i+1} | exp={exp:.6f}s") + + # Inject new exposure into command + pic_command = base_command.replace(f"exp={config.capture.exp}", f"exp={exp}") + + ssh = subprocess.Popen( + ["ssh", f"{config.rpi.username}@{config.rpi.hostname}", pic_command], + shell=False, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + ) + result = ssh.stdout.readlines() + error = ssh.stderr.readlines() + + if error and config.capture.legacy: + print("ERROR:", error) + break + if not result: + print("ERROR:", error) + break + + # Check image exists + if not os.path.exists(output_path): + print("Output image not found") + break + + img = cv2.imread(output_path, cv2.IMREAD_UNCHANGED) + if img is None: + print("Failed to load captured image") + break + + max_val = int(np.max(img)) + sat_ratio = np.sum(img == 4095) / img.size + print(f"Max: {max_val} | Saturated: {sat_ratio*100:.4f}%") + + if max_val >= target_max and sat_ratio <= max_saturation_ratio: + print(f"Ideal exposure found: {exp:.6f}s") + final_result = result + break + + # Adjust exposure + if max_val >= 4095: + exp *= 0.7 if sat_ratio > max_saturation_ratio else 0.95 + elif max_val >= 3800: + exp *= 1.05 + else: + exp *= 1.4 + + return final_result + + + if __name__ == "__main__": liveview() From 3eb4710040cc09ee51fb8c5d00bfeeb7ff4ad5f5 Mon Sep 17 00:00:00 2001 From: mimanerhn <116755181+mimanerhn@users.noreply.github.com> Date: Mon, 26 May 2025 17:20:24 +0200 Subject: [PATCH 2/5] Auto-exp --- scripts/measure/remote_capture.py | 36 +++++-------------------------- 1 file changed, 5 insertions(+), 31 deletions(-) diff --git a/scripts/measure/remote_capture.py b/scripts/measure/remote_capture.py index 36a76180..9dbe1d1d 100644 --- a/scripts/measure/remote_capture.py +++ b/scripts/measure/remote_capture.py @@ -1,31 +1,3 @@ -""" - -For Bayer data with RPI HQ sensor: -``` -python scripts/measure/remote_capture.py \ -rpi.username=USERNAME rpi.hostname=IP_ADDRESS -``` - -For Bayer data with RPI Global shutter sensor: -``` -python scripts/measure/remote_capture.py -cn remote_capture_rpi_gs \ -rpi.username=USERNAME rpi.hostname=IP_ADDRESS -``` - -For RGB data with RPI HQ RPI Global shutter sensor: -``` -python scripts/measure/remote_capture.py -cn remote_capture_rpi_gs \ -rpi.username=USERNAME rpi.hostname=IP_ADDRESS \ -capture.bayer=False capture.down=2 -``` - -Check out the `configs/demo.yaml` file for parameters, specifically: - -- `rpi`: RPi parameters -- `capture`: parameters for taking pictures - -""" - import hydra import os import subprocess @@ -101,6 +73,8 @@ def liveview(config): output_path = os.path.join(config.output or os.getcwd(), f"{config.capture.raw_data_fn}.png") # 3. Run auto-exposure loop + auto_expose_via_remote(config, pic_command, output_path) + result = auto_expose_via_remote(config, pic_command, output_path) result = [res.decode("UTF-8") for res in result] @@ -258,11 +232,11 @@ def liveview(config): # Auto-exposure logic injected before running SSH command def auto_expose_via_remote(config, base_command, output_path): - final_result = [] exp = config.capture.exp max_iter = 10 target_max = 4095 max_saturation_ratio = 0.001 + final_img = None for i in range(max_iter): print(f"\n Attempt {i+1} | exp={exp:.6f}s") @@ -301,7 +275,7 @@ def auto_expose_via_remote(config, base_command, output_path): print(f"Max: {max_val} | Saturated: {sat_ratio*100:.4f}%") if max_val >= target_max and sat_ratio <= max_saturation_ratio: - print(f"Ideal exposure found: {exp:.6f}s") + print(f"✅Ideal exposure found: {exp:.6f}s") final_result = result break @@ -318,4 +292,4 @@ def auto_expose_via_remote(config, base_command, output_path): if __name__ == "__main__": - liveview() + liveview() \ No newline at end of file From 05f403e4f734ea761d42443856a39c0b1918a7a5 Mon Sep 17 00:00:00 2001 From: mimanerhn <116755181+mimanerhn@users.noreply.github.com> Date: Fri, 4 Jul 2025 14:49:54 +0200 Subject: [PATCH 3/5] Updating code --- configs/capture.yaml | 2 + scripts/measure/on_device_capture.py | 61 ++++++++++++++++++++++++++++ 2 files changed, 63 insertions(+) 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..942403e8 100644 --- a/scripts/measure/on_device_capture.py +++ b/scripts/measure/on_device_capture.py @@ -296,6 +296,67 @@ def capture(config): print("Image saved to : {}".format(fn)) +def auto_expose_locally(config): + + max_iter = 10 + target_max = 4095 if config.nbits_out > 8 else 255 + max_saturation_ratio = 0.001 + exp = config.exp + + i = 0 + while i < max_iter: + print(f"\n[Auto Exposure] Attempt {i+1} | exp = {exp:.6f}s") + + # Clone config and update exposure + config_local = copy.deepcopy(config) + config_local.exp = exp + + # Call capture + capture(config_local) + + # Build image path + fn = config.fn + ".png" + + # Load image + img = cv2.imread(fn, cv2.IMREAD_UNCHANGED) + if img is None: + print(f"❌ Failed to load image from {fn}") + break + + # Evaluate brightness + max_val = np.max(img) + sat_ratio = np.sum(img == target_max) / img.size + print(f" Max pixel: {max_val} | Saturated: {sat_ratio*100:.4f}%") + + # Stop if image is well exposed + if max_val >= target_max * 0.99 and sat_ratio <= max_saturation_ratio: + print(f"✅ Good exposure found at exp = {exp:.6f}s") + break + + # Adjust exposure + if max_val >= target_max: + exp *= 0.7 if sat_ratio > max_saturation_ratio else 0.95 + elif max_val >= target_max * 0.93: + exp *= 1.05 + else: + exp *= 1.4 + + # Clamp to hardware limits + min_exp = sensor_dict[config.sensor][SensorParam.MIN_EXPOSURE] + max_exp = sensor_dict[config.sensor][SensorParam.MAX_EXPOSURE] + exp = min(max(exp, min_exp), max_exp) + + i += 1 + + print(f"Final exposure used: {exp:.6f}s") + + +@hydra.main(version_base=None, config_path="../../configs", config_name="capture") +def main(config): + if config.auto_exp: + auto_expose_locally(config) + else: + capture(config) if __name__ == "__main__": capture() From d05204be0891ec7de231d46df320012ed10e9d0e Mon Sep 17 00:00:00 2001 From: mimanerhn <116755181+mimanerhn@users.noreply.github.com> Date: Fri, 4 Jul 2025 14:50:09 +0200 Subject: [PATCH 4/5] Update Code --- scripts/measure/on_device_capture.py | 328 +++++++++++++++++---------- 1 file changed, 207 insertions(+), 121 deletions(-) diff --git a/scripts/measure/on_device_capture.py b/scripts/measure/on_device_capture.py index 942403e8..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,92 +273,176 @@ def capture(config): # -- now set up camera with desired settings camera = PiCamera(framerate=1 / exp, sensor_mode=sensor_mode, resolution=tuple(res)) - - # 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`." - ) + # 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() + print("Image saved to : {}".format(fn)) -def auto_expose_locally(config): +def auto_expose_psf_locally(config): + import copy + import numpy as np max_iter = 10 - target_max = 4095 if config.nbits_out > 8 else 255 - max_saturation_ratio = 0.001 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") + 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 - # Call capture + # Capture image capture(config_local) - - # Build image path fn = config.fn + ".png" + img = load_image(fn, verbose=False, bayer=config.bayer, + blue_gain=None, red_gain=None, nbits_out=config.nbits_out) - # Load image - img = cv2.imread(fn, cv2.IMREAD_UNCHANGED) if img is None: - print(f"❌ Failed to load image from {fn}") + print(f" Failed to load image from {fn}") break - # Evaluate brightness - max_val = np.max(img) - sat_ratio = np.sum(img == target_max) / img.size - print(f" Max pixel: {max_val} | Saturated: {sat_ratio*100:.4f}%") + # 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) - # Stop if image is well exposed - if max_val >= target_max * 0.99 and sat_ratio <= max_saturation_ratio: - print(f"✅ Good exposure found at exp = {exp:.6f}s") + 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 - # Adjust exposure - if max_val >= target_max: - exp *= 0.7 if sat_ratio > max_saturation_ratio else 0.95 - elif max_val >= target_max * 0.93: - exp *= 1.05 - else: + # --- 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 hardware limits - min_exp = sensor_dict[config.sensor][SensorParam.MIN_EXPOSURE] - max_exp = sensor_dict[config.sensor][SensorParam.MAX_EXPOSURE] + # 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") + + @hydra.main(version_base=None, config_path="../../configs", config_name="capture") def main(config): - if config.auto_exp: - auto_expose_locally(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() + From 00fb652cca199f20c1b8116fa295e44b4cf35f94 Mon Sep 17 00:00:00 2001 From: mimanerhn <116755181+mimanerhn@users.noreply.github.com> Date: Tue, 8 Jul 2025 18:53:34 +0200 Subject: [PATCH 5/5] Update Remote Capture --- scripts/measure/remote_capture.py | 147 ++++++++++++------------------ 1 file changed, 59 insertions(+), 88 deletions(-) diff --git a/scripts/measure/remote_capture.py b/scripts/measure/remote_capture.py index 9dbe1d1d..a9b0b60c 100644 --- a/scripts/measure/remote_capture.py +++ b/scripts/measure/remote_capture.py @@ -1,8 +1,35 @@ +""" + +For Bayer data with RPI HQ sensor: +``` +python scripts/measure/remote_capture.py \ +rpi.username=USERNAME rpi.hostname=IP_ADDRESS +``` + +For Bayer data with RPI Global shutter sensor: +``` +python scripts/measure/remote_capture.py -cn remote_capture_rpi_gs \ +rpi.username=USERNAME rpi.hostname=IP_ADDRESS +``` + +For RGB data with RPI HQ RPI Global shutter sensor: +``` +python scripts/measure/remote_capture.py -cn remote_capture_rpi_gs \ +rpi.username=USERNAME rpi.hostname=IP_ADDRESS \ +capture.bayer=False capture.down=2 +``` + +Check out the `configs/demo.yaml` file for parameters, specifically: + +- `rpi`: RPi parameters +- `capture`: parameters for taking pictures + +""" + import hydra import os import subprocess import cv2 -import numpy as np from pprint import pprint import matplotlib.pyplot as plt from lensless.hardware.utils import check_username_hostname @@ -52,15 +79,11 @@ def liveview(config): # take picture remote_fn = "remote_capture" - print("\n Running auto-exposure...") - - # 1. Build base command + print("\nTaking picture...") pic_command = ( - f"{config.rpi.python} {config.capture.script} " - f"sensor={sensor} bayer={bayer} fn={remote_fn} exp={config.capture.exp} iso={config.capture.iso} " - f"config_pause={config.capture.config_pause} sensor_mode={config.capture.sensor_mode} " - f"nbits_out={config.capture.nbits_out} legacy={config.capture.legacy} " - f"rgb={config.capture.rgb} gray={config.capture.gray} " + f"{config.rpi.python} {config.capture.script} sensor={sensor} bayer={bayer} fn={remote_fn} exp={config.capture.exp} iso={config.capture.iso} " + f"config_pause={config.capture.config_pause} sensor_mode={config.capture.sensor_mode} nbits_out={config.capture.nbits_out} " + f"legacy={config.capture.legacy} rgb={config.capture.rgb} gray={config.capture.gray} " ) if config.capture.nbits > 8: pic_command += " sixteen=True" @@ -69,24 +92,34 @@ def liveview(config): if config.capture.awb_gains: pic_command += f" awb_gains=[{config.capture.awb_gains[0]},{config.capture.awb_gains[1]}]" - # 2. Define expected output path - output_path = os.path.join(config.output or os.getcwd(), f"{config.capture.raw_data_fn}.png") - - # 3. Run auto-exposure loop - auto_expose_via_remote(config, pic_command, output_path) - - result = auto_expose_via_remote(config, pic_command, output_path) + print(f"COMMAND : {pic_command}") + ssh = subprocess.Popen( + ["ssh", "%s@%s" % (username, hostname), pic_command], + shell=False, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + ) + result = ssh.stdout.readlines() + error = ssh.stderr.readlines() - result = [res.decode("UTF-8") for res in result] - result = [res for res in result if len(res) > 3] - result_dict = dict() - for res in result: - _key = res.split(":")[0].strip() - _val = "".join(res.split(":")[1:]).strip() - result_dict[_key] = _val - # result_dict = dict(map(lambda s: map(str.strip, s.split(":")), result)) - print("COMMAND OUTPUT : ") - pprint(result_dict) + if error != [] and legacy: # new camera software seems to return error even if it works + print("ERROR: %s" % error) + return + if result == []: + error = ssh.stderr.readlines() + print("ERROR: %s" % error) + return + else: + result = [res.decode("UTF-8") for res in result] + result = [res for res in result if len(res) > 3] + result_dict = dict() + for res in result: + _key = res.split(":")[0].strip() + _val = "".join(res.split(":")[1:]).strip() + result_dict[_key] = _val + # result_dict = dict(map(lambda s: map(str.strip, s.split(":")), result)) + print("COMMAND OUTPUT : ") + pprint(result_dict) if ( "RPi distribution" in result_dict.keys() @@ -229,67 +262,5 @@ def liveview(config): print(f"\nSaved plots to: {save}") -# Auto-exposure logic injected before running SSH command - -def auto_expose_via_remote(config, base_command, output_path): - exp = config.capture.exp - max_iter = 10 - target_max = 4095 - max_saturation_ratio = 0.001 - final_img = None - - for i in range(max_iter): - print(f"\n Attempt {i+1} | exp={exp:.6f}s") - - # Inject new exposure into command - pic_command = base_command.replace(f"exp={config.capture.exp}", f"exp={exp}") - - ssh = subprocess.Popen( - ["ssh", f"{config.rpi.username}@{config.rpi.hostname}", pic_command], - shell=False, - stdout=subprocess.PIPE, - stderr=subprocess.PIPE, - ) - result = ssh.stdout.readlines() - error = ssh.stderr.readlines() - - if error and config.capture.legacy: - print("ERROR:", error) - break - if not result: - print("ERROR:", error) - break - - # Check image exists - if not os.path.exists(output_path): - print("Output image not found") - break - - img = cv2.imread(output_path, cv2.IMREAD_UNCHANGED) - if img is None: - print("Failed to load captured image") - break - - max_val = int(np.max(img)) - sat_ratio = np.sum(img == 4095) / img.size - print(f"Max: {max_val} | Saturated: {sat_ratio*100:.4f}%") - - if max_val >= target_max and sat_ratio <= max_saturation_ratio: - print(f"✅Ideal exposure found: {exp:.6f}s") - final_result = result - break - - # Adjust exposure - if max_val >= 4095: - exp *= 0.7 if sat_ratio > max_saturation_ratio else 0.95 - elif max_val >= 3800: - exp *= 1.05 - else: - exp *= 1.4 - - return final_result - - - if __name__ == "__main__": liveview() \ No newline at end of file