============================================================
Image denoising (Douglas-Rachford, Total Variation, L2-norm)
============================================================

This tutorial presents an image denoising problem solved by the
Douglas-Rachford splitting algorithm. The convex optimization problem, a term
which expresses a prior on the smoothness of the solution constrained by some
data fidelity, is given by

.. math:: \min\limits_x \|x\|_\text{TV} \text{ s.t. } \|x-y\|_2 \leq \epsilon

where :math:`\|\cdot\|_\text{TV}` denotes the total variation, `y` are the
measurements and :math:`\epsilon` expresses the noise level.

Create a white circle on a black background

.. plot::
   :context: reset

   >>> import numpy as np
   >>> N = 650
   >>> im_original = np.resize(np.linspace(-1, 1, N), (N, N))
   >>> im_original = np.sqrt(im_original**2 + im_original.T**2)
   >>> im_original = im_original < 0.7

and add some random Gaussian noise

.. plot::
   :context:

   >>> sigma = 0.5  # Variance of 0.25.
   >>> np.random.seed(7)  # Reproducible results.
   >>> im_noisy = im_original + sigma * np.random.normal(size=im_original.shape)

The prior objective function to minimize is defined by

.. math:: f_1(x) = \|x\|_\text{TV}

which can be expressed by the toolbox TV-norm function object, instantiated
with

.. plot::
   :context:

   >>> from pyunlocbox import functions
   >>> f1 = functions.norm_tv(maxit=50, dim=2)

The fidelity constraint expressed as an objective function to minimize is
defined by

.. math:: f_2(x) = \iota_S(x)

where :math:`\iota_S()` is the indicator function of the set :math:`S =
\left\{z \in \mathbb{R}^n \mid \|z-y\|_2 \leq \epsilon \right\}` which is zero
if :math:`z` is in the set and infinite otherwise. This function can be
expressed by the toolbox L2-ball function, instantiated with

.. plot::
   :context:

   >>> y = np.reshape(im_noisy, -1)  # Reshape the 2D image as a 1D vector.
   >>> epsilon = N * sigma           # Variance multiplied by N^2.
   >>> f = functions.proj_b2(y=y, epsilon=epsilon)
   >>> f2 = functions.func()
   >>> f2._eval = lambda x: 0        # Indicator functions evaluate to zero.
   >>> def prox(x, step):
   ...     return np.reshape(f.prox(np.reshape(x, -1), 0), im_noisy.shape)
   >>> f2._prox = prox

.. note:: We defined a custom proximal operator which transforms the 2D image
   as a 1D vector because :class:`pyunlocbox.functions.proj_b2` operates
   on the columns of `x` while :class:`pyunlocbox.functions.norm_tv`
   needs a two-dimensional array to compute the 2D TV norm.

The Douglas-Rachford splitting algorithm is instantiated with

.. plot::
   :context:

   >>> from pyunlocbox import solvers
   >>> solver = solvers.douglas_rachford(step=0.1)

and the problem solved with

.. plot::
   :context:

   >>> x0 = np.array(im_noisy)  # Make a copy to preserve y aka im_noisy.
   >>> ret = solvers.solve([f1, f2], x0, solver)
   Solution found after 25 iterations:
       objective function f(sol) = 2.080376e+03
       stopping criterion: RTOL

Let's display the results:

.. plot::
   :context:

   >>> import matplotlib.pyplot as plt
   >>> fig = plt.figure(figsize=(8, 2.5))
   >>> ax1 = fig.add_subplot(1, 3, 1)
   >>> _ = ax1.imshow(im_original, cmap='gray')
   >>> _ = ax1.axis('off')
   >>> _ = ax1.set_title('Original image')
   >>> ax2 = fig.add_subplot(1, 3, 2)
   >>> _ = ax2.imshow(im_noisy, cmap='gray')
   >>> _ = ax2.axis('off')
   >>> _ = ax2.set_title('Noisy image')
   >>> ax3 = fig.add_subplot(1, 3, 3)
   >>> _ = ax3.imshow(ret['sol'], cmap='gray')
   >>> _ = ax3.axis('off')
   >>> _ = ax3.set_title('Denoised image')

The above figure shows a good reconstruction which is both smooth (the TV
prior) and close to the measurements (the L2 fidelity constraint).
