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)
_images/05-Experiment_5_0.png

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)
_images/05-Experiment_7_0.png
_images/05-Experiment_7_1.png

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)
_images/05-Experiment_9_0.png
_images/05-Experiment_9_1.png

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)
_images/05-Experiment_11_0.png
_images/05-Experiment_11_1.png

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)
_images/05-Experiment_13_0.png

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)
_images/05-Experiment_15_0.png

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)
_images/05-Experiment_17_1.png

Cropping the image produces much more reasonable values for the diameters.

[11]:
lbs.plot_image_analysis(art2_img[365:565, 543:743], **options)
_images/05-Experiment_19_0.png

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)
_images/05-Experiment_21_0.png
_images/05-Experiment_21_1.png
[ ]: