747 lines
30 KiB
Cython
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)
|