330) -> NDArray:
331 """Enhance the local contrast of an input image.
332
333 Parameters
334 ----------
335 image : `NDArray`
336 Two dimensional numpy array representing the image to have contrast
337 increased.
338 sigma : `float`
339 The scale over which edges are considered real and not noise.
340 highlights : `float`
341 A parameter that controls how highlights are enhansed or reduced,
342 contrary to intuition, negative values increase highlights.
343 shadows : `float`
344 A parameter that controls how shadows are deepened.
345 clarity : `float`
346 A parameter that relates to the contrast between highlights and
347 shadow.
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.
351 numGamma : `int`
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.
358
359 Returns
360 -------
361 image : `NDArray`
362 Two dimensional numpy array of the input image with increased local
363 contrast.
364
365 Raises
366 ------
367 ValueError
368 Raised if the max level to enhance to is greater than the image
369 supports.
370
371 Notes
372 -----
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.
386
387 """
388
389 highlights = float(highlights)
390 shadows = float(shadows)
391 clarity = float(clarity)
392
393
394
395
396 maxImageLevel = int(np.min(np.log2(image.shape)))
397 if maxLevel is None:
398 maxLevel = maxImageLevel
399 if maxImageLevel < maxLevel:
400 raise ValueError(
401 f"The supplied max level {maxLevel} is is greater than the max of the image: {maxImageLevel}"
402 )
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)
409
410
411 gamma = np.linspace(image.min(), image.max(), numGamma)
412
413
414 pyramid = makeGaussianPyramid(imagePadded, padY_amounts, padX_amounts, None)
415
416 finalPyramid = List()
417 for sample in pyramid[:-1]:
418 finalPyramid.append(np.zeros_like(sample))
419 finalPyramid.append(pyramid[-1])
420
421
422
423
424
425 tmpGauss = List()
426 tmpLap1 = List()
427 tmpLap2 = List()
428 upscratch = List()
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))
433 if i == 0:
434 upscratch.append(np.empty((0, 0), dtype=image.dtype))
435 continue
436 upscratch.append(np.empty((sample.shape[0] * 2, sample.shape[1] * 2), dtype=image.dtype))
437
438
439 cycler = iter(cycle((tmpLap1, tmpLap2)))
440
441 outCycle = iter(cycle((np.copy(imagePadded), np.copy(imagePadded))))
442 prevImg = r(
443 imagePadded, next(outCycle), gamma[0], sigma, shadows=shadows, highlights=highlights, clarity=clarity
444 )
445 prevLapPyr = makeLapPyramid(
446 prevImg, padY_amounts, padX_amounts, tmpGauss, next(cycler), upscratch=upscratch
447 )
448
449 for value in range(1, len(gamma) - 1):
450 pyramidVectors = List()
451 pyramidVectors.append(prevLapPyr)
452 newImg = r(
453 imagePadded,
454 next(outCycle),
455 gamma[value],
456 sigma,
457 shadows=shadows,
458 highlights=highlights,
459 clarity=clarity,
460 )
461 prevLapPyr = makeLapPyramid(
462 newImg, padY_amounts, padX_amounts, tmpGauss, next(cycler), upscratch=upscratch
463 )
464 pyramidVectors.append(prevLapPyr)
465
466 _calculateOutput(
467 finalPyramid,
468 pyramid,
469 np.array((gamma[value - 1], gamma[value])),
470 pyramidVectors[0],
471 pyramidVectors[1],
472 )
473 del pyramidVectors
474
475
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]
481 ]
482 output = finalPyramid[i] + upsampled
483 return output[:-support, :-support]