Stein Unbiased Risk Estimation¶
Choose denoising parameters by estimating reconstruction risk from the noisy observation itself. Compare the estimated risk with the error available in a controlled experiment and study where Stein’s identity enters the calculation.
Run this tour¶
Run the cells in order with a Python 3 kernel. The first cell locates the companion data and toolbox and installs missing dependencies when needed. All worked examples include their implementation directly in this notebook. Random seeds make comparisons reproducible; you can change them to explore other samples.
# Locate the companion toolbox locally, or fetch it for a standalone/Colab copy.
from pathlib import Path
import importlib.util
import os
import subprocess
import sys
working = Path.cwd()
candidates = [working, working / "python", working.parent / "python"]
python_dir = next((p for p in candidates if (p / "nt_toolbox").is_dir()), None)
if python_dir is None:
checkout = working / "numerical-tours-support"
if not checkout.exists():
subprocess.run(
[
"git",
"clone",
"--depth",
"1",
"--branch",
"master",
"https://github.com/gpeyre/numerical-tours.git",
str(checkout),
],
check=True,
)
python_dir = checkout / "python"
os.chdir(python_dir)
if str(python_dir) not in sys.path:
sys.path.insert(0, str(python_dir))
requirements = python_dir / "requirements.txt"
if any(
importlib.util.find_spec(name) is None
for name in [
"numpy",
"scipy",
"matplotlib",
"skimage",
"sklearn",
"pywt",
"ipywidgets",
"cvxpy",
"skfmm",
"autograd",
"progressbar",
"celer",
]
):
subprocess.run(
[sys.executable, "-m", "pip", "install", "-r", str(requirements)], check=True
)
import numpy as np
import matplotlib.pyplot as plt
np.random.seed(0)
plt.rcParams.update(
{
"figure.figsize": (8, 4),
"figure.dpi": 100,
"axes.spines.top": False,
"axes.spines.right": False,
"font.size": 11,
"image.cmap": "gray",
}
)
%matplotlib inline
$\newcommand{\dotp}[2]{\langle #1, #2 \rangle}$ $\newcommand{\enscond}[2]{\lbrace #1, #2 \rbrace}$ $\newcommand{\pd}[2]{ \frac{ \partial #1}{\partial #2} }$ $\newcommand{\umin}[1]{\underset{#1}{\min}\;}$ $\newcommand{\umax}[1]{\underset{#1}{\max}\;}$ $\newcommand{\uargmin}[1]{\underset{#1}{argmin}\;}$ $\newcommand{\norm}[1]{\|#1\|}$ $\newcommand{\abs}[1]{\left|#1\right|}$ $\newcommand{\choice}[1]{ \left\{ \begin{array}{l} #1 \end{array} \right. }$ $\newcommand{\pa}[1]{\left(#1\right)}$ $\newcommand{\diag}[1]{{diag}\left( #1 \right)}$ $\newcommand{\qandq}{\quad\text{and}\quad}$ $\newcommand{\qwhereq}{\quad\text{where}\quad}$ $\newcommand{\qifq}{ \quad \text{if} \quad }$ $\newcommand{\qarrq}{ \quad \Longrightarrow \quad }$ $\newcommand{\ZZ}{\mathbb{Z}}$ $\newcommand{\CC}{\mathbb{C}}$ $\newcommand{\RR}{\mathbb{R}}$ $\newcommand{\EE}{\mathbb{E}}$ $\newcommand{\Zz}{\mathcal{Z}}$ $\newcommand{\Ww}{\mathcal{W}}$ $\newcommand{\Vv}{\mathcal{V}}$ $\newcommand{\Nn}{\mathcal{N}}$ $\newcommand{\NN}{\mathcal{N}}$ $\newcommand{\Hh}{\mathcal{H}}$ $\newcommand{\Bb}{\mathcal{B}}$ $\newcommand{\Ee}{\mathcal{E}}$ $\newcommand{\Cc}{\mathcal{C}}$ $\newcommand{\Gg}{\mathcal{G}}$ $\newcommand{\Ss}{\mathcal{S}}$ $\newcommand{\Pp}{\mathcal{P}}$ $\newcommand{\Ff}{\mathcal{F}}$ $\newcommand{\Xx}{\mathcal{X}}$ $\newcommand{\Mm}{\mathcal{M}}$ $\newcommand{\Ii}{\mathcal{I}}$ $\newcommand{\Dd}{\mathcal{D}}$ $\newcommand{\Ll}{\mathcal{L}}$ $\newcommand{\Tt}{\mathcal{T}}$ $\newcommand{\si}{\sigma}$ $\newcommand{\al}{\alpha}$ $\newcommand{\la}{\lambda}$ $\newcommand{\ga}{\gamma}$ $\newcommand{\Ga}{\Gamma}$ $\newcommand{\La}{\Lambda}$ $\newcommand{\Si}{\Sigma}$ $\newcommand{\be}{\beta}$ $\newcommand{\de}{\delta}$ $\newcommand{\De}{\Delta}$ $\newcommand{\phi}{\varphi}$ $\newcommand{\th}{\theta}$ $\newcommand{\om}{\omega}$ $\newcommand{\Om}{\Omega}$
This tour uses the Stein Unbiased Risk Estimator (SURE) to optimize the value of parameters in denoising algorithms.
import numpy as np
import scipy as scp
import pylab as pyl
import matplotlib.pyplot as plt
from nt_toolbox.general import clamp, np, plt, pylab
from nt_toolbox.signal import (
imageplot,
load_image,
np,
perform_wavortho_transf,
plot_wavelet,
plt,
pylab,
snr,
)
import warnings
%matplotlib inline
Denoising and SURE¶
We consider a simple generative model of noisy images $F = f_0+W$ where $f_0 \in \RR^N$ is a deterministic image of $N$ pixels, and $W$ is a Gaussian white noise distributed according to $\Nn(0,\si^2 \text{Id}_N)$, where $\si^2$ is the variance of noise.
The goal of denoising is to define an estimator $h(F)$ of $f_0$ that depends only on $F$, where $h : \RR^N \rightarrow \RR^N$ is a potentially non-linear mapping.
Note that while $f_0$ is a deterministic image, both $F$ and $h(F)$ are random variables (hence the capital letters).
The goal of denoising is to reduce as much as possible the denoising error given some prior knowledge on the (unknown) image $f_0$. A mathematical way to measure this error is to bound the quadratic risk $\EE_W(\norm{h(F) - f_0}^2)$, where the expectation is computed with respect to the distribution of the noise $W$
For real life applications, one does not have access to the underlying image $f_0$. In this tour, we however assume that $f_0$ is known, and $f = f_0 + w\in \RR^N$ is generated using a single realization of the noise $w$ that is drawn from $W$. We define the estimated deterministic image as $h(f)$ which is a realization of the random vector $h(F)$.
Number $N = n \times n$ of pixels.
n = 128 * 2
N = n**2
First we load an image $f \in \RR^N$ where $N=n \times n$ is the number of pixels.
f0 = load_image("nt_toolbox/data/hibiscus.bmp", n)
Display it.
plt.figure(figsize=(5, 5))
imageplot(f0)
Standard deviation $\si$ of the noise.
sigma = 0.08
Then we add Gaussian noise $w$ to obtain $f=f_0+w$.
from numpy import random
f = f0 + sigma * random.standard_normal((n, n))
Display the noisy image. Note the use of the clamp function to force the result to be in $[0,1]$ to avoid a loss of contrast of the display.
plt.figure(figsize=(5, 5))
imageplot(clamp(f), "Noisy, SNR = %.1f dB" % snr(f0, f))
The Stein Unbiased Risk Estimator (SURE) associated to the mapping $h$ is defined as
$$ \text{SURE}(f) = -N\si^2 + \norm{h(f)-f}^2 + 2\si^2 \text{df}(f) $$
where df stands for degree of freedom, and is defined as
$$ \text{df}(f) = \text{div} h(f) = \sum_i \pd{h}{f_i}(f). $$
It has been introduced in:
Stein, Charles M. (November 1981). "Estimation of the Mean of a Multivariate Normal Distribution". The Annals of Statistics 9 (6): 1135-1151.
And it has been applied to wavelet-based non-linear denoising in:
Donoho, David L.; Iain M. Johnstone (December 1995). "Adapting to Unknown Smoothness via Wavelet Shrinkage". Journal of the American Statistical Association (Journal of the American Statistical Association, Vol. 90, No. 432) 90 (432): 1200-1244.
If the mapping $f \mapsto h(f)$ is differentiable outside a set of zero measure (or more generally weakly differentiable), then SURE defines an unbiased estimate of the quadratic risk :
$$ \EE_W(\text{SURE}(F)) = \EE_W( \norm{f_0-h(F)}^2 ). $$
This is especially useful, since the evaluation of SURE does not necessitate the knowledge of the clean signal $f_0$ (but note however that it requires the knowledge of the noise level $\si$).
In practice, one replaces $\text{SURE}(F)$ from its empirical evaluation $\text{SURE}(f)$ on a single realization $f$. One can then minimize $\text{SURE}(f)$ with respect to a parameter $\la$ that parameterizes the denoiser $h=h_\la$.
Linear Denoising SURE¶
We consider a translation-invariant linear denoising operator, which is thus a convolution
$$ h(f) = f \star g $$
where $g \in \RR^N$ is a low pass kernel, and $\star$ denotes the periodic 2-D convolution.
Since we use periodic boundary condition, we compute the convolution as a multiplication over the Fourier domain.
$$ \forall \om, \quad \hat h(f)(\om) = \hat f(\om) \hat g(\om) $$
where $\hat g(\om)$ is the frequency $\om$ of the discrete 2-D Fourier transform of $g$ (computed using the pylab function fft2 from the pylab package).
convol = lambda f, g: np.real(pyl.ifft2(pyl.fft2(f) * np.tile(pyl.fft2(g), (1, 1))))
We define a parameteric kernel $g_\la$ parameterized by its bandwidth $\la>0$. We use here a Gaussian kernel
$$ g_\la(a) = \frac{1}{Z_\la} e^{ -\frac{\norm{a}}{2 \la^2} } $$ where $Z_\la$ ensures that $\sum_a g_\la(a) = 1$.
normalize = lambda f: f / np.sum(f)
x = np.hstack((np.arange(0, n // 2 + 1), np.arange(-n // 2 + 1, 0)))
[Y, X] = np.meshgrid(x, x)
g = lambda lambd: normalize(np.exp(-(X**2 + Y**2) / (2 * lambd**2)))
Define our denoising operator $h=h_\la$ (we make explicit the dependency on $\la$): $$ h_\la(f) = g_\la \star f. $$
h = lambda f, lambd: convol(f, g(lambd))
Example of denoising result.
lambd = 1.5
plt.figure(figsize=(5, 5))
imageplot(clamp(h(f, lambd)))
For linear operator, the dregree of freedom is equal to the trace of the operator, and thus in our case it is equal to the sum of the Fourier transform $$ \text{df}_\la(f) = \text{tr}(h_\la) = \sum_{\om} \hat g_\la(\om) $$ Note that we have made explicit the dependency of df with respect to $\la$. Note also that df$(f)$ actually not actually depend on $f$.
df = lambda lambd: np.real(np.sum(pyl.fft2(g(lambd))))
We can now define the SURE=SURE$_\la$ operator, as a function of $f, h(f), \lambda$.
from numpy import linalg
SURE = lambda f, hf, lambd: (
-N * sigma**2 + linalg.norm(hf - f, "fro") ** 2 + 2 * sigma**2 * df(lambd)
)
Worked example 1
For a given $\lambda$, display the histogram of the repartition of the quadratic error $\norm{y-h(y)}^2$ and of $\text{SURE}(y)$. Compute these repartition using Monte-Carlo simulation (you need to generate lots of different realization of the noise $W$). Display in particular the location of the mean of these quantities.
ntrials = 100
nlaunch = 20
E0 = []
E = []
for i in range(nlaunch):
f = np.tile(f0, (ntrials, 1, 1)) + sigma * random.standard_normal((ntrials, n, n))
hf = h(f, lambd)
# quadratic error
e = np.sum((hf - np.tile(f0, (ntrials, 1, 1))) ** 2, (1, 2))
E0 = np.hstack((E0, e))
# sure error
e = -N * sigma**2 + np.sum((hf - f) ** 2, (1, 2)) + 2 * sigma**2 * df(lambd)
E = np.hstack((E, e))
v_true = np.mean(E0)
v_sure = np.mean(E)
a = v_true - 8 * np.std(E0)
b = v_true + 8 * np.std(E0)
t = np.linspace(a, b, 31)
mybar = lambda e: np.histogram(e[(e > a) & (e < b)], t)
plt.figure(figsize=(10, 7))
plt.subplot(2, 1, 1)
s = mybar(E0)[0]
plt.bar(t[:-1], s, width=(b - a) / 31, color="darkblue", edgecolor="white")
plt.axvline(v_true, color="red", linewidth=3)
plt.subplot(2, 1, 2)
s = mybar(E)[0]
plt.bar(t[:-1], s, width=(b - a) / 31, color="darkblue", edgecolor="white")
plt.axvline(v_sure, color="red", linewidth=3)
plt.show()
In practice, the SURE is used to set up the value of $\la$ from a single realization $f=f_0+w$, by minimizing $\text{SURE}_\la(f)$.
Worked example 2
Compute, for a single realization $f=f_0+w$, the evolution of
$$ E(\la) = \text{SURE}_\la(f) \qandq E_0(\lambda) = \norm{f-h_\la(f)}^2 $$ as a function of $\lambda$.
f = f0 + sigma * random.standard_normal((n, n))
ntrials = 30
lambda_list = np.linspace(0.3, 2, ntrials)
E0 = []
E = []
for i in range(ntrials):
lambd = lambda_list[i]
hf = h(f, lambd)
E0 += [linalg.norm(hf - f0, "fro") ** 2]
E += [SURE(f, hf, lambd)]
plt.figure(figsize=(7, 5))
plt.plot(lambda_list, E0)
plt.plot(lambda_list, E)
plt.legend(["$E_0$", "$E$"])
plt.xlabel(r"$\lambda$")
plt.xlim(np.min(lambda_list), np.max(lambda_list))
plt.ylim([np.min(E), 400])
plt.show()
Worked example 3
Display the best denoising result $h_{\la^*}(f)$ where $$\la^* = \uargmin{\la} \text{SURE}_\la(f) $$
plt.figure(figsize=(5, 5))
tmp = np.min(E)
i = np.argmin(E)
lambd = lambda_list[i]
hf = h(f, lambd)
imageplot(clamp(hf), "SNR = %.1f dB" % snr(f0, hf))
Soft Thresholding SURE¶
In order to enhance the denoising results for piecewise regular signal and image, it is possible to use non-linear thresholding in an orthogonal wavelet basis $ \Bb = \{ \psi_m \}_{m} $ where $\psi_m \in \RR^N$ is a wavelet element.
Re-generate a noisy image.
f = f0 + sigma * random.standard_normal((n, n))
The soft-thresholding estimator thus reads $$ h_\la(f) = \sum_m s_\la( \dotp{f}{\psi_m} ) \psi_m \qwhereq s_\la(\al) = \max\pa{0, 1-\frac{\la}{\abs{\al}}} \al. $$ It can be conveniently written as $$ h_\la = \Ww^* \circ S_\la \circ \Ww $$ where $\Ww$ and $\Ww^*$ are forward and inverse wavelet transform $$ \Ww(f) = ( \dotp{f}{\psi_m} )_m \qandq \Ww^*(x) = \sum_m x_m \psi_m, $$ and $ S_\la $ is the diagonal soft thresholding operator $$ S_\la(x) = ( s_\la(x_m) )_m. $$
Define the wavelet transform and its inverse.
from nt_toolbox.compute_wavelet_filter import compute_wavelet_filter, np
h_daub = compute_wavelet_filter("Daubechies", 4)
W = lambda f1: perform_wavortho_transf(f1, 0, +1, h_daub)
Ws = lambda x: perform_wavortho_transf(x, 0, -1, h_daub)
Display the wavelet transform $\Ww(f_0)$ of the original image.
plt.figure(figsize=(10, 10))
plot_wavelet(W(f0), 1)
plt.show()
Define the soft thresholding operator.
S = lambda x, lambd: (
np.maximum(
np.zeros(list(np.shape(x))),
1 - lambd / np.maximum(1e-9 * np.ones(list(np.shape(x))), abs(x)),
)
* x
)
Define the denoising operator.
h = lambda f1, lambd: Ws(S(W(f1), lambd))
Example of denoising result.
lambd = 3 * sigma / 2
plt.figure(figsize=(5, 5))
imageplot(clamp(h(f, lambd)))
Since $Ww$ is an orthogonal transform, one has $$ \text{df}(f) = \text{div}( S_\la )( \Ww(f) ) = \sum_m s_\la'( \dotp{f}{\psi_m} ) = \norm{\Ww(h(f))}_0 $$ where $ s_\la' $ is the derivative of the 1-D function $s_\la$, and $\norm{\cdot}_0$ is the $\ell^0$ pseudo-norm $$ \norm{x}_0 = \abs{ \enscond{m}{x_m \neq 0} }. $$
To summarize, the degree of freedom is equal to the number of non-zero coefficients in the wavelet coefficients of $h(f)$.
df = lambda hf, lambd: np.sum(abs(W(hf)) > 1e-8)
We can now define the SURE operator, as a function of $f, h(f), \lambda$.
SURE = lambda f, hf, lambd: (
-N * sigma**2 + linalg.norm(hf - f, "fro") ** 2 + 2 * sigma**2 * df(hf, lambd)
)
Worked example 4
For a given $\lambda$, display the histogram of the repartition of the quadratic error $\norm{y-h(y)}^2$ and of $\text{SURE}(y)$. Compute these repartition using Monte-Carlo simulation (you need to generate lots of different realization of the noise $W$). Display in particular the location of the mean of these quantities. Hint: you can do the computation directly over the wavelet domain, i.e. consider that the noise is added to the wavelet transform.
f0w = W(f0)
ntrials = 100
nlaunch = 20
E0 = []
E = []
dfw = lambda fw, lambd: np.sum(abs(fw) > lambd, (1, 2))
for i in range(nlaunch):
fw = np.tile(f0w, (ntrials, 1, 1)) + sigma * random.standard_normal((ntrials, n, n))
hfw = S(fw, lambd)
# quadratic error
e = np.sum((hfw - np.tile(f0w, (ntrials, 1, 1))) ** 2, (1, 2))
E0 = np.hstack((E0, e))
# sure error
e = -N * sigma**2 + np.sum((hfw - fw) ** 2, (1, 2)) + 2 * sigma**2 * dfw(fw, lambd)
E = np.hstack((E, e))
v_true = np.mean(E0)
v_sure = np.mean(E)
a = v_true - 8 * np.std(E0)
b = v_true + 8 * np.std(E0)
t = np.linspace(a, b, 31)
mybar = lambda e: np.histogram(e[(e > a) & (e < b)], t)
plt.figure(figsize=(10, 7))
plt.subplot(2, 1, 1)
s = mybar(E0)[0]
plt.bar(t[:-1], s, width=(b - a) / 31, color="darkblue", edgecolor="white")
plt.axvline(v_true, color="red", linewidth=3)
plt.subplot(2, 1, 2)
s = mybar(E)[0]
plt.bar(t[:-1], s, width=(b - a) / 31, color="darkblue", edgecolor="white")
plt.axvline(v_sure, color="red", linewidth=3)
plt.show()
Worked example 5
Compute, for a single realization $f=f_0+w$, the evolution of
$$ E(\la) = \text{SURE}_\la(f) \qandq E_0(\lambda) = \norm{f-h_\la(f)}^2 $$ as a function of $\lambda$.
f = f0 + sigma * random.standard_normal((n, n))
ntrials = 30
lambda_list = np.linspace(0.5, 2.5, ntrials) * sigma
E0 = []
E = []
for i in range(ntrials):
lambd = lambda_list[i]
hf = h(f, lambd)
E0 += [linalg.norm(hf - f0, "fro") ** 2]
E += [SURE(f, hf, lambd)]
plt.figure(figsize=(7, 5))
plt.plot(lambda_list, E0)
plt.plot(lambda_list, E)
plt.legend(["$E_0$", "$E$"])
plt.xlabel(r"$\lambda/\sigma$")
plt.xlim(np.min(lambda_list), np.max(lambda_list))
plt.ylim([np.min(E), 210])
plt.show()
Worked example 6
Display the best denoising result $h_{\la^*}(f)$ where $$\la^* = \uargmin{\la} \text{SURE}_\la(f) $$
plt.figure(figsize=(5, 5))
tmp = np.min(E)
i = np.argmin(E)
lambd = lambda_list[i]
hf = h(f, lambd)
imageplot(clamp(hf), "SNR = %.1f dB" % snr(f0, hf))
Block-soft Thresholding SURE¶
To improve the result of soft thresholding, it is possible to threshold blocks of coefficients.
We define a partition $ \{1,\ldots,N\} = \cup_k b_k $ of the set of wavelet coefficient indexes. The block thresholding is defined as
$$ h_\la(f) = \sum_k \sum_{m \in b_k} a_\la( e_k ) \dotp{f}{\psi_m} \psi_m \qwhereq e_k = \sum_{m \in b_k} \abs{\dotp{f}{\psi_m}}^2, $$ where we use the James-Stein attenuation threshold $$ a_\la(e) = \max\pa{ 0, 1 - \frac{\la^2}{e^2} }. $$
The block size $q$.
q = 4
A function to extract blocks.
[X, Y, dX, dY] = np.meshgrid(
np.arange(1, n - q + 2, q),
np.arange(1, n - q + 2, q),
np.arange(0, q),
np.arange(0, q),
)
I = (X + dX - 1) + (Y + dY - 1) * n
for i in range(n // q):
for j in range(n // q):
I[i, j] = np.transpose(I[i, j])
blocks = lambda fw: np.ravel(fw)[I]
A function to reconstruct an image from blocks.
def assign(M, I, H):
M_temp = M
np.ravel(M_temp)[I] = H
return np.reshape(M_temp, (n, n))
unblock = lambda H: assign(np.zeros([n, n]), I, H)
Compute the average energy of each block, and duplicate.
def energy(H):
H_tmp = np.copy(H)
for i in range(n // q):
for j in range(n // q):
H_tmp[i][j] = np.mean(H_tmp[i][j] ** 2) * np.ones([q, q])
return H_tmp
Threshold the blocks. We use here a Stein block thresholding. All values within a block are atenuated by the same factor.
S = lambda H, lambd: np.maximum(1 - lambd**2 / energy(H), np.zeros(np.shape(H))) * H
Block thresholding estimator $h_\lambda(f)$.
h = lambda f, lambd: Ws(unblock(S(blocks(W(f)), lambd)))
Example of block denoising.
lambd = 1.1 * sigma
plt.figure(figsize=(5, 5))
imageplot(clamp(h(f, lambd)))
Since the block-thresholding operates in a block diagonal manner over the wavelet coefficients, it degree of freedom is a sum of the divergence of each block James-Stein operator $$ \text{df}(f) = \sum_{ e_k > \la^2 } \text{tr}( \partial \phi (a_k) ) $$ where $ a_k = (\dotp{f}{\psi_m})_{m \in b_k} $ is the set of coefficients inside a block, that satisfies $\norm{a_k}=e_k$, and where $$ \phi(a) = \pa{ 1 - \frac{\la^2}{\norm{a}^2} } a. $$ One can compute explicitely the derivative of $\phi$ $$ \partial \phi(a) = \pa{ 1 - \frac{\la^2}{\norm{a}^2} } \text{Id} + 2 \frac{\la^2}{\norm{a}^2} \Pi_a $$ where $\Pi_a$ is the orthogonal projector on $a$.
This gives the folowing formula for the degree of freedom $$ \text{df}(f) = \norm{\Ww(h_\la(f))}_0 + \sum_{ e_k > \la^2 } \frac{\la^2}{e_k} (2-\abs{b_k}). $$ One can note that the degree of freedom differs from the one of the soft thresholding (it is not in general an integer).
References and further reading¶
Antoni Buades, Bartomeu Coll, and Jean-Michel Morel. A Review of Image Denoising Algorithms, with a New One. 2005, Multiscale Modeling & Simulation 4(2), 490–530. Comparison of local, transform, and nonlocal denoising models.
David L. Donoho. De-noising by Soft-Thresholding. 1995, IEEE Transactions on Information Theory 41(3), 613–627. Why shrinkage of wavelet coefficients suppresses noise.
Charles M. Stein. Estimation of the Mean of a Multivariate Normal Distribution. 1981, Annals of Statistics 9(6), 1135–1151. Unbiased risk estimation for Gaussian observations.
Leonid Rudin, Stanley Osher, and Emad Fatemi. Nonlinear Total Variation Based Noise Removal Algorithms. 1992, Physica D 60, 259–268. The foundational total-variation image restoration model.
Stéphane Mallat. A Wavelet Tour of Signal Processing: The Sparse Way. 2009, 3rd ed., Academic Press. Multiresolution analysis, sparse approximation, and wavelet algorithms.
Gabriel Peyré. Advanced Signal, Image and Surface Processing. 2010, course notes. A mathematical companion to the Numerical Tours.