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.
44 The input image array of shape (n_images, height, width).
46 The output image array where the result will be stored. Should have the same shape as `img`.
48 A parameter for gamma correction.
50 Parameter that defines the scale at which a change should be considered an edge.
52 Shadow adjustment factor.
54 Highlight adjustment factor. Negative values INCREASE highlights.
56 Clarity adjustment factor.
60 The processed image array with the same shape as `out`.
63 h_s = (highlights, shadows)
66 for i
in prange(out.shape[0]):
73 for j
in prange(out.shape[1]):
80 t = s * c / (2.0 * sigma)
89 index = np.uint8(np.bool_(1 + s))
93 val = g + s * sigma * 2 * mt * t + t2 * (s * sigma + s * sigma * h_s[index])
94 val = val + clarity * c * np.exp(-(c * c) / (2.0 * sigma * sigma / 3.0))
103 img: NDArray, padY: list[int], padX: list[int], out: List[NDArray] |
None
104) -> Sequence[NDArray]:
106 Create a Gaussian Pyramid from an input image.
110 The input image, which will be processed to create the pyramid.
111 padY : `list` of `int`
112 List containing padding sizes along the Y-axis for each level of the pyramid.
113 padX : `list` of `int`
114 List containing padding sizes along the X-axis for each level of the pyramid.
115 out `numba.typed.typedlist.List` of `NDarray` or `None`
116 Optional list to store the output images of the pyramid levels.
117 If None, a new list is created.
120 pyramid : `Sequence` of `NDArray`
121 A sequence of images representing the Gaussian Pyramid.
124 - The function creates a padded version of the input image and then
125 reduces its size using `cv2.pyrDown` to generate each level of the
127 - If 'out' is provided, it will be used to store the pyramid levels;
128 otherwise, a new list is dynamically created.
129 - Padding is applied only if specified by non-zero values in `padY` and
139 if padY[0]
or padX[0]:
140 paddedImage = cv2.copyMakeBorder(
141 img, *(0, padY[0]), *(0, padX[0]), cv2.BORDER_REPLICATE,
None if out
is None else pyramid[0],
None
148 pyramid.append(paddedImage)
152 pyramid[0] = paddedImage
155 for i
in range(1, len(padY)):
156 if padY[i]
or padX[i]:
157 paddedImage = cv2.copyMakeBorder(
158 paddedImage, *(0, padY[i]), *(0, padX[i]), cv2.BORDER_REPLICATE,
None,
None
161 paddedImage = cv2.pyrDown(paddedImage,
None if out
is None else pyramid[i])
165 pyramid.append(paddedImage)
173 gaussOut: List[NDArray] |
None,
174 lapOut: List[NDArray] |
None,
175 upscratch: List[NDArray] |
None =
None,
176) -> Sequence[NDArray]:
178 Create a Laplacian pyramid from the input image.
180 This function constructs a Laplacian pyramid from the input image. It first
181 generates a Gaussian pyramid and then, for each level (except the last),
182 subtracts the upsampled version of the next lower level from the current
183 level to obtain the Laplacian levels. If `lapOut` is None, it creates a
184 new list to store the Laplacian pyramid; otherwise, it uses the provided
190 The input image as a numpy array.
191 padY : `list` of `int`
192 List of padding sizes for rows (vertical padding).
193 padX : `list` of `int`
194 List of padding sizes for columns (horizontal padding).
195 gaussOut : `numba.typed.typedlist.List` of `NDArray` or None
196 Preallocated storage for the output of the Gaussian pyramid function.
197 If `None` new storage is allocated.
198 lapOut : `numba.typed.typedlist.List` of `NDArray` or None
199 Preallocated for the output Laplacian pyramid. If None, a new
200 `numba.typed.typedlist.List` is created.
201 upscratch : `numba.typed.typedlist.List` of `NDarray`, optional
202 List to store intermediate results of pyramids (default is None).
206 results : `Sequence` of `NDArray`
207 The Laplacian pyramid as a sequence of numpy arrays.
210 pyramid = makeGaussianPyramid(img, padY, padX, gaussOut)
215 for i
in range(len(pyramid) - 1):
216 upsampled = cv2.pyrUp(pyramid[i + 1],
None if upscratch
is None else upscratch[i + 1])
217 if padY[i + 1]
or padX[i + 1]:
218 upsampled = upsampled[
219 : upsampled.shape[0] - 2 * padY[i + 1], : upsampled.shape[1] - 2 * padX[i + 1]
222 lapPyramid.append(pyramid[i] - upsampled)
224 cv2.subtract(pyramid[i], upsampled, dst=lapPyramid[i])
226 lapPyramid.append(pyramid[-1])
228 lapPyramid[-1][:, :] = pyramid[-1]
232@njit(fastmath=True, parallel=True, error_model="numpy", nogil=True)
235 pyramid: List[NDArray],
237 pyramidVectorsBottom: List[NDArray],
238 pyramidVectorsTop: List[NDArray],
241 Computes the output by interpolating between basis vectors at each pixel in
244 The function iterates over each pixel in the Gaussian pyramids
245 and interpolates between the corresponding basis vectors from
246 `pyramidVectorsBottom` and `pyramidVectorsTop`. If a pixel value is outside
247 the range defined by gamma, it skips interpolation.
251 out : `numba.typed.typedlist.List` of `np.ndarray`
252 A list of numpy arrays representing the output image pyramids.
253 pyramid : `numba.typed.typedlist.List` of `np.ndarray`
254 A list of numpy arrays representing the Gaussian pyramids.
256 A numpy array containing the range for pixel values to be considered in
258 pyramidVectorsBottom : `numba.typed.typedlist.List` of `np.ndarray`
259 A list of numpy arrays representing the basis vectors at the bottom
260 level of each pyramid layer.
261 pyramidVectorsTop : `numba.typed.typedlist.List` of `np.ndarray`
262 A list of numpy arrays representing the basis vectors at the top level
263 of each pyramid layer.
268 for level
in prange(0, len(pyramid) - 1):
269 yshape = pyramid[level].shape[0]
270 xshape = pyramid[level].shape[1]
271 plevel = pyramid[level]
272 outlevel = out[level]
273 basisBottom = pyramidVectorsBottom[level]
274 basisTop = pyramidVectorsTop[level]
275 for y
in prange(yshape):
277 outLevelY = outlevel[y]
278 basisBottomY = basisBottom[y]
279 basisTopY = basisTop[y]
280 for x
in prange(xshape):
282 if not (val >= gamma[0]
and val <= gamma[1]):
284 a = (plevelY[x] - gamma[0]) / (gamma[1] - gamma[0])
285 outLevelY[x] = (1 - a) * basisBottomY[x] + a * basisTopY[x]
325 highlights: float = -0.9,
326 shadows: float = 0.4,
327 clarity: float = 0.15,
328 maxLevel: int |
None =
None,
331 """Enhance the local contrast of an input image.
336 Two dimensional numpy array representing the image to have contrast
339 The scale over which edges are considered real and not noise.
341 A parameter that controls how highlights are enhansed or reduced,
342 contrary to intuition, negative values increase highlights.
344 A parameter that controls how shadows are deepened.
346 A parameter that relates to the contrast between highlights and
348 maxLevel : `int` or `None`
349 The maximum number of image pyramid levels to enhanse the contrast over.
350 Each level has a spatial scale of roughly 2^(level) pixles.
352 This is an optimization parameter. This algorithm divides up contrast
353 space into a certain numbers over which the expensive computation
354 is done. Contrast values in the image which fall between two of these
355 values are interpolated to get the outcome. The higher the numGamma,
356 the smoother the image is post contrast enhancement, though above
357 some number there is no decerable difference.
362 Two dimensional numpy array of the input image with increased local
368 Raised if the max level to enhance to is greater than the image
373 This function, and it's supporting functions, spiritually implement the
374 algorithm outlined at
375 https://people.csail.mit.edu/sparis/publi/2011/siggraph/
376 titled "Local Laplacian Filters: Edge-aware Image Processing with Laplacian
377 Pyramid". This is not a 1:1 implementation, it's optimized for the
378 python language and runtime performance. Most notably it transforms only
379 certain levels and linearly interpolates to find other values. This
380 implementation is inspired by the ony done in the darktable image editor:
381 https://www.darktable.org/2017/11/local-laplacian-pyramids/. None of the
382 code is in common, nor is the implementation 1:1, but reading the original
383 paper and the darktable implementation gives more info about this function.
384 Specifically some variable names follow the paper/other implementation,
385 and may be confusing when viewed without that context.
389 highlights = float(highlights)
390 shadows = float(shadows)
391 clarity = float(clarity)
396 maxImageLevel = int(np.min(np.log2(image.shape)))
398 maxLevel = maxImageLevel
399 if maxImageLevel < maxLevel:
401 f
"The supplied max level {maxLevel} is is greater than the max of the image: {maxImageLevel}"
403 support = 1 << (maxLevel - 1)
404 padY_amounts = levelPadder(image.shape[0] + support, maxLevel)
405 padX_amounts = levelPadder(image.shape[1] + support, maxLevel)
406 imagePadded = cv2.copyMakeBorder(
407 image, *(0, support), *(0, support), cv2.BORDER_REPLICATE,
None,
None
408 ).astype(image.dtype)
411 gamma = np.linspace(image.min(), image.max(), numGamma)
414 pyramid = makeGaussianPyramid(imagePadded, padY_amounts, padX_amounts,
None)
416 finalPyramid = List()
417 for sample
in pyramid[:-1]:
418 finalPyramid.append(np.zeros_like(sample))
419 finalPyramid.append(pyramid[-1])
429 for i, sample
in enumerate(pyramid):
430 tmpGauss.append(np.empty_like(sample))
431 tmpLap1.append(np.empty_like(sample))
432 tmpLap2.append(np.empty_like(sample))
434 upscratch.append(np.empty((0, 0), dtype=image.dtype))
436 upscratch.append(np.empty((sample.shape[0] * 2, sample.shape[1] * 2), dtype=image.dtype))
439 cycler = iter(cycle((tmpLap1, tmpLap2)))
441 outCycle = iter(cycle((np.copy(imagePadded), np.copy(imagePadded))))
443 imagePadded, next(outCycle), gamma[0], sigma, shadows=shadows, highlights=highlights, clarity=clarity
445 prevLapPyr = makeLapPyramid(
446 prevImg, padY_amounts, padX_amounts, tmpGauss, next(cycler), upscratch=upscratch
449 for value
in range(1, len(gamma) - 1):
450 pyramidVectors = List()
451 pyramidVectors.append(prevLapPyr)
458 highlights=highlights,
461 prevLapPyr = makeLapPyramid(
462 newImg, padY_amounts, padX_amounts, tmpGauss, next(cycler), upscratch=upscratch
464 pyramidVectors.append(prevLapPyr)
469 np.array((gamma[value - 1], gamma[value])),
476 output = finalPyramid[-1]
477 for i
in range(-2, -1 * len(finalPyramid) - 1, -1):
478 upsampled = cv2.pyrUp(output)
479 upsampled = upsampled[
480 : upsampled.shape[0] - 2 * padY_amounts[i + 1], : upsampled.shape[1] - 2 * padX_amounts[i + 1]
482 output = finalPyramid[i] + upsampled
483 return output[:-support, :-support]