Total Variation Denoising (ADMM)#

This example compares denoising via isotropic and anisotropic total variation (TV) regularization [44] [26]. It solves the denoising problem

\[\mathrm{argmin}_{\mathbf{x}} \; (1/2) \| \mathbf{y} - \mathbf{x} \|_2^2 + \lambda R(\mathbf{x}) \;,\]

where \(R\) is either the isotropic or anisotropic TV regularizer. In SCICO, switching between these two regularizers is a one-line change: replacing an L1Norm with a L21Norm. Note that the isotropic version exhibits fewer block-like artifacts on edges that are not vertical or horizontal.

[1]:
from xdesign import SiemensStar, discrete_phantom

import scico.numpy as snp
import scico.random
from scico import functional, linop, loss, plot
from scico.optimize.admm import ADMM, LinearSubproblemSolver
from scico.util import device_info
plot.config_notebook_plotting()

Create a ground truth image.

[2]:
N = 256  # image size
phantom = SiemensStar(16)
x_gt = snp.pad(discrete_phantom(phantom, N - 16), 8)
x_gt = x_gt / x_gt.max()

Add noise to create a noisy test image.

[3]:
σ = 0.75  # noise standard deviation
noise, key = scico.random.randn(x_gt.shape, seed=0)
y = x_gt + σ * noise

Denoise with isotropic total variation.

[4]:
λ_iso = 1.4e0
f = loss.SquaredL2Loss(y=y)
g_iso = λ_iso * functional.L21Norm()

# The append=0 option makes the results of horizontal and vertical finite
# differences the same shape, which is required for the L21Norm.
C = linop.FiniteDifference(input_shape=x_gt.shape, append=0)
solver = ADMM(
    f=f,
    g_list=[g_iso],
    C_list=[C],
    rho_list=[1e1],
    x0=y,
    maxiter=100,
    subproblem_solver=LinearSubproblemSolver(cg_kwargs={"tol": 1e-3, "maxiter": 20}),
    itstat_options={"display": True, "period": 10},
)

print(f"Solving on {device_info()}\n")
solver.solve()
x_iso = solver.x
print()
Solving on GPU (NVIDIA GeForce RTX 2080 Ti)

Iter  Time      Objective  Prml Rsdl  Dual Rsdl  CG It  CG Res
-----------------------------------------------------------------
   0  2.91e+00  1.086e+05  1.130e+02  7.375e+02      0  0.000e+00
  10  3.89e+00  3.911e+04  1.613e+01  4.388e+02     12  8.974e-04
  20  4.24e+00  2.333e+04  9.468e+00  1.396e+02     15  8.943e-04
  30  4.52e+00  2.228e+04  2.691e+00  2.984e+01     11  9.646e-04
  40  4.74e+00  2.225e+04  1.089e+00  6.712e+00      7  9.354e-04
  50  4.89e+00  2.226e+04  6.779e-01  2.817e+00      6  9.496e-04
  60  5.02e+00  2.226e+04  4.808e-01  1.298e+00      3  8.914e-04
  70  5.12e+00  2.227e+04  3.566e-01  8.595e-01      2  8.337e-04
  80  5.21e+00  2.227e+04  2.788e-01  6.620e-01      2  9.770e-04
  90  5.31e+00  2.227e+04  2.407e-01  4.794e-01      5  8.334e-04
  99  5.38e+00  2.227e+04  1.948e-01  1.765e-01      1  9.823e-04

Denoise with anisotropic total variation for comparison.

[5]:
# Tune the weight to give the same data fidelity as the isotropic case.
λ_aniso = 1.2e0
g_aniso = λ_aniso * functional.L1Norm()

solver = ADMM(
    f=f,
    g_list=[g_aniso],
    C_list=[C],
    rho_list=[1e1],
    x0=y,
    maxiter=100,
    subproblem_solver=LinearSubproblemSolver(cg_kwargs={"tol": 1e-3, "maxiter": 20}),
    itstat_options={"display": True, "period": 10},
)

solver.solve()
x_aniso = solver.x
print()
Iter  Time      Objective  Prml Rsdl  Dual Rsdl  CG It  CG Res
-----------------------------------------------------------------
   0  3.79e-01  1.165e+05  1.330e+02  8.477e+02      0  0.000e+00
  10  7.20e-01  3.707e+04  2.212e+01  4.390e+02     13  9.291e-04
  20  1.01e+00  2.302e+04  9.601e+00  1.202e+02     15  8.581e-04
  30  1.27e+00  2.226e+04  2.706e+00  2.652e+01     11  8.191e-04
  40  1.46e+00  2.224e+04  1.132e+00  7.486e+00      7  9.416e-04
  50  1.59e+00  2.224e+04  6.684e-01  3.664e+00      3  9.745e-04
  60  1.71e+00  2.224e+04  4.317e-01  2.308e+00      2  8.436e-04
  70  1.85e+00  2.225e+04  3.064e-01  1.535e+00      2  6.615e-04
  80  1.93e+00  2.225e+04  2.309e-01  1.122e+00      2  7.092e-04
  90  2.04e+00  2.225e+04  1.887e-01  7.174e-01      1  8.656e-04
  99  2.12e+00  2.225e+04  1.652e-01  3.941e-01      1  9.124e-04

Compute and print the data fidelity.

[6]:
for x, name in zip((x_iso, x_aniso), ("Isotropic", "Anisotropic")):
    df = f(x)
    print(f"Data fidelity for {name} TV was {df:.2e}")
Data fidelity for Isotropic TV was 1.93e+04
Data fidelity for Anisotropic TV was 1.92e+04

Plot results.

[7]:
plt_args = dict(norm=plot.matplotlib.colors.Normalize(vmin=0, vmax=1.5))
fig, ax = plot.subplots(nrows=2, ncols=2, sharex=True, sharey=True, figsize=(11, 10))
plot.imview(x_gt, title="Ground truth", fig=fig, ax=ax[0, 0], **plt_args)
plot.imview(y, title="Noisy version", fig=fig, ax=ax[0, 1], **plt_args)
plot.imview(x_iso, title="Isotropic TV denoising", fig=fig, ax=ax[1, 0], **plt_args)
plot.imview(x_aniso, title="Anisotropic TV denoising", fig=fig, ax=ax[1, 1], **plt_args)
fig.subplots_adjust(left=0.1, right=0.99, top=0.95, bottom=0.05, wspace=0.2, hspace=0.01)
fig.colorbar(
    ax[0, 0].get_images()[0], ax=ax, location="right", shrink=0.9, pad=0.05, label="Arbitrary Units"
)
fig.suptitle("Denoising comparison")
fig.show()

# zoomed version
fig, ax = plot.subplots(nrows=2, ncols=2, sharex=True, sharey=True, figsize=(11, 10))
plot.imview(x_gt, title="Ground truth", fig=fig, ax=ax[0, 0], **plt_args)
plot.imview(y, title="Noisy version", fig=fig, ax=ax[0, 1], **plt_args)
plot.imview(x_iso, title="Isotropic TV denoising", fig=fig, ax=ax[1, 0], **plt_args)
plot.imview(x_aniso, title="Anisotropic TV denoising", fig=fig, ax=ax[1, 1], **plt_args)
ax[0, 0].set_xlim(N // 4, N // 4 + N // 2)
ax[0, 0].set_ylim(N // 4, N // 4 + N // 2)
fig.subplots_adjust(left=0.1, right=0.99, top=0.95, bottom=0.05, wspace=0.2, hspace=0.01)
fig.colorbar(
    ax[0, 0].get_images()[0], ax=ax, location="right", shrink=0.9, pad=0.05, label="Arbitrary Units"
)
fig.suptitle("Denoising comparison (zoomed)")
fig.show()
../_images/examples_denoise_tv_admm_13_0.png
../_images/examples_denoise_tv_admm_13_1.png