2.4. Filter

The decoding yields the following results:

  • The brightness \(\hat{a}\) is a measure for the reflectance (resp. absorption) of the surface.

  • The modulation \(\hat{b}\) is a measure for the glossiness (resp. scattering) of the surface.

  • The coordinate \(\hat{x}\) is a measure for the local shape or slope of the surface. It is the screen position where each camera pixel, i.e. each camera sight ray, was looking at (got deflected to) during the fringe pattern recording.

 1"""Decode fringe pattern sequence."""
 2
 3from fringes import Fringes
 4
 5f = Fringes()
 6
 7I = f.encode()
 8Irec = I  # todo: replace this line with recording data (cf. example in 'record.py')
 9a, b, x = f.decode(Irec)
10
11# show results
12import matplotlib.pyplot as plt
13from matplotlib import colors
14
15fig, axs = plt.subplots(ncols=f.D, squeeze=False, num="coordinate 'x'")
16norm = colors.Normalize(vmin=x.min(), vmax=x.max())
17for d in range(f.D):
18    axs[0, d].set_title(f"{'yx'[f.axes[d]]}-direction")
19    image = axs[0, d].imshow(x[d, :, :, 0], norm=norm)  # shows only first color channel
20fig.colorbar(image, ax=axs)
21
22fig, axs = plt.subplots(nrows=f.K, ncols=f.D, squeeze=False, num="modulation 'b'")
23norm = colors.Normalize(vmin=b.min(), vmax=b.max())
24for d in range(f.D):
25    axs[0, d].set_title(f"{'yx'[f.axes[d]]}-direction")
26    for i in range(f.K):
27        if d == 0:
28            axs[i, d].set_ylabel(f"{i}-th set")
29        t = d * f.K + i
30        image = axs[i, d].imshow(b[t, :, :, 0], norm=norm)  # shows only first color channel
31fig.colorbar(image, ax=axs)
32
33fig, axs = plt.subplots(ncols=f.D, squeeze=False, num="brightness 'a'")
34norm = colors.Normalize(vmin=a.min(), vmax=a.max())
35for d in range(f.D):
36    axs[0, d].set_title(f"{'yx'[f.axes[d]]}-direction")
37    image = axs[0, d].imshow(a[d, :, :, 0], norm=norm)  # shows only first color channel
38fig.colorbar(image, ax=axs)
39
40plt.show()

These results are in video shape, so for the brightness \(\hat{a}\) and the coordinate \(\hat{x}\), the usually two directions D are along the first i.e. the time axis. For the modulation \(\hat{b}\), the modulation of the sets K for the directions D are flattened into the first dimension; you may reshape them as follows:

24b = b.reshape(f.D, f.K, *b.shape[1:])
brightness

Fig. 2.10 Brightness.

modulation

Fig. 2.11 Modulation.

coordinate

Fig. 2.12 Coordinate.

2.4.1. Direct and Global Illumination Component

The direct illumination component is just twice the measured modulation:

\(I_D = 2 \hat{b}\).

The global illumination component can be determined with

\(I_G = 2 (\hat{a} - \hat{b})\)

under the condition that the spatial frequency \(\nu\) is high enough [Nay06].

Both can be normalized into the range [0, 1) by dividing through the maximal possible gray value \(I_{max}\) of the recording camera.

 1"""Direct and global illumination component.
 2
 3https://dl.acm.org/doi/abs/10.1145/1179352.1141977
 4"""
 5
 6from fringes import Fringes
 7from fringes.filter import direct, indirect
 8
 9f = Fringes()
10f.v = 99, 100, 101  # spatial frequency 'v' must be high enough but still resolved by the recording camera
11
12I = f.encode()
13Irec = I  # todo: replace this line with recording data (cf. example in 'record.py')
14a, b, x = f.decode(Irec)
15
16d = direct(b)
17g = indirect(a, b)
18
19# show results
20import matplotlib.pyplot as plt
21from matplotlib import colors
22
23fig, axs = plt.subplots(nrows=f.K, ncols=f.D)
24fig.suptitle("global 'g'")
25norm = colors.Normalize(vmin=g.min(), vmax=g.max())
26images = []
27for i_d in range(f.D):
28    axs[0, i_d].set_title(f"{'yx'[f.axes[i_d]]}-direction")
29    for i in range(f.K):
30        if i_d == 0:
31            axs[i, i_d].set_ylabel(f"{i}-th set")
32        t = i_d * f.K + i
33        images.append(axs[i, i_d].imshow(g[t, :, :, 0], norm=norm))  # shows only first color channel
34fig.colorbar(images[0], ax=axs)
35
36fig, axs = plt.subplots(nrows=f.K, ncols=f.D)
37fig.suptitle("direct 'd'")
38norm = colors.Normalize(vmin=d.min(), vmax=d.max())
39images = []
40for i_d in range(f.D):
41    axs[0, i_d].set_title(f"{'yx'[f.axes[i_d]]}-direction")
42    for i in range(f.K):
43        if i_d == 0:
44            axs[i, i_d].set_ylabel(f"{i}-th set")
45        t = i_d * f.K + i
46        images.append(axs[i, i_d].imshow(d[t, :, :, 0], norm=norm))  # shows only first color channel
47fig.colorbar(images[0], ax=axs)
48
49plt.show()
direct

Fig. 2.13 Direct illumination component.

global

Fig. 2.14 Global illumination component.

2.4.2. Visibility and Exposure

In an alternative formulation, the absolute quantities offset \(a\) and amplitude \(b\) of the phase shifting equation are replaced by the maximal possible gray value \(I_{max}\), the relative quantities exposure \(E\) (relative average intensity) and visibility \(V\) (relative fringe contrast) [Fis12]:

\(I = a + b \cos(\varPhi) = I_{max} E [1 + V \cos(\varPhi)]\)

The two parameters \(E = \hat{a} / I_{max}\) and \(V = \hat{b_i} / \hat{a}\) describe the phase shifting signal \(I\) independently of the value range \([0, I_{max}]\) of the light source or camera. Both lie within the interval \([0, 1]\) with the additional condition \(E \le 1 / (1 + V)\); else, the radiance of the light source would be higher than the maximal possible value \(I_{max}\). Therefore, the valid values of \(V\) are limited for \(E > 0.5\). The optimal fringe contrast \(V = 1\) can be reached when \(E = 0.5\).

codomain

Fig. 2.15 Fringe pattern as a function of \(E\) and \(V\).

The advantage of this representation is the normalization of the descriptive parameters \(E\) and \(V\) and thereby the separation of additive and multiplicative influences.

The exposure \(E\) is affected by additional, constant light (not modulating the signal):

  • the maximum brightness of the light source,

  • the absorption of the sample

  • the absorption of optical elements (e.g. filters),

  • the exposure time and the aperture setting of the camera.

The visibility \(V\) is influenced by:

  • the maximum contrast of the light source

  • the surface quality of the sample (roughness, scattering),

  • the position of the sample with regard to the focal plane of the lens (blurring due to defocus and depth of field),

  • the camera lens’ modulation transfer function,

  • the camera’s resolution (the camera pixel size projected onto the light source acts as a low pass filter).

 1"""Visibility and Exposure."""
 2
 3from fringes import Fringes
 4from fringes.filter import exposure, visibility
 5
 6f = Fringes()
 7
 8I = f.encode()
 9Irec = I  # todo: replace this line with recording data (cf. example in 'record.py')
10a, b, x = f.decode(Irec)
11
12E = exposure(a, Irec)
13V = visibility(a, b)
14
15# show results
16import matplotlib.pyplot as plt
17from matplotlib import colors
18
19fig, axs = plt.subplots(nrows=f.K, ncols=f.D, squeeze=False, num="visibility 'V'")
20norm = colors.Normalize(vmin=V.min(), vmax=V.max())
21for d in range(f.D):
22    for i in range(f.K):
23        if d == 0:
24            axs[i, d].set_ylabel(f"{i}-th set")
25        t = d * f.K + i
26        image = axs[i, d].imshow(V[t, :, :, 0], norm=norm)  # shows only first color channel
27fig.colorbar(image, ax=axs)
28
29fig, axs = plt.subplots(ncols=f.D, squeeze=False, num="exposure 'E'")
30norm = colors.Normalize(vmin=E.max(), vmax=E.max())
31for d in range(f.D):
32    axs[0, d].set_title(f"{'yx'[f.axes[d]]}-direction")
33    image = axs[0, d].imshow(E[d, :, :, 0], norm=norm)  # shows only first color channel
34fig.colorbar(image, ax=axs)
35
36plt.show()
visibility

Fig. 2.16 Visibility.

exposure

Fig. 2.17 Exposure.

2.4.3. Verbose Results

Additionally to the already mentioned results brightness, modulation, coordinate, visibility and exposure, more intermediate and verbose results can be returned by setting the flag verbose in the method decode() to True:

 1"""Decode verbose results."""
 2
 3from fringes import Fringes
 4
 5f = Fringes()
 6
 7I = f.encode()
 8Irec = I  # todo: replace this line with the recorded data (cf. example in 'record.py')
 9dec = f.decode(Irec, verbose=True)  # here, a namedtuple is returned (see usage below)
10
11# show verbose results
12import matplotlib.pyplot as plt
13import numpy as np
14from matplotlib import colors
15
16fig, axs = plt.subplots(ncols=f.D, squeeze=False, num="uncertainty 'u'")
17norm = colors.Normalize(vmin=dec.u.min(), vmax=dec.u.max())
18for d in range(f.D):
19    axs[0, d].set_title(f"{'yx'[f.axes[d]]}-direction")
20    image = axs[0, d].imshow(dec.u[d, :, :, 0], norm=norm)  # shows only first color channel
21fig.colorbar(image, ax=axs)
22
23fig, axs = plt.subplots(ncols=f.D, squeeze=False, num="residuals 'r'")
24norm = colors.Normalize(vmin=dec.r.max(), vmax=dec.r.max())
25for d in range(f.D):
26    axs[0, d].set_title(f"{'yx'[f.axes[d]]}-direction")
27    image = axs[0, d].imshow(dec.r[d, :, :, 0], norm=norm)  # shows only first color channel
28fig.colorbar(image, ax=axs)
29
30fig, axs = plt.subplots(nrows=f.K, ncols=f.D, squeeze=False, num="fringe orders 'k'")
31norm = colors.Normalize(vmin=dec.k.min(), vmax=dec.k.max())
32for d in range(f.D):
33    axs[0, d].set_title(f"{'yx'[f.axes[d]]}-direction")
34    for i in range(f.K):
35        if d == 0:
36            axs[i, d].set_ylabel(f"{i}-th set")
37        t = d * f.K + i
38        image = axs[i, d].imshow(dec.k[t, :, :, 0], norm=norm)  # shows only first color channel
39fig.colorbar(image, ax=axs)
40
41fig, axs = plt.subplots(nrows=f.K, ncols=f.D, squeeze=False, num="phase 'p'")
42norm = colors.Normalize(vmin=0, vmax=2 * np.pi)
43for d in range(f.D):
44    axs[0, d].set_title(f"{'yx'[f.axes[d]]}-direction")  # shows only first color channel
45    for i in range(f.K):
46        if d == 0:
47            axs[i, d].set_ylabel(f"{i}-th set")
48        t = d * f.K + i
49        image = axs[i, d].imshow(dec.p[t, :, :, 0], norm=norm)
50cbar = fig.colorbar(image, ax=axs, ticks=[0, np.pi, 2 * np.pi])
51cbar.ax.set_yticklabels(["0", "$\\pi$", "$2\\pi$"])
52
53plt.show()

The phase … after temporal demodulation … link to fundamentals

phase

Fig. 2.18 Phase.


The residuals … after optimization-based unwrapping … link to fundamentals

residuals

Fig. 2.19 Residuals.


The uncertainty … after temporal demodulation i.e. unwrapping … best/minimal uncertainty if ui is set and correct fringe orders are found … link to fundamentals

uncertainty

Fig. 2.20 Uncertainty.

2.4.4. Slope

If the deflectometric setup is calibrated, the slope of the surface can be computed from the coordinate.

“[Deflectometry] measures slopes (first order derivatives of the shape). The sensitivity to higher spatial frequencies is therefore amplified, resulting in excellent (sometimes excessive) sensitivity for small-scale irregularities and at the same time poor sensitivity and stability for low-order surface features.” [Bur23]

 1"""Slope."""
 2
 3from fringes import Fringes
 4
 5f = Fringes()
 6
 7I = f.encode()
 8Irec = I  # todo: replace this line with the recorded data (cf. example in 'record.py')
 9a, b, x = f.decode(Irec)
10
11s = x  # todo: compute slope from calibrated setup
12
13# show results
14import matplotlib.pyplot as plt
15from matplotlib import colors
16
17fig, axs = plt.subplots(ncols=f.D)
18fig.suptitle("slope 's'")
19norm = colors.Normalize(vmin=s.min(), vmax=s.max())
20images = []
21for d in range(f.D):
22    axs[d].set_title(f"{'yx'[f.axes[d]]}-direction")
23    images.append(axs[d].imshow(s[d, :, :, 0], norm=norm))  # shows only first color channel
24fig.colorbar(images[0], ax=axs)
25plt.show()
slope_x

Fig. 2.21 Slope map in x-direction. Source: [SAC].

slope_y

Fig. 2.22 Slope map in y-direction. Source: [SAC].

2.4.5. Curvature

“As an alternative use of [deflectometry] data, one may differentiate them and recover surface curvatures (combinations of second order shape derivatives […]. Unlike point positions and slopes, the latter are intrinsic local characteristics of the surface […] that are independent of its embedding in 3D space. As such, curvature maps are useful observables for various quality inspection tasks. Derivation of curvatures is less error-prone than shape integration and does not require accurate prior knowledge of the distance to the object.” [Bur23]

 1"""Curvature."""
 2
 3from fringes import Fringes
 4from fringes.filter import curvature
 5
 6f = Fringes()
 7
 8I = f.encode()
 9Irec = I  # todo: replace this line with recording data (cf. example in 'record.py')
10a, b, x = f.decode(Irec)
11
12s = x  # todo: compute slope from calibrated setup
13c = curvature(s)
14
15# show result
16import matplotlib.pyplot as plt
17
18plt.figure("curvature 'c'")
19plt.imshow(c[:, :, 0])  # only first color channel
20plt.colorbar()
21plt.show()
curvature

Fig. 2.23 Curvature map. Source: [SAC].