_adapthist.py 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317
  1. """
  2. Adapted from "Contrast Limited Adaptive Histogram Equalization" by Karel
  3. Zuiderveld, Graphics Gems IV, Academic Press, 1994.
  4. http://tog.acm.org/resources/GraphicsGems/
  5. Relicensed with permission of the author under the Modified BSD license.
  6. """
  7. import math
  8. import numbers
  9. import numpy as np
  10. from .._shared.utils import _supported_float_type
  11. from ..color.adapt_rgb import adapt_rgb, hsv_value
  12. from .exposure import rescale_intensity
  13. from ..util import img_as_uint
  14. NR_OF_GRAY = 2**14 # number of grayscale levels to use in CLAHE algorithm
  15. @adapt_rgb(hsv_value)
  16. def equalize_adapthist(image, kernel_size=None, clip_limit=0.01, nbins=256):
  17. """Contrast Limited Adaptive Histogram Equalization (CLAHE).
  18. An algorithm for local contrast enhancement, that uses histograms computed
  19. over different tile regions of the image. Local details can therefore be
  20. enhanced even in regions that are darker or lighter than most of the image.
  21. Parameters
  22. ----------
  23. image : (M[, ...][, C]) ndarray
  24. Input image.
  25. kernel_size : int or array_like, optional
  26. Defines the shape of contextual regions used in the algorithm. If
  27. iterable is passed, it must have the same number of elements as
  28. ``image.ndim`` (without color channel). If integer, it is broadcasted
  29. to each `image` dimension. By default, ``kernel_size`` is 1/8 of
  30. ``image`` height by 1/8 of its width.
  31. clip_limit : float, optional
  32. Clipping limit, normalized between 0 and 1 (higher values give more
  33. contrast).
  34. nbins : int, optional
  35. Number of gray bins for histogram ("data range").
  36. Returns
  37. -------
  38. out : (M[, ...][, C]) ndarray
  39. Equalized image with float64 dtype.
  40. See Also
  41. --------
  42. equalize_hist, rescale_intensity
  43. Notes
  44. -----
  45. * For color images, the following steps are performed:
  46. - The image is converted to HSV color space
  47. - The CLAHE algorithm is run on the V (Value) channel
  48. - The image is converted back to RGB space and returned
  49. * For RGBA images, the original alpha channel is removed.
  50. .. versionchanged:: 0.17
  51. The values returned by this function are slightly shifted upwards
  52. because of an internal change in rounding behavior.
  53. References
  54. ----------
  55. .. [1] http://tog.acm.org/resources/GraphicsGems/
  56. .. [2] https://en.wikipedia.org/wiki/CLAHE#CLAHE
  57. """
  58. float_dtype = _supported_float_type(image.dtype)
  59. image = img_as_uint(image)
  60. image = np.round(rescale_intensity(image, out_range=(0, NR_OF_GRAY - 1))).astype(
  61. np.min_scalar_type(NR_OF_GRAY)
  62. )
  63. if kernel_size is None:
  64. kernel_size = tuple([max(s // 8, 1) for s in image.shape])
  65. elif isinstance(kernel_size, numbers.Number):
  66. kernel_size = (kernel_size,) * image.ndim
  67. elif len(kernel_size) != image.ndim:
  68. raise ValueError(f'Incorrect value of `kernel_size`: {kernel_size}')
  69. kernel_size = [int(k) for k in kernel_size]
  70. image = _clahe(image, kernel_size, clip_limit, nbins)
  71. image = image.astype(float_dtype, copy=False)
  72. return rescale_intensity(image)
  73. def _clahe(image, kernel_size, clip_limit, nbins):
  74. """Contrast Limited Adaptive Histogram Equalization.
  75. Parameters
  76. ----------
  77. image : (M[, ...]) ndarray
  78. Input image.
  79. kernel_size : int or N-tuple of int
  80. Defines the shape of contextual regions used in the algorithm.
  81. clip_limit : float
  82. Normalized clipping limit between 0 and 1 (higher values give more
  83. contrast).
  84. nbins : int
  85. Number of gray bins for histogram ("data range").
  86. Returns
  87. -------
  88. out : (M[, ...]) ndarray
  89. Equalized image.
  90. The number of "effective" graylevels in the output image is set by `nbins`;
  91. selecting a small value (e.g. 128) speeds up processing and still produces
  92. an output image of good quality. A clip limit of 0 or larger than or equal
  93. to 1 results in standard (non-contrast limited) AHE.
  94. """
  95. ndim = image.ndim
  96. dtype = image.dtype
  97. # pad the image such that the shape in each dimension
  98. # - is a multiple of the kernel_size and
  99. # - is preceded by half a kernel size
  100. pad_start_per_dim = [k // 2 for k in kernel_size]
  101. pad_end_per_dim = [
  102. (k - s % k) % k + int(np.ceil(k / 2.0))
  103. for k, s in zip(kernel_size, image.shape)
  104. ]
  105. image = np.pad(
  106. image,
  107. [[p_i, p_f] for p_i, p_f in zip(pad_start_per_dim, pad_end_per_dim)],
  108. mode='reflect',
  109. )
  110. # determine gray value bins
  111. bin_size = 1 + NR_OF_GRAY // nbins
  112. lut = np.arange(NR_OF_GRAY, dtype=np.min_scalar_type(NR_OF_GRAY))
  113. lut //= bin_size
  114. image = lut[image]
  115. # calculate graylevel mappings for each contextual region
  116. # rearrange image into flattened contextual regions
  117. ns_hist = [int(s / k) - 1 for s, k in zip(image.shape, kernel_size)]
  118. hist_blocks_shape = np.array([ns_hist, kernel_size]).T.flatten()
  119. hist_blocks_axis_order = np.array(
  120. [np.arange(0, ndim * 2, 2), np.arange(1, ndim * 2, 2)]
  121. ).flatten()
  122. hist_slices = [slice(k // 2, k // 2 + n * k) for k, n in zip(kernel_size, ns_hist)]
  123. hist_blocks = image[tuple(hist_slices)].reshape(hist_blocks_shape)
  124. hist_blocks = np.transpose(hist_blocks, axes=hist_blocks_axis_order)
  125. hist_block_assembled_shape = hist_blocks.shape
  126. hist_blocks = hist_blocks.reshape((math.prod(ns_hist), -1))
  127. # Calculate actual clip limit
  128. kernel_elements = math.prod(kernel_size)
  129. if clip_limit > 0.0:
  130. clim = int(np.clip(clip_limit * kernel_elements, 1, None))
  131. else:
  132. # largest possible value, i.e., do not clip (AHE)
  133. clim = kernel_elements
  134. hist = np.apply_along_axis(np.bincount, -1, hist_blocks, minlength=nbins)
  135. hist = np.apply_along_axis(clip_histogram, -1, hist, clip_limit=clim)
  136. hist = map_histogram(hist, 0, NR_OF_GRAY - 1, kernel_elements)
  137. hist = hist.reshape(hist_block_assembled_shape[:ndim] + (-1,))
  138. # duplicate leading mappings in each dim
  139. map_array = np.pad(hist, [[1, 1] for _ in range(ndim)] + [[0, 0]], mode='edge')
  140. # Perform multilinear interpolation of graylevel mappings
  141. # using the convention described here:
  142. # https://en.wikipedia.org/w/index.php?title=Adaptive_histogram_
  143. # equalization&oldid=936814673#Efficient_computation_by_interpolation
  144. # rearrange image into blocks for vectorized processing
  145. ns_proc = [int(s / k) for s, k in zip(image.shape, kernel_size)]
  146. blocks_shape = np.array([ns_proc, kernel_size]).T.flatten()
  147. blocks_axis_order = np.array(
  148. [np.arange(0, ndim * 2, 2), np.arange(1, ndim * 2, 2)]
  149. ).flatten()
  150. blocks = image.reshape(blocks_shape)
  151. blocks = np.transpose(blocks, axes=blocks_axis_order)
  152. blocks_flattened_shape = blocks.shape
  153. blocks = np.reshape(blocks, (math.prod(ns_proc), math.prod(blocks.shape[ndim:])))
  154. # calculate interpolation coefficients
  155. coeffs = np.meshgrid(
  156. *tuple([np.arange(k) / k for k in kernel_size[::-1]]), indexing='ij'
  157. )
  158. coeffs = [np.transpose(c).flatten() for c in coeffs]
  159. inv_coeffs = [1 - c for dim, c in enumerate(coeffs)]
  160. # sum over contributions of neighboring contextual
  161. # regions in each direction
  162. result = np.zeros(blocks.shape, dtype=np.float32)
  163. for iedge, edge in enumerate(np.ndindex(*([2] * ndim))):
  164. edge_maps = map_array[tuple([slice(e, e + n) for e, n in zip(edge, ns_proc)])]
  165. edge_maps = edge_maps.reshape((math.prod(ns_proc), -1))
  166. # apply map
  167. edge_mapped = np.take_along_axis(edge_maps, blocks, axis=-1)
  168. # interpolate
  169. edge_coeffs = np.prod(
  170. [[inv_coeffs, coeffs][e][d] for d, e in enumerate(edge[::-1])], 0
  171. )
  172. result += (edge_mapped * edge_coeffs).astype(result.dtype)
  173. result = result.astype(dtype)
  174. # rebuild result image from blocks
  175. result = result.reshape(blocks_flattened_shape)
  176. blocks_axis_rebuild_order = np.array(
  177. [np.arange(0, ndim), np.arange(ndim, ndim * 2)]
  178. ).T.flatten()
  179. result = np.transpose(result, axes=blocks_axis_rebuild_order)
  180. result = result.reshape(image.shape)
  181. # undo padding
  182. unpad_slices = tuple(
  183. [
  184. slice(p_i, s - p_f)
  185. for p_i, p_f, s in zip(pad_start_per_dim, pad_end_per_dim, image.shape)
  186. ]
  187. )
  188. result = result[unpad_slices]
  189. return result
  190. def clip_histogram(hist, clip_limit):
  191. """Perform clipping of the histogram and redistribution of bins.
  192. The histogram is clipped and the number of excess pixels is counted.
  193. Afterwards the excess pixels are equally redistributed across the
  194. whole histogram (providing the bin count is smaller than the cliplimit).
  195. Parameters
  196. ----------
  197. hist : ndarray
  198. Histogram array.
  199. clip_limit : int
  200. Maximum allowed bin count.
  201. Returns
  202. -------
  203. hist : ndarray
  204. Clipped histogram.
  205. """
  206. # calculate total number of excess pixels
  207. excess_mask = hist > clip_limit
  208. excess = hist[excess_mask]
  209. n_excess = excess.sum() - excess.size * clip_limit
  210. hist[excess_mask] = clip_limit
  211. # Second part: clip histogram and redistribute excess pixels in each bin
  212. bin_incr = n_excess // hist.size # average binincrement
  213. upper = clip_limit - bin_incr # Bins larger than upper set to cliplimit
  214. low_mask = hist < upper
  215. n_excess -= hist[low_mask].size * bin_incr
  216. hist[low_mask] += bin_incr
  217. mid_mask = np.logical_and(hist >= upper, hist < clip_limit)
  218. mid = hist[mid_mask]
  219. n_excess += mid.sum() - mid.size * clip_limit
  220. hist[mid_mask] = clip_limit
  221. while n_excess > 0: # Redistribute remaining excess
  222. prev_n_excess = n_excess
  223. for index in range(hist.size):
  224. under_mask = hist < clip_limit
  225. step_size = max(1, np.count_nonzero(under_mask) // n_excess)
  226. under_mask = under_mask[index::step_size]
  227. hist[index::step_size][under_mask] += 1
  228. n_excess -= np.count_nonzero(under_mask)
  229. if n_excess <= 0:
  230. break
  231. if prev_n_excess == n_excess:
  232. break
  233. return hist
  234. def map_histogram(hist, min_val, max_val, n_pixels):
  235. """Calculate the equalized lookup table (mapping).
  236. It does so by cumulating the input histogram.
  237. Histogram bins are assumed to be represented by the last array dimension.
  238. Parameters
  239. ----------
  240. hist : ndarray
  241. Clipped histogram.
  242. min_val : int
  243. Minimum value for mapping.
  244. max_val : int
  245. Maximum value for mapping.
  246. n_pixels : int
  247. Number of pixels in the region.
  248. Returns
  249. -------
  250. out : ndarray
  251. Mapped intensity LUT.
  252. """
  253. out = np.cumsum(hist, axis=-1).astype(float)
  254. out *= (max_val - min_val) / n_pixels
  255. out += min_val
  256. np.clip(out, a_min=None, a_max=max_val, out=out)
  257. return out.astype(int)