CofeehousePy/deps/scikit-image/skimage/restoration/_nl_means_denoising.pyx

747 lines
30 KiB
Cython

#cython: initializedcheck=False
#cython: wraparound=False
#cython: boundscheck=False
#cython: cdivision=True
import numpy as np
cimport numpy as cnp
from .._shared.fused_numerics cimport np_floats
from .._shared.fast_exp cimport _fast_exp
cnp.import_array()
cdef inline np_floats patch_distance_2d(np_floats [:, :, :] p1,
np_floats [:, :, :] p2,
np_floats [:, ::] w,
Py_ssize_t s, np_floats var,
Py_ssize_t n_channels) nogil:
"""
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 : Py_ssize_t
Linear size of the patches.
var_diff : np_floats
The double of the expected noise variance.
n_channels : Py_ssize_t
The number of channels.
Returns
-------
distance : np_floats
Gaussian distance between the two patches
Notes
-----
The returned distance is given by
.. math:: \exp( -w ((p1 - p2)^2 - 2*var))
"""
cdef Py_ssize_t i, j, channel
cdef np_floats DISTANCE_CUTOFF = 5.0
cdef np_floats tmp_diff = 0
cdef np_floats 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 channel in range(n_channels):
tmp_diff = p1[i, j, channel] - p2[i, j, channel]
distance += w[i, j] * (tmp_diff * tmp_diff - var)
return _fast_exp(-max(0.0, distance))
cdef inline np_floats patch_distance_3d(np_floats [:, :, :] p1,
np_floats [:, :, :] p2,
np_floats [:, :, ::] w,
Py_ssize_t s, np_floats var) nogil:
"""
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 : Py_ssize_t
Linear size of the patches.
var_diff : np_floats
The double of the expected noise variance.
Returns
-------
distance : np_floats
Gaussian distance between the two patches
Notes
-----
The returned distance is given by
.. math:: \exp( -w ((p1 - p2)^2 - 2*var))
"""
cdef Py_ssize_t i, j, k
cdef np_floats DISTANCE_CUTOFF = 5.0
cdef np_floats distance = 0
cdef np_floats 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 - var)
return _fast_exp(-max(0.0, distance))
def _nl_means_denoising_2d(cnp.ndarray[np_floats, ndim=3] image, Py_ssize_t s,
Py_ssize_t d, double h, double var):
"""
Perform non-local means denoising on 2-D RGB image
Parameters
----------
image : ndarray
Input RGB image to be denoised
s : Py_ssize_t, optional
Size of patches used for denoising
d : Py_ssize_t, optional
Maximal distance in pixels where to search patches used for denoising
h : np_floats, optional
Cut-off distance (in gray levels). The higher h, the more permissive
one is in accepting patches.
var : np_floats
Expected noise variance. If non-zero, this is used to reduce the
apparent patch distances by the expected distance due to the noise.
Notes
-----
This function operates on 2D grayscale and multichannel images. For
2D grayscale images, the input should be 3D with size 1 along the last
axis. The code is compatible with an arbitrary number of channels.
Returns
-------
result : ndarray
Denoised image, of same shape as input image.
"""
if s % 2 == 0:
s += 1 # odd value for symmetric patch
if np_floats is cnp.float32_t:
dtype = np.float32
else:
dtype = np.float64
cdef Py_ssize_t n_row, n_col, n_channels
n_row, n_col, n_channels = image.shape[0], image.shape[1], image.shape[2]
cdef Py_ssize_t offset = s / 2
cdef Py_ssize_t row, col, i, j, channel, i_start, i_end, j_start, j_end
cdef np_floats[::1] new_values = np.zeros(n_channels, dtype=dtype)
cdef np_floats[:, :, ::1] padded = np.ascontiguousarray(
np.pad(image, ((offset, offset), (offset, offset), (0, 0)),
mode='reflect'))
cdef np_floats [:, :, ::1] result = np.empty_like(image)
cdef np_floats new_value
cdef np_floats weight_sum, weight
cdef np_floats A = ((s - 1.) / 4.)
cdef np_floats [::1] range_vals = np.arange(-offset, offset + 1,
dtype=dtype)
xg_row, xg_col = np.meshgrid(range_vals, range_vals, indexing='ij')
cdef np_floats [:, ::1] w = np.ascontiguousarray(
np.exp(-(xg_row * xg_row + xg_col * xg_col) / (2 * A * A)))
w *= 1. / (n_channels * np.sum(w) * h * h)
cdef np_floats [:, :, :] central_patch
var *= 2
# Iterate over rows, taking padding into account
with nogil:
for row in range(n_row):
# Iterate over columns, taking padding into account
i_start = row - min(d, row)
i_end = row + min(d + 1, n_row - row)
for col in range(n_col):
# Initialize per-channel bins
new_values[:] = 0
# Reset weights for each local region
weight_sum = 0
central_patch = padded[row:row+s, col:col+s, :]
j_start = col - min(d, col)
j_end = col + min(d + 1, n_col - col)
# Iterate over local 2d patch for each pixel
for i in range(i_start, i_end):
for j in range(j_start, j_end):
weight = patch_distance_2d[np_floats](
central_patch,
padded[i:i+s, j:j+s, :],
w, s, var, n_channels)
# Collect results in weight sum
weight_sum += weight
# Apply to each channel multiplicatively
for channel in range(n_channels):
new_values[channel] += weight * padded[i+offset,
j+offset,
channel]
# Normalize the result
for channel in range(n_channels):
result[row, col, channel] = new_values[channel] / weight_sum
return np.squeeze(np.asarray(result))
def _nl_means_denoising_3d(cnp.ndarray[np_floats, ndim=3] image,
Py_ssize_t s, Py_ssize_t d,
double h, double var):
"""
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 : Py_ssize_t, optional
Maximal distance in pixels where to search patches used for denoising.
h : np_floats, optional
Cut-off distance (in gray levels).
var : np_floats
Expected noise variance. If non-zero, this is used to reduce the
apparent patch distances by the expected distance due to the noise.
Returns
-------
result : ndarray
Denoised image, of same shape as input image.
"""
if s % 2 == 0:
s += 1 # odd value for symmetric patch
if np_floats is cnp.float32_t:
dtype = np.float32
else:
dtype = np.float64
cdef Py_ssize_t n_pln, n_row, n_col
n_pln, n_row, n_col = image.shape[0], image.shape[1], image.shape[2]
cdef Py_ssize_t i_start, i_end, j_start, j_end, k_start, k_end
cdef Py_ssize_t pln, row, col, i, j, k
cdef Py_ssize_t offset = s / 2
# padd the image so that boundaries are denoised as well
cdef np_floats [:, :, ::1] padded = np.ascontiguousarray(
np.pad(image, offset, mode='reflect'))
cdef np_floats [:, :, ::1] result = np.empty_like(image)
cdef np_floats new_value
cdef np_floats weight_sum, weight
cdef np_floats A = ((s - 1.) / 4.)
cdef np_floats [::] range_vals = np.arange(-offset, offset + 1,
dtype=dtype)
xg_pln, xg_row, xg_col = np.meshgrid(range_vals, range_vals, range_vals,
indexing='ij')
cdef np_floats [:, :, ::1] w = np.ascontiguousarray(
np.exp(-(xg_pln * xg_pln + xg_row * xg_row + xg_col * xg_col) /
(2 * A * A)))
w *= 1. / (np.sum(w) * h * h)
cdef np_floats [:, :, :] central_patch
var *= 2
# Iterate over planes, taking padding into account
with nogil:
for pln in range(n_pln):
i_start = pln - min(d, pln)
i_end = pln + min(d + 1, n_pln - pln)
# Iterate over rows, taking padding into account
for row in range(n_row):
j_start = row - min(d, row)
j_end = row + min(d + 1, n_row - row)
# Iterate over columns, taking padding into account
for col in range(n_col):
k_start = col - min(d, col)
k_end = col + min(d + 1, n_col - col)
central_patch = padded[pln:pln+s, row:row+s, col:col+s]
new_value = 0
weight_sum = 0
# Iterate over local 3d patch for each pixel
for i in range(i_start, i_end):
for j in range(j_start, j_end):
for k in range(k_start, k_end):
weight = patch_distance_3d[np_floats](
central_patch,
padded[i:i+s, j:j+s, k:k+s],
w, s, var)
# Collect results in weight sum
weight_sum += weight
new_value += weight * padded[i+offset,
j+offset,
k+offset]
# Normalize the result
result[pln, row, col] = new_value / weight_sum
return np.asarray(result)
#-------------- Accelerated algorithm of Froment 2015 ------------------
cdef inline double _integral_to_distance_2d(double [:, ::] integral,
Py_ssize_t row, Py_ssize_t col,
Py_ssize_t offset,
double h2s2) nogil:
"""
References
----------
J. Darbon, A. Cunha, T.F. Chan, S. Osher, and G.J. Jensen, Fast
nonlocal filtering applied to electron cryomicroscopy, in 5th IEEE
International Symposium on Biomedical Imaging: From Nano to Macro,
2008, pp. 1331-1334.
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 double distance = (integral[row + offset, col + offset] +
integral[row - offset, col - offset] -
integral[row - offset, col + offset] -
integral[row + offset, col - offset])
return max(distance, 0.0) / h2s2
cdef inline double _integral_to_distance_3d(double[:, :, ::] integral,
Py_ssize_t pln, Py_ssize_t row,
Py_ssize_t col, Py_ssize_t offset,
double s_cube_h_square) nogil:
"""
References
----------
J. Darbon, A. Cunha, T.F. Chan, S. Osher, and G.J. Jensen, Fast
nonlocal filtering applied to electron cryomicroscopy, in 5th IEEE
International Symposium on Biomedical Imaging: From Nano to Macro,
2008, pp. 1331-1334.
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 double 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])
return max(distance, 0.0) / (s_cube_h_square)
cdef inline void _integral_image_2d(double [:, :, ::] padded,
double [:, ::] integral,
Py_ssize_t t_row, Py_ssize_t t_col,
Py_ssize_t n_row, Py_ssize_t n_col,
Py_ssize_t n_channels,
double var_diff) nogil:
"""
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_channels)
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 : Py_ssize_t
Shift along the row axis.
t_col : Py_ssize_t
Shift along the column axis (positive).
n_row : Py_ssize_t
n_col : Py_ssize_t
n_channels : Py_ssize_t
var_diff : double
The double of the expected noise variance. If non-zero, this
is used to reduce the apparent patch distances by the expected
distance due to the noise.
Notes
-----
The integral computation could be performed using
``transform.integral_image``, but this helper function saves memory
by avoiding copies of ``padded``.
"""
cdef Py_ssize_t row, col, channel
cdef Py_ssize_t row_start = max(1, -t_row)
cdef Py_ssize_t row_end = min(n_row, n_row - t_row)
cdef double t, distance
for row in range(row_start, row_end):
for col in range(1, n_col - t_col):
distance = 0
for channel in range(n_channels):
t = (padded[row, col, channel] -
padded[row + t_row, col + t_col, channel])
distance += t * t
distance -= n_channels * var_diff
integral[row, col] = (distance +
integral[row - 1, col] +
integral[row, col - 1] -
integral[row - 1, col - 1])
cdef inline void _integral_image_3d(double [:, :, ::] padded,
double [:, :, ::] integral,
Py_ssize_t t_pln, Py_ssize_t t_row,
Py_ssize_t t_col, Py_ssize_t n_pln,
Py_ssize_t n_row, Py_ssize_t n_col,
double var_diff) nogil:
"""
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 : Py_ssize_t
Shift along the plane axis.
t_row : Py_ssize_t
Shift along the row axis.
t_col : Py_ssize_t
Shift along the column axis (positive).
n_pln : Py_ssize_t
n_row : Py_ssize_t
n_col : Py_ssize_t
var_diff : np_floats
The double of the expected noise variance. If non-zero, this
is used to reduce the apparent patch distances by the expected
distance due to the noise.
Notes
-----
The integral computation could be performed using
``transform.integral_image``, but this helper function saves memory
by avoiding copies of ``padded``.
"""
cdef Py_ssize_t pln, row, col
cdef Py_ssize_t pln_start = max(1, -t_pln)
cdef Py_ssize_t pln_end = min(n_pln, n_pln - t_pln)
cdef Py_ssize_t row_start = max(1, -t_row)
cdef Py_ssize_t row_end = min(n_row, n_row - t_row)
cdef double distance
for pln in range(pln_start, pln_end):
for row in range(row_start, row_end):
for col in range(1, n_col - t_col):
distance = (padded[pln, row, col] -
padded[pln + t_pln, row + t_row, col + t_col])
distance *= distance
distance -= var_diff
integral[pln, row, col] = (
distance +
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])
def _fast_nl_means_denoising_2d(cnp.ndarray[np_floats, ndim=3] image,
Py_ssize_t s, Py_ssize_t d,
double h, double var):
"""
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 : Py_ssize_t, optional
Size of patches used for denoising.
d : Py_ssize_t, optional
Maximal distance in pixels where to search patches used for denoising.
h : double, optional
Cut-off distance (in gray levels). The higher h, the more permissive
one is in accepting patches.
var : double
Expected noise variance. If non-zero, this is used to reduce the
apparent patch distances by the expected distance due to the noise.
Returns
-------
result : ndarray
Denoised image, of same shape as input image.
References
----------
J. Darbon, A. Cunha, T.F. Chan, S. Osher, and G.J. Jensen, Fast
nonlocal filtering applied to electron cryomicroscopy, in 5th IEEE
International Symposium on Biomedical Imaging: From Nano to Macro,
2008, pp. 1331-1334.
Jacques Froment. Parameter-Free Fast Pixelwise Non-Local Means
Denoising. Image Processing On Line, 2014, vol. 4, p. 300-326.
"""
cdef double DISTANCE_CUTOFF = 5.0
if s % 2 == 0:
s += 1 # odd value for symmetric patch
if np_floats is cnp.float32_t:
dtype = np.float32
else:
dtype = np.float64
# Image padding: we need to account for patch size, possible shift,
# + 1 for the boundary effects in finite differences
cdef Py_ssize_t n_row, n_col, t_row, t_col, row, col, n_channels, channel
cdef Py_ssize_t row_start, row_end, row_shift, col_shift
cdef Py_ssize_t offset = s / 2
cdef Py_ssize_t pad_size = offset + d + 1
cdef double [:, :, ::1] padded = np.ascontiguousarray(
np.pad(image, ((pad_size, pad_size), (pad_size, pad_size), (0, 0)),
mode='reflect').astype(np.float64))
cdef double [:, ::1] weights = np.zeros_like(padded[..., 0])
cdef double [:, ::1] integral = np.zeros_like(weights)
cdef double [:, :, ::1] result = np.zeros_like(padded)
cdef double distance, h2s2, weight, alpha
n_row, n_col, n_channels = padded.shape[0], padded.shape[1], padded.shape[2]
h2s2 = n_channels * h * h * s * s
var *= 2
with nogil:
# 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):
# alpha is to account for patches on the same column
# distance is computed twice in this case
if t_row != 0:
alpha = 0.5
else:
alpha = 1.0
row_start = max(offset, offset - t_row)
row_end = min(n_row - offset, n_row - offset - t_row)
# Iterate over shifts along the column axis
for t_col in range(0, d + 1):
# Compute integral image of the squared difference between
# padded and the same image shifted by (t_row, t_col)
_integral_image_2d(padded, integral, t_row, t_col,
n_row, n_col, n_channels, var)
# Inner loops on pixel coordinates
# Iterate over rows, taking offset and shift into account
for row in range(row_start, row_end):
row_shift = row + t_row
# Iterate over columns, taking offset and shift into account
for col in range(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
col_shift = col + t_col
weight = alpha * _fast_exp(-distance)
# Accumulate weights corresponding to different shifts
weights[row, col] += weight
weights[row_shift, col_shift] += weight
# Iterate over channels
for channel in range(n_channels):
result[row, col, channel] += weight * \
padded[row_shift, col_shift, channel]
result[row_shift, col_shift, channel] += \
weight * padded[row, col, channel]
alpha = 1
# Normalize pixel values using sum of weights of contributing patches
for row in range(pad_size, n_row - pad_size):
for col in range(pad_size, n_col - pad_size):
for channel in range(n_channels):
# 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 np.squeeze(np.asarray(result[pad_size: -pad_size,
pad_size: -pad_size, :]).astype(dtype))
def _fast_nl_means_denoising_3d(cnp.ndarray[np_floats, ndim=3] image,
Py_ssize_t s, Py_ssize_t d, double h,
double var):
"""
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 : Py_ssize_t, optional
Size of patches used for denoising.
d : Py_ssize_t, optional
Maximal distance in pixels where to search patches used for denoising.
h : double, optional
cut-off distance (in gray levels). The higher h, the more permissive
one is in accepting patches.
var : double
Expected noise variance. If non-zero, this is used to reduce the
apparent patch distances by the expected distance due to the noise.
Returns
-------
result : ndarray
Denoised image, of same shape as input image.
References
----------
J. Darbon, A. Cunha, T.F. Chan, S. Osher, and G.J. Jensen, Fast
nonlocal filtering applied to electron cryomicroscopy, in 5th IEEE
International Symposium on Biomedical Imaging: From Nano to Macro,
2008, pp. 1331-1334.
Jacques Froment. Parameter-Free Fast Pixelwise Non-Local Means
Denoising. Image Processing On Line, 2014, vol. 4, p. 300-326.
"""
cdef double DISTANCE_CUTOFF = 5.0
if s % 2 == 0:
s += 1 # odd value for symmetric patch
if np_floats is cnp.float32_t:
dtype = np.float32
else:
dtype = np.float64
cdef Py_ssize_t offset = s / 2
# Image padding: we need to account for patch size, possible shift,
# + 1 for the boundary effects in finite differences
cdef Py_ssize_t pad_size = offset + d + 1
cdef double [:, :, ::1] padded = np.ascontiguousarray(
np.pad(image, pad_size, mode='reflect').astype(np.float64))
cdef double [:, :, ::1] weights = np.zeros_like(padded)
cdef double [:, :, ::1] integral = np.zeros_like(padded)
cdef double [:, :, ::1] result = np.zeros_like(padded)
cdef Py_ssize_t n_pln, n_row, n_col, t_pln, t_row, t_col, \
pln, row, col
cdef Py_ssize_t pln_dist_min, pln_dist_max, row_dist_min, row_dist_max, \
col_dist_min, col_dist_max
cdef double weight, distance, alpha
cdef double s_cube_h_square = h * h * s * s * s
n_pln, n_row, n_col = padded.shape[0], padded.shape[1], padded.shape[2]
var *= 2
with nogil:
# 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)
# alpha is to account for patches on the same column
# distance is computed twice in this case
if t_pln == 0:
alpha = 1.0
else:
alpha = 0.5
# 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)
if t_row == 0:
alpha = 1.0
else:
alpha = 0.5
# Iterate over shifts along the column axis
for t_col in range(0, d + 1):
col_dist_min = offset
col_dist_max = n_col - offset - t_col
# Compute integral image of the squared difference between
# padded and the same image shifted by (t_pln, t_row, t_col)
_integral_image_3d(padded, integral, t_pln,
t_row, t_col, n_pln, n_row,
n_col, var)
# 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 * _fast_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]
alpha = 1.0
# Normalize pixel values using sum of weights of contributing patches
for pln in range(pad_size, n_pln - pad_size):
for row in range(pad_size, n_row - pad_size):
for col in range(pad_size, n_col - pad_size):
# 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 np.asarray(result[pad_size:-pad_size, pad_size:-pad_size,
pad_size:-pad_size]).astype(dtype)