Measuring an optical PSF with an Arduino, an LED, and a cardboard box
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.
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.
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.
- 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.
(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
- Technical paper on measuring the PSF. In their approach they measure the PSF of narrowband sources separately. Note that the link will prompt you to download a PDF.
- Overview of measuring optical properties: https://www2.ph.ed.ac.uk/~wjh/teaching/mo/slides/properties/properties.pdf
- More on optical PSFs: http://zeiss-campus.magnet.fsu.edu/articles/basics/psf.html