Skip to content

Automatically determining exposure #174

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 5 commits into
base: main
Choose a base branch
from

Conversation

mimanerhn
Copy link

@mimanerhn mimanerhn commented Jul 6, 2025

Changes in the on_device_capture.py script

1. New CLI flags

  • auto_exp_psf and auto_exp_img
    Both flags initiate an iterative auto-exposure routine that captures multiple frames, analyzes their histograms, and adjusts the shutter time to avoid clipping while maximizing tonal range.

    We distinguish them because auto_exp_img is more forgiving: even if its raw maximum pixel value settles around 3800, subsequent color correction can stretch it up to 4095. For full-frame images, our primary goal is simply to prevent large white highlights, not to hit an exact exposure level. In contrast, auto_exp_psf demands the highest precision for point-spread function measurements.

These flags integrate seamlessly with the existing Hydra configuration options.


2. Auto-exposure logic

PSF routine (auto_exp_psf_locally)

  1. Start with the user’s initial exposure value.
  2. Loop (up to 10 iterations):
    • Capture a frame at the current exposure.
    • Build a histogram of pixel intensities.
  3. Evaluate:
  • Good exposure: when the histogram’s highest bin sits at the sensor’s maximum code value (4095 for our 12-bit HQ camera) and its count does not exceed the threshold we’ve defined relative to the preceding bins.
  • Abort conditions: Stop the loop if any of the following occur:
    1. A good exposure has been achieved.
    2. Exposure has been reduced to the sensor’s minimum and the histogram still shows clipping.
    3. The maximum number of iterations has been reached.
  1. Adjust:
    • If slight clipping: multiply exposure by 0.95 (small reduction).
    • If under-exposed: multiply by 1.4 (brighten).
    • If heavily clipped: multiply by 0.8 (darken).
    • Clamp back to the sensor’s allowed min/max.

Image routine (auto_expose_image_locally)

  1. Start with the user’s initial exposure value.
  2. Loop (up to 10 iterations):
    • Capture a full-frame image.
    • Find the maximum pixel value and its histogram count.
  3. Evaluate in priority order:
    1. In-range: maximum falls within a safe window (e.g. 3900–4080).
    2. Clipped but high-contrast: The peak is at full scale (4095), but its count ≤ treshold × a mid-range bin.
      Note: the “target range” check usually succeeds first, so the second condition is rarely needed.
  4. Adjust if neither condition is met:
    • Too dark → ×1.4 (increase exposure).
    • Too bright or low contrast → ×0.8 (or ×0.85 for contrast case).
    • Clamp to sensor limits.

PS : In both routines, each iteration logs attempt number, current exposure value, and histogram summary to help diagnose the process.


3. How it hooks into main()

  • If auto_exp_psf is true, run the PSF loop.
  • Else if auto_exp_img is true, run the image loop.
  • Otherwise, fall back to the original single-capture behavior.

This lets users drive end-to-end auto-exposure entirely via flags, without manual adjusting.

Comment on lines 260 to 261
print("ERROR:", error)
break
Copy link
Member

Choose a reason for hiding this comment

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

Since this is inside a function being called, it might be better to raise an error instead (to kill the script):

raise ValueError(error)

Comment on lines 257 to 258
print("ERROR:", error)
break
Copy link
Member

Choose a reason for hiding this comment

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

Since this is inside a function being called, it might be better to raise an error instead (to kill the script):

raise ValueError(error)

Comment on lines 265 to 266
print("Output image not found")
break
Copy link
Member

Choose a reason for hiding this comment

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

Since this is inside a function being called, it might be better to raise an error instead (to kill the script):

raise ValueError("Output image not found")

Comment on lines 270 to 271
print("Failed to load captured image")
break
Copy link
Member

Choose a reason for hiding this comment

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

same here? I guess it's an error so:

Since this is inside a function being called, it might be better to raise an error instead (to kill the script):

raise ValueError("Failed to load captured image")

Comment on lines 236 to 238
max_iter = 10
target_max = 4095
max_saturation_ratio = 0.001
Copy link
Member

@ebezzam ebezzam Jul 8, 2025

Choose a reason for hiding this comment

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

can these be exposed in the Hydra config? (max_iter, target max, max_saturation_ratio)

Comment on lines 284 to 290
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
Copy link
Member

@ebezzam ebezzam Jul 8, 2025

Choose a reason for hiding this comment

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

these increase and decrease factors (and other hardcoded values) would also be nice to set in the Hydra config. Like we do when measuring a dataset:

fact_increase: 2 # multiplicative factor to increase exposure

in short, removing hard-coded values

Copy link
Member

Choose a reason for hiding this comment

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

and can add a sanity check before returning, namely assert final_result is not None

max_iter = 10
target_max = 4095
max_saturation_ratio = 0.001
final_img = None
Copy link
Member

Choose a reason for hiding this comment

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

should this be final_result? this seems to be how you call the image below

break

max_val = int(np.max(img))
sat_ratio = np.sum(img == 4095) / img.size
Copy link
Member

Choose a reason for hiding this comment

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

instead of 4095, use config value

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:
Copy link
Member

Choose a reason for hiding this comment

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

in certain cases, we may want no saturation at all (like when measuring images). For example, max_saturation_ratio may be None, and we should handle that case

raise ValueError(
"Out of resources! Use bayer for higher resolution, or increase `gpu_mem` in `/boot/config.txt`."
)
max_iter = 10
Copy link
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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants