Skip to content
Open
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions configs/capture.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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
329 changes: 238 additions & 91 deletions scripts/measure/on_device_capture.py
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -53,7 +54,6 @@
]


@hydra.main(version_base=None, config_path="../../configs", config_name="capture")
def capture(config):

sensor = config.sensor
Expand Down Expand Up @@ -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:

Expand All @@ -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
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

move this value to the config

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()

Loading