Measuring an optical PSF with an Arduino, an LED, and a cardboard box

Eric Bezzam
8 min readDec 4, 2021

--

In this tutorial, we measure the point spread function (PSF) of a simple optical system. Specifically, we measure the PSF of a Raspberry Pi HQ camera with a white LED driven by an Arduino Uno.

The PSF characterizes the response between a single point in space and what is measured at the sensor. In this manner, we capture the combined effect of the aperture, lens, etc in our optical system. A faithful measurement/estimation of the PSF can be critical for image deconvolution tasks.

We’ll be using scripts from this library.

What you need:

  • Raspberry Pi flashed and fitted with a lens (we’ll use a 6mm CS-mount lens). And with passwordless SSH setup (this is needed for the remote capture script but don’t give a password to your key pair!).
  • Arduino Uno + power cable.
  • White LED.
  • Breadboard, a few jumper cables, and a 2.2k ohm resistor (perhaps larger if you are facing issues with saturation).
  • Cardboard box.
  • Electric tape.
  • 10k ohm potentiometer (optional, useful to adjust brightness).
  • Adjustable iris / aperture (optional, for more precision).

We will be using software available at this GitHub repository, which can can clone and install like so:

# download from GitHub
git clone git@github.com:LCAV/LenslessPiCam.git

# install in virtual environment
cd LenslessPiCam
python3.9 -m venv lensless_env
source lensless_env/bin/activate
pip install -e .

In the example commands below, we use measurements you can download here and place inside the data folder. Note that as we have 12-bit data stored as 16-bit the images will appear to be all black.

1) Building a wide-band point source

We’ll be making a very rudimentary point source, but it will suffice for our purpose. Namely, we will wire up a white LED (hence wide-band as it produces a spectrum of colors) to an Arduino and poke a hole into a cardboard box for a pinhole aperture. Inside the box, we will have all the electronics with the LED on the other side of the pinhole.

Wiring up the LED

We refer to this tutorial for wiring up the LED and the potentiometer to the Arduino. Be sure to connect the long end of the LED (anode) to the 5V and the short end of the LED (cathode) to the resistor.

Source: https://www.engineersgarage.com/wp-content/uploads/2/2/1/5/22159166/potentiometer-and-led-with-arduino_orig.png

When wiring the LED, connect it to some long jumper cables so that you can move it freely (as shown below). We wrapped the ends with electric tape to hold the LED in place.

Programming the Arduino

Plug in the Arduino Uno to your laptop and from the Arduino IDE upload the code below.

Source: https://www.engineersgarage.com/fading-led-with-potentiometer-using-arduino/

Below is a screenshot from the Arduino IDE of a successful upload.

Note the “Done uploading” message below and how the Arduino Uno is detected on a serial port in the bottom right corner (/dev/ttyACM0).

The LED should light up and vary as you turn the potentiometer knob. You can unplug the Arduino, and whenever you power it up again it will run the above program.

Boxing the light source

Find a decent sized cardboard box in order to cover your Arduino and poke as small of a hole as you can at the same height as your camera.

Tape the LED to the inside of the box on the other side of the pinhole.

And now you have a wide-band point source!

Note that most white LEDs actually consist of a single short-wavelength LED whose spectrum is spread with a phosphor (article and discussion). So the spectrum may not be as flat as you think. It is nonetheless wide-band, but we will see what issues this leads to.

2) Calibration and setup

Before measuring the PSF, we will want to focus the lens on the plane of the light source. You can check out this tutorial for how to do that for the Raspberry Pi HQ camera.

Below is an image captured by the HQ camera with raspistill / libcamera-jpeg (for Bullseye and onwards).

We’ve centered the text for focusing purposes, but for our PSF measurements the pinhole will be at the center.

When measuring the PSF of an optical system, there are two external influences that we must take care to minimize:

  • Stray light, as we are interested in just the response between the point and the sensor. To this end, we will measure the response in as dark of a room as possible and connect remotely (SSH) to the Raspberry Pi with the HQ camera.
Remotely connect to Raspberry Pi.
  • Processing / enhancement (in particular non-linear) during the digital acquisition, as we are interested in the analog response between the point source and the sensor. To this end, we will capture the raw Bayer data as described in this tutorial and adjust certain camera settings for the low-light environment + disable any enhancement.

Using this library (installed on both the Raspberry Pi and the local machine on the same network), we can remotely raw Bayer data with the desired settings. Running the command below, we can conveniently set the exposure and ISO and collect Bayer data via the command line.

python scripts/remote_capture.py --exp 0.1 -iso 100 --bayer --hostname <HOSTNAME>

where <HOSTNAME> is the hostname (e.g. raspberrypi.local on a default installation) or the IP address of the Raspberry Pi (can be determined with ifconfig on the Raspberry Pi). You may have to use the IP address if you are on a WPA Enterprise WiFi, e.g. at a university.

3) PSF measurements

We are now ready to measure some PSFs 🔦

Cardboard pinhole

Below is the PSF we measured for our cardboard pinhole. Note that we plot the PSF in grayscale, and we are not doing any gamma correction.

python scripts/analyze_image.py --fp data/psf/lens_cardboard.png --gamma 1

If you look really really close, you can see a tiny point source around (1500, 2000)! Using the GUI of matplotlib, let’s zoom in to see how compact the support is, namely how many pixels it occupies.

The PSF occupies an area of roughly 9x9 pixels. Normally, the wider the support of this PSF, the more blur our optical system has. But remember we are not measuring with a perfect point source either!

Adjustable iris

Let’s try putting an adjustable iris in front of our LED to see if we can get a more distinct point source. We’ll be using a 20mm adjustable iris by ThorLabs, but any size should work as we want to have the smallest possible aperture. Like with the cardboard box, we’ll tape the LED to the aperture.

Let’s look at the zoomed-in PSF.

python scripts/analyze_image.py --fp data/psf/lens_iris.png --gamma 1

A bit more compact (roughly 6x6 pixels) and centralized. However, our cardboard pinhole is not that bad!

Analyzing the PSF

Histogram

So how do we know that we have a good PSF measurement? Ideally we would like to have no saturation, which means that pixels don’t clip. Looking at the histogram of pixel intensities can inform us about clipping.

A large peak at the high pixel value end of a histogram is indicative of saturation / clipping, as shown below (a quite extreme example).

python scripts/analyze_image.py --fp data/psf/lens_saturating.png --gamma 1

Below is the corresponding PSF.

So we don’t want saturation, but we also want to use the full dynamic range. For example, for a 12-bit data (as for the Raspberry Pi HQ camera), we should expect a maximum value of 2**12 — 1 = 4095, and want pixel values close to this maximum.

The histogram below corresponds to a PSF which does not use enough of the dynamic range.

python scripts/analyze_image.py --fp data/psf/lens_weak.png --gamma 1

Below are better looking histograms (corresponding to the PSFs we measured for the cardboard and iris apertures) which don’t saturate and use the full dynamic range.

Cardboard aperture
Iris aperture

(One problem we can see in the above histograms is that there is a lack of red pixels with high intensity. This has to do with the spectrum of white LEDs, as mentioned before.)

The remote capture script from above (scripts/remote_capture.py) will plot both the PSF and its histogram (in RGB and grayscale) after transferring over the image from the Raspberry Pi.

Numerically determine PSF support

Another interesting characteristic of the PSF is its support / width, which characterizes the blur of our system. Previously, we determined this by visual inspection, but this is certainly something we can evaluate numerically.

Below we plot the cross-section of the PSF of the cardboard and iris apertures and determine/plot at which point the PSF drops by 3 dB from its peak value. With the --lens flag, we can generate these plots.

python scripts/analyze_image.py --fp data/psf/lens_cardboard.png --plot_width 100 --lens
python scripts/analyze_image.py --fp data/psf/lens_iris.png --plot_width 100 --lens

A couple observations:

  • Our point source is lacking in wavelengths around red (~650nm).
  • The iris aperture produces a slightly more compact point source.

Troubleshooting

Large PSF width

If your PSF is much wider, perhaps your lens is not focused correctly or the aperture of the cardboard/iris is too large.

For example, below is a PSF we’ve measured with a larger aperture.

Saturation / clipping

If your PSF is saturating too much, there are a few fixes:

  • Try a lower exposure with scripts/remote_capture.py. The lowest we could get with the Raspberry Pi HQ camera was 0.02.
  • Try a smaller aperture.
  • Try a larger resistor in series with your LED.

Extra links

--

--

Eric Bezzam

PhD student at EPFL. Previously at Snips/Sonos, DSP Concepts, Fraunhofer IDMT, and Jacobs University. Most of past work in audio and now breaking into optics!