37 img: NDArray, out: NDArray, g: float, sigma: float, shadows: float, highlights: float, clarity: float
40 Apply a post-processing effect to an image using the specified parameters.
45 The input image array of shape (n_images, height, width).
47 The output image array where the result will be stored. Should have the same shape as `img`.
49 A parameter for gamma correction.
51 Parameter that defines the scale at which a change should be considered an edge.
53 Shadow adjustment factor.
55 Highlight adjustment factor. Negative values INCREASE highlights.
57 Clarity adjustment factor.
61 result : `numpy.ndarray`
62 The processed image array with the same shape as `out`.
65 h_s = (highlights, shadows)
68 for i
in prange(out.shape[0]):
75 for j
in prange(out.shape[1]):
82 t = s * c / (2.0 * sigma)
91 index = np.uint8(np.bool_(1 + s))
95 val = g + s * sigma * 2 * mt * t + t2 * (s * sigma + s * sigma * h_s[index])
96 val = val + clarity * c * np.exp(-(c * c) / (2.0 * sigma * sigma / 3.0))
105 img: NDArray, padY: list[int], padX: list[int], out: List[NDArray] |
None
106) -> Sequence[NDArray]:
108 Create a Gaussian Pyramid from an input image.
112 img : `numpy.ndarray`
113 The input image, which will be processed to create the pyramid.
114 padY : `list` of `int`
115 List containing padding sizes along the Y-axis for each level of the pyramid.
116 padX : `list` of `int`
117 List containing padding sizes along the X-axis for each level of the pyramid.
118 out : `~numba.typed.typedlist.List` of `NDArray` or `None`
119 Optional list to store the output images of the pyramid levels.
120 If None, a new list is created.
124 pyramid : `~collections.abc.sequence` of `numpy.ndarray`
125 A sequence of images representing the Gaussian Pyramid.
129 - The function creates a padded version of the input image and then
130 reduces its size using `cv2.pyrDown` to generate each level of the
132 - If 'out' is provided, it will be used to store the pyramid levels;
133 otherwise, a new list is dynamically created.
134 - Padding is applied only if specified by non-zero values in `padY` and
144 if padY[0]
or padX[0]:
145 paddedImage = cv2.copyMakeBorder(
146 img, *(0, padY[0]), *(0, padX[0]), cv2.BORDER_REPLICATE,
None if out
is None else pyramid[0],
None
153 pyramid.append(paddedImage)
157 pyramid[0] = paddedImage
160 for i
in range(1, len(padY)):
161 if padY[i]
or padX[i]:
162 paddedImage = cv2.copyMakeBorder(
163 paddedImage, *(0, padY[i]), *(0, padX[i]), cv2.BORDER_REPLICATE,
None,
None
166 paddedImage = cv2.pyrDown(paddedImage,
None if out
is None else pyramid[i])
170 pyramid.append(paddedImage)
178 gaussOut: List[NDArray] |
None,
179 lapOut: List[NDArray] |
None,
180 upscratch: List[NDArray] |
None =
None,
181) -> Sequence[NDArray]:
183 Create a Laplacian pyramid from the input image.
185 This function constructs a Laplacian pyramid from the input image. It first
186 generates a Gaussian pyramid and then, for each level (except the last),
187 subtracts the upsampled version of the next lower level from the current
188 level to obtain the Laplacian levels. If `lapOut` is None, it creates a
189 new list to store the Laplacian pyramid; otherwise, it uses the provided
194 img : `numpy.ndarray`
195 The input image as a numpy array.
196 padY : `list` of `int`
197 List of padding sizes for rows (vertical padding).
198 padX : `list` of `int`
199 List of padding sizes for columns (horizontal padding).
200 gaussOut : `~numba.typed.typedlist.List` of `numpy.ndarray` or None
201 Preallocated storage for the output of the Gaussian pyramid function.
202 If `None` new storage is allocated.
203 lapOut : `~numba.typed.typedlist.List` of `numpy.ndarray` or None
204 Preallocated for the output Laplacian pyramid. If None, a new
206 upscratch : `~numba.typed.typedlist.List` of `numpy.ndarray`, optional
207 List to store intermediate results of pyramids (default is None).
211 results : `~collections.abc.sequence` of `numpy.ndarray`
212 The Laplacian pyramid as a sequence of numpy arrays.
220 for i
in range(len(pyramid) - 1):
221 upsampled = cv2.pyrUp(pyramid[i + 1],
None if upscratch
is None else upscratch[i + 1])
222 if padY[i + 1]
or padX[i + 1]:
223 upsampled = upsampled[
224 : upsampled.shape[0] - 2 * padY[i + 1], : upsampled.shape[1] - 2 * padX[i + 1]
227 lapPyramid.append(pyramid[i] - upsampled)
229 cv2.subtract(pyramid[i], upsampled, dst=lapPyramid[i])
231 lapPyramid.append(pyramid[-1])
233 lapPyramid[-1][:, :] = pyramid[-1]
237@njit(fastmath=True, parallel=True, error_model="numpy", nogil=True)
240 pyramid: List[NDArray],
242 pyramidVectorsBottom: List[NDArray],
243 pyramidVectorsTop: List[NDArray],
246 Computes the output by interpolating between basis vectors at each pixel in
249 The function iterates over each pixel in the Gaussian pyramids
250 and interpolates between the corresponding basis vectors from
251 `pyramidVectorsBottom` and `pyramidVectorsTop`. If a pixel value is outside
252 the range defined by gamma, it skips interpolation.
256 out : `~numba.typed.typedlist.List` of `numpy.ndarray`
257 A list of numpy arrays representing the output image pyramids.
258 pyramid : `~numba.typed.typedlist.List` of `numpy.ndarray`
259 A list of numpy arrays representing the Gaussian pyramids.
260 gamma : `numpy.ndarray`
261 A numpy array containing the range for pixel values to be considered in
263 pyramidVectorsBottom : `~numba.typed.typedlist.List` of `numpy.ndarray`
264 A list of numpy arrays representing the basis vectors at the bottom
265 level of each pyramid layer.
266 pyramidVectorsTop : `~numba.typed.typedlist.List` of `numpy.ndarray`
267 A list of numpy arrays representing the basis vectors at the top level
268 of each pyramid layer.
272 for level
in prange(0, len(pyramid)):
273 yshape = pyramid[level].shape[0]
274 xshape = pyramid[level].shape[1]
275 plevel = pyramid[level]
276 outlevel = out[level]
277 basisBottom = pyramidVectorsBottom[level]
278 basisTop = pyramidVectorsTop[level]
279 for y
in prange(yshape):
281 outLevelY = outlevel[y]
282 basisBottomY = basisBottom[y]
283 basisTopY = basisTop[y]
284 for x
in prange(xshape):
286 if not (val >= gamma[0]
and val < gamma[1]):
288 a = (plevelY[x] - gamma[0]) / (gamma[1] - gamma[0])
289 outLevelY[x] = (1 - a) * basisBottomY[x] + a * basisTopY[x]
329 highlights: float = -0.9,
330 shadows: float = 0.4,
331 clarity: float = 0.15,
332 maxLevel: int |
None =
None,
336 """Enhance the local contrast of an input image.
340 image : `numpy.ndarray`
341 Two dimensional numpy array representing the image to have contrast
342 increased. Data must be in the range 0 to 1.
344 The scale over which edges are considered real and not noise.
346 A parameter that controls how highlights are enhanced or reduced,
347 contrary to intuition, negative values increase highlights.
349 A parameter that controls how shadows are deepened.
351 A parameter that relates to the contrast between highlights and
353 maxLevel : `int` or `None`
354 The maximum number of image pyramid levels to enhance the contrast over.
355 Each level has a spatial scale of roughly 2^(level) pixels.
357 This is an optimization parameter. This algorithm divides up contrast
358 space into a certain number of bins over which the expensive computation
359 is done. Contrast values in the image which fall between two of these
360 values are interpolated to get the outcome. The higher the numGamma,
361 the smoother the image is post contrast enhancement, though above
362 some number there is no discernible difference.
364 When calculating the local contrast skip the specified number of levels
365 starting at the lowest level.
369 image : `numpy.ndarray`
370 Two dimensional numpy array of the input image with increased local
376 Raised if the max level to enhance to is greater than the image
381 This function, and its supporting functions, spiritually implement the
382 algorithm outlined at
383 https://people.csail.mit.edu/sparis/publi/2011/siggraph/
384 titled "Local Laplacian Filters: Edge-aware Image Processing with Laplacian
385 Pyramid". This is not a 1:1 implementation, it's optimized for the
386 python language and runtime performance. Most notably it transforms only
387 certain levels and linearly interpolates to find other values. This
388 implementation is inspired by the one done in the darktable image editor:
389 https://www.darktable.org/2017/11/local-laplacian-pyramids/. None of the
390 code is in common, nor is the implementation 1:1, but reading the original
391 paper and the darktable implementation gives more info about this function.
392 Specifically some variable names follow the paper/other implementation,
393 and may be confusing when viewed without that context.
397 highlights = float(highlights)
398 shadows = float(shadows)
399 clarity = float(clarity)
404 maxImageLevel = int(np.min(np.log2(image.shape)))
406 maxLevel = maxImageLevel
407 if maxImageLevel < maxLevel:
409 f
"The supplied max level {maxLevel} is greater than the max of the image: {maxImageLevel}"
411 support = 1 << (maxLevel - 1)
412 padY_amounts =
levelPadder(image.shape[0] + support, maxLevel)
413 padX_amounts =
levelPadder(image.shape[1] + support, maxLevel)
414 imagePadded = cv2.copyMakeBorder(
415 image, *(0, support), *(0, support), cv2.BORDER_REPLICATE,
None,
None
416 ).astype(image.dtype)
419 gamma = np.linspace(0, 1, numGamma)
423 base_lap_pyr =
makeLapPyramid(imagePadded, padY_amounts, padX_amounts,
None,
None,
None)
425 finalPyramid = List()
426 for sample
in pyramid[:-1]:
427 finalPyramid.append(np.zeros_like(sample))
428 finalPyramid.append(pyramid[-1])
438 for i, sample
in enumerate(pyramid):
439 tmpGauss.append(np.empty_like(sample))
440 tmpLap1.append(np.empty_like(sample))
441 tmpLap2.append(np.empty_like(sample))
443 upscratch.append(np.empty((0, 0), dtype=image.dtype))
445 upscratch.append(np.empty((sample.shape[0] * 2, sample.shape[1] * 2), dtype=image.dtype))
448 cycler = iter(cycle((tmpLap1, tmpLap2)))
450 outCycle = iter(cycle((np.copy(imagePadded), np.copy(imagePadded))))
452 imagePadded, next(outCycle), gamma[0], sigma, shadows=shadows, highlights=highlights, clarity=clarity
455 prevImg, padY_amounts, padX_amounts, tmpGauss, next(cycler), upscratch=upscratch
458 for value
in range(1, len(gamma)):
459 pyramidVectors = List()
460 pyramidVectors.append(prevLapPyr)
467 highlights=highlights,
471 newImg, padY_amounts, padX_amounts, tmpGauss, next(cycler), upscratch=upscratch
473 pyramidVectors.append(prevLapPyr)
478 np.array((gamma[value - 1], gamma[value])),
484 for i
in range(skipLevels):
485 finalPyramid[i] = base_lap_pyr[i]
488 output = finalPyramid[-1]
489 for i
in range(-2, -1 * len(finalPyramid) - 1, -1):
490 upsampled = cv2.pyrUp(output)
491 upsampled = upsampled[
492 : upsampled.shape[0] - 2 * padY_amounts[i + 1], : upsampled.shape[1] - 2 * padX_amounts[i + 1]
494 output = finalPyramid[i] + upsampled
495 return output[:-support, :-support]