Merge pull request #874 from emmanuelle/nlm_denoise

FEAT: NL-means denoising
This commit is contained in:
Josh Warner
2015-02-19 16:53:58 -06:00
7 changed files with 956 additions and 3 deletions
+3
View File
@@ -149,6 +149,9 @@ Library:
Extension: skimage.restoration._denoise_cy
Sources:
skimage/restoration/_denoise_cy.pyx
Extension: skimage.restoration._nl_means_denoising
Sources:
skimage/restoration/_nl_means_denoising.pyx
Extension: skimage.feature._hessian_det_appx
Sources:
skimage/exposure/_hessian_det_appx.pyx
+41
View File
@@ -0,0 +1,41 @@
"""
=================================================
Non-local means denoising for preserving textures
=================================================
In this example, we denoise a detail of the astronaut image using the non-local
means filter. The non-local means algorithm replaces the value of a pixel by an
average of a selection of other pixels values: small patches centered on the
other pixels are compared to the patch centered on the pixel of interest, and
the average is performed only for pixels that have patches close to the current
patch. As a result, this algorithm can restore well textures, that would be
blurred by other denoising algoritm.
"""
import numpy as np
import matplotlib.pyplot as plt
from skimage import data, img_as_float
from skimage.restoration import nl_means_denoising
astro = img_as_float(data.astronaut())
astro = astro[30:180, 150:300]
noisy = astro + 0.3 * np.random.random(astro.shape)
noisy = np.clip(noisy, 0, 1)
denoise = nl_means_denoising(noisy, 7, 9, 0.08)
fig, ax = plt.subplots(ncols=2, figsize=(8, 4))
ax[0].imshow(noisy)
ax[0].axis('off')
ax[0].set_title('noisy')
ax[1].imshow(denoise)
ax[1].axis('off')
ax[1].set_title('non-local means')
fig.subplots_adjust(wspace=0.02, hspace=0.2,
top=0.9, bottom=0.05, left=0, right=1)
plt.show()
+3 -2
View File
@@ -22,7 +22,7 @@ from .deconvolution import wiener, unsupervised_wiener, richardson_lucy
from .unwrap import unwrap_phase
from ._denoise import denoise_tv_chambolle, denoise_tv_bregman, \
denoise_bilateral
from .non_local_means import nl_means_denoising
__all__ = ['wiener',
'unsupervised_wiener',
@@ -30,4 +30,5 @@ __all__ = ['wiener',
'unwrap_phase',
'denoise_tv_bregman',
'denoise_tv_chambolle',
'denoise_bilateral']
'denoise_bilateral',
'nl_means_denoising']
+712
View File
@@ -0,0 +1,712 @@
import numpy as np
from skimage import util
cimport numpy as np
cimport cython
from libc.math cimport exp
ctypedef np.float32_t IMGDTYPE
cdef float DISTANCE_CUTOFF = 5.
@cython.boundscheck(False)
cdef inline float patch_distance_2d(IMGDTYPE [:, :] p1,
IMGDTYPE [:, :] p2,
IMGDTYPE [:, ::] w, int s):
"""
Compute a Gaussian distance between two image patches.
Parameters
----------
p1 : 2-D array_like
First patch.
p2 : 2-D array_like
Second patch.
w : 2-D array_like
Array of weigths for the different pixels of the patches.
s : int
Linear size of the patches.
Returns
-------
distance : float
Gaussian distance between the two patches
Notes
-----
The returned distance is given by
.. math:: \exp( -w (p1 - p2)^2)
"""
cdef int i, j
cdef int center = s / 2
# Check if central pixel is too different in the 2 patches
cdef float tmp_diff = p1[center, center] - p2[center, center]
cdef float init = w[center, center] * tmp_diff * tmp_diff
if init > 1:
return 0.
cdef float distance = 0
for i in range(s):
# exp of large negative numbers will be 0, so we'd better stop
if distance > DISTANCE_CUTOFF:
return 0.
for j in range(s):
tmp_diff = p1[i, j] - p2[i, j]
distance += (w[i, j] * tmp_diff * tmp_diff)
distance = exp(-distance)
return distance
@cython.boundscheck(False)
cdef inline float patch_distance_2drgb(IMGDTYPE [:, :, :] p1,
IMGDTYPE [:, :, :] p2,
IMGDTYPE [:, ::] w, int s):
"""
Compute a Gaussian distance between two image patches.
Parameters
----------
p1 : 3-D array_like
First patch, 2D image with last dimension corresponding to channels.
p2 : 3-D array_like
Second patch, 2D image with last dimension corresponding to channels.
w : 2-D array_like
Array of weights for the different pixels of the patches.
s : int
Linear size of the patches.
Returns
-------
distance : float
Gaussian distance between the two patches
Notes
-----
The returned distance is given by
.. math:: \exp( -w (p1 - p2)^2)
"""
cdef int i, j
cdef int center = s / 2
cdef int color
cdef float tmp_diff = 0
cdef float distance = 0
for i in range(s):
# exp of large negative numbers will be 0, so we'd better stop
if distance > DISTANCE_CUTOFF:
return 0.
for j in range(s):
for color in range(3):
tmp_diff = p1[i, j, color] - p2[i, j, color]
distance += w[i, j] * tmp_diff * tmp_diff
distance = exp(-distance)
return distance
@cython.boundscheck(False)
cdef inline float patch_distance_3d(IMGDTYPE [:, :, :] p1,
IMGDTYPE [:, :, :] p2,
IMGDTYPE [:, :, ::] w, int s):
"""
Compute a Gaussian distance between two image patches.
Parameters
----------
p1 : 3-D array_like
First patch.
p2 : 3-D array_like
Second patch.
w : 3-D array_like
Array of weights for the different pixels of the patches.
s : int
Linear size of the patches.
Returns
-------
distance : float
Gaussian distance between the two patches
Notes
-----
The returned distance is given by
.. math:: \exp( -w (p1 - p2)^2)
"""
cdef int i, j, k
cdef float distance = 0
cdef float tmp_diff
for i in range(s):
# exp of large negative numbers will be 0, so we'd better stop
if distance > DISTANCE_CUTOFF:
return 0.
for j in range(s):
for k in range(s):
tmp_diff = p1[i, j, k] - p2[i, j, k]
distance += w[i, j, k] * tmp_diff * tmp_diff
distance = exp(-distance)
return distance
@cython.cdivision(True)
@cython.boundscheck(False)
def _nl_means_denoising_2d(image, int s=7, int d=13, float h=0.1):
"""
Perform non-local means denoising on 2-D RGB image
Parameters
----------
image : ndarray
Input RGB image to be denoised
s : int, optional
Size of patches used for denoising
d : int, optional
Maximal distance in pixels where to search patches used for denoising
h : float, optional
Cut-off distance (in gray levels). The higher h, the more permissive
one is in accepting patches.
Returns
-------
result : ndarray
Denoised image, of same shape as input image.
"""
if s % 2 == 0:
s += 1 # odd value for symmetric patch
cdef int n_row, n_col, n_ch
n_row, n_col, n_ch = image.shape
cdef int offset = s / 2
cdef int row, col, i, j, color
cdef int row_start, row_end, col_start, col_end
cdef int row_start_i, row_end_i, col_start_j, col_end_j
cdef IMGDTYPE [::1] new_values = np.zeros(n_ch).astype(np.float32)
cdef IMGDTYPE [:, :, ::1] padded = np.ascontiguousarray(util.pad(image,
((offset, offset), (offset, offset), (0, 0)),
mode='reflect').astype(np.float32))
cdef IMGDTYPE [:, :, ::1] result = padded.copy()
cdef float A = ((s - 1.) / 4.)
cdef float new_value
cdef float weight_sum, weight
xg_row, xg_col = np.mgrid[-offset:offset + 1, -offset:offset + 1]
cdef IMGDTYPE [:, ::1] w = np.ascontiguousarray(np.exp(
-(xg_row ** 2 + xg_col ** 2) / (2 * A ** 2)).
astype(np.float32))
cdef float distance
w = 1. / (n_ch * np.sum(w) * h ** 2) * w
# Coordinates of central pixel
# Iterate over rows, taking padding into account
for row in range(offset, n_row + offset):
row_start = row - offset
row_end = row + offset + 1
# Iterate over columns, taking padding into account
for col in range(offset, n_col + offset):
# Initialize per-channel bins
for color in range(n_ch):
new_values[color] = 0
# Reset weights for each local region
weight_sum = 0
col_start = col - offset
col_end = col + offset + 1
# Iterate over local 2d patch for each pixel
# First rows
for i in range(max(-d, offset - row),
min(d + 1, n_row + offset - row)):
row_start_i = row_start + i
row_end_i = row_end + i
# Local patch columns
for j in range(max(-d, offset - col),
min(d + 1, n_col + offset - col)):
col_start_j = col_start + j
col_end_j = col_end + j
# Shortcut for grayscale, else assume RGB
if n_ch == 1:
weight = patch_distance_2d(
padded[row_start:row_end,
col_start:col_end, 0],
padded[row_start_i:row_end_i,
col_start_j:col_end_j, 0],
w, s)
else:
weight = patch_distance_2drgb(
padded[row_start:row_end,
col_start:col_end, :],
padded[row_start_i:row_end_i,
col_start_j:col_end_j, :],
w, s)
# Collect results in weight sum
weight_sum += weight
# Apply to each channel multiplicatively
for color in range(n_ch):
new_values[color] += weight * padded[row + i,
col + j, color]
# Normalize the result
for color in range(n_ch):
result[row, col, color] = new_values[color] / weight_sum
# Return cropped result, undoing padding
return result[offset:-offset, offset:-offset]
@cython.cdivision(True)
@cython.boundscheck(False)
def _nl_means_denoising_3d(image, int s=7,
int d=13, float h=0.1):
"""
Perform non-local means denoising on 3-D array
Parameters
----------
image : ndarray
Input data to be denoised.
s : int, optional
Size of patches used for denoising.
d : int, optional
Maximal distance in pixels where to search patches used for denoising.
h : float, optional
Cut-off distance (in gray levels).
Returns
-------
result : ndarray
Denoised image, of same shape as input image.
"""
if s % 2 == 0:
s += 1 # odd value for symmetric patch
cdef int n_pln, n_row, n_col
n_pln, n_row, n_col = image.shape
cdef int offset = s / 2
# padd the image so that boundaries are denoised as well
cdef IMGDTYPE [:, :, ::1] padded = np.ascontiguousarray(util.pad(
image.astype(np.float32),
offset, mode='reflect'))
cdef IMGDTYPE [:, :, ::1] result = padded.copy()
cdef float A = ((s - 1.) / 4.)
cdef float new_value
cdef float weight_sum, weight
xg_pln, xg_row, xg_col = np.mgrid[-offset: offset + 1,
-offset: offset + 1,
-offset: offset + 1]
cdef IMGDTYPE [:, :, ::1] w = np.ascontiguousarray(np.exp(
-(xg_pln ** 2 + xg_row ** 2 + xg_col ** 2) /
(2 * A ** 2)).astype(np.float32))
cdef float distance
cdef int pln, row, col, i, j, k
cdef int pln_start, pln_end, row_start, row_end, col_start, col_end
cdef int pln_start_i, pln_end_i, row_start_j, row_end_j, \
col_start_k, col_end_k
w = 1. / (np.sum(w) * h ** 2) * w
# Coordinates of central pixel
# Iterate over planes, taking padding into account
for pln in range(offset, n_pln + offset):
pln_start = pln - offset
pln_end = pln + offset + 1
# Iterate over rows, taking padding into account
for row in range(offset, n_row + offset):
row_start = row - offset
row_end = row + offset + 1
# Iterate over columns, taking padding into account
for col in range(offset, n_col + offset):
col_start = col - offset
col_end = col + offset + 1
new_value = 0
weight_sum = 0
# Iterate over local 3d patch for each pixel
# First planes
for i in range(max(-d, offset - pln),
min(d + 1, n_pln + offset - pln)):
pln_start_i = pln_start + i
pln_end_i = pln_end + i
# Rows
for j in range(max(-d, offset - row),
min(d + 1, n_row + offset - row)):
row_start_j = row_start + j
row_end_j = row_end + j
# Columns
for k in range(max(-d, offset - col),
min(d + 1, n_col + offset - col)):
col_start_k = col_start + k
col_end_k = col_end + k
weight = patch_distance_3d(
padded[pln_start:pln_end,
row_start:row_end,
col_start:col_end],
padded[pln_start_i:pln_end_i,
row_start_j:row_end_j,
col_start_k:col_end_k],
w, s)
# Collect results in weight sum
weight_sum += weight
new_value += weight * padded[pln + i,
row + j, col + k]
# Normalize the result
result[pln, row, col] = new_value / weight_sum
# Return cropped result, undoing padding
return result[offset:-offset, offset:-offset, offset:-offset]
#-------------- Accelerated algorithm of Froment 2015 ------------------
@cython.cdivision(True)
@cython.boundscheck(False)
cdef inline float _integral_to_distance_2d(IMGDTYPE [:, ::] integral,
int row, int col, int offset, float h2s2):
"""
References
----------
Jacques Froment. Parameter-Free Fast Pixelwise Non-Local Means
Denoising. Image Processing On Line, 2014, vol. 4, p. 300-326.
Used in _fast_nl_means_denoising_2d
"""
cdef float distance
distance = integral[row + offset, col + offset] + \
integral[row - offset, col - offset] - \
integral[row - offset, col + offset] - \
integral[row + offset, col - offset]
distance /= h2s2
return distance
@cython.cdivision(True)
@cython.boundscheck(False)
cdef inline float _integral_to_distance_3d(IMGDTYPE [:, :, ::] integral,
int pln, int row, int col, int offset,
float s_cube_h_square):
"""
References
----------
Jacques Froment. Parameter-Free Fast Pixelwise Non-Local Means
Denoising. Image Processing On Line, 2014, vol. 4, p. 300-326.
Used in _fast_nl_means_denoising_3d
"""
cdef float distance
distance = (integral[pln + offset, row + offset, col + offset] -
integral[pln - offset, row - offset, col - offset] +
integral[pln - offset, row - offset, col + offset] +
integral[pln - offset, row + offset, col - offset] +
integral[pln + offset, row - offset, col - offset] -
integral[pln - offset, row + offset, col + offset] -
integral[pln + offset, row - offset, col + offset] -
integral[pln + offset, row + offset, col - offset])
distance /= s_cube_h_square
return distance
@cython.cdivision(True)
@cython.boundscheck(False)
cdef inline _integral_image_2d(IMGDTYPE [:, :, ::] padded,
IMGDTYPE [:, ::] integral, int t_row,
int t_col, int n_row, int n_col, int n_ch):
"""
Computes the integral of the squared difference between an image ``padded``
and the same image shifted by ``(t_row, t_col)``.
Parameters
----------
padded : ndarray of shape (n_row, n_col, n_ch)
Image of interest.
integral : ndarray
Output of the function. The array is filled with integral values.
``integral`` should have the same shape as ``padded``.
t_row : int
Shift along the row axis.
t_col : int
Shift along the column axis.
n_row : int
n_col : int
n_ch : int
Notes
-----
The integral computation could be performed using
``transform.integral_image``, but this helper function saves memory
by avoiding copies of ``padded``.
"""
cdef int row, col
cdef float distance
for row in range(max(1, -t_row), min(n_row, n_row - t_row)):
for col in range(max(1, -t_col), min(n_col, n_col - t_col)):
if n_ch == 1:
distance = (padded[row, col, 0] -
padded[row + t_row, col + t_col, 0])**2
else:
distance = ((padded[row, col, 0] -
padded[row + t_row, col + t_col, 0])**2 +
(padded[row, col, 1] -
padded[row + t_row, col + t_col, 1])**2 +
(padded[row, col, 2] -
padded[row + t_row, col + t_col, 2])**2)
integral[row, col] = distance + \
integral[row - 1, col] + \
integral[row, col - 1] - \
integral[row - 1, col - 1]
@cython.cdivision(True)
@cython.boundscheck(False)
cdef inline _integral_image_3d(IMGDTYPE [:, :, ::] padded,
IMGDTYPE [:, :, ::] integral, int t_pln,
int t_row, int t_col, int n_pln, int n_row,
int n_col):
"""
Computes the integral of the squared difference between an image ``padded``
and the same image shifted by ``(t_pln, t_row, t_col)``.
Parameters
----------
padded : ndarray of shape (n_pln, n_row, n_col)
Image of interest.
integral : ndarray
Output of the function. The array is filled with integral values.
``integral`` should have the same shape as ``padded``.
t_pln : int
Shift along the plane axis.
t_row : int
Shift along the row axis.
t_col : int
Shift along the column axis.
n_pln : int
n_row : int
n_col : int
Notes
-----
The integral computation could be performed using
``transform.integral_image``, but this helper function saves memory
by avoiding copies of ``padded``.
"""
cdef int pln, row, col
cdef float distance
for pln in range(max(1, -t_pln), min(n_pln, n_pln - t_pln)):
for row in range(max(1, -t_row), min(n_row, n_row - t_row)):
for col in range(max(1, -t_col), min(n_col, n_col - t_col)):
integral[pln, row, col] = \
((padded[pln, row, col] -
padded[pln + t_pln, row + t_row, col + t_col])**2 +
integral[pln - 1, row, col] +
integral[pln, row - 1, col] +
integral[pln, row, col - 1] +
integral[pln - 1, row - 1, col - 1] -
integral[pln - 1, row - 1, col] -
integral[pln, row - 1, col - 1] -
integral[pln - 1, row, col - 1])
@cython.cdivision(True)
@cython.boundscheck(False)
def _fast_nl_means_denoising_2d(image, int s=7, int d=13, float h=0.1):
"""
Perform fast non-local means denoising on 2-D array, with the outer
loop on patch shifts in order to reduce the number of operations.
Parameters
----------
image : ndarray
2-D input data to be denoised, grayscale or RGB.
s : int, optional
Size of patches used for denoising.
d : int, optional
Maximal distance in pixels where to search patches used for denoising.
h : float, optional
Cut-off distance (in gray levels). The higher h, the more permissive
one is in accepting patches.
Returns
-------
result : ndarray
Denoised image, of same shape as input image.
"""
if s % 2 == 0:
s += 1 # odd value for symmetric patch
cdef int offset = s / 2
# Image padding: we need to account for patch size, possible shift,
# + 1 for the boundary effects in finite differences
cdef int pad_size = offset + d + 1
cdef IMGDTYPE [:, :, ::1] padded = np.ascontiguousarray(util.pad(image,
((pad_size, pad_size), (pad_size, pad_size), (0, 0)),
mode='reflect').astype(np.float32))
cdef IMGDTYPE [:, :, ::1] result = np.zeros_like(padded)
cdef IMGDTYPE [:, ::1] weights = np.zeros_like(padded[..., 0], order='C')
cdef IMGDTYPE [:, ::1] integral = np.zeros_like(padded[..., 0], order='C')
cdef int n_row, n_col, n_ch, t_row, t_col, row, col
cdef float weight, distance
cdef float alpha
cdef float h2 = h ** 2.
cdef float s2 = s ** 2.
n_row, n_col, n_ch = image.shape
cdef float h2s2 = n_ch * h2 * s2
n_row += 2 * pad_size
n_col += 2 * pad_size
# Outer loops on patch shifts
# With t2 >= 0, reference patch is always on the left of test patch
# Iterate over shifts along the row axis
for t_row in range(-d, d + 1):
# Iterate over shifts along the column axis
for t_col in range(0, d + 1):
# alpha is to account for patches on the same column
# distance is computed twice in this case
if t_col == 0 and t_row is not 0:
alpha = 0.5
else:
alpha = 1.
# Compute integral image of the squared difference between
# padded and the same image shifted by (t_row, t_col)
integral = np.zeros_like(padded[..., 0], order='C')
_integral_image_2d(padded, integral, t_row, t_col,
n_row, n_col, n_ch)
# Inner loops on pixel coordinates
# Iterate over rows, taking offset and shift into account
for row in range(max(offset, offset - t_row),
min(n_row - offset, n_row - offset - t_row)):
# Iterate over columns, taking offset and shift into account
for col in range(max(offset, offset - t_col),
min(n_col - offset, n_col - offset - t_col)):
# Compute squared distance between shifted patches
distance = _integral_to_distance_2d(integral, row, col,
offset, h2s2)
# exp of large negative numbers is close to zero
if distance > DISTANCE_CUTOFF:
continue
weight = alpha * exp(-distance)
# Accumulate weights corresponding to different shifts
weights[row, col] += weight
weights[row + t_row, col + t_col] += weight
# Iterate over channels
for ch in range(n_ch):
result[row, col, ch] += weight * \
padded[row + t_row, col + t_col, ch]
result[row + t_row, col + t_col, ch] += \
weight * padded[row, col, ch]
# Normalize pixel values using sum of weights of contributing patches
for row in range(offset, n_row - offset):
for col in range(offset, n_col - offset):
for channel in range(n_ch):
# No risk of division by zero, since the contribution
# of a null shift is strictly positive
result[row, col, channel] /= weights[row, col]
# Return cropped result, undoing padding
return result[pad_size:-pad_size, pad_size:-pad_size]
@cython.cdivision(True)
@cython.boundscheck(False)
def _fast_nl_means_denoising_3d(image, int s=5, int d=7, float h=0.1):
"""
Perform fast non-local means denoising on 3-D array, with the outer
loop on patch shifts in order to reduce the number of operations.
Parameters
----------
image : ndarray
3-D input data to be denoised.
s : int, optional
Size of patches used for denoising.
d : int, optional
Maximal distance in pixels where to search patches used for denoising.
h : float, optional
cut-off distance (in gray levels). The higher h, the more permissive
one is in accepting patches.
Returns
-------
result : ndarray
Denoised image, of same shape as input image.
"""
if s % 2 == 0:
s += 1 # odd value for symmetric patch
cdef int offset = s / 2
# Image padding: we need to account for patch size, possible shift,
# + 1 for the boundary effects in finite differences
cdef int pad_size = offset + d + 1
cdef IMGDTYPE [:, :, ::1] padded = np.ascontiguousarray(util.pad(image,
pad_size, mode='reflect').astype(np.float32))
cdef IMGDTYPE [:, :, ::1] result = np.zeros_like(padded)
cdef IMGDTYPE [:, :, ::1] weights = np.zeros_like(padded)
cdef IMGDTYPE [:, :, ::1] integral = np.zeros_like(padded)
cdef int n_pln, n_row, n_col, t_pln, t_row, t_col, \
pln, row, col
cdef int pln_dist_min, pln_dist_max, row_dist_min, row_dist_max, \
col_dist_min, col_dist_max
cdef float weight, distance
cdef float alpha
cdef float h_square = h ** 2.
cdef float s_cube = s ** 3.
cdef float s_cube_h_square = h_square * s_cube
n_pln, n_row, n_col = image.shape
n_pln += 2 * pad_size
n_row += 2 * pad_size
n_col += 2 * pad_size
# Outer loops on patch shifts
# With t2 >= 0, reference patch is always on the left of test patch
# Iterate over shifts along the plane axis
for t_pln in range(-d, d + 1):
pln_dist_min = max(offset, offset - t_pln)
pln_dist_max = min(n_pln - offset, n_pln - offset - t_pln)
# Iterate over shifts along the row axis
for t_row in range(-d, d + 1):
row_dist_min = max(offset, offset - t_row)
row_dist_max = min(n_row - offset, n_row - offset - t_row)
# Iterate over shifts along the column axis
for t_col in range(0, d + 1):
col_dist_min = max(offset, offset - t_col)
col_dist_max = min(n_col - offset, n_col - offset - t_col)
# alpha is to account for patches on the same column
# distance is computed twice in this case
if t_col == 0 and (t_pln is not 0 or t_row is not 0):
alpha = 0.5
else:
alpha = 1.
# Compute integral image of the squared difference between
# padded and the same image shifted by (t_pln, t_row, t_col)
integral = np.zeros_like(padded)
_integral_image_3d(padded, integral, t_pln, t_row, t_col,
n_pln, n_row, n_col)
# Inner loops on pixel coordinates
# Iterate over planes, taking offset and shift into account
for pln in range(pln_dist_min, pln_dist_max):
# Iterate over rows, taking offset and shift into account
for row in range(row_dist_min, row_dist_max):
# Iterate over columns
for col in range(col_dist_min, col_dist_max):
# Compute squared distance between shifted patches
distance = _integral_to_distance_3d(integral,
pln, row, col, offset, s_cube_h_square)
# exp of large negative numbers is close to zero
if distance > DISTANCE_CUTOFF:
continue
weight = alpha * exp(-distance)
# Accumulate weights for the different shifts
weights[pln, row, col] += weight
weights[pln + t_pln, row + t_row,
col + t_col] += weight
result[pln, row, col] += weight * \
padded[pln + t_pln, row + t_row,
col + t_col]
result[pln + t_pln, row + t_row,
col + t_col] += weight * \
padded[pln, row, col]
# Normalize pixel values using sum of weights of contributing patches
for pln in range(offset, n_pln - offset):
for row in range(offset, n_row - offset):
for col in range(offset, n_col - offset):
# No risk of division by zero, since the contribution
# of a null shift is strictly positive
result[pln, row, col] /= weights[pln, row, col]
# Return cropped result, undoing padding
return result[pad_size:-pad_size, pad_size:-pad_size, pad_size:-pad_size]
+120
View File
@@ -0,0 +1,120 @@
import numpy as np
from skimage.restoration._nl_means_denoising import _nl_means_denoising_2d, \
_nl_means_denoising_3d, \
_fast_nl_means_denoising_2d, _fast_nl_means_denoising_3d
def nl_means_denoising(image, patch_size=7, patch_distance=11, h=0.1,
multichannel=True, fast_mode=True):
"""
Perform non-local means denoising on 2-D or 3-D grayscale images, and
2-D RGB images.
Parameters
----------
image : 2D or 3D ndarray
Input image to be denoised, which can be 2D or 3D, and grayscale
or RGB (for 2D images only, see ``multichannel`` parameter).
patch_size : int, optional
Size of patches used for denoising.
patch_distance : int, optional
Maximal distance in pixels where to search patches used for denoising.
h : float, optional
Cut-off distance (in gray levels). The higher h, the more permissive
one is in accepting patches. A higher h results in a smoother image,
at the expense of blurring features. For a Gaussian noise of standard
deviation sigma, a rule of thumb is to choose the value of h to be
sigma of slightly less.
multichannel : bool, optional
Whether the last axis of the image is to be interpreted as multiple
channels or another spatial dimension. Set to ``False`` for 3-D images.
fast_mode : bool, optional
If True (default value), a fast version of the non-local means
algorithm is used. If False, the original version of non-local means is
used. See the Notes section for more details about the algorithms.
Returns
-------
result : ndarray
Denoised image, of same shape as `image`.
See Also
--------
fast_nl_means_denoising
Notes
-----
The non-local means algorithm is well suited for denoising images with
specific textures. The principle of the algorithm is to average the value
of a given pixel with values of other pixels in a limited neighbourhood,
provided that the *patches* centered on the other pixels are similar enough
to the patch centered on the pixel of interest.
In the original version of the algorithm [1]_, corresponding to
``fast=False``, the computational complexity is
image.size * patch_size ** image.ndim * patch_distance ** image.ndim
Hence, changing the size of patches or their maximal distance has a
strong effect on computing times, especially for 3-D images.
However, the default behavior corresponds to ``fast_mode=True``, for which
another version of non-local means [2]_ is used, corresponding to a
complexity of
image.size * patch_distance ** image.ndim
The computing time depends only weakly on the patch size, thanks to the
computation of the integral of patches distances for a given shift, that
reduces the number of operations [1]_. Therefore, this algorithm executes
faster than `nl_means_denoising`, at the expense of using twice as much
memory.
Compared to the classic non-local means algorithm implemented in
`nl_means_denoising`, all pixels of a patch contribute to the distance to
another patch with the same weight, no matter their distance to the center
of the patch. This coarser computation of the distance can result in a
slightly poorer denoising performance. Moreover, for small images (images
with a linear size that is only a few times the patch size), the classic
algorithm can be faster due to boundary effects.
The image is padded using the `reflect` mode of `skimage.util.pad`
before denoising.
References
----------
.. [1] Buades, A., Coll, B., & Morel, J. M. (2005, June). A non-local
algorithm for image denoising. In CVPR 2005, Vol. 2, pp. 60-65, IEEE.
.. [2] Jacques Froment. Parameter-Free Fast Pixelwise Non-Local Means
Denoising. Image Processing On Line, 2014, vol. 4, p. 300-326.
Examples
--------
>>> a = np.zeros((40, 40))
>>> a[10:-10, 10:-10] = 1.
>>> a += 0.3*np.random.randn(*a.shape)
>>> denoised_a = nl_means_denoising(a, 7, 5, 0.1)
"""
if image.ndim == 2:
image = image[..., np.newaxis]
multichannel = True
if image.ndim != 3:
raise NotImplementedError("Non-local means denoising is only \
implemented for 2D grayscale and RGB images or 3-D grayscale images.")
if multichannel: # 2-D images
if fast_mode:
return np.squeeze(np.array(_fast_nl_means_denoising_2d(image,
patch_size, patch_distance, h)))
else:
return np.squeeze(np.array(_nl_means_denoising_2d(image,
patch_size, patch_distance, h)))
else: # 3-D grayscale
if fast_mode:
return np.array(_fast_nl_means_denoising_3d(image, s=patch_size,
d=patch_distance, h=h))
else:
return np.array(_nl_means_denoising_3d(image, patch_size,
patch_distance, h))
+4
View File
@@ -17,6 +17,7 @@ def configuration(parent_package='', top_path=None):
cython(['_unwrap_2d.pyx'], working_path=base_path)
cython(['_unwrap_3d.pyx'], working_path=base_path)
cython(['_denoise_cy.pyx'], working_path=base_path)
cython(['_nl_means_denoising.pyx'], working_path=base_path)
config.add_extension('_unwrap_1d', sources=['_unwrap_1d.c'],
include_dirs=[get_numpy_include_dirs()])
@@ -28,6 +29,9 @@ def configuration(parent_package='', top_path=None):
include_dirs=[get_numpy_include_dirs()])
config.add_extension('_denoise_cy', sources=['_denoise_cy.c'],
include_dirs=[get_numpy_include_dirs(), '../_shared'])
config.add_extension('_nl_means_denoising',
sources=['_nl_means_denoising.c'],
include_dirs=[get_numpy_include_dirs()])
return config
+73 -1
View File
@@ -46,7 +46,8 @@ def test_denoise_tv_chambolle_float_result_range():
img = astro_gray
int_astro = np.multiply(img, 255).astype(np.uint8)
assert np.max(int_astro) > 1
denoised_int_astro = restoration.denoise_tv_chambolle(int_astro, weight=60.0)
denoised_int_astro = restoration.denoise_tv_chambolle(int_astro,
weight=60.0)
# test if the value range of output float data is within [0.0:1.0]
assert denoised_int_astro.dtype == np.float
assert np.max(denoised_int_astro) <= 1.0
@@ -143,5 +144,76 @@ def test_denoise_bilateral_3d():
assert out1[30:45, 5:15].std() > out2[30:45, 5:15].std()
def test_nl_means_denoising_2d():
img = np.zeros((40, 40))
img[10:-10, 10:-10] = 1.
img += 0.3*np.random.randn(*img.shape)
denoised = restoration.nl_means_denoising(img, 7, 5, 0.2, fast_mode=True)
# make sure noise is reduced
assert img.std() > denoised.std()
denoised = restoration.nl_means_denoising(img, 7, 5, 0.2, fast_mode=False)
# make sure noise is reduced
assert img.std() > denoised.std()
def test_nl_means_denoising_2drgb():
# reduce image size because nl means is very slow
img = np.copy(astro[:50, :50])
# add some random noise
img += 0.5 * img.std() * np.random.random(img.shape)
img = np.clip(img, 0, 1)
denoised = restoration.nl_means_denoising(img, 7, 9, 0.3, fast_mode=True)
# make sure noise is reduced
assert img.std() > denoised.std()
denoised = restoration.nl_means_denoising(img, 7, 9, 0.3, fast_mode=False)
# make sure noise is reduced
assert img.std() > denoised.std()
def test_nl_means_denoising_3d():
img = np.zeros((20, 20, 10))
img[5:-5, 5:-5, 3:-3] = 1.
img += 0.3*np.random.randn(*img.shape)
denoised = restoration.nl_means_denoising(img, 5, 4, 0.2, fast_mode=True,
multichannel=False)
# make sure noise is reduced
assert img.std() > denoised.std()
denoised = restoration.nl_means_denoising(img, 5, 4, 0.2, fast_mode=False,
multichannel=False)
# make sure noise is reduced
assert img.std() > denoised.std()
def test_nl_means_denoising_multichannel():
img = np.zeros((21, 20, 10))
img[10, 9:11, 2:-2] = 1.
img += 0.3*np.random.randn(*img.shape)
denoised_wrong_multichannel = restoration.nl_means_denoising(img,
5, 4, 0.1, fast_mode=True, multichannel=True)
denoised_ok_multichannel = restoration.nl_means_denoising(img,
5, 4, 0.1, fast_mode=True, multichannel=False)
snr_wrong = 10 * np.log10(1. /
((denoised_wrong_multichannel - img)**2).mean())
snr_ok = 10 * np.log10(1. /
((denoised_ok_multichannel - img)**2).mean())
assert snr_ok > snr_wrong
def test_nl_means_denoising_wrong_dimension():
img = np.zeros((5, 5, 5, 5))
assert_raises(NotImplementedError, restoration.nl_means_denoising, img)
def test_no_denoising_for_small_h():
img = np.zeros((40, 40))
img[10:-10, 10:-10] = 1.
img += 0.3*np.random.randn(*img.shape)
# very small h should result in no averaging with other patches
denoised = restoration.nl_means_denoising(img, 7, 5, 0.01, fast_mode=True)
assert np.allclose(denoised, img)
denoised = restoration.nl_means_denoising(img, 7, 5, 0.01, fast_mode=False)
assert np.allclose(denoised, img)
if __name__ == "__main__":
run_module_suite()