Experimental Concerns for Beam Measurement
Scott Prahl
Nov 2025
Many mistakes can be made when making a beam measurement. This notebook collects a number of these.
The ISO 11146 standard recommends masking the image with a rotated rectangular mask around the center of the image. This notebook explains how that was implemented and then shows some results for artifically generated images of non-circular Gaussian beams. As noise increases, the first-order parameters (beam center) are robust, but the second-order parameters (diameters) are shown to be much more sensitive to image noise.
[1]:
%config InlineBackend.figure_format = 'retina'
import sys
import imageio.v3 as iio
import numpy as np
import matplotlib.pyplot as plt
if sys.platform == "emscripten":
import piplite
await piplite.install("laserbeamsize")
import laserbeamsize as lbs
[2]:
if sys.platform == "emscripten":
repo = "images/"
else:
repo = "https://github.com/scottprahl/laserbeamsize/raw/main/docs/images/"
[3]:
def side_by_side_plot(h, v, xc, yc, d_major, d_minor, phi, noise=0):
"""Creates plot of original and of fitted beam."""
test = lbs.create_test_image(h, v, xc, yc, d_major, d_minor, phi, noise=noise)
xc_found, yc_found, d_major_found, d_minor_found, phi_found = lbs.beam_size(test, iso_noise=True)
plt.subplots(1, 2, figsize=(12, 5))
plt.subplot(1, 2, 1)
plt.imshow(test, cmap=lbs.create_plus_minus_cmap(test))
plt.plot(xc, yc, "ob", markersize=2)
plt.title(
r"Original (%d,%d), $d_{major}$=%.0f, $d_{minor}$=%.0f, $\phi$=%.0f°"
% (xc, yc, d_major, d_minor, np.degrees(phi))
)
plt.subplot(1, 2, 2)
plt.imshow(test, cmap="gist_ncar")
xp, yp = lbs.rotated_rect_arrays(xc_found, yc_found, d_major_found, d_minor_found, phi_found)
plt.plot(xp, yp, ":y")
xp, yp = lbs.ellipse_arrays(xc_found, yc_found, d_major_found, d_minor_found, phi_found)
plt.plot(xp, yp, ":b")
plt.plot(xc_found, yc_found, "ob", markersize=2)
plt.title(
r"Found (%d,%d), $d_{major}$=%.0f, $d_{minor}$=%.0f, $\phi$=%.0f°"
% (xc_found, yc_found, d_major_found, d_minor_found, np.degrees(phi_found))
)
pixel_size_µm = 3.75 # microns
Image of beam is saturated
[4]:
sat_img = iio.imread(repo + "k-200mm.png")
lbs.plot_image_analysis(sat_img, pixel_size=pixel_size_µm)
Image of beam is too small
To illustrate we create a 20x40 pixel beam in a 600x600 image and a 200x200 image. 2% noise is added to the image and we try to extract the beam parameters. The beam in the larger image fails and the smaller image succeeds.
[13]:
h, v, xc, yc, d_major, d_minor, phi = 600, 600, 100, 100, 40, 20, np.pi / 2
side_by_side_plot(h, v, xc, yc, d_major, d_minor, phi, noise=5)
h, v, xc, yc, d_major, d_minor, phi = 200, 200, 100, 100, 40, 20, np.pi / 2
side_by_side_plot(h, v, xc, yc, d_major, d_minor, phi, noise=5)
Image of beam is too large
The algorithm does not do too badly with these. Here artifical beams 250x250 pixels in size are placed in 250x250 images or 900x900 images. No noise is added.
The small image used in the first pair of plots underestimates the size by 25%. The the larger image yields diameters within 2% of the expected values.
[14]:
h, v, xc, yc, d_major, d_minor, phi = 250, 250, 150, 150, 250, 250, 0
side_by_side_plot(h, v, xc, yc, d_major, d_minor, phi, noise=0)
h, v, xc, yc, d_major, d_minor, phi = 900, 900, 450, 450, 250, 250, 0
side_by_side_plot(h, v, xc, yc, d_major, d_minor, phi, noise=0)
Image of beam is too close to margins
[15]:
margin_img = iio.imread(repo + "TEM00_300mm.pgm") >> 4
options = {"pixel_size": pixel_size_µm, "units": "µm"}
lbs.plot_image_analysis(margin_img, **options)
# works much better with iso_noise=False
lbs.plot_image_analysis(margin_img, **options, iso_noise=False)
Image has artifacts, example 1
With the standard color map it is not obvious why the vertical dimension is nuts.
[8]:
art1_img = iio.imread(repo + "TEM00_150mm.pgm")
options = {
"pixel_size": pixel_size_µm,
"units": "µm",
"iso_noise": True,
"cmap": "gist_ncar",
}
lbs.plot_image_analysis(art1_img, **options)
Cropping the beam allows a reasonable fit to be obtained. However, in the image on the top right below, the integration rectangle extends beyond the image. Thus the result is not ISO 11146 compliant.
[9]:
options = {
"pixel_size": pixel_size_µm,
"units": "µm",
"iso_noise": True,
"cmap": "gist_ncar",
}
lbs.plot_image_analysis(art1_img[420:620, 550:750], **options)
Image has artifacts, 2
Here a small artifact above the primary beam image (perhaps a reflection) dramatically changes the vertical beam diameter.
We even turn off iso_noise so that the sloping background is removed.
[10]:
options = {"pixel_size": pixel_size_µm, "units": "µm", "cmap": "gist_ncar", "iso_noise": False, "crop": True}
art2_img = iio.imread(repo + "sb_259mm_10.pgm")
print(art2_img.shape)
lbs.plot_image_analysis(art2_img, **options)
(960, 1280)
Cropping the image produces much more reasonable values for the diameters.
[11]:
lbs.plot_image_analysis(art2_img[365:565, 543:743], **options)
Image of beam has too much noise
The background is nearly half of the central beam intensity. Although subtracting the background kind of worked, the offset affects the fitted gaussian and makes the diameter too wide. In this case, assuming iso_noise=True fails but when turned off, a reasonable fit is achieved.
[18]:
noise_img = iio.imread(repo + "k-800mm.png")
options = {
"pixel_size": pixel_size_µm,
"units": "µm",
}
lbs.plot_image_analysis(noise_img, **options, iso_noise=True)
lbs.plot_image_analysis(noise_img, **options, iso_noise=False)
[ ]: