37 images: list[FloatImagePlane], sigma: float = 0.1, maxLevel: int |
None = 3
39 """Fuse multiple exposure images using Laplacian pyramid blending.
43 images : `list` of `FloatImagePlane`
44 List of exposure images to fuse. Each image should be a
45 `FloatImagePlane` with pixel values typically in the range [0, 1].
46 sigma : `float`, optional
47 Controls the exposure weighting sensitivity. Lower values make the
48 weighting more sensitive to deviations from the reference exposure
49 (0.7). Default is 0.1.
50 maxLevel : `int` | `None`, optional
51 Maximum pyramid level to use for blending. If None, automatically
52 determined based on image dimensions. Default is 3.
56 result : `FloatImagePlane`
57 Fused image with balanced exposure.
62 Raised if maxLevel is greater than the maximum allowed level based on
67 This function implements exposure fusion using a Laplacian pyramid
68 approach. The algorithm works as follows:
70 1. Compute exposure weights for each image based on how close pixel
71 values are to the reference exposure (0.7). Values above 1.0 receive
73 2. Build Gaussian pyramids from the weights and Laplacian pyramids from
75 3. Blend the Laplacian pyramids using the Gaussian pyramid weights at
77 4. Reconstruct the final image from the blended pyramid.
79 The fusion preserves local contrast while balancing exposure across
82 weights = np.zeros((len(images), *images[0].shape[:2]))
83 for i, image
in enumerate(images):
84 exposure = np.exp(-((image[:, :] - 0.7) ** 2) / (2 * sigma))
86 exposure[image > 1] *= 0.5
88 weights[i, :, :] = exposure
89 norm = np.sum(weights, axis=0)
90 np.divide(weights, norm, out=weights)
95 maxImageLevel = int(np.min(np.log2(images[0].shape[:2])))
97 maxLevel = maxImageLevel
98 if maxImageLevel < maxLevel:
100 f
"The supplied max level {maxLevel} is greater than the max of the image: {maxImageLevel}"
102 support = 1 << (maxLevel - 1)
103 padY_amounts =
levelPadder(image.shape[0] + support, maxLevel)
104 padX_amounts =
levelPadder(image.shape[1] + support, maxLevel)
105 for image, weight
in zip(images, weights):
106 imagePadded = cv2.copyMakeBorder(
107 image, *(0, support), *(0, support), cv2.BORDER_REFLECT,
None,
None
108 ).astype(image.dtype)
109 weightPadded = cv2.copyMakeBorder(
110 weight, *(0, support), *(0, support), cv2.BORDER_REFLECT,
None,
None
111 ).astype(image.dtype)
114 l_pyr.append(list(
makeLapPyramid(imagePadded, padY_amounts, padX_amounts,
None,
None)))
118 for level
in range(len(padY_amounts)):
119 accumulate = np.zeros_like(l_pyr[0][level])
120 for img
in range(len(g_pyr)):
121 accumulate[:, :] += l_pyr[img][level][:, :] * g_pyr[img][level]
122 blended.append(accumulate)
126 for i
in range(-2, -1 * len(blended) - 1, -1):
127 upsampled = cv2.pyrUp(output)
128 upsampled = upsampled[
129 : upsampled.shape[0] - 2 * padY_amounts[i + 1], : upsampled.shape[1] - 2 * padX_amounts[i + 1]
131 output = blended[i] + upsampled
132 return output[:-support, :-support]