"""
This module implements a series of functions for noise level estimation.
"""
import numpy as np
from scipy.ndimage import correlate
from scipy.stats import gamma
from skimage.util import view_as_windows
__all__ = ["conv2d_matrix", "noise_estimation", "noiselevel", "weak_texture_mask"]
[docs]
def noise_estimation(img, patchsize=7, decim=0, confidence=1 - 1e-6, iterations=3):
"""
Estimates the noise level of an image.
Additive white Gaussian noise (AWGN) is a basic noise model used in Information Theory
to mimic the effect of many random processes that occur in nature.
Parameters
----------
img: `numpy.ndarray`
Single Numpy image array.
patchsize : `int`, optional
Patch size, defaults to 7.
decim : `int`, optional
Decimation factor, defaults to 0.
If you use large number, the calculation will be accelerated.
confidence : `float`, optional
Confidence interval to determine the threshold for the weak texture.
In this algorithm, this value is usually set the value very close to one.
Defaults to 0.99.
iterations : `int`, optional
Number of iterations, defaults to 3.
Returns
-------
`dict`
A dictionary containing the estimated noise levels, ``nlevel``; threshold to extract weak texture
patches at the last iteration, ``thresh``; number of extracted weak texture patches ``num`` and the
weak texture mask, ``mask``.
Examples
--------
>>> import numpy as np
>>> rng = np.random.default_rng(0)
>>> noisy_image_array = rng.standard_normal((100, 100))
>>> estimate = noise_estimation(noisy_image_array, patchsize=11, iterations=10)
>>> estimate["mask"] # doctest: +SKIP
array([[1., 1., 1., ..., 1., 1., 0.],
[1., 1., 1., ..., 1., 1., 0.],
[1., 1., 1., ..., 1., 1., 0.],
...,
[1., 1., 1., ..., 1., 1., 0.],
[1., 1., 1., ..., 1., 1., 0.],
[0., 0., 0., ..., 0., 0., 0.]])
>>> estimate["nlevel"] # doctest: +SKIP
array([0.97398633])
>>> estimate["thresh"] # doctest: +SKIP
array([164.21965135])
>>> estimate["num"] # doctest: +SKIP
array([8100.])
References
----------
* Xinhao Liu, Masayuki Tanaka and Masatoshi Okutomi
Noise Level Estimation Using Weak Textured Patches of a Single Noisy Image
IEEE International Conference on Image Processing (ICIP), 2012.
DOI: 10.1109/ICIP.2012.6466947
* Xinhao Liu, Masayuki Tanaka and Masatoshi Okutomi
Single-Image Noise Level Estimation for Blind Denoising Noisy Image
IEEE Transactions on Image Processing, Vol.22, No.12, pp.5226-5237, December, 2013.
DOI: 10.1109/TIP.2013.2283400
"""
try:
img = np.array(img)
except ValueError as e:
msg = "Input image should be a numpy ndarray, or ndarray-compatible"
raise TypeError(msg) from e
try:
patchsize = int(patchsize)
except ValueError as e:
msg = "patchsize must be an integer, or int-compatible"
raise TypeError(msg) from e
try:
decim = int(decim)
except ValueError as e:
msg = "decim must be an integer, or int-compatible"
raise TypeError(msg) from e
try:
confidence = float(confidence)
except ValueError as e:
msg = "confidence must be a float, or float-compatible, value between 0 and 1"
raise TypeError(msg) from e
if confidence < 0 or confidence > 1:
msg = "confidence must be defined in the interval 0 <= confidence <= 1"
raise ValueError(msg)
try:
iterations = int(iterations)
except ValueError as e:
msg = "iterations must be an integer, or int-compatible."
raise TypeError(msg) from e
nlevel, thresh, num = noiselevel(img, patchsize, decim, confidence, iterations)
mask = weak_texture_mask(img, patchsize, thresh)
return {"nlevel": nlevel, "thresh": thresh, "num": num, "mask": mask}
[docs]
def noiselevel(img, patchsize, decim, confidence, iterations):
"""
Calculates the noise level of the input array.
Parameters
----------
img: `numpy.ndarray`
Single Numpy image array.
patchsize : `int`, optional
Patch size, defaults to 7.
decim : `int`, optional
Decimation factor, defaults to 0.
If you use large number, the calculation will be accelerated.
confidence : `float`, optional
Confidence interval to determine the threshold for the weak texture.
In this algorithm, this value is usually set the value very close to one.
Defaults to 0.99.
iterations : `int`, optional
Number of iterations, defaults to 3.
Returns
-------
`tuple`
A tuple containing the estimated noise levels, threshold to extract weak texture
patches at the last iteration, and number of extracted weak texture patches.
"""
if len(img.shape) < 3:
img = np.expand_dims(img, 2)
nlevel = np.ndarray(img.shape[2])
thresh = np.ndarray(img.shape[2])
num = np.ndarray(img.shape[2])
kh = np.expand_dims(np.expand_dims(np.array([-0.5, 0, 0.5]), 0), 2)
imgh = correlate(img, kh, mode="nearest")
imgh = imgh[:, 1 : imgh.shape[1] - 1, :]
imgh = imgh * imgh
kv = np.expand_dims(np.vstack(np.array([-0.5, 0, 0.5])), 2)
imgv = correlate(img, kv, mode="nearest")
imgv = imgv[1 : imgv.shape[0] - 1, :, :]
imgv = imgv * imgv
Dh = conv2d_matrix(np.squeeze(kh, 2), patchsize, patchsize)
Dv = conv2d_matrix(np.squeeze(kv, 2), patchsize, patchsize)
DD = np.transpose(Dh) @ Dh + np.transpose(Dv) @ Dv
r = np.double(np.linalg.matrix_rank(DD))
Dtr = np.trace(DD)
tau0 = gamma.ppf(confidence, r / 2, scale=(2 * Dtr / r))
for cha in range(img.shape[2]):
X = view_as_windows(img[:, :, cha], (patchsize, patchsize))
X = X.reshape(int(X.size / patchsize**2), patchsize**2, order="F").transpose()
Xh = view_as_windows(imgh[:, :, cha], (patchsize, patchsize - 2))
Xh = Xh.reshape(
int(Xh.size / ((patchsize - 2) * patchsize)),
((patchsize - 2) * patchsize),
order="F",
).transpose()
Xv = view_as_windows(imgv[:, :, cha], (patchsize - 2, patchsize))
Xv = Xv.reshape(
int(Xv.size / ((patchsize - 2) * patchsize)),
((patchsize - 2) * patchsize),
order="F",
).transpose()
Xtr = np.expand_dims(np.sum(np.concatenate((Xh, Xv), axis=0), axis=0), 0)
if decim > 0:
XtrX = np.transpose(np.concatenate((Xtr, X), axis=0))
XtrX = np.transpose(XtrX[XtrX[:, 0].argsort(),])
p = np.floor(XtrX.shape[1] / (decim + 1))
p = np.expand_dims(np.arange(0, p) * (decim + 1), 0)
Xtr = XtrX[0, p.astype("int")]
X = np.squeeze(XtrX[1 : XtrX.shape[1], p.astype("int")])
# noise level estimation
tau = np.inf
if X.shape[1] < X.shape[0]:
sig2 = 0
else:
cov = (X @ np.transpose(X)) / (X.shape[1] - 1)
d = np.flip(np.linalg.eig(cov)[0], axis=0)
sig2 = d[0]
for _ in range(1, iterations):
# weak texture selection
tau = sig2 * tau0
p = Xtr < tau
Xtr = Xtr[p]
X = X[:, np.squeeze(p)]
# noise level estimation
if X.shape[1] < X.shape[0]:
break
cov = (X @ np.transpose(X)) / (X.shape[1] - 1)
d = np.flip(np.linalg.eig(cov)[0], axis=0)
sig2 = d[0]
nlevel[cha] = np.sqrt(sig2)
thresh[cha] = tau
num[cha] = X.shape[1]
# clean up
img = np.squeeze(img)
return nlevel, thresh, num
[docs]
def conv2d_matrix(H, rows, columns):
"""
Specialized 2D convolution matrix generation.
Parameters
----------
H : `numpy.ndarray`
Input matrix.
rows : `numpy.ndarray`
Rows in convolution matrix.
columns : `numpy.ndarray`
Columns in convolution matrix.
Returns
-------
T : `numpy.ndarray`
The new convoluted matrix.
"""
s = np.shape(H)
rows = int(rows)
columns = int(columns)
matr_row = rows - s[0] + 1
matr_column = columns - s[1] + 1
T = np.zeros([matr_row * matr_column, rows * columns])
k = 0
for i in range(matr_row):
for j in range(matr_column):
for p in range(s[0]):
start = (i + p) * columns + j
T[k, start : start + s[1]] = H[p, :]
k += 1
return T
[docs]
def weak_texture_mask(img, patchsize, thresh):
"""
Calculates the weak texture mask.
Parameters
----------
img: `numpy.ndarray`
Single Numpy image array.
patchsize : `int`, optional
Patch size, defaults to 7.
thresh: `numpy.ndarray`
Threshold to extract weak texture patches at the last iteration.
Returns
-------
mask: `numpy.ndarray`
Weak-texture mask. 0 and 1 represent non-weak-texture and weak-texture regions, respectively.
"""
if img.ndim < 3:
img = np.expand_dims(img, 2)
kh = np.expand_dims(np.transpose(np.vstack(np.array([-0.5, 0, 0.5]))), 2)
imgh = correlate(img, kh, mode="nearest")
imgh = imgh[:, 1 : imgh.shape[1] - 1, :]
imgh = imgh * imgh
kv = np.expand_dims(np.vstack(np.array([-0.5, 0, 0.5])), 1)
imgv = correlate(img, kv, mode="nearest")
imgv = imgv[1 : imgv.shape[0] - 1, :, :]
imgv = imgv * imgv
s = img.shape
msk = np.zeros_like(img)
for cha in range(s[2]):
m = view_as_windows(img[:, :, cha], (patchsize, patchsize))
m = np.zeros_like(m.reshape(int(m.size / patchsize**2), patchsize**2, order="F").transpose())
Xh = view_as_windows(imgh[:, :, cha], (patchsize, patchsize - 2))
Xh = Xh.reshape(
int(Xh.size / ((patchsize - 2) * patchsize)),
((patchsize - 2) * patchsize),
order="F",
).transpose()
Xv = view_as_windows(imgv[:, :, cha], (patchsize - 2, patchsize))
Xv = Xv.reshape(
int(Xv.size / ((patchsize - 2) * patchsize)),
((patchsize - 2) * patchsize),
order="F",
).transpose()
Xtr = np.expand_dims(np.sum(np.concatenate((Xh, Xv), axis=0), axis=0), 0)
p = Xtr < thresh[cha]
ind = 0
for col in range(s[1] - patchsize + 1):
for row in range(s[0] - patchsize + 1):
if p[:, ind]:
msk[row : row + patchsize - 1, col : col + patchsize - 1, cha] = 1
ind = ind + 1
# clean up
img = np.squeeze(img)
return np.squeeze(msk)