#!/usr/bin/env python # coding: utf-8 # Mesh Denoising # ============== # # *Important:* Please read the [installation page](http://gpeyre.github.io/numerical-tours/installation_python/) for details about how to install the toolboxes. # $\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{\umin}[1]{\underset{#1}{\min}\;}$ # $\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{\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 explores denoising of 3-D meshes using linear filtering, heat # diffusion and Sobolev regularization. # In[1]: from __future__ import division import numpy as np import scipy as scp import pylab as pyl import matplotlib.pyplot as plt from nt_toolbox.general import * from nt_toolbox.signal import * import warnings warnings.filterwarnings('ignore') get_ipython().run_line_magic('matplotlib', 'inline') get_ipython().run_line_magic('load_ext', 'autoreload') get_ipython().run_line_magic('autoreload', '2') # 3-D Triangulated Meshes # ----------------------- # The topology of a triangulation is defined via a set of indexes $\Vv = \{1,\ldots,n\}$ # that indexes the $n$ vertices, a set of edges $\Ee \subset \Vv \times \Vv$ # and a set of $m$ faces $\Ff \subset \Vv \times \Vv \times \Vv$. # # # We load a mesh. The set of faces $\Ff$ is stored in a matrix $F \in # \{1,\ldots,n\}^{3 \times m}$. # The positions $x_i \in \RR^3$, for $i \in V$, of the $n$ vertices # are stored in a matrix $X_0 = (x_{0,i})_{i=1}^n \in \RR^{3 \times n}$. # In[2]: from nt_toolbox.read_mesh import * X0,F = read_mesh("nt_toolbox/data/elephant-50kv.off") # Number $n$ of vertices and number $m$ of faces. # In[3]: n = np.shape(X0)[1] m = np.shape(F)[1] # Display the mesh in 3-D. # In[4]: from nt_toolbox.plot_mesh import * plt.figure(figsize=(10,10)) plot_mesh(X0, F) # Noisy Mesh # ---------- # We generate artificially a noisy mesh by random normal displacement along the normal. # We only perform normal displacements because tangencial displacements # do not impact the geometry of the mesh. # # # The parameter $\rho>0$ controls the amount of noise. # In[5]: rho = 0.015 # We compute the normals $N = (N_i)_{i=1}^n$ to the mesh. # This is obtained by averaging the normal to the faces ajacent to each # vertex. # In[6]: from nt_toolbox.compute_normal import * N = compute_normal(X0, F) # We create a noisy mesh by displacement of the vertices along the # normal direction # $$ x_i = x_{0,i} + \rho \epsilon_i N_i \in \RR^3 $$ # where $\epsilon_i \sim \Nn(0,1)$ is a realization of a Gaussian random # variable, # and where $N_i \in \RR^3$ is the normal of the mesh for each vertex index # $i$. # In[7]: from numpy import random X = X0 + np.tile(rho*random.randn(n), (3,1))*N # Display the noisy mesh. # In[8]: plt.figure(figsize=(10,10)) plot_mesh(X, F) # Adjacency Matrix # ---------------- # We define linear operators that compute local averages and differences on # the mesh. # # # First we compute the index of the edges that are in the mesh, # by extracting pairs of index in the $F$ matrix. # In[9]: E = np.hstack((F[[0,1],:],F[[1,2],:],F[[2,0],:])) # Add the reversed edges. This defines the set of edges $\Ee$ # that is stored in a matrix $E \in \{1,\ldots,n\}^{2 \times p}$. # In[10]: from nt_toolbox.unique_columns import * E = unique_columns(np.hstack((E,E[[1,0],:]))) # We keep only oriented pairs of index $(i,j)$ such that $i0$ as # $$ \forall t>0, \quad \pd{f_t}{t} = -\tilde L f_t # \qandq f_0 = f, $$ # where $ \tilde L $ is the symetric normaled Laplacian defined as # $$ \tilde L = D^{-1} L = \text{Id}_n - \tilde W. $$ # In[35]: tL = iD.dot(L) # This PDE is applied to the three components of a 3-D mesh to define a # surface evolution # $$ \forall t>0, \quad \pd{X_t}{t} = -X_t \tilde L^* # \qandq f_0 = f. $$ # # # One can approximate the solution to this PDE using explicit finite # difference in time (Euler explicit scheme) # $$ X^{(\ell+1)} = X^{(\ell)} - \tau X^{(\ell)} \tilde L^* # = (1-\tau) X^{(\ell)} + \tau X^{(\ell)} \tilde W^* $$ # where $0 < \tau < 1$ is a (small enough) time step and $f^{(\ell)}$ is # intended to be an approximation of $X_t$ at time $t=\tau \ell$. # The smaller $\tau$, the better the approximation. # # # One can see that with $\tau=1$, one recovers the iterative filtering # method. # # # Time step $\tau$. # In[36]: tau = .2 # Maximum time of resolution. # In[37]: Tmax = 40 # Number of iterations needed to reach this time. # In[38]: niter = int(np.ceil(Tmax/tau)) # Initial solution at time $t=0$. # In[39]: Xt = np.copy(X) # We use an explicit discretization in time of the PDE. Here is one # iteration. # In[40]: Xt = Xt - tau*(tL.dot(np.transpose(Xt))).transpose() # __Exercise 3__ # # Compute the linear heat diffusion. # Monitor the denoising # SNR $err(l)$ between $X_t$ and $X_0$ at iteration index $l$. # In[41]: run -i nt_solutions/meshproc_3_denoising/exo3 # In[42]: ## Insert your code here. # Plot the error as a function of time. # In[43]: t = np.linspace(0, Tmax, niter) plt.figure(figsize=(10,7)) plt.plot(t, err) plt.xlabel("Time") plt.ylabel("SNR") plt.show() # Mesh Denoising with Sobolev Regularization # ------------------------------------------ # Instead of solving an evolution PDE, it is possible to do denoising by # solving a quadratic regularization. # # # Denoting $G \in \RR^{p_0 \times n}$ the gradient operator, the Soboleb # norm of a signal $f \in \RR^n$ is defined as # $$ J(f) = \norm{G f}^2 = \dotp{L f}{f}. $$ # It is extended to mesh poisition $X \in \RR^{3 \times n}$ as # $$ J(X) = \norm{X G^*}^2 = \dotp{X L}{X}, $$ # (remeber that $L$ is symmetric). # # # # Denoising of a noisy set of vertices $X$ is then defined as the solution of a quadratic minimization # $$ X_\mu = \uargmin{Z \in \RR^{3 \times n}} \norm{Z-X}^2 + \mu J(Z)^2. $$ # Here $\mu \geq 0$ controls the amount of denoising, and should be # proportional to the noise level. # # # The solution to this problem is obtained by solving the following # symmetric linear system # $$ X_\mu^* = (\text{Id}_n + \mu L )^{-1} X^* $$ # (remember that the mesh vertex position are stored as rows, hence the transposed). # # # We select a penalization weight $\mu$. The larger, the smoother the result will # be (more denoising). # In[44]: mu = 10 # We set up the matrix of the system. # It is important to use sparse matrix to have fast resolution scheme. # In[45]: A = sparse.identity(n) + mu*L # We solve the system for each coordinate of the mesh. # Since the matrix is highly sparse, it is very interesting # to use an iterative method to solve the system, so here # we use a conjugate gradient descent (function cg from sparse.linalg). # In[ ]: Xmu = np.copy(X) for i in range(3): b = X[i,:] Xmu[i,:] = sparse.linalg.cg(A, b)[0].transpose() # Display the result. # In[ ]: plt.figure(figsize=(10,10)) plot_mesh(Xmu, F) # __Exercise 4__ # # Solve this problem for various $\mu$ on a 3D mesh. # Draw the evolution of the SNR denoising error as a function of $\mu$. # In[ ]: run -i nt_solutions/meshproc_3_denoising/exo4 # In[ ]: ## Insert your code here.