├── figs ├── psf.png ├── adjoint.png ├── forward.png ├── blur_model.png ├── diagonal.png ├── firstder.png ├── firstder1.png ├── smoothder.png ├── Rotated_Grid.png ├── convolve_1D.png ├── unsharp_mask.png ├── filtered_image.png └── Fourier_Slice_Theorem.png ├── README.md ├── solutions ├── intro_sol.py ├── deblurring_sol.py └── radon_sol.py ├── WhatsNext.ipynb ├── Deblurring.ipynb ├── solutions_colab ├── Deblurring.ipynb └── Radon.ipynb └── Intro.ipynb /figs/psf.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PyLops/pylops_pydata2020/HEAD/figs/psf.png -------------------------------------------------------------------------------- /figs/adjoint.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PyLops/pylops_pydata2020/HEAD/figs/adjoint.png -------------------------------------------------------------------------------- /figs/forward.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PyLops/pylops_pydata2020/HEAD/figs/forward.png -------------------------------------------------------------------------------- /figs/blur_model.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PyLops/pylops_pydata2020/HEAD/figs/blur_model.png -------------------------------------------------------------------------------- /figs/diagonal.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PyLops/pylops_pydata2020/HEAD/figs/diagonal.png -------------------------------------------------------------------------------- /figs/firstder.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PyLops/pylops_pydata2020/HEAD/figs/firstder.png -------------------------------------------------------------------------------- /figs/firstder1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PyLops/pylops_pydata2020/HEAD/figs/firstder1.png -------------------------------------------------------------------------------- /figs/smoothder.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PyLops/pylops_pydata2020/HEAD/figs/smoothder.png -------------------------------------------------------------------------------- /figs/Rotated_Grid.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PyLops/pylops_pydata2020/HEAD/figs/Rotated_Grid.png -------------------------------------------------------------------------------- /figs/convolve_1D.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PyLops/pylops_pydata2020/HEAD/figs/convolve_1D.png -------------------------------------------------------------------------------- /figs/unsharp_mask.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PyLops/pylops_pydata2020/HEAD/figs/unsharp_mask.png -------------------------------------------------------------------------------- /figs/filtered_image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PyLops/pylops_pydata2020/HEAD/figs/filtered_image.png -------------------------------------------------------------------------------- /figs/Fourier_Slice_Theorem.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PyLops/pylops_pydata2020/HEAD/figs/Fourier_Slice_Theorem.png -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # PyLops Tutorial at PyData Global 2020 2 | 3 | Material for tutorial **Solving large-scale inverse problems in Python with PyLops**, to be taught 4 | at [PyData Global 2020](https://global.pydata.org) course. 5 | 6 | The material covered during the tutorial is composed of 3 jupyter notebooks. Participants can either use: 7 | 8 | - local Python installation (follow these [instructions](https://pylops.readthedocs.io/en/latest/installation.html) 9 | to setup your environment) 10 | - a Cloud-hosted environment such as Google Colab (use links provided below to open the notebook 11 | directly in Colab). 12 | 13 | ## Instructors 14 | 15 | - Matteo Ravasi (mrava87), Equinor ASA 16 | - David Vargas (davofis), Utrecht University 17 | - Ivan Vasconcelos (ivasconcelosUU), Utrecht University 18 | 19 | 20 | ## Notebooks 21 | 22 | | Session | Exercise (Github) | Exercise (Colab) | Solutions (Colab) | Videos | 23 | |-----------|------------------|------------------|------------------|------------------| 24 | | 1: Introduction | [Link](Intro.ipynb) | [![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/mrava87/pylops_pydata2020/blob/master/Intro.ipynb) | [![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/mrava87/pylops_pydata2020/blob/master/solutions_colab/Intro.ipynb) | [Link](https://www.youtube.com/watch?v=1INffyzZ-O8&t=518s) | 25 | | 2: Image Deblurring | [Link](Deblurring.ipynb) | [![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/mrava87/pylops_pydata2020/blob/master/Deblurring.ipynb) | [![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/mrava87/pylops_pydata2020/blob/master/solutions_colab/Deblurring.ipynb) | [Link](https://www.youtube.com/watch?v=LQTJs-EJQIs&t=12s) | 26 | | 3: Radon Transforms | [Link](Radon.ipynb) | [![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/mrava87/pylops_pydata2020/blob/master/Radon.ipynb) | [![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/mrava87/pylops_pydata2020/blob/master/solutions_colab/Radon.ipynb) | [Link](https://www.youtube.com/watch?v=WekWA-3bg9U) | 27 | | 4: What's next? | [Link](Whatsnext.ipynb) | | | [Link](https://www.youtube.com/watch?v=NwLo_yGF7EQ) | 28 | 29 | 30 | ## License 31 | The material in this repository is open and can be modified and redistributed according to the chosen licences. 32 | 33 | Text is licensed under [CC BY Creative Commons License](http://creativecommons.org/licenses/by/4.0/). 34 | 35 | Code is licensed under the [Apache License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0). 36 | 37 | -------------------------------------------------------------------------------- /solutions/intro_sol.py: -------------------------------------------------------------------------------- 1 | # Solutions for Intro tutorial - PyData Global 2020 Tutorial 2 | 3 | class Diagonal(LinearOperator): 4 | """Short version of a Diagonal operator. See 5 | https://github.com/equinor/pylops/blob/master/pylops/basicoperators/Diagonal.py 6 | for a more detailed implementation 7 | """ 8 | def __init__(self, diag, dtype='float64'): 9 | self.diag = diag.flatten() 10 | self.shape = (len(self.diag), len(self.diag)) 11 | self.dtype = np.dtype(dtype) 12 | 13 | def _matvec(self, x): 14 | y = self.diag*x 15 | return y 16 | 17 | def _rmatvec(self, x): 18 | y = self.diag*x 19 | return y 20 | 21 | 22 | class FirstDerivative(LinearOperator): 23 | """Short version of a FirstDerivative operator. See 24 | https://github.com/equinor/pylops/blob/master/pylops/basicoperators/FirstDerivative.py 25 | for a more detailed implementation 26 | """ 27 | def __init__(self, N, sampling=1., dtype='float64'): 28 | self.N = N 29 | self.sampling = sampling 30 | self.shape = (N, N) 31 | self.dtype = dtype 32 | self.explicit = False 33 | 34 | def _matvec(self, x): 35 | x, y = x.squeeze(), np.zeros(self.N, self.dtype) 36 | y[1:-1] = (0.5 * x[2:] - 0.5 * x[0:-2]) / self.sampling 37 | # edges 38 | y[0] = (x[1] - x[0]) / self.sampling 39 | y[-1] = (x[-1] - x[-2]) / self.sampling 40 | return y 41 | 42 | def _rmatvec(self, x): 43 | x, y = x.squeeze(), np.zeros(self.N, self.dtype) 44 | y[0:-2] -= (0.5 * x[1:-1]) / self.sampling 45 | y[2:] += (0.5 * x[1:-1]) / self.sampling 46 | # edges 47 | y[0] -= x[0] / self.sampling 48 | y[1] += x[0] / self.sampling 49 | y[-2] -= x[-1] / self.sampling 50 | y[-1] += x[-1] / self.sampling 51 | return y 52 | 53 | 54 | def Diagonal_timing(): 55 | """Timing of Diagonal operator 56 | """ 57 | n = 10000 58 | diag = np.arange(n) 59 | x = np.ones(n) 60 | 61 | # dense 62 | D = np.diag(diag) 63 | 64 | from scipy import sparse 65 | Ds = sparse.diags(diag, 0) 66 | 67 | # lop 68 | Dop = Diagonal(diag) 69 | 70 | # uncomment these 71 | #%timeit -n3 -r3 np.dot(D, x) 72 | #%timeit -n3 -r3 Ds.dot(x) 73 | #%timeit -n3 -r3 Dop._matvec(x) 74 | 75 | 76 | def FirstDerivative_timing(): 77 | """Timing of FirstDerivative operator 78 | """ 79 | nx = 2001 80 | x = np.arange(nx) - (nx-1)/2 81 | 82 | # dense 83 | D = np.diag(0.5*np.ones(nx-1),k=1) - np.diag(0.5*np.ones(nx-1),-1) 84 | D[0, 0] = D[-1, -2] = -1 85 | D[0, 1] = D[-1, -1] = 1 86 | 87 | # lop 88 | Dop = pylops.FirstDerivative(nx, edge=True) 89 | 90 | # uncomment these 91 | # %timeit -n3 -r3 np.dot(D, x) 92 | # %timeit -n3 -r3 Dop._matvec(x) 93 | 94 | 95 | def FirstDerivative_memory(): 96 | """Memory footprint of Diagonal operator 97 | """ 98 | from pympler import asizeof 99 | from scipy.sparse import diags 100 | nn = (10 ** np.arange(2, 4, 0.5)).astype(np.int) 101 | 102 | mem_D = [] 103 | mem_Ds = [] 104 | mem_Dop = [] 105 | for n in nn: 106 | D = np.diag(0.5 * np.ones(n - 1), k=1) - np.diag(0.5 * np.ones(n - 1), 107 | -1) 108 | D[0, 0] = D[-1, -2] = -1 109 | D[0, 1] = D[-1, -1] = 1 110 | Ds = diags((0.5 * np.ones(n - 1), -0.5 * np.ones(n - 1)), 111 | offsets=(1, -1)) 112 | Dop = pylops.FirstDerivative(n, edge=True) 113 | mem_D.append(asizeof.asizeof(D)) 114 | mem_Ds.append(asizeof.asizeof(Ds)) 115 | mem_Dop.append(asizeof.asizeof(Dop)) 116 | 117 | plt.figure(figsize=(12, 3)) 118 | plt.semilogy(nn, mem_D, '.-k', label='D') 119 | plt.semilogy(nn, mem_Ds, '.-b', label='Ds') 120 | plt.semilogy(nn, mem_Dop, '.-r', label='Dop') 121 | plt.legend() 122 | plt.title('Memory comparison') -------------------------------------------------------------------------------- /solutions/deblurring_sol.py: -------------------------------------------------------------------------------- 1 | # Solutions for Deblurring tutorial - PyData Global 2020 Tutorial 2 | 3 | def Unsharp_Mask(): 4 | """Unsharp Mask operator 5 | """ 6 | # load image from scikit-image 7 | image = data.microaneurysms() 8 | ny, nx = image.shape 9 | 10 | # %load -s Diagonal_timing solutions/intro_sol.py 11 | 12 | # Define matrix kernel 13 | kernel_unsharp = np.array([[0, -1, 0], 14 | [-1, 5, -1], 15 | [0, -1, 0]]) 16 | 17 | # PyLops Operator 18 | unsharp_op = Convolve2D(N=ny*nx, 19 | h=kernel_unsharp, 20 | offset=( 21 | kernel_unsharp.shape[0]//2, kernel_unsharp.shape[1]//2), 22 | dims=(ny, nx)) 23 | 24 | img_unsharp = unsharp_op * image.flatten() 25 | img_unsharp = img_unsharp.reshape(image.shape) 26 | 27 | # PLOTTING 28 | fig, ax = plt.subplots(1, 3, figsize=(15, 5)) 29 | 30 | ax[0].imshow(image, cmap='gray') 31 | ax[0].set_title('Image') 32 | 33 | ax[1].imshow(kernel_unsharp, cmap='gray') 34 | ax[1].set_title('Kernel') 35 | 36 | ax[2].imshow(img_unsharp, cmap='gray') 37 | ax[2].set_title('Blurred Image') 38 | 39 | 40 | def Noisy_Inversion(): 41 | """Solve Inverse problems for a noisy image 42 | """ 43 | scale = 5.8 44 | noise = np.random.normal(0, scale, img_gauss_.shape) 45 | img_gauss_noisy_ = img_gauss_ + noise 46 | img_gauss_noisy = img_gauss_noisy_.flatten() 47 | 48 | 49 | # PLOTTING 50 | fig, ax = plt.subplots(1, 3, figsize=(15, 5)) 51 | 52 | ax[0].imshow(img_gauss_, cmap='gray') 53 | ax[0].set_title('Blurred Image') 54 | 55 | ax[1].imshow(img_gauss_noisy_, cmap='gray') 56 | ax[1].set_title('Blurred Image + Noise') 57 | 58 | him2 = ax[2].imshow(img_gauss_noisy_ - img_gauss_, cmap='gray') 59 | ax[2].set_title('Noise level') 60 | plt.show() 61 | 62 | 63 | # least squares inversion 64 | deblur_l2 = leastsquares.NormalEquationsInversion(Op=Gauss_op, 65 | Regs=None, 66 | data=img_gauss_noisy, 67 | maxiter=40) 68 | 69 | # least squares - regularized inversion 70 | deblur_l2_reg = leastsquares.RegularizedInversion(Op=Gauss_op, 71 | Regs=[D2op], 72 | data=img_gauss_noisy, 73 | epsRs=[1e0], 74 | show=True, 75 | **dict(iter_lim=20)) 76 | 77 | # TV inversion 78 | deblur_tv = sparsity.SplitBregman(Op=Gauss_op, 79 | RegsL1=Dop, 80 | data=img_gauss_noisy, 81 | niter_outer=10, 82 | niter_inner=2, 83 | mu=1.8, 84 | epsRL1s=[8e-1, 8e-1], 85 | tol=1e-4, 86 | tau=1., 87 | ** dict(iter_lim=5, damp=1e-4, show=True))[0] 88 | 89 | # FISTA inversion 90 | deblur_fista = sparsity.FISTA(Op=Gauss_op, 91 | data=img_gauss_noisy, 92 | eps=1e-1, 93 | niter=100, 94 | show=True)[0] 95 | 96 | # Reshape images 97 | deblur_l2 = deblur_l2.reshape(image.shape) 98 | deblur_l2_reg = np.real(deblur_l2_reg.reshape(image.shape)) 99 | deblur_tv = deblur_tv.reshape(image.shape) 100 | deblur_fista = deblur_fista.reshape(image.shape) 101 | 102 | 103 | # PLOTTING 104 | fig, ax = plt.subplots(2, 3, sharex=True, sharey=True, figsize=(12, 8)) 105 | 106 | ax[0, 0].imshow(image, aspect='auto', cmap='gray') 107 | ax[0, 0].set_title('Original Image') 108 | 109 | ax[0, 1].imshow(deblur_l2, aspect='auto', cmap='gray') 110 | ax[0, 1].set_title('LS-Inversion') 111 | 112 | ax[0, 2].imshow(deblur_l2_reg, aspect='auto', cmap='gray') 113 | ax[0, 2].set_title('Regularized LS-Inversion') 114 | 115 | ax[1, 0].imshow(img_gauss_, aspect='auto', cmap='gray') 116 | ax[1, 0].set_title('Blurred Image') 117 | 118 | ax[1, 1].imshow(deblur_tv, aspect='auto', cmap='gray') 119 | ax[1, 1].set_title('TV-Inversion') 120 | 121 | ax[1, 2].imshow(deblur_fista, aspect='auto', cmap='gray') 122 | ax[1, 2].set_title('FISTA Inversion') 123 | fig.tight_layout() 124 | -------------------------------------------------------------------------------- /WhatsNext.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "metadata": {}, 6 | "source": [ 7 | "# What's next?\n", 8 | "\n", 9 | "**Authors: M. Ravasi, D. Vargas, I. Vasconcelos**" 10 | ] 11 | }, 12 | { 13 | "cell_type": "markdown", 14 | "metadata": {}, 15 | "source": [ 16 | "It is time to wrap up. We hope you enjoyed this tutorial and learned how to use PyLops.\n", 17 | "\n", 18 | "But, where to go next?" 19 | ] 20 | }, 21 | { 22 | "cell_type": "markdown", 23 | "metadata": {}, 24 | "source": [ 25 | "### Implementation details\n", 26 | "\n", 27 | "To learn more about the implementation details:\n", 28 | "\n", 29 | "- Guide to add new operators (we welcome contributions!): https://pylops.readthedocs.io/en/latest/adding.html\n", 30 | "\n", 31 | "- Our reference [paper](https://www.sciencedirect.com/science/article/pii/S2352711019301086): *Ravasi, M., and Vasconcelos I., PyLops–A Linear-Operator Python Library for large scale optimization, Software X, (2019)* " 32 | ] 33 | }, 34 | { 35 | "cell_type": "markdown", 36 | "metadata": {}, 37 | "source": [ 38 | "### More examples\n", 39 | "\n", 40 | "You can find more examples to get started at:\n", 41 | " \n", 42 | "- https://pylops.readthedocs.io/en/latest/tutorials/index.html\n", 43 | "\n", 44 | "- https://github.com/mrava87/pylops_notebooks" 45 | ] 46 | }, 47 | { 48 | "cell_type": "markdown", 49 | "metadata": {}, 50 | "source": [ 51 | "### PyLops in research\n", 52 | "\n", 53 | "If you are going to use PyLops in your research, please let us know! We like to keep track of all the publications that use it [link](https://pylops.readthedocs.io/en/latest/papers.html).\n", 54 | "\n", 55 | "So far, most of our publications are currently gravitating around geophysical applications. Another area where PyLops is currently being used is Astronomy to image strong gravitational lenses (see https://github.com/Jammy2211/PyAutoLens)." 56 | ] 57 | }, 58 | { 59 | "cell_type": "markdown", 60 | "metadata": {}, 61 | "source": [ 62 | "### Way forward\n", 63 | "\n", 64 | "These are some directions of active development, if interested to contribute reach out to us!\n", 65 | "\n", 66 | "- **PyLops-distributed** (https://pylops-distributed.readthedocs.io): Distributed operators via [Dask](https://dask.org). So far it contains an incomplete port of PyLops operators and solvers to Dask in order to leverage distributed computing when applying the forward/adjoint of an operator.\n", 67 | "\n", 68 | "- **PyLops-gpu** (https://pylops-gpu.readthedocs.io): GPU-powered operators via [PyTorch](https://http://pytorch.org). Similar to PyLops-distributed, it only contains a small subset of PyLops operators that have been re-implemented using PyTorch. It allow:\n", 69 | " - running forward/adjoint steps (or even the entire inverse problem) on a GPU. \n", 70 | " - easy wrapping of any of PyLops operators into a ``torch.autograd.Function`` via ``TorchOperator`` (https://pylops-gpu.readthedocs.io/en/latest/api/generated/pylops_gpu.TorchOperator.html) - use PyLops operators within an autograd graph.\n", 71 | " \n", 72 | "- **Cupy** Integration in PyLops: given the level of maturity of Cupy, we have started to consider a direct integration in PyLops. This will allow operators to transparently use numpy or cupy depending on the input vectors" 73 | ] 74 | }, 75 | { 76 | "cell_type": "markdown", 77 | "metadata": {}, 78 | "source": [ 79 | "### Getting in touch\n", 80 | "\n", 81 | "Feel free to reach out for any questions or suggestions:\n", 82 | " \n", 83 | "- **Github issues** (specific to the codebase)\n", 84 | "\n", 85 | "- Our **Slack** workshape (https://pylops.slack.com)\n", 86 | "\n", 87 | "- **Software Underground** Slack (https://swung.slack.com)\n", 88 | "\n", 89 | "- Or just reach out directly via email or Linkedin!" 90 | ] 91 | } 92 | ], 93 | "metadata": { 94 | "kernelspec": { 95 | "display_name": "Python 3", 96 | "language": "python", 97 | "name": "python3" 98 | }, 99 | "language_info": { 100 | "codemirror_mode": { 101 | "name": "ipython", 102 | "version": 3 103 | }, 104 | "file_extension": ".py", 105 | "mimetype": "text/x-python", 106 | "name": "python", 107 | "nbconvert_exporter": "python", 108 | "pygments_lexer": "ipython3", 109 | "version": "3.7.2" 110 | }, 111 | "toc": { 112 | "base_numbering": 1, 113 | "nav_menu": {}, 114 | "number_sections": true, 115 | "sideBar": true, 116 | "skip_h1_title": false, 117 | "title_cell": "Table of Contents", 118 | "title_sidebar": "Contents", 119 | "toc_cell": false, 120 | "toc_position": { 121 | "height": "calc(100% - 180px)", 122 | "left": "10px", 123 | "top": "150px", 124 | "width": "197.796875px" 125 | }, 126 | "toc_section_display": true, 127 | "toc_window_display": true 128 | } 129 | }, 130 | "nbformat": 4, 131 | "nbformat_minor": 1 132 | } 133 | -------------------------------------------------------------------------------- /solutions/radon_sol.py: -------------------------------------------------------------------------------- 1 | # Solutions for Radon tutorial - PyData Global 2020 Tutorial 2 | 3 | def radon_noise(): 4 | """Create noise to add to projections 5 | """ 6 | sigman = 5e-1 # play with this... 7 | n = np.random.normal(0., sigman, projection.shape) 8 | projection_n = projection + n 9 | projection1_n = projection1 + \ 10 | n[projection.shape[0] // 2 - (nx - inner) // 2: projection.shape[0] // 2 + (nx - inner) // 2].T 11 | 12 | # copy-paste here the inversion code(s)... 13 | 14 | 15 | def radon_morereg(): 16 | """Add Wavelet Transform regularization to SplitBregman reconstruction 17 | """ 18 | # Wavelet transform operator 19 | Wop = pylops.signalprocessing.DWT2D(image.shape, wavelet='haar', level=5) 20 | DWop = Dop + [Wop, ] 21 | 22 | # Solve inverse problem 23 | mu = 0.2 24 | lamda = [1., 1., .5] 25 | niter = 5 26 | niterinner = 1 27 | 28 | image_tvw, niter = pylops.optimization.sparsity.SplitBregman(Radop, DWop, 29 | projection1.ravel(), 30 | niter, 31 | niterinner, 32 | mu=mu, 33 | epsRL1s=lamda, 34 | tol=1e-4, 35 | tau=1., 36 | show=True, 37 | **dict( 38 | iter_lim=10, 39 | damp=1e-2)) 40 | image_tvw = np.real(image_tvw.reshape(nx, ny)) 41 | mse_tvw = np.linalg.norm( 42 | image_tvw[pad // 2:-pad // 2, pad // 2:-pad // 2] - 43 | image[pad // 2:-pad // 2, pad // 2:-pad // 2]) 44 | print(f"TV+W MSE reconstruction error: {mse_tvw:.3f}") 45 | 46 | fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(12, 6)) 47 | ax1.imshow(image_tvw[pad // 2:-pad // 2, pad // 2:-pad // 2], cmap='gray', 48 | vmin=0, vmax=1) 49 | ax1.set_title("Reconstruction\nTV+W Regularized inversion") 50 | ax1.axis('tight') 51 | ax2.imshow(image_tvw[pad // 2:-pad // 2, pad // 2:-pad // 2] - 52 | image[pad // 2:-pad // 2, pad // 2:-pad // 2], 53 | cmap='gray', vmin=-0.2, vmax=0.2) 54 | ax2.set_title("Reconstruction error\nTV+W Regularized inversion") 55 | ax2.axis('tight') 56 | 57 | 58 | def radon_kk(): 59 | """Perform reconstruction in KK domain 60 | """ 61 | # 2D FFT operator 62 | Fop = pylops.signalprocessing.FFT2D(dims=(nx, ny), dtype=np.complex) 63 | 64 | # Restriction operator 65 | thetas = np.arange(-43, 47, 5) 66 | kx = np.arange(nx) - nx // 2 67 | ikx = np.arange(nx) 68 | 69 | mask = np.zeros((nx, ny)) 70 | for theta in thetas: 71 | ky = kx * np.tan(np.deg2rad(theta)) 72 | iky = np.round(ky).astype(np.int) + nx // 2 73 | sel = (iky >= 0) & (iky < nx) 74 | mask[ikx[sel], iky[sel]] = 1 75 | mask = np.logical_or(mask, np.fliplr(mask.T)) 76 | mask = np.fft.ifftshift(mask) 77 | 78 | Rop = pylops.Restriction(nx * ny, np.where(mask.ravel() == 1)[0], 79 | dtype=np.complex) 80 | 81 | # kk spectrum 82 | kk = Fop * image.ravel() 83 | kk = kk.reshape(image.shape) 84 | 85 | # restricted kk spectrum 86 | kkrestr = Rop.mask(kk.ravel()) 87 | kkrestr = kkrestr.reshape(ny, nx) 88 | kkrestr.data[:] = np.fft.fftshift(kkrestr.data) 89 | kkrestr.mask[:] = np.fft.fftshift(kkrestr.mask) 90 | 91 | # data 92 | KOp = Rop * Fop 93 | y = KOp * image.ravel() 94 | 95 | fig, (ax1, ax2, ax3, ax4) = plt.subplots(1, 4, figsize=(17, 4)) 96 | ax1.imshow(image, cmap='gray') 97 | ax1.set_title(r"Input $\mathbf{i}$") 98 | ax1.axis('tight') 99 | ax2.imshow(np.fft.fftshift(np.abs(kk)), vmin=0, vmax=1, cmap='rainbow') 100 | ax2.set_title("KK Spectrum") 101 | ax2.axis('tight') 102 | ax3.imshow(kkrestr.mask, vmin=0, vmax=1, cmap='gray') 103 | ax3.set_title(r"Sampling matrix") 104 | ax3.axis('tight') 105 | ax4.imshow(np.abs(kkrestr), vmin=0, vmax=1, cmap='rainbow') 106 | ax4.set_title(r"Sampled KK Spectrum $\mathbf{k}$") 107 | ax4.axis('tight') 108 | 109 | # Solve L2 inverse problem 110 | D2op = pylops.Laplacian(dims=(nx, ny), edge=True, dtype=np.complex) 111 | 112 | image_l2 = \ 113 | pylops.optimization.leastsquares.RegularizedInversion(KOp, [D2op], 114 | y, epsRs=[5e-1], 115 | show=True, 116 | **dict(iter_lim=20)) 117 | image_l2 = np.real(image_l2.reshape(nx, ny)) 118 | 119 | fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(12, 6)) 120 | ax1.imshow(image_l2[pad // 2:-pad // 2, pad // 2:-pad // 2], cmap='gray', 121 | vmin=0, vmax=1) 122 | ax1.set_title("Reconstruction\nL2 Regularized inversion") 123 | ax1.axis('tight') 124 | ax2.imshow(image_l2[pad // 2:-pad // 2, pad // 2:-pad // 2] - 125 | image[pad // 2:-pad // 2, pad // 2:-pad // 2], 126 | cmap='gray', vmin=-0.2, vmax=0.2) 127 | ax2.set_title("Reconstruction error\nL2 Regularized inversion") 128 | ax2.axis('tight') 129 | 130 | # Solve TV inverse problem 131 | Dop = [pylops.FirstDerivative(ny * nx, dims=(nx, ny), dir=0, edge=True, 132 | kind='backward', dtype=np.complex), 133 | pylops.FirstDerivative(ny * nx, dims=(nx, ny), dir=1, edge=True, 134 | kind='backward', dtype=np.complex)] 135 | 136 | mu = 0.5 137 | lamda = [.05, .05] 138 | niter = 5 139 | niterinner = 1 140 | 141 | image_tv, niter = \ 142 | pylops.optimization.sparsity.SplitBregman(KOp, Dop, y, 143 | niter, 144 | niterinner, 145 | mu=mu, 146 | epsRL1s=lamda, 147 | tol=1e-4, 148 | tau=1., 149 | show=True, 150 | **dict(iter_lim=10, 151 | damp=1e-2)) 152 | image_tv = np.real(image_tv.reshape(nx, ny)) 153 | 154 | fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(12, 6)) 155 | ax1.imshow(image_tv[pad // 2:-pad // 2, pad // 2:-pad // 2], cmap='gray', 156 | vmin=0, vmax=1) 157 | ax1.set_title("Reconstruction\nTV Regularized inversion") 158 | ax1.axis('tight') 159 | ax2.imshow(image_tv[pad // 2:-pad // 2, pad // 2:-pad // 2] - 160 | image[pad // 2:-pad // 2, pad // 2:-pad // 2], 161 | cmap='gray', vmin=-0.2, vmax=0.2) 162 | ax2.set_title("Reconstruction error\nTV Regularized inversion") 163 | ax2.axis('tight') -------------------------------------------------------------------------------- /Deblurring.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "metadata": {}, 6 | "source": [ 7 | "# Image deblurring\n", 8 | "\n", 9 | "**Authors: M. Ravasi, D. Vargas, I. Vasconcelos**" 10 | ] 11 | }, 12 | { 13 | "cell_type": "markdown", 14 | "metadata": {}, 15 | "source": [ 16 | "Welcome to the second session of our **Solving large-scale inverse problems in Python with PyLops** tutorial!\n", 17 | "\n", 18 | "Throughout this tutorial, we aim at developing the following aspects in image processing:\n", 19 | "\n", 20 | "- We connect with the concepts learned in the previous session by defining a simple PyLops operator implementing a convolutional kernel for image processing \n", 21 | "- Learn how to use build-in PyLops operators for image manipulation including, blurring, sharpening, and edge detection\n", 22 | "- Demonstrate the versatility of the linear operators when implemented in the solution of inverse problems, in particular, the image deblurring problem\n", 23 | "- Analyze the influence of noise in contrast with the performance of different solvers, least-squares, TV-Regularization, and FISTA" 24 | ] 25 | }, 26 | { 27 | "cell_type": "markdown", 28 | "metadata": {}, 29 | "source": [ 30 | "## Useful links\n", 31 | "\n", 32 | "- Tutorial Github repository: https://github.com/mrava87/pylops_pydata2020\n", 33 | " \n", 34 | "- PyLops Github repository: https://github.com/equinor/pylops\n", 35 | "\n", 36 | "- PyLops reference documentation: https://pylops.readthedocs.io/en/latest/" 37 | ] 38 | }, 39 | { 40 | "cell_type": "markdown", 41 | "metadata": {}, 42 | "source": [ 43 | "# Deblurring Images\n", 44 | "\n", 45 | "Compact digital cameras were introduced in the early seventies thanks to the development of CCD sensors allowing the recording and storage of images in digital format. The goal of an image process is to capture an accurate representation of a particular scene. However, any image is influenced by the optical system itself and the overall effect is a blurred image reconstruction, therefore, specialized algorithms performing deblurring need to be implemented in order to achieve a sharper image. Image deblurring is a method that aims at recovering the original sharp-image by removing effect caused by limited aperture, lens aberrations, defocus, and unintended motions. This can be done by defining a mathematical model of the blurring process with the idea of removing from the image the blurring effects.\n", 46 | "\n", 47 | "Traditionally, the main assumption is that the blurring process is linear. When this is the case, a 2-dimensional convolutional model describes the imaging problem. The blurred Image (output) is the result of a point-spread function acting on a target sharp image (input). Such process is described by the following transformation, \n", 48 | "\n", 49 | "\\begin{equation}\n", 50 | "g(x, y) = \\int_{-\\infty}^{\\infty}\\int_{-\\infty}^{\\infty} h(x-x_0,y-y_0) f(x_0, y_0) dx_0 dy_0.\n", 51 | "\\end{equation}\n", 52 | "\n", 53 | "This operation can be discretized as follows\n", 54 | "\n", 55 | "\\begin{equation}\n", 56 | "g[i,n] = \\sum_{j=-\\infty}^{\\infty} \\sum_{m=-\\infty}^{\\infty} h[i-j,n-m] f[j,m] \n", 57 | "\\end{equation}\n", 58 | "\n", 59 | "\n", 60 | "\n", 61 | "and also implemented in the frequency domain using the convolution theorem\n", 62 | "\n", 63 | "\\begin{equation}\n", 64 | "G(k_x, k_y) = H(k_x, k_y) F(k_x, k_y).\n", 65 | "\\end{equation}\n", 66 | "\n", 67 | "Here, the spectrum of the point-spread function $H(k_x, k_y) = \\mathscr{F} \\{h(x,y)\\}$ is called transfer function, similarly, $F(k_x, k_y) = \\mathscr{F} \\{f(x,y)\\}$ is the spectrum of the sharp image. The previous definition is an specific case of the more general problem $\\mathbf{y} = \\mathbf{A} \\mathbf{x}$ (*forward operation*), where $\\mathbf{A}:\\mathbb{F}^m \\to \\mathbb{F}^n$ is a linear operator that maps a vector of size $m$ in the *model space* to a vector of size $n$ in the *data space*, as explained in the previous session. The (*adjoint operation*) is $\\mathbf{x} = \\mathbf{A}^H \\mathbf{y}$. In this context, our deblurring operation is an inverse problem design to find a pseudoinverse, $\\mathbf{A}^{-1}$, that aims at removing the effect of the operator $\\mathbf{A}$ from the data $\\mathbf{y}$ to retrieve the model $\\mathbf{x}$, i.e., $\\hat{\\mathbf{x}} = \\mathbf{A}^{-1} \\mathbf{y}$. \n", 68 | "\n", 69 | "This Jupyter Notebook is intended to guide you through the main steps in the implementation of the blurring and deblurring process using the pylops.signalprocessing.Convolve2D operator, in this case, we assume to know the structure of the point-spread function for an specific problem.\n", 70 | "\n" 71 | ] 72 | }, 73 | { 74 | "cell_type": "code", 75 | "execution_count": null, 76 | "metadata": {}, 77 | "outputs": [], 78 | "source": [ 79 | "# Run this when using Colab (will install the missing libraries)\n", 80 | "# !pip install pylops scooby" 81 | ] 82 | }, 83 | { 84 | "cell_type": "code", 85 | "execution_count": null, 86 | "metadata": {}, 87 | "outputs": [], 88 | "source": [ 89 | "%matplotlib inline\n", 90 | "\n", 91 | "import numpy as np\n", 92 | "from skimage import data\n", 93 | "import matplotlib.pyplot as plt\n", 94 | "from matplotlib.colors import LogNorm\n", 95 | "from scipy.signal import convolve, correlate\n", 96 | "import scooby\n", 97 | "\n", 98 | "from pylops.optimization import leastsquares, sparsity\n", 99 | "from pylops.signalprocessing import Convolve2D\n", 100 | "from pylops import FirstDerivative, Laplacian\n", 101 | "from pylops import LinearOperator\n", 102 | "from pylops.utils import dottest" 103 | ] 104 | }, 105 | { 106 | "cell_type": "markdown", 107 | "metadata": {}, 108 | "source": [ 109 | "# A simple deconvolution problem\n", 110 | "\n", 111 | "The first problem we will solve with the aid of pylops is a simple one-dimensional convolution. We start by defining a pylops operator applying convolution with a given impulse response to a model vector. For simplicity, we select a *[ricker](https://en.wikipedia.org/wiki/Mexican_hat_wavelet)* wavelet and used it as a compact filter to be convolved with a 1D input signal. Assuming our input signal (model) is a time series constructed by superposition of three gaussian functions with different time shifts, the forward problem can be interpreted as a weighted sum of the function $f(\\tau)$ at the moment $t$ with weights given by $h$.\n", 112 | "\n", 113 | "\n", 114 | "\n", 115 | "The resulting signal (data), $g(t)$, is a 'blurred' shifted version of the input model mathematically computed by integration over time according to \n", 116 | "\n", 117 | "\\begin{equation}\n", 118 | "g(t) = \\int_{-\\infty}^{\\infty} h(t - \\tau) f(\\tau) d\\tau\n", 119 | "\\end{equation}\n", 120 | "\n", 121 | "Next, we solve the inverse problem (**deconvolution**) where we ask the question. What model vector would reproduce a given data set when convolved with a specific convolutional kernel?. Here we try to reconstruct an unknown model based on the given data (blue line in the figure) and impulse response (red line in the figure). The expected output of this operation should approximate our target model (black_line in the figure)\n", 122 | "\n", 123 | "\n", 124 | "**Pylops convolution operator:**\n", 125 | "A simple pylops operator is defined as a Child class that inherits from **[`pylops.LinearOperator`](https://pylops.readthedocs.io/en/latest/api/generated/pylops.LinearOperator.html#pylops.LinearOperator)**. We initilize the class by defining at least three atributes in the the class constructor; `shape`, `dtype`, and `explicit`. Next, we difine the forward mode operation through the method `_matvec`, and `_rmatvec` for the adjoint mode." 126 | ] 127 | }, 128 | { 129 | "cell_type": "code", 130 | "execution_count": null, 131 | "metadata": {}, 132 | "outputs": [], 133 | "source": [ 134 | "# Defining a convolution Pylops operator\n", 135 | "class my_convolve(LinearOperator):\n", 136 | " def __init__(self, A, dims, dtype=\"float64\"):\n", 137 | " self.A = A\n", 138 | " self.dims = dims\n", 139 | " self.shape = (np.prod(self.dims), np.prod(self.dims))\n", 140 | " self.dtype = np.dtype(dtype)\n", 141 | " self.explicit = False\n", 142 | "\n", 143 | " def _matvec(self, x):\n", 144 | " x = np.reshape(x, self.dims)\n", 145 | " y = convolve(x, self.A, mode='same')\n", 146 | " return np.ndarray.flatten(y)\n", 147 | "\n", 148 | " def _rmatvec(self, x):\n", 149 | " x = np.reshape(x, self.dims)\n", 150 | " y = correlate(x, self.A, mode='same')\n", 151 | " return np.ndarray.flatten(y)" 152 | ] 153 | }, 154 | { 155 | "cell_type": "code", 156 | "execution_count": null, 157 | "metadata": {}, 158 | "outputs": [], 159 | "source": [ 160 | "t_min, t_max, nt = -4.0, 4.0, 10000\n", 161 | "f_central = 15\n", 162 | "t_delay = 1\n", 163 | "t_delay_wav = np.array([0.0, 0.6, 1.2])[:, np.newaxis]\n", 164 | "\n", 165 | "# Time axis\n", 166 | "t = np.linspace(t_min, t_max, nt)\n", 167 | "sigma = 1 / (np.pi * f_central) ** 2\n", 168 | "\n", 169 | "# Model vector\n", 170 | "wav = np.exp(-((t - t_delay_wav) ** 2) / sigma)\n", 171 | "model_ = wav[0] - 1.5*wav[1] + wav[2]\n", 172 | "\n", 173 | "# Impulse response - Convolutional Operator\n", 174 | "impulse_response = (1 - (2 * (t - t_delay) ** 2) / sigma) * \\\n", 175 | " np.exp(-((t - t_delay) ** 2) / sigma)\n", 176 | "\n", 177 | "# Data vector\n", 178 | "R_op = my_convolve(A=impulse_response, dims=(nt))\n", 179 | "data_ = R_op * model_\n", 180 | "\n", 181 | "\n", 182 | "\n", 183 | "# Model reconstruction - Inverse problem\n", 184 | "model_reconstructed = sparsity.FISTA(Op=R_op,\n", 185 | " data=data_,\n", 186 | " eps=1e-1,\n", 187 | " niter=200)[0]\n", 188 | "\n", 189 | "# Plotting input vectors\n", 190 | "fig, ax = plt.subplots(1, 3, figsize=[10, 3], facecolor='w', edgecolor='k')\n", 191 | "ax[0].plot(t, impulse_response, 'r', lw=1.5)\n", 192 | "ax[0].set_xlabel('Time - (sec)')\n", 193 | "ax[0].set_ylabel('Amplitude')\n", 194 | "ax[0].set_xlim([0.8, 1.2])\n", 195 | "ax[0].set_title(\"Operator\")\n", 196 | "\n", 197 | "ax[1].plot(t, model_, 'k', lw=1.5)\n", 198 | "ax[1].set_xlabel('Time - (sec)')\n", 199 | "ax[1].set_xlim([-0.1, 2.5])\n", 200 | "ax[1].set_title(\"Model\")\n", 201 | "\n", 202 | "ax[2].plot(t, data_, 'b', lw=1.5)\n", 203 | "ax[2].set_xlabel('Time - (sec)')\n", 204 | "ax[2].set_xlim([-0.1, 2.5])\n", 205 | "ax[2].set_title(\"Data\")\n", 206 | "fig.tight_layout()\n", 207 | "\n", 208 | "\n", 209 | "# Plotting Model reconstruction vectors\n", 210 | "fig = plt.figure(figsize=[4, 3], facecolor='w', edgecolor='k')\n", 211 | "ax = fig.add_subplot(1, 1, 1)\n", 212 | "ax.plot(t, model_, 'k', lw=3, label='Target Model')\n", 213 | "ax.plot(t, model_reconstructed, '--g', lw=2, label='Reconstruction')\n", 214 | "ax.set_ylabel('Amplitude')\n", 215 | "ax.set_xlabel('Time - (sec)')\n", 216 | "ax.set_xlim([-0.1, 1.5])\n", 217 | "ax.set_title(\"Model Reconstruction\")\n", 218 | "ax.legend()" 219 | ] 220 | }, 221 | { 222 | "cell_type": "code", 223 | "execution_count": null, 224 | "metadata": {}, 225 | "outputs": [], 226 | "source": [ 227 | "R_op" 228 | ] 229 | }, 230 | { 231 | "cell_type": "markdown", 232 | "metadata": {}, 233 | "source": [ 234 | "# Image filtereing\n", 235 | "\n", 236 | "PyLops comes with multiple build-in operators ready to use. In this section, we are using `pylops.signalprocessing.Convolve2D` to perform convolutions. First, we need to define a compact filter to construct the actual operator and then apply it to a pre-loaded image. Before we do that, let's first see if under the given parameters this linear operator passes the dot test. Once we make sure that this is the case, we can compute the convolution using NumPy matrix-like syntax. \n", 237 | "\n", 238 | "\n", 239 | "\n", 240 | "In the cell below, we select the most simple point spread function, the identity kernel. It should not blur the image and the error of the output compared to the input should be very low (to machine precession)\n", 241 | "\n", 242 | "Let's start by uploading an image. Here we use the **scikit-image** module skimage.data containing standard test images. The very first line selects the image of your preference to be used in the next cells, feel free to play around, and even upload your own pictures. " 243 | ] 244 | }, 245 | { 246 | "cell_type": "code", 247 | "execution_count": null, 248 | "metadata": {}, 249 | "outputs": [], 250 | "source": [ 251 | "image = data.camera() # Load image from scikit-image\n", 252 | "# image = data.binary_blobs()\n", 253 | "# image = data.checkerboard()\n", 254 | "ny, nx = image.shape\n", 255 | "\n", 256 | "# Identity kernel\n", 257 | "kernel = np.zeros([3, 3])\n", 258 | "kernel[1, 1] = 1\n", 259 | "\n", 260 | "# Define the PyLops blurring Operator\n", 261 | "C_op = Convolve2D(N=ny*nx,\n", 262 | " h=kernel,\n", 263 | " offset=(kernel.shape[0]//2, kernel.shape[1]//2),\n", 264 | " dims=(ny, nx))\n", 265 | "\n", 266 | "# Dot test\n", 267 | "dottest(C_op, ny*nx, ny*nx, verb=True)\n", 268 | "\n", 269 | "\n", 270 | "img_blur = C_op * image.flatten()\n", 271 | "img_blur = img_blur.reshape(image.shape)\n", 272 | "\n", 273 | "\n", 274 | "# PLOTTING\n", 275 | "fig, ax = plt.subplots(1, 3, figsize=(15, 5))\n", 276 | "ax[0].imshow(image, cmap='gray')\n", 277 | "ax[0].set_title('Input image')\n", 278 | "\n", 279 | "ax[1].imshow(img_blur, cmap='gray')\n", 280 | "ax[1].set_title('Response to identity')\n", 281 | "\n", 282 | "him = ax[2].imshow(img_blur-image, cmap='gray')\n", 283 | "ax[2].set_title('Error output-input')\n", 284 | "fig.colorbar(him, ax=ax[2])" 285 | ] 286 | }, 287 | { 288 | "cell_type": "code", 289 | "execution_count": null, 290 | "metadata": {}, 291 | "outputs": [], 292 | "source": [ 293 | "print(\"image.shape {}\".format(image.shape))\n", 294 | "C_op" 295 | ] 296 | }, 297 | { 298 | "cell_type": "markdown", 299 | "metadata": {}, 300 | "source": [ 301 | "**EX: Write your own kernel.** A very common practice in digital image processing consists of applying filters as a way to highlight specific image features. One of the most useful is known as unsharp mask and is used attenuate low frequencies features while enhancing small contrast changes. A very simple version of this sharpening filter is given by the matrix kernel,\n", 302 | "\n", 303 | "$$ \n", 304 | "\\mathbf{A} = \n", 305 | "\\begin{bmatrix}\n", 306 | " 0 & -1 & 0\\\\\n", 307 | " -1 & 5 & -1\\\\\n", 308 | " 0 & -1 & 0\n", 309 | "\\end{bmatrix} \n", 310 | "$$\n", 311 | "\n", 312 | "Define a pylops operator for this mask and apply it to your preferred image. A code snippet for the plotting the input/output images is given below \n", 313 | "\n", 314 | "" 315 | ] 316 | }, 317 | { 318 | "cell_type": "code", 319 | "execution_count": null, 320 | "metadata": {}, 321 | "outputs": [], 322 | "source": [ 323 | "# %load -s Unsharp_Mask solutions/deblurring_sol.py" 324 | ] 325 | }, 326 | { 327 | "cell_type": "code", 328 | "execution_count": null, 329 | "metadata": {}, 330 | "outputs": [], 331 | "source": [ 332 | "# Unsharp_Mask()" 333 | ] 334 | }, 335 | { 336 | "cell_type": "markdown", 337 | "metadata": {}, 338 | "source": [ 339 | "#### Edge detection - Sobel operator\n", 340 | "\n", 341 | "Edge detection is a process that tries to identify areas on an image where strong changes in intensity take place. A simple way of performing this task is by implementing a Sobel operator. This kind of mask approximates the directional derivates of an image along with directions $x$, and $y$. Vertical, $\\mathbf{G_x}$, and Horizontal, $\\mathbf{G_y}$, gradients are give by the 3x3 kernels\n", 342 | "\n", 343 | "\\begin{equation}\n", 344 | " \\mathbf{G_x}=\n", 345 | " \\begin{bmatrix}\n", 346 | " 1 & 2 & 1\\\\\n", 347 | " 0 & 0 & 0\\\\\n", 348 | " -1 & -2 & -1\n", 349 | " \\end{bmatrix} \n", 350 | " \\qquad\\text{,}\\qquad \n", 351 | " \\mathbf{G_y} =\n", 352 | " \\begin{bmatrix}\n", 353 | " 1 & 0 & -1\\\\\n", 354 | " 2 & 0 & -2\\\\\n", 355 | " 1 & 0 & -1\n", 356 | " \\end{bmatrix}\n", 357 | "\\end{equation}\n", 358 | "\n", 359 | "note that each mask is designed to detect either horizontal or vertical changes. A Combination of these gradient approximations provides the total gradient magnitude, $\\mathbf{G} = \\sqrt{\\mathbf{G_x}^2 + \\mathbf{G_x}^2}$, at an specific pixel of the image.\n", 360 | "\n", 361 | "As we did in the previous exercise, we now implement edge detection by defining vertical and horizontal gradient kernels, apply them to an image, and assemble the gradient magnitude." 362 | ] 363 | }, 364 | { 365 | "cell_type": "code", 366 | "execution_count": null, 367 | "metadata": {}, 368 | "outputs": [], 369 | "source": [ 370 | "# vertical edge detector\n", 371 | "kernel_v = np.array([[1, 0, -1],\n", 372 | " [2, 0, -2],\n", 373 | " [1, 0, -1]])\n", 374 | "\n", 375 | "# horizontal edge detector\n", 376 | "kernel_h = np.array([[1, 2, 1],\n", 377 | " [0, 0, 0],\n", 378 | " [-1, -2, -1]])\n", 379 | "\n", 380 | "# Define the PyLops blurring Operator\n", 381 | "Sobel_x_op = Convolve2D(N=ny*nx,\n", 382 | " h=kernel_h,\n", 383 | " offset=(kernel_h.shape[0]//2, kernel_h.shape[1]//2),\n", 384 | " dims=(ny, nx))\n", 385 | "Sobel_y_op = Convolve2D(N=ny*nx,\n", 386 | " h=kernel_v,\n", 387 | " offset=(kernel_v.shape[0]//2, kernel_v.shape[1]//2),\n", 388 | " dims=(ny, nx))\n", 389 | "\n", 390 | "Gx = Sobel_x_op * image.flatten()\n", 391 | "Gy = Sobel_y_op * image.flatten()\n", 392 | "Gx = Gx.reshape(image.shape)\n", 393 | "Gy = Gy.reshape(image.shape)\n", 394 | "\n", 395 | "# Gradient magnitude\n", 396 | "G = np.sqrt(Gy**2 + Gy**2)\n", 397 | "\n", 398 | "# PLOTTING\n", 399 | "fig, ax = plt.subplots(1, 3, figsize=(15, 5))\n", 400 | "\n", 401 | "ax[0].imshow(image, cmap='gray')\n", 402 | "ax[0].set_title('Image')\n", 403 | "\n", 404 | "ax[1].imshow(kernel_v, cmap='gray')\n", 405 | "ax[1].set_title('vertical Kernel')\n", 406 | "\n", 407 | "ax[2].imshow(G, cmap='gray')\n", 408 | "ax[2].set_title('Image edges')\n" 409 | ] 410 | }, 411 | { 412 | "cell_type": "markdown", 413 | "metadata": {}, 414 | "source": [ 415 | "# Image Blurring\n", 416 | "\n", 417 | "The Gaussian filter is an operator used to suppress noise and reduce the contrast of an image. In this case, an image is rendered smooth under the action of a Gaussian mask by removing the high-frequency components. Therefore, we can understand a Gaussian blur as a low-pass filter, attenuating the high-frequency components in an image. Similarly, the maximum value of the weighted sum in the convolution located around the center of the distribution decreases as a function of distance. Mathematically the two-dimensional Gaussian function is defined as\n", 418 | "\n", 419 | "\\begin{equation}\n", 420 | "G(x,y) = \\frac{1}{2 \\pi \\sigma ^{2}} e^{- \\frac{x^{2} + y^{2}}{2 \\sigma ^{2}}}\n", 421 | "\\label{2dgaussian}\n", 422 | "\\end{equation}\n", 423 | "\n", 424 | "where the standard deviation of the kernel is given for each axis and determines the effective extension of the filter.\n", 425 | "\n", 426 | "\n", 427 | "\n", 428 | "In the following cell, we will prepare a data vector represented by a Gaussian-blurred image as preparation for the next section, where we will use the smoothed image together with the given kernel and try to remove the blurring effects by solving a deconvolution problem " 429 | ] 430 | }, 431 | { 432 | "cell_type": "code", 433 | "execution_count": null, 434 | "metadata": {}, 435 | "outputs": [], 436 | "source": [ 437 | "# Play around with this parameters\n", 438 | "mx, my = 25, 15 # 25, 15\n", 439 | "sigma_x, sigma_y = 4.5, 2.5 # 4.5, 2.5\n", 440 | "\n", 441 | "x = np.linspace(-mx//2, mx//2, mx)\n", 442 | "y = np.linspace(-my//2, my//2, my)[..., None]\n", 443 | "kernel_gauss = 1/(2*np.pi*sigma_x*sigma_y) * \\\n", 444 | " np.exp(-(x**2/(2*sigma_x**2) + y**2/(2*sigma_y**2)))\n", 445 | "\n", 446 | "# Define the PyLops blurring Operator\n", 447 | "Gauss_op = Convolve2D(N=ny*nx,\n", 448 | " h=kernel_gauss,\n", 449 | " offset=(kernel_gauss.shape[0]//2,\n", 450 | " kernel_gauss.shape[1]//2),\n", 451 | " dims=(ny, nx))\n", 452 | "\n", 453 | "img_gauss = Gauss_op * image.flatten()\n", 454 | "img_gauss_ = img_gauss.reshape(image.shape)\n", 455 | "\n", 456 | "# image spectrum\n", 457 | "image_shift = np.fft.fftshift(np.fft.fft2(image))\n", 458 | "image_spectrum = np.abs(image_shift)\n", 459 | "\n", 460 | "# PSF spectrum - Transfer function\n", 461 | "kernel_gauss_shift = np.fft.fftshift(np.fft.fft2(kernel_gauss))\n", 462 | "kernel_gauss_spectrum = np.abs(kernel_gauss_shift)\n", 463 | "\n", 464 | "# blurred-image spectrum\n", 465 | "img_gauss_shift = np.fft.fftshift(np.fft.fft2(img_gauss_))\n", 466 | "img_gauss_spectrum = np.abs(img_gauss_shift)\n", 467 | "\n", 468 | "# PLOTTING\n", 469 | "fig, ax = plt.subplots(2, 3, figsize=(9, 6))\n", 470 | "\n", 471 | "ax[0, 0].imshow(image, cmap='gray')\n", 472 | "ax[0, 0].set_title('Image')\n", 473 | "\n", 474 | "ax[0, 1].imshow(kernel_gauss, cmap='gray')\n", 475 | "ax[0, 1].set_title('Gaussian Kernel')\n", 476 | "\n", 477 | "him2 = ax[0, 2].imshow(img_gauss_, cmap='gray')\n", 478 | "ax[0, 2].set_title('Blurred Image')\n", 479 | "\n", 480 | "ax[1, 0].imshow(image_spectrum, cmap='gray', norm=LogNorm(vmin=10))\n", 481 | "ax[1, 0].set_title('Image Spectrum')\n", 482 | "\n", 483 | "ax[1, 1].imshow(kernel_gauss_spectrum, cmap='gray')\n", 484 | "ax[1, 1].set_title('Gaussian Kernel Spectrum')\n", 485 | "\n", 486 | "him2 = ax[1, 2].imshow(img_gauss_spectrum, cmap='gray', norm=LogNorm(vmin=10))\n", 487 | "ax[1, 2].set_title('Blurred Image Spectrum')\n", 488 | "fig.tight_layout()" 489 | ] 490 | }, 491 | { 492 | "cell_type": "markdown", 493 | "metadata": {}, 494 | "source": [ 495 | "# Image Deblurring \n", 496 | "\n", 497 | "Finally, we consider a standard deblurring case. Here we assume a picture was taken in real life; therefore, a whole set of potential artifacts are present in it. Our mission is to try and reconstruct a shaper image representation of the scene by removing the blurring effects. Luckily, we assume to have some information on the optical system and decided to implement an inverse problem. Depending on the application, different solvers are available, below we will compare the performance of some well-known algorithms to deblur the image and compare it with its original version.\n", 498 | "\n", 499 | "- least squares inversion\n", 500 | "- least squares - regularized inversion \n", 501 | "- TV inversion\n", 502 | "- FISTA - Fast Iterative Shrinkage-Thresholding Algorithm\n", 503 | "\n", 504 | "A set of ready to use solvers are implemented in the PyLops module `pylops.optimization.leastsquares` and `pylops.optimization.sparsity`." 505 | ] 506 | }, 507 | { 508 | "cell_type": "markdown", 509 | "metadata": {}, 510 | "source": [ 511 | "**least squares inversion**\n", 512 | "\\begin{equation}\n", 513 | "J= ||\\mathbf{d} - \\mathbf{A} \\mathbf{m}||_2^2\n", 514 | "\\end{equation}" 515 | ] 516 | }, 517 | { 518 | "cell_type": "code", 519 | "execution_count": null, 520 | "metadata": {}, 521 | "outputs": [], 522 | "source": [ 523 | "# least squares inversion\n", 524 | "deblur_l2 = leastsquares.NormalEquationsInversion(Op=Gauss_op,\n", 525 | " Regs=None,\n", 526 | " data=img_gauss,\n", 527 | " maxiter=40)\n", 528 | "# Reshape image\n", 529 | "deblur_l2 = deblur_l2.reshape(image.shape)" 530 | ] 531 | }, 532 | { 533 | "cell_type": "markdown", 534 | "metadata": {}, 535 | "source": [ 536 | "**least squares - regularized inversion**\n", 537 | "\\begin{equation}\n", 538 | "J= ||\\mathbf{d} - \\mathbf{A} \\mathbf{m}||_2^2 + ||\\nabla \\mathbf{m}||_2^2\n", 539 | "\\end{equation}" 540 | ] 541 | }, 542 | { 543 | "cell_type": "code", 544 | "execution_count": null, 545 | "metadata": {}, 546 | "outputs": [], 547 | "source": [ 548 | "# L2 regularization term - Second derivative\n", 549 | "D2op = Laplacian(dims=(ny, nx), dtype=np.float)\n", 550 | "\n", 551 | "# least squares - regularized inversion\n", 552 | "deblur_l2_reg = leastsquares.RegularizedInversion(Op=Gauss_op,\n", 553 | " Regs=[D2op],\n", 554 | " data=img_gauss,\n", 555 | " epsRs=[1e0],\n", 556 | " show=True,\n", 557 | " **dict(iter_lim=20))\n", 558 | "# Reshape image\n", 559 | "deblur_l2_reg = np.real(deblur_l2_reg.reshape(image.shape))" 560 | ] 561 | }, 562 | { 563 | "cell_type": "markdown", 564 | "metadata": {}, 565 | "source": [ 566 | "**TV inversion**\n", 567 | "\\begin{equation}\n", 568 | "J= ||\\mathbf{d} - \\mathbf{A} \\mathbf{m}||_2^2 + ||\\mathbf{D}_x \\mathbf{m}||_1 + ||\\mathbf{D}_y \\mathbf{m}||_1\n", 569 | "\\end{equation}" 570 | ] 571 | }, 572 | { 573 | "cell_type": "code", 574 | "execution_count": null, 575 | "metadata": {}, 576 | "outputs": [], 577 | "source": [ 578 | "# L1 regularization term - First derivative\n", 579 | "Dop = [FirstDerivative(ny * nx, dims=(ny, nx), dir=0),\n", 580 | " FirstDerivative(ny * nx, dims=(ny, nx), dir=1)]\n", 581 | "\n", 582 | "# TV inversion\n", 583 | "deblur_tv = sparsity.SplitBregman(Op=Gauss_op,\n", 584 | " RegsL1=Dop,\n", 585 | " data=img_gauss,\n", 586 | " niter_outer=10,\n", 587 | " niter_inner=5,\n", 588 | " mu=1.8,\n", 589 | " epsRL1s=[1e-1, 1e-1],\n", 590 | " tol=1e-4,\n", 591 | " tau=1.,\n", 592 | " ** dict(iter_lim=5, damp=1e-4, show=True))[0]\n", 593 | "# Reshape image\n", 594 | "deblur_tv = deblur_tv.reshape(image.shape)" 595 | ] 596 | }, 597 | { 598 | "cell_type": "markdown", 599 | "metadata": {}, 600 | "source": [ 601 | "**FISTA inversion**\n", 602 | "\\begin{equation}\n", 603 | "J= ||\\mathbf{d} - \\mathbf{A} \\mathbf{m}||_2^2 + \\epsilon||\\mathbf{m}||_p\n", 604 | "\\end{equation}" 605 | ] 606 | }, 607 | { 608 | "cell_type": "code", 609 | "execution_count": null, 610 | "metadata": {}, 611 | "outputs": [], 612 | "source": [ 613 | "# FISTA inversion\n", 614 | "deblur_fista = sparsity.FISTA(Op=Gauss_op,\n", 615 | " data=img_gauss,\n", 616 | " eps=1e-1,\n", 617 | " niter=100,\n", 618 | " show=True)[0]\n", 619 | "# Reshape image\n", 620 | "deblur_fista = deblur_fista.reshape(image.shape)" 621 | ] 622 | }, 623 | { 624 | "cell_type": "code", 625 | "execution_count": null, 626 | "metadata": {}, 627 | "outputs": [], 628 | "source": [ 629 | "# PLOTTING\n", 630 | "fig, ax = plt.subplots(2, 3, sharex=True, sharey=True, figsize=(12, 8))\n", 631 | "\n", 632 | "ax[0, 0].imshow(image, aspect='auto', cmap='gray')\n", 633 | "ax[0, 0].set_title('Original Image')\n", 634 | "\n", 635 | "ax[0, 1].imshow(deblur_l2, aspect='auto', cmap='gray')\n", 636 | "ax[0, 1].set_title('LS-Inversion')\n", 637 | "\n", 638 | "ax[0, 2].imshow(deblur_l2_reg, aspect='auto', cmap='gray')\n", 639 | "ax[0, 2].set_title('Regularized LS-Inversion')\n", 640 | "\n", 641 | "ax[1, 0].imshow(img_gauss_, aspect='auto', cmap='gray')\n", 642 | "ax[1, 0].set_title('Blurred Image')\n", 643 | "\n", 644 | "ax[1, 1].imshow(deblur_tv, aspect='auto', cmap='gray')\n", 645 | "ax[1, 1].set_title('TV-Inversion')\n", 646 | "\n", 647 | "ax[1, 2].imshow(deblur_fista, aspect='auto', cmap='gray')\n", 648 | "ax[1, 2].set_title('FISTA Inversion')\n", 649 | "fig.tight_layout()" 650 | ] 651 | }, 652 | { 653 | "cell_type": "markdown", 654 | "metadata": {}, 655 | "source": [ 656 | "# Image Deblurring - The effect of noise Noise\n", 657 | "\n", 658 | "We have so far discussed the blurring of images. In practical image processing, observed images are commonly polluted with noise. The nature of the noise is influenced by multiple sources, and mathematically can be linear, nonlinear, multiplicative, and additive. In this exercise, we consider common additive noise, which can be included in our linear model by a term perturbing the blurred image \n", 659 | "\n", 660 | "$$\n", 661 | "\\mathbf{B} = \\mathbf{B_{blur}} + \\mathbf{N}\n", 662 | "$$\n", 663 | "\n", 664 | "**EX: Adding noise.** Add Gaussian white noise to the blurred image with a given standard deviation and perform image deblurring to evaluate the performace of different solvers" 665 | ] 666 | }, 667 | { 668 | "cell_type": "code", 669 | "execution_count": null, 670 | "metadata": {}, 671 | "outputs": [], 672 | "source": [ 673 | "# %load -s Noisy_Inversion solutions/deblurring_sol.py" 674 | ] 675 | }, 676 | { 677 | "cell_type": "code", 678 | "execution_count": null, 679 | "metadata": { 680 | "scrolled": false 681 | }, 682 | "outputs": [], 683 | "source": [ 684 | "# Noisy_Inversion()" 685 | ] 686 | }, 687 | { 688 | "cell_type": "markdown", 689 | "metadata": {}, 690 | "source": [ 691 | "# Recap\n", 692 | "\n", 693 | "In this tutorial we have learned to:\n", 694 | "\n", 695 | "- Define a simple PyLops operator implementing a convolutional kernel for image processing \n", 696 | "- Use build-in PyLops operators for image manipulation including\n", 697 | "- Solve the image deblurring problem using different inverse problems schemes\n", 698 | "- Denoise a blurred image with the help of least-squares, TV-Regularization, and FISTA\n" 699 | ] 700 | }, 701 | { 702 | "cell_type": "code", 703 | "execution_count": null, 704 | "metadata": {}, 705 | "outputs": [], 706 | "source": [ 707 | "scooby.Report(core='pylops')" 708 | ] 709 | } 710 | ], 711 | "metadata": { 712 | "kernelspec": { 713 | "display_name": "Python 3", 714 | "language": "python", 715 | "name": "python3" 716 | }, 717 | "language_info": { 718 | "codemirror_mode": { 719 | "name": "ipython", 720 | "version": 3 721 | }, 722 | "file_extension": ".py", 723 | "mimetype": "text/x-python", 724 | "name": "python", 725 | "nbconvert_exporter": "python", 726 | "pygments_lexer": "ipython3", 727 | "version": "3.7.2" 728 | }, 729 | "toc": { 730 | "base_numbering": 1, 731 | "nav_menu": {}, 732 | "number_sections": true, 733 | "sideBar": true, 734 | "skip_h1_title": false, 735 | "title_cell": "Table of Contents", 736 | "title_sidebar": "Contents", 737 | "toc_cell": false, 738 | "toc_position": {}, 739 | "toc_section_display": true, 740 | "toc_window_display": false 741 | } 742 | }, 743 | "nbformat": 4, 744 | "nbformat_minor": 4 745 | } 746 | -------------------------------------------------------------------------------- /solutions_colab/Deblurring.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "metadata": {}, 6 | "source": [ 7 | "# Image deblurring\n", 8 | "\n", 9 | "**Authors: M. Ravasi, D. Vargas, I. Vasconcelos**" 10 | ] 11 | }, 12 | { 13 | "cell_type": "markdown", 14 | "metadata": {}, 15 | "source": [ 16 | "Welcome to the second session of our **Solving large-scale inverse problems in Python with PyLops** tutorial!\n", 17 | "\n", 18 | "Throughout this tutorial, we aim at developing the following aspects in image processing:\n", 19 | "\n", 20 | "- We connect with the concepts learned in the previous session by defining a simple PyLops operator implementing a convolutional kernel for image processing \n", 21 | "- Learn how to use build-in PyLops operators for image manipulation including, blurring, sharpening, and edge detection\n", 22 | "- Demonstrate the versatility of the linear operators when implemented in the solution of inverse problems, in particular, the image deblurring problem\n", 23 | "- Analyze the influence of noise in contrast with the performance of different solvers, least-squares, TV-Regularization, and FISTA" 24 | ] 25 | }, 26 | { 27 | "cell_type": "markdown", 28 | "metadata": {}, 29 | "source": [ 30 | "## Useful links\n", 31 | "\n", 32 | "- Tutorial Github repository: https://github.com/mrava87/pylops_pydata2020\n", 33 | " \n", 34 | "- PyLops Github repository: https://github.com/equinor/pylops\n", 35 | "\n", 36 | "- PyLops reference documentation: https://pylops.readthedocs.io/en/latest/" 37 | ] 38 | }, 39 | { 40 | "cell_type": "markdown", 41 | "metadata": {}, 42 | "source": [ 43 | "# Deblurring Images\n", 44 | "\n", 45 | "Compact digital cameras were introduced in the early seventies thanks to the development of CCD sensors allowing the recording and storage of images in digital format. The goal of an image process is to capture an accurate representation of a particular scene. However, any image is influenced by the optical system itself and the overall effect is a blurred image reconstruction, therefore, specialized algorithms performing deblurring need to be implemented in order to achieve a sharper image. Image deblurring is a method that aims at recovering the original sharp-image by removing effect caused by limited aperture, lens aberrations, defocus, and unintended motions. This can be done by defining a mathematical model of the blurring process with the idea of removing from the image the blurring effects.\n", 46 | "\n", 47 | "Traditionally, the main assumption is that the blurring process is linear. When this is the case, a 2-dimensional convolutional model describes the imaging problem. The blurred Image (output) is the result of a point-spread function acting on a target sharp image (input). Such process is described by the following transformation, \n", 48 | "\n", 49 | "\\begin{equation}\n", 50 | "g(x, y) = \\int_{-\\infty}^{\\infty}\\int_{-\\infty}^{\\infty} h(x-x_0,y-y_0) f(x_0, y_0) dx_0 dy_0.\n", 51 | "\\end{equation}\n", 52 | "\n", 53 | "This operation can be discretized as follows\n", 54 | "\n", 55 | "\\begin{equation}\n", 56 | "g[i,n] = \\sum_{j=-\\infty}^{\\infty} \\sum_{m=-\\infty}^{\\infty} h[i-j,n-m] f[j,m] \n", 57 | "\\end{equation}\n", 58 | "\n", 59 | "and also implemented in the frequency domain using the convolution theorem\n", 60 | "\n", 61 | "\\begin{equation}\n", 62 | "G(k_x, k_y) = H(k_x, k_y) F(k_x, k_y).\n", 63 | "\\end{equation}\n", 64 | "\n", 65 | "Here, the spectrum of the point-spread function $H(k_x, k_y) = \\mathscr{F} \\{h(x,y)\\}$ is called transfer function, similarly, $F(k_x, k_y) = \\mathscr{F} \\{f(x,y)\\}$ is the spectrum of the sharp image. The previous definition is an specific case of the more general problem $\\mathbf{y} = \\mathbf{A} \\mathbf{x}$ (*forward operation*), where $\\mathbf{A}:\\mathbb{F}^m \\to \\mathbb{F}^n$ is a linear operator that maps a vector of size $m$ in the *model space* to a vector of size $n$ in the *data space*, as explained in the previous session. The (*adjoint operation*) is $\\mathbf{x} = \\mathbf{A}^H \\mathbf{y}$. In this context, our deblurring operation is an inverse problem design to find a pseudoinverse, $\\mathbf{A}^{-1}$, that aims at removing the effect of the operator $\\mathbf{A}$ from the data $\\mathbf{y}$ to retrieve the model $\\mathbf{x}$, i.e., $\\hat{\\mathbf{x}} = \\mathbf{A}^{-1} \\mathbf{y}$. \n", 66 | "\n", 67 | "This Jupyter Notebook is intended to guide you through the main steps in the implementation of the blurring and deblurring process using the pylops.signalprocessing.Convolve2D operator, in this case, we assume to know the structure of the point-spread function for an specific problem.\n", 68 | "\n" 69 | ] 70 | }, 71 | { 72 | "cell_type": "code", 73 | "execution_count": null, 74 | "metadata": {}, 75 | "outputs": [], 76 | "source": [ 77 | "# Run this when using Colab (will install the missing libraries)\n", 78 | "# !pip install pylops scooby" 79 | ] 80 | }, 81 | { 82 | "cell_type": "code", 83 | "execution_count": null, 84 | "metadata": {}, 85 | "outputs": [], 86 | "source": [ 87 | "%matplotlib inline\n", 88 | "\n", 89 | "import numpy as np\n", 90 | "from skimage import data\n", 91 | "import matplotlib.pyplot as plt\n", 92 | "from matplotlib.colors import LogNorm\n", 93 | "from scipy.signal import convolve, correlate\n", 94 | "import scooby\n", 95 | "\n", 96 | "from pylops.optimization import leastsquares, sparsity\n", 97 | "from pylops.signalprocessing import Convolve2D\n", 98 | "from pylops import FirstDerivative, Laplacian\n", 99 | "from pylops import LinearOperator\n", 100 | "from pylops.utils import dottest" 101 | ] 102 | }, 103 | { 104 | "cell_type": "markdown", 105 | "metadata": {}, 106 | "source": [ 107 | "# A simple deconvolution problem\n", 108 | "\n", 109 | "The first problem we will solve with the aid of pylops is a simple one-dimensional convolution. We start by defining a pylops operator applying convolution with a given impulse response to a model vector. For simplicity, we select a *[ricker](https://en.wikipedia.org/wiki/Mexican_hat_wavelet)* wavelet and used it as a compact filter to be convolved with a 1D input signal. Assuming our input signal (model) is a time series constructed by superposition of three gaussian functions with different time shifts, the forward problem can be interpreted as a weighted sum of the function $f(\\tau)$ at the moment $t$ with weights given by $h$.\n", 110 | "\n", 111 | "The resulting signal (data), $g(t)$, is a 'blurred' shifted version of the input model mathematically computed by integration over time according to \n", 112 | "\n", 113 | "\\begin{equation}\n", 114 | "g(t) = \\int_{-\\infty}^{\\infty} h(t - \\tau) f(\\tau) d\\tau\n", 115 | "\\end{equation}\n", 116 | "\n", 117 | "Next, we solve the inverse problem (**deconvolution**) where we ask the question. What model vector would reproduce a given data set when convolved with a specific convolutional kernel?. Here we try to reconstruct an unknown model based on the given data (blue line in the figure) and impulse response (red line in the figure). The expected output of this operation should approximate our target model (black_line in the figure)\n", 118 | "\n", 119 | "\n", 120 | "**Pylops convolution operator:**\n", 121 | "A simple pylops operator is defined as a Child class that inherits from **[`pylops.LinearOperator`](https://pylops.readthedocs.io/en/latest/api/generated/pylops.LinearOperator.html#pylops.LinearOperator)**. We initilize the class by defining at least three atributes in the the class constructor; `shape`, `dtype`, and `explicit`. Next, we difine the forward mode operation through the method `_matvec`, and `_rmatvec` for the adjoint mode." 122 | ] 123 | }, 124 | { 125 | "cell_type": "code", 126 | "execution_count": null, 127 | "metadata": {}, 128 | "outputs": [], 129 | "source": [ 130 | "# Defining a convolution Pylops operator\n", 131 | "class my_convolve(LinearOperator):\n", 132 | " def __init__(self, A, dims, dtype=\"float64\"):\n", 133 | " self.A = A\n", 134 | " self.dims = dims\n", 135 | " self.shape = (np.prod(self.dims), np.prod(self.dims))\n", 136 | " self.dtype = np.dtype(dtype)\n", 137 | " self.explicit = False\n", 138 | "\n", 139 | " def _matvec(self, x):\n", 140 | " x = np.reshape(x, self.dims)\n", 141 | " y = convolve(x, self.A, mode='same')\n", 142 | " return np.ndarray.flatten(y)\n", 143 | "\n", 144 | " def _rmatvec(self, x):\n", 145 | " x = np.reshape(x, self.dims)\n", 146 | " y = correlate(x, self.A, mode='same')\n", 147 | " return np.ndarray.flatten(y)" 148 | ] 149 | }, 150 | { 151 | "cell_type": "code", 152 | "execution_count": null, 153 | "metadata": {}, 154 | "outputs": [], 155 | "source": [ 156 | "t_min, t_max, nt = -4.0, 4.0, 10000\n", 157 | "f_central = 15\n", 158 | "t_delay = 1\n", 159 | "t_delay_wav = np.array([0.0, 0.6, 1.2])[:, np.newaxis]\n", 160 | "\n", 161 | "# Time axis\n", 162 | "t = np.linspace(t_min, t_max, nt)\n", 163 | "sigma = 1 / (np.pi * f_central) ** 2\n", 164 | "\n", 165 | "# Model vector\n", 166 | "wav = np.exp(-((t - t_delay_wav) ** 2) / sigma)\n", 167 | "model_ = wav[0] - 1.5*wav[1] + wav[2]\n", 168 | "\n", 169 | "# Impulse response - Convolutional Operator\n", 170 | "impulse_response = (1 - (2 * (t - t_delay) ** 2) / sigma) * \\\n", 171 | " np.exp(-((t - t_delay) ** 2) / sigma)\n", 172 | "\n", 173 | "# Data vector\n", 174 | "R_op = my_convolve(A=impulse_response, dims=(nt))\n", 175 | "data_ = R_op * model_\n", 176 | "\n", 177 | "\n", 178 | "\n", 179 | "# Model reconstruction - Inverse problem\n", 180 | "model_reconstructed = sparsity.FISTA(Op=R_op,\n", 181 | " data=data_,\n", 182 | " eps=1e-1,\n", 183 | " niter=200)[0]\n", 184 | "\n", 185 | "# Plotting input vectors\n", 186 | "fig, ax = plt.subplots(1, 3, figsize=[10, 3], facecolor='w', edgecolor='k')\n", 187 | "ax[0].plot(t, impulse_response, 'r', lw=1.5)\n", 188 | "ax[0].set_xlabel('Time - (sec)')\n", 189 | "ax[0].set_ylabel('Amplitude')\n", 190 | "ax[0].set_xlim([0.8, 1.2])\n", 191 | "ax[0].set_title(\"Operator\")\n", 192 | "\n", 193 | "ax[1].plot(t, model_, 'k', lw=1.5)\n", 194 | "ax[1].set_xlabel('Time - (sec)')\n", 195 | "ax[1].set_xlim([-0.1, 2.5])\n", 196 | "ax[1].set_title(\"Model\")\n", 197 | "\n", 198 | "ax[2].plot(t, data_, 'b', lw=1.5)\n", 199 | "ax[2].set_xlabel('Time - (sec)')\n", 200 | "ax[2].set_xlim([-0.1, 2.5])\n", 201 | "ax[2].set_title(\"Data\")\n", 202 | "fig.tight_layout()\n", 203 | "\n", 204 | "\n", 205 | "# Plotting Model reconstruction vectors\n", 206 | "fig = plt.figure(figsize=[4, 3], facecolor='w', edgecolor='k')\n", 207 | "ax = fig.add_subplot(1, 1, 1)\n", 208 | "ax.plot(t, model_, 'k', lw=3, label='Target Model')\n", 209 | "ax.plot(t, model_reconstructed, '--g', lw=2, label='Reconstruction')\n", 210 | "ax.set_ylabel('Amplitude')\n", 211 | "ax.set_xlabel('Time - (sec)')\n", 212 | "ax.set_xlim([-0.1, 1.5])\n", 213 | "ax.set_title(\"Model Reconstruction\")\n", 214 | "ax.legend()" 215 | ] 216 | }, 217 | { 218 | "cell_type": "code", 219 | "execution_count": null, 220 | "metadata": {}, 221 | "outputs": [], 222 | "source": [ 223 | "R_op" 224 | ] 225 | }, 226 | { 227 | "cell_type": "markdown", 228 | "metadata": {}, 229 | "source": [ 230 | "# Image filtereing\n", 231 | "\n", 232 | "PyLops comes with multiple build-in operators ready to use. In this section, we are using `pylops.signalprocessing.Convolve2D` to perform convolutions. First, we need to define a compact filter to construct the actual operator and then apply it to a pre-loaded image. Before we do that, let's first see if under the given parameters this linear operator passes the dot test. Once we make sure that this is the case, we can compute the convolution using NumPy matrix-like syntax. \n", 233 | "\n", 234 | "In the cell below, we select the most simple point spread function, the identity kernel. It should not blur the image and the error of the output compared to the input should be very low (to machine precession)\n", 235 | "\n", 236 | "Let's start by uploading an image. Here we use the **scikit-image** module skimage.data containing standard test images. The very first line selects the image of your preference to be used in the next cells, feel free to play around, and even upload your own pictures. " 237 | ] 238 | }, 239 | { 240 | "cell_type": "code", 241 | "execution_count": null, 242 | "metadata": {}, 243 | "outputs": [], 244 | "source": [ 245 | "image = data.camera() # Load image from scikit-image\n", 246 | "# image = data.binary_blobs()\n", 247 | "# image = data.checkerboard()\n", 248 | "ny, nx = image.shape\n", 249 | "\n", 250 | "# Identity kernel\n", 251 | "kernel = np.zeros([3, 3])\n", 252 | "kernel[1, 1] = 1\n", 253 | "\n", 254 | "# Define the PyLops blurring Operator\n", 255 | "C_op = Convolve2D(N=ny*nx,\n", 256 | " h=kernel,\n", 257 | " offset=(kernel.shape[0]//2, kernel.shape[1]//2),\n", 258 | " dims=(ny, nx))\n", 259 | "\n", 260 | "# Dot test\n", 261 | "dottest(C_op, ny*nx, ny*nx, verb=True)\n", 262 | "\n", 263 | "\n", 264 | "img_blur = C_op * image.flatten()\n", 265 | "img_blur = img_blur.reshape(image.shape)\n", 266 | "\n", 267 | "\n", 268 | "# PLOTTING\n", 269 | "fig, ax = plt.subplots(1, 3, figsize=(15, 5))\n", 270 | "ax[0].imshow(image, cmap='gray')\n", 271 | "ax[0].set_title('Input image')\n", 272 | "\n", 273 | "ax[1].imshow(img_blur, cmap='gray')\n", 274 | "ax[1].set_title('Response to identity')\n", 275 | "\n", 276 | "him = ax[2].imshow(img_blur-image, cmap='gray')\n", 277 | "ax[2].set_title('Error output-input')\n", 278 | "fig.colorbar(him, ax=ax[2])" 279 | ] 280 | }, 281 | { 282 | "cell_type": "code", 283 | "execution_count": null, 284 | "metadata": {}, 285 | "outputs": [], 286 | "source": [ 287 | "print(\"image.shape {}\".format(image.shape))\n", 288 | "C_op" 289 | ] 290 | }, 291 | { 292 | "cell_type": "markdown", 293 | "metadata": {}, 294 | "source": [ 295 | "**EX: Write your own kernel.** A very common practice in digital image processing consists of applying filters as a way to highlight specific image features. One of the most useful is known as unsharp mask and is used attenuate low frequencies features while enhancing small contrast changes. A very simple version of this sharpening filter is given by the matrix kernel,\n", 296 | "\n", 297 | "$$ \n", 298 | "\\mathbf{A} = \n", 299 | "\\begin{bmatrix}\n", 300 | " 0 & -1 & 0\\\\\n", 301 | " -1 & 5 & -1\\\\\n", 302 | " 0 & -1 & 0\n", 303 | "\\end{bmatrix} \n", 304 | "$$\n", 305 | "\n", 306 | "Define a pylops operator for this mask and apply it to your preferred image. A code snippet for the plotting the input/output images is given below " 307 | ] 308 | }, 309 | { 310 | "cell_type": "code", 311 | "execution_count": null, 312 | "metadata": {}, 313 | "outputs": [], 314 | "source": [ 315 | "def Unsharp_Mask():\n", 316 | " \"\"\"Unsharp Mask operator\n", 317 | " \"\"\"\n", 318 | " # load image from scikit-image\n", 319 | " image = data.microaneurysms()\n", 320 | " ny, nx = image.shape\n", 321 | "\n", 322 | " # Define matrix kernel\n", 323 | " kernel_unsharp = np.array([[0, -1, 0],\n", 324 | " [-1, 5, -1],\n", 325 | " [0, -1, 0]])\n", 326 | "\n", 327 | " # PyLops Operator\n", 328 | " unsharp_op = Convolve2D(N=ny*nx,\n", 329 | " h=kernel_unsharp,\n", 330 | " offset=(\n", 331 | " kernel_unsharp.shape[0]//2, kernel_unsharp.shape[1]//2),\n", 332 | " dims=(ny, nx))\n", 333 | "\n", 334 | " img_unsharp = unsharp_op * image.flatten()\n", 335 | " img_unsharp = img_unsharp.reshape(image.shape)\n", 336 | "\n", 337 | " # PLOTTING\n", 338 | " fig, ax = plt.subplots(1, 3, figsize=(15, 5))\n", 339 | "\n", 340 | " ax[0].imshow(image, cmap='gray')\n", 341 | " ax[0].set_title('Image')\n", 342 | "\n", 343 | " ax[1].imshow(kernel_unsharp, cmap='gray')\n", 344 | " ax[1].set_title('Kernel')\n", 345 | "\n", 346 | " ax[2].imshow(img_unsharp, cmap='gray')\n", 347 | " ax[2].set_title('Blurred Image')\n" 348 | ] 349 | }, 350 | { 351 | "cell_type": "code", 352 | "execution_count": null, 353 | "metadata": {}, 354 | "outputs": [], 355 | "source": [ 356 | "Unsharp_Mask()" 357 | ] 358 | }, 359 | { 360 | "cell_type": "markdown", 361 | "metadata": {}, 362 | "source": [ 363 | "#### Edge detection - Sobel operator\n", 364 | "\n", 365 | "Edge detection is a process that tries to identify areas on an image where strong changes in intensity take place. A simple way of performing this task is by implementing a Sobel operator. This kind of mask approximates the directional derivates of an image along with directions $x$, and $y$. Vertical, $\\mathbf{G_x}$, and Horizontal, $\\mathbf{G_y}$, gradients are give by the 3x3 kernels\n", 366 | "\n", 367 | "\\begin{equation}\n", 368 | " \\mathbf{G_x}=\n", 369 | " \\begin{bmatrix}\n", 370 | " 1 & 2 & 1\\\\\n", 371 | " 0 & 0 & 0\\\\\n", 372 | " -1 & -2 & -1\n", 373 | " \\end{bmatrix} \n", 374 | " \\qquad\\text{,}\\qquad \n", 375 | " \\mathbf{G_y} =\n", 376 | " \\begin{bmatrix}\n", 377 | " 1 & 0 & -1\\\\\n", 378 | " 2 & 0 & -2\\\\\n", 379 | " 1 & 0 & -1\n", 380 | " \\end{bmatrix}\n", 381 | "\\end{equation}\n", 382 | "\n", 383 | "note that each mask is designed to detect either horizontal or vertical changes. A Combination of these gradient approximations provides the total gradient magnitude, $\\mathbf{G} = \\sqrt{\\mathbf{G_x}^2 + \\mathbf{G_x}^2}$, at an specific pixel of the image.\n", 384 | "\n", 385 | "As we did in the previous exercise, we now implement edge detection by defining vertical and horizontal gradient kernels, apply them to an image, and assemble the gradient magnitude." 386 | ] 387 | }, 388 | { 389 | "cell_type": "code", 390 | "execution_count": null, 391 | "metadata": {}, 392 | "outputs": [], 393 | "source": [ 394 | "# vertical edge detector\n", 395 | "kernel_v = np.array([[1, 0, -1],\n", 396 | " [2, 0, -2],\n", 397 | " [1, 0, -1]])\n", 398 | "\n", 399 | "# horizontal edge detector\n", 400 | "kernel_h = np.array([[1, 2, 1],\n", 401 | " [0, 0, 0],\n", 402 | " [-1, -2, -1]])\n", 403 | "\n", 404 | "# Define the PyLops blurring Operator\n", 405 | "Sobel_x_op = Convolve2D(N=ny*nx,\n", 406 | " h=kernel_h,\n", 407 | " offset=(kernel_h.shape[0]//2, kernel_h.shape[1]//2),\n", 408 | " dims=(ny, nx))\n", 409 | "Sobel_y_op = Convolve2D(N=ny*nx,\n", 410 | " h=kernel_v,\n", 411 | " offset=(kernel_v.shape[0]//2, kernel_v.shape[1]//2),\n", 412 | " dims=(ny, nx))\n", 413 | "\n", 414 | "Gx = Sobel_x_op * image.flatten()\n", 415 | "Gy = Sobel_y_op * image.flatten()\n", 416 | "Gx = Gx.reshape(image.shape)\n", 417 | "Gy = Gy.reshape(image.shape)\n", 418 | "\n", 419 | "# Gradient magnitude\n", 420 | "G = np.sqrt(Gy**2 + Gy**2)\n", 421 | "\n", 422 | "# PLOTTING\n", 423 | "fig, ax = plt.subplots(1, 3, figsize=(15, 5))\n", 424 | "\n", 425 | "ax[0].imshow(image, cmap='gray')\n", 426 | "ax[0].set_title('Image')\n", 427 | "\n", 428 | "ax[1].imshow(kernel_v, cmap='gray')\n", 429 | "ax[1].set_title('vertical Kernel')\n", 430 | "\n", 431 | "ax[2].imshow(G, cmap='gray')\n", 432 | "ax[2].set_title('Image edges')\n" 433 | ] 434 | }, 435 | { 436 | "cell_type": "markdown", 437 | "metadata": {}, 438 | "source": [ 439 | "# Image Blurring\n", 440 | "\n", 441 | "The Gaussian filter is an operator used to suppress noise and reduce the contrast of an image. In this case, an image is rendered smooth under the action of a Gaussian mask by removing the high-frequency components. Therefore, we can understand a Gaussian blur as a low-pass filter, attenuating the high-frequency components in an image. Similarly, the maximum value of the weighted sum in the convolution located around the center of the distribution decreases as a function of distance. Mathematically the two-dimensional Gaussian function is defined as\n", 442 | "\n", 443 | "\\begin{equation}\n", 444 | "G(x,y) = \\frac{1}{2 \\pi \\sigma ^{2}} e^{- \\frac{x^{2} + y^{2}}{2 \\sigma ^{2}}}\n", 445 | "\\label{2dgaussian}\n", 446 | "\\end{equation}\n", 447 | "\n", 448 | "where the standard deviation of the kernel is given for each axis and determines the effective extension of the filter.\n", 449 | "\n", 450 | "In the following cell, we will prepare a data vector represented by a Gaussian-blurred image as preparation for the next section, where we will use the smoothed image together with the given kernel and try to remove the blurring effects by solving a deconvolution problem " 451 | ] 452 | }, 453 | { 454 | "cell_type": "code", 455 | "execution_count": null, 456 | "metadata": {}, 457 | "outputs": [], 458 | "source": [ 459 | "# Play around with this parameters\n", 460 | "mx, my = 25, 15 # 25, 15\n", 461 | "sigma_x, sigma_y = 4.5, 2.5 # 4.5, 2.5\n", 462 | "\n", 463 | "x = np.linspace(-mx//2, mx//2, mx)\n", 464 | "y = np.linspace(-my//2, my//2, my)[..., None]\n", 465 | "kernel_gauss = 1/(2*np.pi*sigma_x*sigma_y) * \\\n", 466 | " np.exp(-(x**2/(2*sigma_x**2) + y**2/(2*sigma_y**2)))\n", 467 | "\n", 468 | "# Define the PyLops blurring Operator\n", 469 | "Gauss_op = Convolve2D(N=ny*nx,\n", 470 | " h=kernel_gauss,\n", 471 | " offset=(kernel_gauss.shape[0]//2,\n", 472 | " kernel_gauss.shape[1]//2),\n", 473 | " dims=(ny, nx))\n", 474 | "\n", 475 | "img_gauss = Gauss_op * image.flatten()\n", 476 | "img_gauss_ = img_gauss.reshape(image.shape)\n", 477 | "\n", 478 | "# image spectrum\n", 479 | "image_shift = np.fft.fftshift(np.fft.fft2(image))\n", 480 | "image_spectrum = np.abs(image_shift)\n", 481 | "\n", 482 | "# PSF spectrum - Transfer function\n", 483 | "kernel_gauss_shift = np.fft.fftshift(np.fft.fft2(kernel_gauss))\n", 484 | "kernel_gauss_spectrum = np.abs(kernel_gauss_shift)\n", 485 | "\n", 486 | "# blurred-image spectrum\n", 487 | "img_gauss_shift = np.fft.fftshift(np.fft.fft2(img_gauss_))\n", 488 | "img_gauss_spectrum = np.abs(img_gauss_shift)\n", 489 | "\n", 490 | "# PLOTTING\n", 491 | "fig, ax = plt.subplots(2, 3, figsize=(9, 6))\n", 492 | "\n", 493 | "ax[0, 0].imshow(image, cmap='gray')\n", 494 | "ax[0, 0].set_title('Image')\n", 495 | "\n", 496 | "ax[0, 1].imshow(kernel_gauss, cmap='gray')\n", 497 | "ax[0, 1].set_title('Gaussian Kernel')\n", 498 | "\n", 499 | "him2 = ax[0, 2].imshow(img_gauss_, cmap='gray')\n", 500 | "ax[0, 2].set_title('Blurred Image')\n", 501 | "\n", 502 | "ax[1, 0].imshow(image_spectrum, cmap='gray', norm=LogNorm(vmin=10))\n", 503 | "ax[1, 0].set_title('Image Spectrum')\n", 504 | "\n", 505 | "ax[1, 1].imshow(kernel_gauss_spectrum, cmap='gray')\n", 506 | "ax[1, 1].set_title('Gaussian Kernel Spectrum')\n", 507 | "\n", 508 | "him2 = ax[1, 2].imshow(img_gauss_spectrum, cmap='gray', norm=LogNorm(vmin=10))\n", 509 | "ax[1, 2].set_title('Blurred Image Spectrum')\n", 510 | "fig.tight_layout()" 511 | ] 512 | }, 513 | { 514 | "cell_type": "markdown", 515 | "metadata": {}, 516 | "source": [ 517 | "# Image Deblurring \n", 518 | "\n", 519 | "Finally, we consider a standard deblurring case. Here we assume a picture was taken in real life; therefore, a whole set of potential artifacts are present in it. Our mission is to try and reconstruct a shaper image representation of the scene by removing the blurring effects. Luckily, we assume to have some information on the optical system and decided to implement an inverse problem. Depending on the application, different solvers are available, below we will compare the performance of some well-known algorithms to deblur the image and compare it with its original version.\n", 520 | "\n", 521 | "- least squares inversion\n", 522 | "- least squares - regularized inversion \n", 523 | "- TV inversion\n", 524 | "- FISTA - Fast Iterative Shrinkage-Thresholding Algorithm\n", 525 | "\n", 526 | "A set of ready to use solvers are implemented in the PyLops module `pylops.optimization.leastsquares` and `pylops.optimization.sparsity`." 527 | ] 528 | }, 529 | { 530 | "cell_type": "markdown", 531 | "metadata": {}, 532 | "source": [ 533 | "**least squares inversion**\n", 534 | "\\begin{equation}\n", 535 | "J= ||\\mathbf{d} - \\mathbf{A} \\mathbf{m}||_2^2\n", 536 | "\\end{equation}" 537 | ] 538 | }, 539 | { 540 | "cell_type": "code", 541 | "execution_count": null, 542 | "metadata": {}, 543 | "outputs": [], 544 | "source": [ 545 | "# least squares inversion\n", 546 | "deblur_l2 = leastsquares.NormalEquationsInversion(Op=Gauss_op,\n", 547 | " Regs=None,\n", 548 | " data=img_gauss,\n", 549 | " maxiter=40)\n", 550 | "# Reshape image\n", 551 | "deblur_l2 = deblur_l2.reshape(image.shape)" 552 | ] 553 | }, 554 | { 555 | "cell_type": "markdown", 556 | "metadata": {}, 557 | "source": [ 558 | "**least squares - regularized inversion**\n", 559 | "\\begin{equation}\n", 560 | "J= ||\\mathbf{d} - \\mathbf{A} \\mathbf{m}||_2^2 + ||\\nabla \\mathbf{m}||_2^2\n", 561 | "\\end{equation}" 562 | ] 563 | }, 564 | { 565 | "cell_type": "code", 566 | "execution_count": null, 567 | "metadata": {}, 568 | "outputs": [], 569 | "source": [ 570 | "# L2 regularization term - Second derivative\n", 571 | "D2op = Laplacian(dims=(ny, nx), dtype=np.float)\n", 572 | "\n", 573 | "# least squares - regularized inversion\n", 574 | "deblur_l2_reg = leastsquares.RegularizedInversion(Op=Gauss_op,\n", 575 | " Regs=[D2op],\n", 576 | " data=img_gauss,\n", 577 | " epsRs=[1e0],\n", 578 | " show=True,\n", 579 | " **dict(iter_lim=20))\n", 580 | "# Reshape image\n", 581 | "deblur_l2_reg = np.real(deblur_l2_reg.reshape(image.shape))" 582 | ] 583 | }, 584 | { 585 | "cell_type": "markdown", 586 | "metadata": {}, 587 | "source": [ 588 | "**TV inversion**\n", 589 | "\\begin{equation}\n", 590 | "J= ||\\mathbf{d} - \\mathbf{A} \\mathbf{m}||_2^2 + ||\\mathbf{D}_x \\mathbf{m}||_1 + ||\\mathbf{D}_y \\mathbf{m}||_1\n", 591 | "\\end{equation}" 592 | ] 593 | }, 594 | { 595 | "cell_type": "code", 596 | "execution_count": null, 597 | "metadata": {}, 598 | "outputs": [], 599 | "source": [ 600 | "# L1 regularization term - First derivative\n", 601 | "Dop = [FirstDerivative(ny * nx, dims=(ny, nx), dir=0),\n", 602 | " FirstDerivative(ny * nx, dims=(ny, nx), dir=1)]\n", 603 | "\n", 604 | "# TV inversion\n", 605 | "deblur_tv = sparsity.SplitBregman(Op=Gauss_op,\n", 606 | " RegsL1=Dop,\n", 607 | " data=img_gauss,\n", 608 | " niter_outer=10,\n", 609 | " niter_inner=5,\n", 610 | " mu=1.8,\n", 611 | " epsRL1s=[1e-1, 1e-1],\n", 612 | " tol=1e-4,\n", 613 | " tau=1.,\n", 614 | " ** dict(iter_lim=5, damp=1e-4, show=True))[0]\n", 615 | "# Reshape image\n", 616 | "deblur_tv = deblur_tv.reshape(image.shape)" 617 | ] 618 | }, 619 | { 620 | "cell_type": "markdown", 621 | "metadata": {}, 622 | "source": [ 623 | "**FISTA inversion**\n", 624 | "\\begin{equation}\n", 625 | "J= ||\\mathbf{d} - \\mathbf{A} \\mathbf{m}||_2^2 + \\epsilon||\\mathbf{m}||_p\n", 626 | "\\end{equation}" 627 | ] 628 | }, 629 | { 630 | "cell_type": "code", 631 | "execution_count": null, 632 | "metadata": {}, 633 | "outputs": [], 634 | "source": [ 635 | "# FISTA inversion\n", 636 | "deblur_fista = sparsity.FISTA(Op=Gauss_op,\n", 637 | " data=img_gauss,\n", 638 | " eps=1e-1,\n", 639 | " niter=100,\n", 640 | " show=True)[0]\n", 641 | "# Reshape image\n", 642 | "deblur_fista = deblur_fista.reshape(image.shape)" 643 | ] 644 | }, 645 | { 646 | "cell_type": "code", 647 | "execution_count": null, 648 | "metadata": {}, 649 | "outputs": [], 650 | "source": [ 651 | "# PLOTTING\n", 652 | "fig, ax = plt.subplots(2, 3, sharex=True, sharey=True, figsize=(12, 8))\n", 653 | "\n", 654 | "ax[0, 0].imshow(image, aspect='auto', cmap='gray')\n", 655 | "ax[0, 0].set_title('Original Image')\n", 656 | "\n", 657 | "ax[0, 1].imshow(deblur_l2, aspect='auto', cmap='gray')\n", 658 | "ax[0, 1].set_title('LS-Inversion')\n", 659 | "\n", 660 | "ax[0, 2].imshow(deblur_l2_reg, aspect='auto', cmap='gray')\n", 661 | "ax[0, 2].set_title('Regularized LS-Inversion')\n", 662 | "\n", 663 | "ax[1, 0].imshow(img_gauss_, aspect='auto', cmap='gray')\n", 664 | "ax[1, 0].set_title('Blurred Image')\n", 665 | "\n", 666 | "ax[1, 1].imshow(deblur_tv, aspect='auto', cmap='gray')\n", 667 | "ax[1, 1].set_title('TV-Inversion')\n", 668 | "\n", 669 | "ax[1, 2].imshow(deblur_fista, aspect='auto', cmap='gray')\n", 670 | "ax[1, 2].set_title('FISTA Inversion')\n", 671 | "fig.tight_layout()" 672 | ] 673 | }, 674 | { 675 | "cell_type": "markdown", 676 | "metadata": {}, 677 | "source": [ 678 | "# Image Deblurring - The effect of noise Noise\n", 679 | "\n", 680 | "We have so far discussed the blurring of images. In practical image processing, observed images are commonly polluted with noise. The nature of the noise is influenced by multiple sources, and mathematically can be linear, nonlinear, multiplicative, and additive. In this exercise, we consider common additive noise, which can be included in our linear model by a term perturbing the blurred image \n", 681 | "\n", 682 | "$$\n", 683 | "\\mathbf{B} = \\mathbf{B_{blur}} + \\mathbf{N}\n", 684 | "$$\n", 685 | "\n", 686 | "**EX: Adding noise.** Add Gaussian white noise to the blurred image with a given standard deviation and perform image deblurring to evaluate the performace of different solvers" 687 | ] 688 | }, 689 | { 690 | "cell_type": "code", 691 | "execution_count": null, 692 | "metadata": {}, 693 | "outputs": [], 694 | "source": [ 695 | "def Noisy_Inversion():\n", 696 | " \"\"\"Solve Inverse problems for a noisy image\n", 697 | " \"\"\"\n", 698 | " scale = 5.8\n", 699 | " noise = np.random.normal(0, scale, img_gauss_.shape)\n", 700 | " img_gauss_noisy_ = img_gauss_ + noise\n", 701 | " img_gauss_noisy = img_gauss_noisy_.flatten()\n", 702 | "\n", 703 | "\n", 704 | " # PLOTTING\n", 705 | " fig, ax = plt.subplots(1, 3, figsize=(15, 5))\n", 706 | "\n", 707 | " ax[0].imshow(img_gauss_, cmap='gray')\n", 708 | " ax[0].set_title('Blurred Image')\n", 709 | "\n", 710 | " ax[1].imshow(img_gauss_noisy_, cmap='gray')\n", 711 | " ax[1].set_title('Blurred Image + Noise')\n", 712 | "\n", 713 | " him2 = ax[2].imshow(img_gauss_noisy_ - img_gauss_, cmap='gray')\n", 714 | " ax[2].set_title('Noise level')\n", 715 | " plt.show()\n", 716 | " \n", 717 | " \n", 718 | " # least squares inversion\n", 719 | " deblur_l2 = leastsquares.NormalEquationsInversion(Op=Gauss_op,\n", 720 | " Regs=None,\n", 721 | " data=img_gauss_noisy,\n", 722 | " maxiter=40)\n", 723 | "\n", 724 | " # least squares - regularized inversion\n", 725 | " deblur_l2_reg = leastsquares.RegularizedInversion(Op=Gauss_op,\n", 726 | " Regs=[D2op],\n", 727 | " data=img_gauss_noisy,\n", 728 | " epsRs=[1e0],\n", 729 | " show=True,\n", 730 | " **dict(iter_lim=20))\n", 731 | "\n", 732 | " # TV inversion\n", 733 | " deblur_tv = sparsity.SplitBregman(Op=Gauss_op,\n", 734 | " RegsL1=Dop,\n", 735 | " data=img_gauss_noisy,\n", 736 | " niter_outer=10,\n", 737 | " niter_inner=2, \n", 738 | " mu=1.8,\n", 739 | " epsRL1s=[8e-1, 8e-1],\n", 740 | " tol=1e-4,\n", 741 | " tau=1.,\n", 742 | " ** dict(iter_lim=5, damp=1e-4, show=True))[0]\n", 743 | "\n", 744 | " # FISTA inversion\n", 745 | " deblur_fista = sparsity.FISTA(Op=Gauss_op,\n", 746 | " data=img_gauss_noisy,\n", 747 | " eps=1e-1,\n", 748 | " niter=100,\n", 749 | " show=True)[0]\n", 750 | "\n", 751 | " # Reshape images\n", 752 | " deblur_l2 = deblur_l2.reshape(image.shape)\n", 753 | " deblur_l2_reg = np.real(deblur_l2_reg.reshape(image.shape))\n", 754 | " deblur_tv = deblur_tv.reshape(image.shape)\n", 755 | " deblur_fista = deblur_fista.reshape(image.shape)\n", 756 | "\n", 757 | "\n", 758 | " # PLOTTING\n", 759 | " fig, ax = plt.subplots(2, 3, sharex=True, sharey=True, figsize=(12, 8))\n", 760 | "\n", 761 | " ax[0, 0].imshow(image, aspect='auto', cmap='gray')\n", 762 | " ax[0, 0].set_title('Original Image')\n", 763 | "\n", 764 | " ax[0, 1].imshow(deblur_l2, aspect='auto', cmap='gray')\n", 765 | " ax[0, 1].set_title('LS-Inversion')\n", 766 | "\n", 767 | " ax[0, 2].imshow(deblur_l2_reg, aspect='auto', cmap='gray')\n", 768 | " ax[0, 2].set_title('Regularized LS-Inversion')\n", 769 | "\n", 770 | " ax[1, 0].imshow(img_gauss_, aspect='auto', cmap='gray')\n", 771 | " ax[1, 0].set_title('Blurred Image')\n", 772 | "\n", 773 | " ax[1, 1].imshow(deblur_tv, aspect='auto', cmap='gray')\n", 774 | " ax[1, 1].set_title('TV-Inversion')\n", 775 | "\n", 776 | " ax[1, 2].imshow(deblur_fista, aspect='auto', cmap='gray')\n", 777 | " ax[1, 2].set_title('FISTA Inversion')\n", 778 | " fig.tight_layout()\n" 779 | ] 780 | }, 781 | { 782 | "cell_type": "code", 783 | "execution_count": null, 784 | "metadata": { 785 | "scrolled": false 786 | }, 787 | "outputs": [], 788 | "source": [ 789 | "Noisy_Inversion()" 790 | ] 791 | }, 792 | { 793 | "cell_type": "markdown", 794 | "metadata": {}, 795 | "source": [ 796 | "# Recap\n", 797 | "\n", 798 | "In this tutorial we have learned to:\n", 799 | "\n", 800 | "- Define a simple PyLops operator implementing a convolutional kernel for image processing \n", 801 | "- Use build-in PyLops operators for image manipulation including\n", 802 | "- Solve the image deblurring problem using different inverse problems schemes\n", 803 | "- Denoise a blurred image with the help of least-squares, TV-Regularization, and FISTA\n" 804 | ] 805 | }, 806 | { 807 | "cell_type": "code", 808 | "execution_count": null, 809 | "metadata": {}, 810 | "outputs": [], 811 | "source": [ 812 | "scooby.Report(core='pylops')" 813 | ] 814 | } 815 | ], 816 | "metadata": { 817 | "kernelspec": { 818 | "display_name": "Python 3", 819 | "language": "python", 820 | "name": "python3" 821 | }, 822 | "language_info": { 823 | "codemirror_mode": { 824 | "name": "ipython", 825 | "version": 3 826 | }, 827 | "file_extension": ".py", 828 | "mimetype": "text/x-python", 829 | "name": "python", 830 | "nbconvert_exporter": "python", 831 | "pygments_lexer": "ipython3", 832 | "version": "3.7.2" 833 | }, 834 | "toc": { 835 | "base_numbering": 1, 836 | "nav_menu": {}, 837 | "number_sections": true, 838 | "sideBar": true, 839 | "skip_h1_title": false, 840 | "title_cell": "Table of Contents", 841 | "title_sidebar": "Contents", 842 | "toc_cell": false, 843 | "toc_position": {}, 844 | "toc_section_display": true, 845 | "toc_window_display": false 846 | } 847 | }, 848 | "nbformat": 4, 849 | "nbformat_minor": 4 850 | } 851 | -------------------------------------------------------------------------------- /solutions_colab/Radon.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "metadata": {}, 6 | "source": [ 7 | "# Image reconstruction from parallel projections\n", 8 | "\n", 9 | "**Authors: M. Ravasi, D. Vargas, I. Vasconcelos**" 10 | ] 11 | }, 12 | { 13 | "cell_type": "markdown", 14 | "metadata": {}, 15 | "source": [ 16 | "Welcome to the last notebook of our **Solving large-scale inverse problems in Python with PyLops** tutorial!\n", 17 | "\n", 18 | "The aim of this tutorial is to:\n", 19 | "\n", 20 | "- experiment with the PyLops framework to setup non-trivial linear operators\n", 21 | "- understand when and how to leverage third-party code in the creation of a PyLops operator\n", 22 | "- bring everything together in a (hopefully) familiar context - that of reconstruction of an image from a limited set of projections (aka solving the *Inverse Radon transform*)" 23 | ] 24 | }, 25 | { 26 | "cell_type": "markdown", 27 | "metadata": {}, 28 | "source": [ 29 | "## Setting the scene\n", 30 | "\n", 31 | "The problem of reconstructing an image from a limited set of projections is well-known problem in a variety of disciplines. Here we will specifically focus on *CT image reconstruction with parallel beam projections*.\n", 32 | "\n", 33 | "Let's start from the **Fourier Slice Theorem** (Source: wikipedia https://en.wikipedia.org/wiki/Projection-slice_theorem):\n", 34 | "\n", 35 | "In words, given a two-dimensional function (e.g., an image), line projections and the image's Fourier spectrum are instrinsically related:\n", 36 | "\n", 37 | "- project a target image $\\mathbf{i}$ onto lines with given slopes $\\mathbf{p}$: this models the projection data that is input to the image reconstruction problem\n", 38 | "- apply a 1D Fourier transform of the image projection for each $\\mathbf{p}$\n", 39 | "- place said 1D Fourier-domain image projections along fixed sets of $\\mathbf{k}$ values onto the 2D Fourier plane, according to the orientation of the projection $\\mathbf{p}$\n", 40 | "- *or* apply a two-dimensional Fourier transform of the image (i.e., compute its wavenumber spectrum), and then extract a slice through its origin along the projection line.\n", 41 | "\n", 42 | "Based on the fact that physically we can only acquire projections $\\mathbf{p}$ - e.g., with a CT scanner - we can think about a couple of options of inverse problems for image reconstruction:\n", 43 | "\n", 44 | "1. Simply invert the projection operator $\\mathbf{P}$: $\\mathbf{p} = \\mathbf{P} \\mathbf{i}$\n", 45 | "1. Fourier transform the projections and place them at their right location in the wavenumber spectrum and then invert the restricted ($\\mathbf{R}$) two-dimensional Fourier transform ($\\mathbf{K}$): $\\mathbf{k} = \\mathbf{R} \\mathbf{K} \\mathbf{i}$\n", 46 | "\n", 47 | "To start, taking a quickly look at what *scikit-image* has to offer (https://scikit-image.org/docs/dev/auto_examples/transform/plot_radon_transform.html). We see that they provide a forward Radon and the classical Filtered Back Projection (FBP). \n", 48 | "\n", 49 | "- Can we use anything to create either of our forward modelling operators and then feed them to any of PyLops solvers?\n", 50 | "- What about the adjoint?" 51 | ] 52 | }, 53 | { 54 | "cell_type": "markdown", 55 | "metadata": {}, 56 | "source": [ 57 | "Let's first import the libraries we need in this tutorial" 58 | ] 59 | }, 60 | { 61 | "cell_type": "code", 62 | "execution_count": null, 63 | "metadata": {}, 64 | "outputs": [], 65 | "source": [ 66 | "# Run this when using Colab (will install the missing libraries)\n", 67 | "# !pip install pylops scooby" 68 | ] 69 | }, 70 | { 71 | "cell_type": "code", 72 | "execution_count": null, 73 | "metadata": {}, 74 | "outputs": [], 75 | "source": [ 76 | "%matplotlib inline\n", 77 | "\n", 78 | "import warnings\n", 79 | "warnings.filterwarnings('ignore')\n", 80 | "\n", 81 | "import numpy as np\n", 82 | "import matplotlib.pyplot as plt\n", 83 | "import pylops\n", 84 | "import scooby\n", 85 | "\n", 86 | "from skimage.data import shepp_logan_phantom\n", 87 | "from skimage.transform import radon, iradon\n", 88 | "\n", 89 | "from pylops import FunctionOperator\n", 90 | "from pylops.utils import dottest" 91 | ] 92 | }, 93 | { 94 | "cell_type": "markdown", 95 | "metadata": {}, 96 | "source": [ 97 | "We also import the famous Shepp Logan image - a well-known benchmark for this problem - which we will use in this tutorial.\n", 98 | "\n", 99 | "Notice that we pad the Shepp Logan phantom image with zeros. We will come back to this later on in the notebook - let's not care about it for the moment." 100 | ] 101 | }, 102 | { 103 | "cell_type": "code", 104 | "execution_count": null, 105 | "metadata": {}, 106 | "outputs": [], 107 | "source": [ 108 | "# Import the benchmark Shepp-Logan\n", 109 | "image = shepp_logan_phantom()[::2, ::2]\n", 110 | "\n", 111 | "# Zero padding\n", 112 | "pad = 50\n", 113 | "image = np.pad(image, ((pad, pad), (pad, pad)), mode='constant')\n", 114 | "\n", 115 | "plt.figure(figsize=(6, 6))\n", 116 | "plt.imshow(image, cmap='gray')\n", 117 | "plt.title('Shepp Logan Phantom')\n", 118 | "plt.axis('tight');" 119 | ] 120 | }, 121 | { 122 | "cell_type": "markdown", 123 | "metadata": {}, 124 | "source": [ 125 | "## scikit-image Radon" 126 | ] 127 | }, 128 | { 129 | "cell_type": "markdown", 130 | "metadata": {}, 131 | "source": [ 132 | "It turns out scikit-image has off-the-shelf tools for this problem...\n", 133 | "\n", 134 | "Let's with scikit-image's ``radon`` function to see what we can do with it and also to set up a benchmark calculation." 135 | ] 136 | }, 137 | { 138 | "cell_type": "code", 139 | "execution_count": null, 140 | "metadata": {}, 141 | "outputs": [], 142 | "source": [ 143 | "# Create a sampling of projection angles\n", 144 | "thetamin, thetamax, dtheta = 0, 180, 3 \n", 145 | "thetas = np.arange(thetamin, thetamax, dtheta)\n", 146 | "\n", 147 | "# Calculate image projections\n", 148 | "projection = radon(image, theta=thetas, circle=False, preserve_range=False)\n", 149 | "\n", 150 | "fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(12, 6))\n", 151 | "ax1.imshow(image, cmap='gray')\n", 152 | "ax1.set_title(r\"Input $\\mathbf{i}$\")\n", 153 | "ax1.axis('tight')\n", 154 | "ax2.imshow(projection, cmap='gray', extent=(0, 180, 0, projection.shape[0]))\n", 155 | "ax2.set_title(r\"Radon transform $\\mathbf{p}$\")\n", 156 | "ax2.set_xlabel(\"Projection angle (deg)\")\n", 157 | "ax2.axis('tight');" 158 | ] 159 | }, 160 | { 161 | "cell_type": "markdown", 162 | "metadata": {}, 163 | "source": [ 164 | "Ok, so going back to our projection concept, each of the Radon transform values at fixed projection angles is a discrete, separate projection at a fixed $\\mathbf{p}$ - here projection angles are with respect to the horizontal axis of the image. \n", 165 | "\n", 166 | "Great - now we have projection data! We can the try to reconstruct our original image using ``iradon`` - scikit-images' baseline image-reconstruction routine - that is based on the well-known Filtered Back Projection (FBP) algorithm. To quatify the accuracy of this image estimate, we also compute the mean square error (MSE) of this reconstruction relative to the original image. " 167 | ] 168 | }, 169 | { 170 | "cell_type": "code", 171 | "execution_count": null, 172 | "metadata": {}, 173 | "outputs": [], 174 | "source": [ 175 | "# Image reconstruction with scikit-image's FBP\n", 176 | "image_fbp = iradon(projection, theta=thetas, circle=False)\n", 177 | "\n", 178 | "# Measure its MSE\n", 179 | "mse_fbp = np.linalg.norm(image_fbp[pad//2:-pad//2, pad//2:-pad//2] - image[pad//2:-pad//2, pad//2:-pad//2])\n", 180 | "\n", 181 | "fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(12, 6))\n", 182 | "ax1.imshow(image_fbp[pad//2:-pad//2, pad//2:-pad//2], cmap='gray', vmin=0, vmax=1)\n", 183 | "ax1.set_title(\"Reconstruction\\nFiltered back projection\")\n", 184 | "ax1.axis('tight')\n", 185 | "ax2.imshow(image_fbp[pad//2:-pad//2, pad//2:-pad//2] - image[pad//2:-pad//2, pad//2:-pad//2], \n", 186 | " cmap='gray', vmin=-0.2, vmax=0.2)\n", 187 | "ax2.set_title(\"Reconstruction error\\nFiltered back projection\")\n", 188 | "ax2.axis('tight');" 189 | ] 190 | }, 191 | { 192 | "cell_type": "markdown", 193 | "metadata": {}, 194 | "source": [ 195 | "Even if we do not know exactly what ``radon``-``iradon`` do (we just used them out-of-the-box), we wonder whether they form numerical forward-adjoint pairs, i.e., whether they pass a dot-product test... If they do, then ``radon``-``iradon`` would fit right in a PyLops operator!\n", 196 | "\n", 197 | "One way to go about this is simply to use ``radon``-``iradon`` within a Pylops operator and see what happens... Let's try to see how can approach this using the ``FunctionOperator`` functionality in PyLops (https://pylops.readthedocs.io/en/latest/api/generated/pylops.FunctionOperator.html#pylops.FunctionOperator). We can then use our ``dottest`` to understand if this is an actual forward-adjoint pair." 198 | ] 199 | }, 200 | { 201 | "cell_type": "code", 202 | "execution_count": null, 203 | "metadata": {}, 204 | "outputs": [], 205 | "source": [ 206 | "## NOTE: using Python lambda functions is a quick-and-easy way \n", 207 | "## to assign a function object to a variable.\n", 208 | "## We could have used def and function calls as alternative. \n", 209 | "\n", 210 | "# Forward function\n", 211 | "mvec = lambda x: radon(x.reshape(image.shape), theta=thetas, circle=False, preserve_range=False)\n", 212 | "\n", 213 | "# Adjoint function\n", 214 | "rmvec = lambda x: iradon(x.reshape(projection.shape), theta=thetas, circle=False, filter=None, \n", 215 | " interpolation='nearest')\n", 216 | "\n", 217 | "# Pass these to Pylop's FunctionOperator\n", 218 | "Fop = FunctionOperator(mvec, rmvec, projection.size, image.size)" 219 | ] 220 | }, 221 | { 222 | "cell_type": "markdown", 223 | "metadata": {}, 224 | "source": [ 225 | "So, now we have effectively **wrapped** the ``radon``-``iradon`` *functions* into the forward and adjoint *methods* of the Pylops operator `Fop`.\n", 226 | "\n", 227 | "**Note**: we use the filter-free version of ``iradon`` - because the filtering is not part of an explicit adjoint. \n", 228 | "\n", 229 | "Now, let's see what this does..." 230 | ] 231 | }, 232 | { 233 | "cell_type": "code", 234 | "execution_count": null, 235 | "metadata": {}, 236 | "outputs": [], 237 | "source": [ 238 | "# First, model projections used the forward method of Fop\n", 239 | "projection1 = Fop * image.ravel() # note: Pylops operators act on 'vector' objects\n", 240 | "projection1 = projection.reshape(projection.shape) # note: Pylops operators also output 'vector' objects\n", 241 | "\n", 242 | "# Then, apply the adjoint\n", 243 | "image_adj = Fop.H * projection1.ravel()\n", 244 | "image_adj = image_adj.reshape(image.shape)\n", 245 | "\n", 246 | "fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(12, 6))\n", 247 | "ax1.imshow(projection, cmap='gray', extent=(0, 180, 0, projection.shape[0]))\n", 248 | "ax1.set_title(r\"Radon transform $\\mathbf{p}$\")\n", 249 | "ax1.set_xlabel(\"Projection angle (deg)\")\n", 250 | "ax1.axis('tight')\n", 251 | "ax2.imshow(image_adj, cmap='gray')\n", 252 | "ax2.set_title(\"Adjoint reconstruction with iradon\")\n", 253 | "ax2.axis('tight');" 254 | ] 255 | }, 256 | { 257 | "cell_type": "markdown", 258 | "metadata": {}, 259 | "source": [ 260 | "Clearly, the ajoint-projection is different from the previous FBP result. \n", 261 | "\n", 262 | "It looks... OK? But, more importantly is it a **true adjoint**?" 263 | ] 264 | }, 265 | { 266 | "cell_type": "code", 267 | "execution_count": null, 268 | "metadata": {}, 269 | "outputs": [], 270 | "source": [ 271 | "dottest(Fop, Fop.shape[0], Fop.shape[1])" 272 | ] 273 | }, 274 | { 275 | "cell_type": "markdown", 276 | "metadata": {}, 277 | "source": [ 278 | "Unfortunately, as we suspected it, `iradon` does not behave as the true numerical adjoint of `radon`- even without any filter options.\n", 279 | "\n", 280 | "Nevertheless, this exercise is useful to see how we can quickly implement PyLops-compatible linear operators using ``FunctionOperator``, something that is very useful to test out ideas when prototyping in notebooks." 281 | ] 282 | }, 283 | { 284 | "cell_type": "markdown", 285 | "metadata": {}, 286 | "source": [ 287 | "## Radon with PyLops" 288 | ] 289 | }, 290 | { 291 | "cell_type": "markdown", 292 | "metadata": {}, 293 | "source": [ 294 | "Let's see if we can use any of PyLops operators to create an operator that mimics the ``radon`` of scikit-image. Provided this is possible, we would get automatically access to the adjoint of such an operator and can solve the inverse problem with any of our solvers (including those that allow adding sparsity to the solution).\n", 295 | "\n", 296 | "In pseudocode the projection operator (1 in figure above) is simply a rotation and summation:\n", 297 | "\n", 298 | "``\n", 299 | "for each theta\n", 300 | " rotate image by theta\n", 301 | " sum over horizontal axis\n", 302 | "``\n", 303 | "\n", 304 | "and its adjoint would be something like\n", 305 | "\n", 306 | "``\n", 307 | "for each theta\n", 308 | " spread over horizontal axis\n", 309 | " rotate image by -theta\n", 310 | "``\n", 311 | "\n", 312 | "Because rotating an image is equivalent to performing an interpolation from the original unrotated grid to a new rotated grid, we can use the PyLops ``Bilinear`` interpolation operator $\\mathbf{B}$ (https://pylops.readthedocs.io/en/latest/api/generated/pylops.signalprocessing.Bilinear.html). Then, summing over horizonal lines in the new rotated grid can be easily performed using the ``Sum`` operator $\\mathbf{S}$ (https://pylops.readthedocs.io/en/latest/api/generated/pylops.Sum.html). We will need to repeat it for a certain $N_\\theta$ angles: this is equivalent to vertically stacking the different $\\mathbf{B}$ operators at fixed $\\theta_i$. In mathematical terms our projection can be written as:\n", 313 | "\n", 314 | "$$\n", 315 | "\\mathbf{P}=\n", 316 | "\\begin{bmatrix}\n", 317 | " \\mathbf{S} \\mathbf{B}_{\\theta_0} \\\\\n", 318 | " \\mathbf{S} \\mathbf{B}_{\\theta_1} \\\\\n", 319 | " \\vdots \\\\\n", 320 | " \\mathbf{S} \\mathbf{B}_{\\theta_i} \\\\\n", 321 | " \\vdots \\\\\n", 322 | " \\mathbf{S} \\mathbf{B}_{\\theta_{N-1}} \n", 323 | "\\end{bmatrix}\n", 324 | "$$" 325 | ] 326 | }, 327 | { 328 | "cell_type": "markdown", 329 | "metadata": {}, 330 | "source": [ 331 | "First of all, let us see how we can perform one rotation using the `Bilinear` operator. As shown in figure, we can simply identify the coordinates of each grid point after a rotation and use bilinear interpolation to interpolate the original grid into this new grid. \n", 332 | "\n", 333 | "Moreover, to avoid some points to fall outside of the original grid we only perform the rotation for a portion of the grid inside the original one. This is the reason we originally extended our image dimensions - in this case by zero padding - so the image part of interest would always be sampled under a rotation operator. " 334 | ] 335 | }, 336 | { 337 | "cell_type": "code", 338 | "execution_count": null, 339 | "metadata": {}, 340 | "outputs": [], 341 | "source": [ 342 | "## Single image rotation illustration\n", 343 | "\n", 344 | "nx, ny = image.shape\n", 345 | "inner = 100\n", 346 | "\n", 347 | "# define axis for rotation via bilinear interpolation\n", 348 | "x0, y0 = nx//2, ny//2\n", 349 | "x = np.arange(nx - inner) - x0 + inner//2\n", 350 | "y = np.arange(ny - inner) - y0 + inner//2\n", 351 | "X, Y = np.meshgrid(x, y, indexing='ij')\n", 352 | "X, Y = X.ravel(), Y.ravel()\n", 353 | "XY = np.vstack((X, Y))\n", 354 | "\n", 355 | "# rotate\n", 356 | "theta = 45\n", 357 | "theta = np.deg2rad(theta)\n", 358 | "R = np.array([[np.cos(theta), -np.sin(theta)],\n", 359 | " [np.sin(theta), np.cos(theta)]])\n", 360 | "XYrot = R @ XY\n", 361 | "\n", 362 | "# recenter to positive axes\n", 363 | "XY[0] += x0\n", 364 | "XY[1] += y0\n", 365 | "\n", 366 | "XYrot[0] += x0\n", 367 | "XYrot[1] += y0\n", 368 | "\n", 369 | "plt.figure()\n", 370 | "plt.scatter(XY[0] , XY[1], c='r')\n", 371 | "plt.scatter(XYrot[0], XYrot[1], c='b', s=2)\n", 372 | "plt.axis('equal');" 373 | ] 374 | }, 375 | { 376 | "cell_type": "markdown", 377 | "metadata": {}, 378 | "source": [ 379 | "Create the rotation operator" 380 | ] 381 | }, 382 | { 383 | "cell_type": "code", 384 | "execution_count": null, 385 | "metadata": {}, 386 | "outputs": [], 387 | "source": [ 388 | "# Pylops operator\n", 389 | "Rop = pylops.signalprocessing.Bilinear(XYrot, dims=(nx, ny))" 390 | ] 391 | }, 392 | { 393 | "cell_type": "markdown", 394 | "metadata": {}, 395 | "source": [ 396 | "Rotate the image" 397 | ] 398 | }, 399 | { 400 | "cell_type": "code", 401 | "execution_count": null, 402 | "metadata": {}, 403 | "outputs": [], 404 | "source": [ 405 | "imagerot = Rop * image.ravel()\n", 406 | "imagerot = imagerot.reshape(nx-inner, ny-inner)\n", 407 | "\n", 408 | "fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(8, 4.5))\n", 409 | "ax1.imshow(image, cmap='gray', vmin=0, vmax=1)\n", 410 | "ax1.set_title(\"Image\")\n", 411 | "ax2.imshow(imagerot, cmap='gray', vmin=0, vmax=1)\n", 412 | "ax2.set_title(\"Rotated image\");" 413 | ] 414 | }, 415 | { 416 | "cell_type": "markdown", 417 | "metadata": {}, 418 | "source": [ 419 | "Let us now make an operator that applies $N_\\theta$ repeated rotations and sums for all angles" 420 | ] 421 | }, 422 | { 423 | "cell_type": "code", 424 | "execution_count": null, 425 | "metadata": {}, 426 | "outputs": [], 427 | "source": [ 428 | "def RadonRotate(dims, inner, thetas):\n", 429 | " # create original grid\n", 430 | " nx, ny = dims\n", 431 | " x0, y0 = nx//2, ny//2\n", 432 | " x = np.arange(nx - inner) - x0 + inner//2\n", 433 | " y = np.arange(ny - inner) - y0 + inner//2\n", 434 | " X, Y = np.meshgrid(x, y, indexing='ij')\n", 435 | " X, Y = X.ravel(), Y.ravel()\n", 436 | " XY = np.vstack((X, Y))\n", 437 | "\n", 438 | " thetas = np.deg2rad(thetas) # convert angles to radiants\n", 439 | " Rops = [] # to append operators at each angle\n", 440 | " for theta in thetas:\n", 441 | " # defined rotated coordinates\n", 442 | " R = np.array([[np.cos(theta), -np.sin(theta)],\n", 443 | " [np.sin(theta), np.cos(theta)]])\n", 444 | " XYrot = R @ XY\n", 445 | " XYrot[0] += x0\n", 446 | " XYrot[1] += y0\n", 447 | " \n", 448 | " # create S*B operator for current angle\n", 449 | " Rops.append(pylops.Sum(dims=(nx-inner, ny-inner), dir=0) * \n", 450 | " pylops.signalprocessing.Bilinear(XYrot, dims=(nx, ny)))\n", 451 | " # stack all operators together\n", 452 | " Radop = pylops.VStack(Rops)\n", 453 | " return Radop" 454 | ] 455 | }, 456 | { 457 | "cell_type": "markdown", 458 | "metadata": {}, 459 | "source": [ 460 | "We can now create our operator." 461 | ] 462 | }, 463 | { 464 | "cell_type": "code", 465 | "execution_count": null, 466 | "metadata": {}, 467 | "outputs": [], 468 | "source": [ 469 | "inner = 100\n", 470 | "Radop = RadonRotate((nx, ny), inner, thetas)\n", 471 | "print(Radop)" 472 | ] 473 | }, 474 | { 475 | "cell_type": "markdown", 476 | "metadata": {}, 477 | "source": [ 478 | "Let's now compare the projection computed before with scikit-image with our one" 479 | ] 480 | }, 481 | { 482 | "cell_type": "code", 483 | "execution_count": null, 484 | "metadata": {}, 485 | "outputs": [], 486 | "source": [ 487 | "projection1 = Radop * image.ravel()\n", 488 | "projection1 = projection1.reshape(len(thetas), ny-inner)" 489 | ] 490 | }, 491 | { 492 | "cell_type": "code", 493 | "execution_count": null, 494 | "metadata": {}, 495 | "outputs": [], 496 | "source": [ 497 | "fig, (ax1, ax2, ax3) = plt.subplots(1, 3, figsize=(17, 6))\n", 498 | "ax1.imshow(projection1.T, cmap='gray')\n", 499 | "ax1.set_title(\"PyLops forward radon\")\n", 500 | "ax1.axis(\"tight\")\n", 501 | "ax2.imshow(projection[projection.shape[0]//2-(nx-inner)//2:projection.shape[0]//2+(nx-inner)//2], cmap='gray')\n", 502 | "ax2.set_title(\"scikit-image radon\")\n", 503 | "ax2.axis(\"tight\")\n", 504 | "ax3.imshow(projection1.T-projection[projection.shape[0]//2-(nx-inner)//2:projection.shape[0]//2+(nx-inner)//2], \n", 505 | " cmap='gray')\n", 506 | "ax3.set_title(\"difference\")\n", 507 | "ax3.axis(\"tight\");" 508 | ] 509 | }, 510 | { 511 | "cell_type": "markdown", 512 | "metadata": {}, 513 | "source": [ 514 | "As you can see we have pretty much produced the same result in terms of the forward proejction operatarion, but now our operator also has a proper adjoint (i.e., satisifes the dot product test - you can verify this for yourself).\n", 515 | "\n", 516 | "Let us see if we can now feed it into one of PyLops solvers. We will consider two cases:\n", 517 | "\n", 518 | "- L2 regularized inversion, solving the problem that optimises: $J= ||\\mathbf{p} - \\mathbf{P} \\mathbf{i}||_2^2 + ||\\nabla \\mathbf{i}||_2^2$\n", 519 | "- TV regularized inversion, solving the problem that optimises: $J= ||\\mathbf{p} - \\mathbf{P} \\mathbf{i}||_2^2 + ||\\mathbf{D}_x \\mathbf{i}||_1 + ||\\mathbf{D}_y \\mathbf{i}||_1$" 520 | ] 521 | }, 522 | { 523 | "cell_type": "markdown", 524 | "metadata": {}, 525 | "source": [ 526 | "The first inversion can be easily carried out using PyLops ``RegularizedInversion`` (https://pylops.readthedocs.io/en/latest/api/generated/pylops.optimization.leastsquares.RegularizedInversion.html) which allows including a number of L2 regularization terms to the main functional" 527 | ] 528 | }, 529 | { 530 | "cell_type": "code", 531 | "execution_count": null, 532 | "metadata": {}, 533 | "outputs": [], 534 | "source": [ 535 | "# Regularised LS\n", 536 | "# Define laplacian for regularisation (i.e., smoothing)\n", 537 | "D2op = pylops.Laplacian(dims=(nx, ny), edge=True, dtype=np.float)\n", 538 | "\n", 539 | "# Solve inverse problem with smoothing regularisation\n", 540 | "image_l2 = pylops.optimization.leastsquares.RegularizedInversion(Radop, [D2op], \n", 541 | " projection1.ravel(), \n", 542 | " epsRs=[1e0], show=True,\n", 543 | " **dict(iter_lim=20))\n", 544 | "image_l2 = np.real(image_l2.reshape(nx, ny))\n", 545 | "mse_l2 = np.linalg.norm(image_l2[pad//2:-pad//2, pad//2:-pad//2] - image[pad//2:-pad//2, pad//2:-pad//2])\n", 546 | "\n", 547 | "fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(12, 6))\n", 548 | "ax1.imshow(image_l2[pad//2:-pad//2, pad//2:-pad//2], cmap='gray', vmin=0, vmax=1)\n", 549 | "ax1.set_title(\"Reconstruction\\nL2 Regularized inversion\")\n", 550 | "ax1.axis('tight')\n", 551 | "ax2.imshow(image_l2[pad//2:-pad//2, pad//2:-pad//2] - image[pad//2:-pad//2, pad//2:-pad//2], \n", 552 | " cmap='gray', vmin=-0.2, vmax=0.2)\n", 553 | "ax2.set_title(\"Reconstruction error\\nL2 Regularized inversion\")\n", 554 | "ax2.axis('tight');" 555 | ] 556 | }, 557 | { 558 | "cell_type": "markdown", 559 | "metadata": {}, 560 | "source": [ 561 | "The second inversion will use the **Split-Bregman** solver - designed to solve L1 optimisation problems, subject to the Least-Squares fitting norm. This is an appropriate choice for the TV regularisation problem as shown above." 562 | ] 563 | }, 564 | { 565 | "cell_type": "code", 566 | "execution_count": null, 567 | "metadata": {}, 568 | "outputs": [], 569 | "source": [ 570 | "# TV solution with Split-Bregman solver\n", 571 | "# Define first derivative operators\n", 572 | "Dop = [pylops.FirstDerivative(ny*nx, dims=(nx, ny), dir=0, edge=True, kind='backward', dtype=np.float),\n", 573 | " pylops.FirstDerivative(ny*nx, dims=(nx, ny), dir=1, edge=True, kind='backward', dtype=np.float)]\n", 574 | "\n", 575 | "# Solve inverse problem\n", 576 | "mu = 0.2\n", 577 | "lamda = [1., 1.]\n", 578 | "niter = 5\n", 579 | "niterinner = 1\n", 580 | "\n", 581 | "# using Split-Bregman solver\n", 582 | "image_tv, niter = pylops.optimization.sparsity.SplitBregman(Radop, Dop, projection1.ravel(), niter, niterinner,\n", 583 | " mu=mu, epsRL1s=lamda, tol=1e-4, tau=1., show=True,\n", 584 | " **dict(iter_lim=10, damp=1e-2))\n", 585 | "image_tv = np.real(image_tv.reshape(nx, ny))\n", 586 | "mse_tv = np.linalg.norm(image_tv[pad//2:-pad//2, pad//2:-pad//2] - image[pad//2:-pad//2, pad//2:-pad//2])\n", 587 | "\n", 588 | "fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(12, 6))\n", 589 | "ax1.imshow(image_tv[pad//2:-pad//2, pad//2:-pad//2], cmap='gray', vmin=0, vmax=1)\n", 590 | "ax1.set_title(\"Reconstruction\\nTV Regularized inversion\")\n", 591 | "ax1.axis('tight')\n", 592 | "ax2.imshow(image_tv[pad//2:-pad//2, pad//2:-pad//2] - image[pad//2:-pad//2, pad//2:-pad//2], \n", 593 | " cmap='gray', vmin=-0.2, vmax=0.2)\n", 594 | "ax2.set_title(\"Reconstruction error\\nTV Regularized inversion\")\n", 595 | "ax2.axis('tight');" 596 | ] 597 | }, 598 | { 599 | "cell_type": "markdown", 600 | "metadata": {}, 601 | "source": [ 602 | "We can now visualize the diffferent reconstructions all together" 603 | ] 604 | }, 605 | { 606 | "cell_type": "code", 607 | "execution_count": null, 608 | "metadata": {}, 609 | "outputs": [], 610 | "source": [ 611 | "fig, axs = plt.subplots(1, 4, figsize=(18, 4))\n", 612 | "axs[0].imshow(image[pad//2:-pad//2, pad//2:-pad//2], vmin=0, vmax=1, cmap='gray')\n", 613 | "axs[0].axis('tight')\n", 614 | "axs[0].set_title('Image')\n", 615 | "axs[1].imshow(image_fbp[pad//2:-pad//2, pad//2:-pad//2], vmin=0, vmax=1, cmap='gray')\n", 616 | "axs[1].axis('tight')\n", 617 | "axs[1].set_title('Filtered Back Projection')\n", 618 | "axs[2].imshow(image_l2[pad//2:-pad//2, pad//2:-pad//2], vmin=0, vmax=1, cmap='gray')\n", 619 | "axs[2].axis('tight')\n", 620 | "axs[2].set_title('LS Inversion')\n", 621 | "axs[3].imshow(image_tv[pad//2:-pad//2, pad//2:-pad//2], vmin=0, vmax=1, cmap='gray')\n", 622 | "axs[3].axis('tight')\n", 623 | "axs[3].set_title('TV Inversion');\n", 624 | "\n", 625 | "# zoomed image\n", 626 | "fig, axs = plt.subplots(1, 4, sharex=True, sharey=True, figsize=(18, 4))\n", 627 | "axs[0].imshow(image[pad//2:-pad//2, pad//2:-pad//2], vmin=0, vmax=1, cmap='gray')\n", 628 | "axs[0].axis('tight')\n", 629 | "axs[0].set_title('Image')\n", 630 | "axs[1].imshow(image_fbp[pad//2:-pad//2, pad//2:-pad//2], vmin=0, vmax=1, cmap='gray')\n", 631 | "axs[1].axis('tight')\n", 632 | "axs[1].set_title('Filtered Back Projection')\n", 633 | "axs[2].imshow(image_l2[pad//2:-pad//2, pad//2:-pad//2], vmin=0, vmax=1, cmap='gray')\n", 634 | "axs[2].axis('tight')\n", 635 | "axs[2].set_title('LS Inversion')\n", 636 | "axs[3].imshow(image_tv[pad//2:-pad//2, pad//2:-pad//2], vmin=0, vmax=1, cmap='gray')\n", 637 | "axs[3].axis('tight')\n", 638 | "axs[3].set_title('TV Inversion');\n", 639 | "axs[3].set_xlim(100, 150)\n", 640 | "axs[3].set_ylim(220, 170);" 641 | ] 642 | }, 643 | { 644 | "cell_type": "markdown", 645 | "metadata": {}, 646 | "source": [ 647 | "And finally we print the Mean Squared Error (MSE) for the different reconstuctions. This is commonly as a metric for image similarity in the imaging science community." 648 | ] 649 | }, 650 | { 651 | "cell_type": "code", 652 | "execution_count": null, 653 | "metadata": {}, 654 | "outputs": [], 655 | "source": [ 656 | "print(f\"FBP MSE reconstruction error: {mse_fbp:.3f}\")\n", 657 | "print(f\"L2inv MSE reconstruction error: {mse_l2:.3f}\")\n", 658 | "print(f\"TV MSE reconstruction error: {mse_tv:.3f}\")" 659 | ] 660 | }, 661 | { 662 | "cell_type": "markdown", 663 | "metadata": {}, 664 | "source": [ 665 | "We can see that FBP is a good algorithm for this problem. Whilst being very fast it produces a reasonably accurate solution.\n", 666 | "\n", 667 | "Our inverse based reconstructions on other hand require repeated applications of the forward and adjoint projection - thereby considerably increasing computational cost/time. However, by re-casting the reconstruction as an inverse problem we can include constraints on the solution, such as smoothness or sharpness.\n", 668 | "\n", 669 | "When a limited number of projections is acquired, we see how L2 inversion provides already a small improvement with respect to FBP. This imporvement is mostly associated with the additional regularization terms that favour a smoother solution - i.e., by imposing a smoothness constraint.\n", 670 | "\n", 671 | "For the TV regularized inversion we instead enforce sparsity in the first-order spatial derivatives of the solution - thus imposing a sharpness constraint. This leads to a shapr, piece-wise image which, in this case, drives the MSE down even further." 672 | ] 673 | }, 674 | { 675 | "cell_type": "markdown", 676 | "metadata": {}, 677 | "source": [ 678 | "## Exercises" 679 | ] 680 | }, 681 | { 682 | "cell_type": "markdown", 683 | "metadata": {}, 684 | "source": [ 685 | "**EX** Try adding different regularization terms to either of the inverse problems. Can you improve the MSE further?" 686 | ] 687 | }, 688 | { 689 | "cell_type": "code", 690 | "execution_count": null, 691 | "metadata": {}, 692 | "outputs": [], 693 | "source": [ 694 | "def radon_morereg():\n", 695 | " \"\"\"Add Wavelet Transform regularization to SplitBregman reconstruction\n", 696 | " \"\"\"\n", 697 | " # Wavelet transform operator\n", 698 | " Wop = pylops.signalprocessing.DWT2D(image.shape, wavelet='haar', level=5)\n", 699 | " DWop = Dop + [Wop, ]\n", 700 | "\n", 701 | " # Solve inverse problem\n", 702 | " mu = 0.2\n", 703 | " lamda = [1., 1., .5]\n", 704 | " niter = 5\n", 705 | " niterinner = 1\n", 706 | "\n", 707 | " image_tvw, niter = pylops.optimization.sparsity.SplitBregman(Radop, DWop,\n", 708 | " projection1.ravel(),\n", 709 | " niter,\n", 710 | " niterinner,\n", 711 | " mu=mu,\n", 712 | " epsRL1s=lamda,\n", 713 | " tol=1e-4,\n", 714 | " tau=1.,\n", 715 | " show=True,\n", 716 | " **dict(\n", 717 | " iter_lim=10,\n", 718 | " damp=1e-2))\n", 719 | " image_tvw = np.real(image_tvw.reshape(nx, ny))\n", 720 | " mse_tvw = np.linalg.norm(\n", 721 | " image_tvw[pad // 2:-pad // 2, pad // 2:-pad // 2] -\n", 722 | " image[pad // 2:-pad // 2, pad // 2:-pad // 2])\n", 723 | " print(f\"TV+W MSE reconstruction error: {mse_tvw:.3f}\")\n", 724 | "\n", 725 | " fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(12, 6))\n", 726 | " ax1.imshow(image_tvw[pad // 2:-pad // 2, pad // 2:-pad // 2], cmap='gray',\n", 727 | " vmin=0, vmax=1)\n", 728 | " ax1.set_title(\"Reconstruction\\nTV+W Regularized inversion\")\n", 729 | " ax1.axis('tight')\n", 730 | " ax2.imshow(image_tvw[pad // 2:-pad // 2, pad // 2:-pad // 2] -\n", 731 | " image[pad // 2:-pad // 2, pad // 2:-pad // 2],\n", 732 | " cmap='gray', vmin=-0.2, vmax=0.2)\n", 733 | " ax2.set_title(\"Reconstruction error\\nTV+W Regularized inversion\")\n", 734 | " ax2.axis('tight')\n" 735 | ] 736 | }, 737 | { 738 | "cell_type": "code", 739 | "execution_count": null, 740 | "metadata": {}, 741 | "outputs": [], 742 | "source": [ 743 | "radon_morereg()" 744 | ] 745 | }, 746 | { 747 | "cell_type": "markdown", 748 | "metadata": {}, 749 | "source": [ 750 | "**EX** Let's add now some noise to the data. How sensitive are the different methods used in this notebook to noise? Why?" 751 | ] 752 | }, 753 | { 754 | "cell_type": "code", 755 | "execution_count": null, 756 | "metadata": {}, 757 | "outputs": [], 758 | "source": [ 759 | "def radon_noise():\n", 760 | " \"\"\"Create noise to add to projections\n", 761 | " \"\"\"\n", 762 | " sigman = 5e-1 # play with this...\n", 763 | " n = np.random.normal(0., sigman, projection.shape)\n", 764 | " projection_n = projection + n\n", 765 | " projection1_n = projection1 + n[projection.shape[0] // 2 - (nx - inner) // 2:projection.shape[0] // 2 + (nx - inner) // 2].T\n", 766 | "\n", 767 | " # copy-paste here the inversion code(s)..." 768 | ] 769 | }, 770 | { 771 | "cell_type": "code", 772 | "execution_count": null, 773 | "metadata": {}, 774 | "outputs": [], 775 | "source": [ 776 | "radon_noise()" 777 | ] 778 | }, 779 | { 780 | "cell_type": "markdown", 781 | "metadata": {}, 782 | "source": [ 783 | "**EX** Imagine now that you have been able to perform steps 2 and 3 in the Fourier Slice theorem and have access to the subsampled wavenumber response of the Shepp Logan Phantom. How would you set up your forward model and inverse problem?" 784 | ] 785 | }, 786 | { 787 | "cell_type": "code", 788 | "execution_count": null, 789 | "metadata": {}, 790 | "outputs": [], 791 | "source": [ 792 | "def radon_kk():\n", 793 | " \"\"\"Perform reconstruction in KK domain\n", 794 | " \"\"\"\n", 795 | " # 2D FFT operator\n", 796 | " Fop = pylops.signalprocessing.FFT2D(dims=(nx, ny), dtype=np.complex)\n", 797 | "\n", 798 | " # Restriction operator\n", 799 | " thetas = np.arange(-43, 47, 5)\n", 800 | " kx = np.arange(nx) - nx // 2\n", 801 | " ikx = np.arange(nx)\n", 802 | "\n", 803 | " mask = np.zeros((nx, ny))\n", 804 | " for theta in thetas:\n", 805 | " ky = kx * np.tan(np.deg2rad(theta))\n", 806 | " iky = np.round(ky).astype(np.int) + nx // 2\n", 807 | " sel = (iky >= 0) & (iky < nx)\n", 808 | " mask[ikx[sel], iky[sel]] = 1\n", 809 | " mask = np.logical_or(mask, np.fliplr(mask.T))\n", 810 | " mask = np.fft.ifftshift(mask)\n", 811 | "\n", 812 | " Rop = pylops.Restriction(nx * ny, np.where(mask.ravel() == 1)[0],\n", 813 | " dtype=np.complex)\n", 814 | "\n", 815 | " # kk spectrum\n", 816 | " kk = Fop * image.ravel()\n", 817 | " kk = kk.reshape(image.shape)\n", 818 | "\n", 819 | " # restricted kk spectrum\n", 820 | " kkrestr = Rop.mask(kk.ravel())\n", 821 | " kkrestr = kkrestr.reshape(ny, nx)\n", 822 | " kkrestr.data[:] = np.fft.fftshift(kkrestr.data)\n", 823 | " kkrestr.mask[:] = np.fft.fftshift(kkrestr.mask)\n", 824 | "\n", 825 | " # data\n", 826 | " KOp = Rop * Fop\n", 827 | " y = KOp * image.ravel()\n", 828 | "\n", 829 | " fig, (ax1, ax2, ax3, ax4) = plt.subplots(1, 4, figsize=(17, 4))\n", 830 | " ax1.imshow(image, cmap='gray')\n", 831 | " ax1.set_title(r\"Input $\\mathbf{i}$\")\n", 832 | " ax1.axis('tight')\n", 833 | " ax2.imshow(np.fft.fftshift(np.abs(kk)), vmin=0, vmax=1, cmap='rainbow')\n", 834 | " ax2.set_title(\"KK Spectrum\")\n", 835 | " ax2.axis('tight')\n", 836 | " ax3.imshow(kkrestr.mask, vmin=0, vmax=1, cmap='gray')\n", 837 | " ax3.set_title(r\"Sampling matrix\")\n", 838 | " ax3.axis('tight')\n", 839 | " ax4.imshow(np.abs(kkrestr), vmin=0, vmax=1, cmap='rainbow')\n", 840 | " ax4.set_title(r\"Sampled KK Spectrum $\\mathbf{k}$\")\n", 841 | " ax4.axis('tight')\n", 842 | "\n", 843 | " # Solve L2 inverse problem\n", 844 | " D2op = pylops.Laplacian(dims=(nx, ny), edge=True, dtype=np.complex)\n", 845 | "\n", 846 | " image_l2 = \\\n", 847 | " pylops.optimization.leastsquares.RegularizedInversion(KOp, [D2op],\n", 848 | " y, epsRs=[5e-1],\n", 849 | " show=True,\n", 850 | " **dict(iter_lim=20))\n", 851 | " image_l2 = np.real(image_l2.reshape(nx, ny))\n", 852 | "\n", 853 | " fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(12, 6))\n", 854 | " ax1.imshow(image_l2[pad // 2:-pad // 2, pad // 2:-pad // 2], cmap='gray',\n", 855 | " vmin=0, vmax=1)\n", 856 | " ax1.set_title(\"Reconstruction\\nL2 Regularized inversion\")\n", 857 | " ax1.axis('tight')\n", 858 | " ax2.imshow(image_l2[pad // 2:-pad // 2, pad // 2:-pad // 2] -\n", 859 | " image[pad // 2:-pad // 2, pad // 2:-pad // 2],\n", 860 | " cmap='gray', vmin=-0.2, vmax=0.2)\n", 861 | " ax2.set_title(\"Reconstruction error\\nL2 Regularized inversion\")\n", 862 | " ax2.axis('tight')\n", 863 | "\n", 864 | " # Solve TV inverse problem\n", 865 | " Dop = [pylops.FirstDerivative(ny * nx, dims=(nx, ny), dir=0, edge=True,\n", 866 | " kind='backward', dtype=np.complex),\n", 867 | " pylops.FirstDerivative(ny * nx, dims=(nx, ny), dir=1, edge=True,\n", 868 | " kind='backward', dtype=np.complex)]\n", 869 | "\n", 870 | " mu = 0.5\n", 871 | " lamda = [.05, .05]\n", 872 | " niter = 5\n", 873 | " niterinner = 1\n", 874 | "\n", 875 | " image_tv, niter = \\\n", 876 | " pylops.optimization.sparsity.SplitBregman(KOp, Dop, y,\n", 877 | " niter,\n", 878 | " niterinner,\n", 879 | " mu=mu,\n", 880 | " epsRL1s=lamda,\n", 881 | " tol=1e-4,\n", 882 | " tau=1.,\n", 883 | " show=True,\n", 884 | " **dict(iter_lim=10,\n", 885 | " damp=1e-2))\n", 886 | " image_tv = np.real(image_tv.reshape(nx, ny))\n", 887 | "\n", 888 | " fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(12, 6))\n", 889 | " ax1.imshow(image_tv[pad // 2:-pad // 2, pad // 2:-pad // 2], cmap='gray',\n", 890 | " vmin=0, vmax=1)\n", 891 | " ax1.set_title(\"Reconstruction\\nTV Regularized inversion\")\n", 892 | " ax1.axis('tight')\n", 893 | " ax2.imshow(image_tv[pad // 2:-pad // 2, pad // 2:-pad // 2] -\n", 894 | " image[pad // 2:-pad // 2, pad // 2:-pad // 2],\n", 895 | " cmap='gray', vmin=-0.2, vmax=0.2)\n", 896 | " ax2.set_title(\"Reconstruction error\\nTV Regularized inversion\")\n", 897 | " ax2.axis('tight')\n" 898 | ] 899 | }, 900 | { 901 | "cell_type": "code", 902 | "execution_count": null, 903 | "metadata": {}, 904 | "outputs": [], 905 | "source": [ 906 | "radon_kk()" 907 | ] 908 | }, 909 | { 910 | "cell_type": "markdown", 911 | "metadata": {}, 912 | "source": [ 913 | "## Recap\n", 914 | "\n", 915 | "In this last tutorial we have learned to:\n", 916 | "\n", 917 | "- understand how to wrap third-party code into PyLops linear operators (and evaluate their validity)\n", 918 | "- create a fairly complex forward operator combinining several of PyLops operators\n", 919 | "- use both L2 and sparse solvers\n", 920 | "\n", 921 | "To conclude, we hope that this example provided you with the understanding of several of PyLops key features and its flexibility. If you find yourself having to solve large inverse problems, PyLops can help you to quickly create complex linear operators and access a wide range of solvers - *it allows you not to spend time on building the foundations but you can focus on the innovative aspects of your problem!.*\n", 922 | "\n", 923 | "On the other hand, PyLops is not specifically built with the problem of CT tomography in mind. Whilst we showed you how it is possible to quickly create an operator to achieve this, this is far from being highly optimized for that task. In this case, note that there are much better libraries out there, e.g. ASTRA-Toolbox (https://www.astra-toolbox.com). Note that a nice exercise would be indeed to substitute our forward and backward projections with ASTRA's fast ones and subsequently leverage our solvers and done in this notebook." 924 | ] 925 | }, 926 | { 927 | "cell_type": "code", 928 | "execution_count": null, 929 | "metadata": {}, 930 | "outputs": [], 931 | "source": [ 932 | "scooby.Report(core='pylops')" 933 | ] 934 | } 935 | ], 936 | "metadata": { 937 | "kernelspec": { 938 | "display_name": "Python 3", 939 | "language": "python", 940 | "name": "python3" 941 | }, 942 | "language_info": { 943 | "codemirror_mode": { 944 | "name": "ipython", 945 | "version": 3 946 | }, 947 | "file_extension": ".py", 948 | "mimetype": "text/x-python", 949 | "name": "python", 950 | "nbconvert_exporter": "python", 951 | "pygments_lexer": "ipython3", 952 | "version": "3.7.2" 953 | }, 954 | "toc": { 955 | "base_numbering": 1, 956 | "nav_menu": {}, 957 | "number_sections": true, 958 | "sideBar": true, 959 | "skip_h1_title": false, 960 | "title_cell": "Table of Contents", 961 | "title_sidebar": "Contents", 962 | "toc_cell": false, 963 | "toc_position": { 964 | "height": "calc(100% - 180px)", 965 | "left": "10px", 966 | "top": "150px", 967 | "width": "297.1690979003906px" 968 | }, 969 | "toc_section_display": true, 970 | "toc_window_display": true 971 | } 972 | }, 973 | "nbformat": 4, 974 | "nbformat_minor": 4 975 | } 976 | -------------------------------------------------------------------------------- /Intro.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "metadata": {}, 6 | "source": [ 7 | "# Introduction\n", 8 | "\n", 9 | "**Authors: M. Ravasi, D. Vargas, I. Vasconcelos**" 10 | ] 11 | }, 12 | { 13 | "cell_type": "markdown", 14 | "metadata": {}, 15 | "source": [ 16 | "Welcome to the **Solving large-scale inverse problems in Python with PyLops** tutorial!\n", 17 | "\n", 18 | "The aim of this tutorial is to:\n", 19 | "\n", 20 | "- introduce you to the concept of *linear operators* and their usage in the solution of *inverse problems*;\n", 21 | "- show how PyLops can be used to set-up non-trivial linear operators and solve inverse problems in Python; \n", 22 | "- Walk you through a set of use cases where PyLops has been leveraged to solve real scientific problems and present future directions of development." 23 | ] 24 | }, 25 | { 26 | "cell_type": "markdown", 27 | "metadata": {}, 28 | "source": [ 29 | "## Useful links\n", 30 | "\n", 31 | "- Tutorial Github repository: https://github.com/mrava87/pylops_pydata2020\n", 32 | " \n", 33 | "- PyLops Github repository: https://github.com/equinor/pylops\n", 34 | "\n", 35 | "- PyLops reference documentation: https://pylops.readthedocs.io/en/latest/" 36 | ] 37 | }, 38 | { 39 | "cell_type": "markdown", 40 | "metadata": {}, 41 | "source": [ 42 | "## Theory in a nutshell\n", 43 | "\n", 44 | "In this tutorial we will try to keep the theory to a minimum and quickly expose you to practical examples. However, we want to make sure that some of the basic underlying concepts are clear to everyone and define a common mathematical notation.\n", 45 | "\n", 46 | "At the core of PyLops lies the concept of **linear operators**. A linear operator is generally a mapping or function that acts linearly on elements of a space to produce elements of another space. More specifically we say that $\\mathbf{A}:\\mathbb{F}^m \\to \\mathbb{F}^n$ is a linear operator that maps a vector of size $m$ in the *model space* to a vector of size $n$ in the *data space*:\n", 47 | "\n", 48 | "\n", 49 | "\n", 50 | "We will refer to this as **forward model (or operation)**. \n", 51 | "\n", 52 | "Conversely the application of its adjoint to a data vector is referred to as **adjoint modelling (or operation)**:\n", 53 | "\n", 54 | "\n", 55 | "\n", 56 | "In its simplest form, a linear operator can be seen as a **matrix** of size $n \\times m$ (and the adjoint is simply its transpose and complex conjugate). However in a more general sense we can think of a linear operator as any pair of software code that mimics the effect a matrix on a model vector as well as that of its adjoint to a data vector.\n", 57 | "\n", 58 | "Solving an inverse problems accounts to removing the effect of the operator/matrix $\\mathbf{A}$ from the data $\\mathbf{y}$ to retrieve the model $\\mathbf{x}$ (or an approximation of it).\n", 59 | "\n", 60 | "$$\\hat{\\mathbf{x}} = \\mathbf{A}^{-1} \\mathbf{y}$$\n", 61 | "\n", 62 | "In practice, the inverse of $\\mathbf{A}$ is generally not explicitely required. A solution can be obtained using either direct methods, matrix decompositions (eg SVD) or iterative solvers. Luckily, many iterative methods (e.g. cg, lsqr) do not need to know the individual entries of a matrix to solve a linear system. Such solvers only require the computation of forward and adjoint matrix-vector products - exactly what a linear operator does!\n", 63 | "\n", 64 | "**So what?**\n", 65 | "We have learned that to solve an inverse problem, we do not need to express the modelling operator in terms of its dense (or sparse) matrix. All we need to know is how to perform the forward and adjoint operations - ideally as fast as possible and using the least amount of memory. \n", 66 | "\n", 67 | "Our first task will be to understand how we can effectively write a linear operator on pen and paper and translate it into computer code. We will consider 2 examples:\n", 68 | "\n", 69 | "- Element-wise multiplication (also known as Hadamard product)\n", 70 | "- First Derivative" 71 | ] 72 | }, 73 | { 74 | "cell_type": "markdown", 75 | "metadata": {}, 76 | "source": [ 77 | "Let's first import the libraries we need in this tutorial" 78 | ] 79 | }, 80 | { 81 | "cell_type": "code", 82 | "execution_count": null, 83 | "metadata": {}, 84 | "outputs": [], 85 | "source": [ 86 | "# Run this when using Colab (will install the missing libraries)\n", 87 | "# !pip install pylops pympler scooby" 88 | ] 89 | }, 90 | { 91 | "cell_type": "code", 92 | "execution_count": 1, 93 | "metadata": {}, 94 | "outputs": [], 95 | "source": [ 96 | "%matplotlib inline\n", 97 | "\n", 98 | "import numpy as np\n", 99 | "import matplotlib.pyplot as plt\n", 100 | "import pylops\n", 101 | "import scooby\n", 102 | "\n", 103 | "from scipy.linalg import lstsq\n", 104 | "from pylops import LinearOperator\n", 105 | "from pylops.utils import dottest" 106 | ] 107 | }, 108 | { 109 | "cell_type": "markdown", 110 | "metadata": {}, 111 | "source": [ 112 | "## Element-wise multiplication" 113 | ] 114 | }, 115 | { 116 | "cell_type": "markdown", 117 | "metadata": {}, 118 | "source": [ 119 | "We start by creating a barebore linear operator that performs a simple element-wise multiplication between two vectors (the so-called Hadamart product):\n", 120 | "\n", 121 | "$$ y_i = d_i x_i \\quad \\forall i=0,1,...,n-1 $$\n", 122 | "\n", 123 | "If we think about the forward problem the way we wrote it before, we can see that this operator can be equivalently expressed as a dot-product between a square matrix $\\mathbf{D}$ that has the $d_i$ elements along its main diagonal and a vector $\\mathbf{x}$:\n", 124 | "\n", 125 | "\n", 126 | "\n", 127 | "Because of this, the related linear operator is called *Diagonal* operator in PyLops.\n", 128 | "\n", 129 | "We are ready to implement this operator in 2 different ways:\n", 130 | "\n", 131 | "- directly as a diagonal matrix; \n", 132 | "- as a linear operator that performs directly element-wise multiplication." 133 | ] 134 | }, 135 | { 136 | "cell_type": "markdown", 137 | "metadata": {}, 138 | "source": [ 139 | "### Dense matrix definition" 140 | ] 141 | }, 142 | { 143 | "cell_type": "code", 144 | "execution_count": 2, 145 | "metadata": {}, 146 | "outputs": [ 147 | { 148 | "name": "stdout", 149 | "output_type": "stream", 150 | "text": [ 151 | "D:\n", 152 | " [[0 0 0 0 0 0 0 0 0 0]\n", 153 | " [0 1 0 0 0 0 0 0 0 0]\n", 154 | " [0 0 2 0 0 0 0 0 0 0]\n", 155 | " [0 0 0 3 0 0 0 0 0 0]\n", 156 | " [0 0 0 0 4 0 0 0 0 0]\n", 157 | " [0 0 0 0 0 5 0 0 0 0]\n", 158 | " [0 0 0 0 0 0 6 0 0 0]\n", 159 | " [0 0 0 0 0 0 0 7 0 0]\n", 160 | " [0 0 0 0 0 0 0 0 8 0]\n", 161 | " [0 0 0 0 0 0 0 0 0 9]]\n" 162 | ] 163 | } 164 | ], 165 | "source": [ 166 | "n = 10\n", 167 | "diag = np.arange(n)\n", 168 | "\n", 169 | "D = np.diag(diag)\n", 170 | "print('D:\\n', D)" 171 | ] 172 | }, 173 | { 174 | "cell_type": "markdown", 175 | "metadata": {}, 176 | "source": [ 177 | "We can now apply the forward by simply using `np.dot`" 178 | ] 179 | }, 180 | { 181 | "cell_type": "code", 182 | "execution_count": 3, 183 | "metadata": {}, 184 | "outputs": [ 185 | { 186 | "name": "stdout", 187 | "output_type": "stream", 188 | "text": [ 189 | "y: [0. 1. 2. 3. 4. 5. 6. 7. 8. 9.]\n" 190 | ] 191 | } 192 | ], 193 | "source": [ 194 | "x = np.ones(n)\n", 195 | "y = np.dot(D, x) # or D.dot(x) or D @ x\n", 196 | "print('y: ', y)" 197 | ] 198 | }, 199 | { 200 | "cell_type": "markdown", 201 | "metadata": {}, 202 | "source": [ 203 | "As we have access to all the entries of the matrix, it is very easy to write the adjoint" 204 | ] 205 | }, 206 | { 207 | "cell_type": "code", 208 | "execution_count": 4, 209 | "metadata": {}, 210 | "outputs": [ 211 | { 212 | "name": "stdout", 213 | "output_type": "stream", 214 | "text": [ 215 | "xadj: [ 0. 1. 4. 9. 16. 25. 36. 49. 64. 81.]\n" 216 | ] 217 | } 218 | ], 219 | "source": [ 220 | "xadj = np.dot(np.conj(D.T), y)\n", 221 | "print('xadj: ', xadj)" 222 | ] 223 | }, 224 | { 225 | "cell_type": "markdown", 226 | "metadata": {}, 227 | "source": [ 228 | "*Note:* since the elements of our matrix are real numbers, we can avoid applying the complex conjugation here." 229 | ] 230 | }, 231 | { 232 | "cell_type": "markdown", 233 | "metadata": {}, 234 | "source": [ 235 | "Everything seems very easy so far. This approach does however carry some problems:\n", 236 | " \n", 237 | "- we are storing $N^2$ numbers, even though we know that our matrix has only elements along its diagonal.\n", 238 | "- we are applying a dot product which requires $N^2$ multiplications and summations (most of them with zeros)\n", 239 | "\n", 240 | "Of course in this case we could use a sparse matrix, which allows to store only non-zero elements (and their index) and provides a faster way to perform the dot product." 241 | ] 242 | }, 243 | { 244 | "cell_type": "markdown", 245 | "metadata": {}, 246 | "source": [ 247 | "### Linear operator definition" 248 | ] 249 | }, 250 | { 251 | "cell_type": "markdown", 252 | "metadata": {}, 253 | "source": [ 254 | "Let's take a leap of faith, and see if we can avoid thinking about the matrix altogether and write just an equivalent (ideally faster) piece of code that mimics this operation.\n", 255 | "\n", 256 | "To write its equivalent linear operator, we define a class with an init method, and 2 other methods:\n", 257 | " \n", 258 | "- _matvec: we write the forward operation here\n", 259 | "- _rmatvec: we write the adjoint operation here\n", 260 | " \n", 261 | "We see that we are also subclassing a PyLops LinearOperator. For the moment let's not get into the details of what that entails and simply focus on writing the content of these three methods." 262 | ] 263 | }, 264 | { 265 | "cell_type": "code", 266 | "execution_count": 5, 267 | "metadata": {}, 268 | "outputs": [], 269 | "source": [ 270 | "class Diagonal(LinearOperator):\n", 271 | " \"\"\"Short version of a Diagonal operator. See\n", 272 | " https://github.com/equinor/pylops/blob/master/pylops/basicoperators/Diagonal.py\n", 273 | " for a more detailed implementation\n", 274 | " \"\"\"\n", 275 | " def __init__(self, diag, dtype='float64'):\n", 276 | " self.diag = diag\n", 277 | " self.shape = (len(self.diag), len(self.diag))\n", 278 | " self.dtype = np.dtype(dtype)\n", 279 | "\n", 280 | " def _matvec(self, x):\n", 281 | " y = self.diag * x\n", 282 | " return y\n", 283 | "\n", 284 | " def _rmatvec(self, x):\n", 285 | " y = np.conj(self.diag) * x\n", 286 | " return y" 287 | ] 288 | }, 289 | { 290 | "cell_type": "markdown", 291 | "metadata": {}, 292 | "source": [ 293 | "Now we create the operator" 294 | ] 295 | }, 296 | { 297 | "cell_type": "code", 298 | "execution_count": 6, 299 | "metadata": {}, 300 | "outputs": [ 301 | { 302 | "name": "stdout", 303 | "output_type": "stream", 304 | "text": [ 305 | "Dop: <10x10 Diagonal with dtype=float64>\n" 306 | ] 307 | } 308 | ], 309 | "source": [ 310 | "Dop = Diagonal(diag)\n", 311 | "print('Dop: ', Dop)" 312 | ] 313 | }, 314 | { 315 | "cell_type": "markdown", 316 | "metadata": {}, 317 | "source": [ 318 | "### Linear operator application" 319 | ] 320 | }, 321 | { 322 | "cell_type": "markdown", 323 | "metadata": {}, 324 | "source": [ 325 | "Forward" 326 | ] 327 | }, 328 | { 329 | "cell_type": "code", 330 | "execution_count": 7, 331 | "metadata": {}, 332 | "outputs": [ 333 | { 334 | "name": "stdout", 335 | "output_type": "stream", 336 | "text": [ 337 | "y: [0. 1. 2. 3. 4. 5. 6. 7. 8. 9.]\n" 338 | ] 339 | } 340 | ], 341 | "source": [ 342 | "y = Dop * x # Dop @ x\n", 343 | "print('y: ', y)" 344 | ] 345 | }, 346 | { 347 | "cell_type": "markdown", 348 | "metadata": {}, 349 | "source": [ 350 | "Adjoint" 351 | ] 352 | }, 353 | { 354 | "cell_type": "code", 355 | "execution_count": 8, 356 | "metadata": {}, 357 | "outputs": [ 358 | { 359 | "name": "stdout", 360 | "output_type": "stream", 361 | "text": [ 362 | "xadj: [ 0. 1. 4. 9. 16. 25. 36. 49. 64. 81.]\n" 363 | ] 364 | } 365 | ], 366 | "source": [ 367 | "xadj = Dop.H * y\n", 368 | "print('xadj: ', xadj)" 369 | ] 370 | }, 371 | { 372 | "cell_type": "markdown", 373 | "metadata": {}, 374 | "source": [ 375 | "As expected we obtain the same results!" 376 | ] 377 | }, 378 | { 379 | "cell_type": "markdown", 380 | "metadata": {}, 381 | "source": [ 382 | "**EX:** try making a much bigger vector $\\mathbf{x}$ and time the forward and adjoint for the two approaches" 383 | ] 384 | }, 385 | { 386 | "cell_type": "code", 387 | "execution_count": null, 388 | "metadata": {}, 389 | "outputs": [], 390 | "source": [ 391 | "# %load -s Diagonal_timing solutions/intro_sol.py" 392 | ] 393 | }, 394 | { 395 | "cell_type": "markdown", 396 | "metadata": {}, 397 | "source": [ 398 | "### Linear operator testing" 399 | ] 400 | }, 401 | { 402 | "cell_type": "markdown", 403 | "metadata": {}, 404 | "source": [ 405 | "One of the most important aspect of writing a Linear operator is to be able to verify that the code implemented in forward mode and the code implemented in adjoint mode are effectively adjoint to each other. \n", 406 | "\n", 407 | "If this is not the case, we will struggle to invert our linear operator - some iterative solvers will diverge and other show very slow convergence.\n", 408 | "\n", 409 | "This is instead the case if the so-called *dot-test* is passed within a certain treshold:\n", 410 | "\n", 411 | "$$\n", 412 | "(\\mathbf{A}*\\mathbf{u})^H*\\mathbf{v} = \\mathbf{u}^H*(\\mathbf{A}^H*\\mathbf{v})\n", 413 | "$$\n", 414 | "\n", 415 | "where $\\mathbf{u}$ and $\\mathbf{v}$ are two random vectors.\n", 416 | "\n", 417 | "Let's use `pylops.utils.dottest`" 418 | ] 419 | }, 420 | { 421 | "cell_type": "code", 422 | "execution_count": 9, 423 | "metadata": {}, 424 | "outputs": [ 425 | { 426 | "name": "stdout", 427 | "output_type": "stream", 428 | "text": [ 429 | "Dot test passed, v^T(Opu)=-1.805170 - u^T(Op^Tv)=-1.805170\n" 430 | ] 431 | } 432 | ], 433 | "source": [ 434 | "dottest(Dop, n, n, verb=True);" 435 | ] 436 | }, 437 | { 438 | "cell_type": "markdown", 439 | "metadata": {}, 440 | "source": [ 441 | "## First Derivative" 442 | ] 443 | }, 444 | { 445 | "cell_type": "markdown", 446 | "metadata": {}, 447 | "source": [ 448 | "Let's consider now something less trivial. \n", 449 | "\n", 450 | "\n", 451 | "\n", 452 | "We use a first-order centered first derivative stencil:\n", 453 | "\n", 454 | "$$ y_i = \\frac{x_{i+1} - x_{i-1}}{2 \\Delta} \\quad \\forall i=1,2,...,N $$\n", 455 | "\n", 456 | "where $\\Delta$ is the sampling step of the input signal. Note that we will deal differently with the edges, using a forward/backward derivative.\n", 457 | "\n", 458 | "" 459 | ] 460 | }, 461 | { 462 | "cell_type": "markdown", 463 | "metadata": {}, 464 | "source": [ 465 | "### Dense matrix definition" 466 | ] 467 | }, 468 | { 469 | "cell_type": "code", 470 | "execution_count": 10, 471 | "metadata": {}, 472 | "outputs": [ 473 | { 474 | "name": "stdout", 475 | "output_type": "stream", 476 | "text": [ 477 | "D:\n", 478 | " [[-1. 1. 0. 0. 0. 0. 0. 0. 0. 0. 0. ]\n", 479 | " [-0.5 0. 0.5 0. 0. 0. 0. 0. 0. 0. 0. ]\n", 480 | " [ 0. -0.5 0. 0.5 0. 0. 0. 0. 0. 0. 0. ]\n", 481 | " [ 0. 0. -0.5 0. 0.5 0. 0. 0. 0. 0. 0. ]\n", 482 | " [ 0. 0. 0. -0.5 0. 0.5 0. 0. 0. 0. 0. ]\n", 483 | " [ 0. 0. 0. 0. -0.5 0. 0.5 0. 0. 0. 0. ]\n", 484 | " [ 0. 0. 0. 0. 0. -0.5 0. 0.5 0. 0. 0. ]\n", 485 | " [ 0. 0. 0. 0. 0. 0. -0.5 0. 0.5 0. 0. ]\n", 486 | " [ 0. 0. 0. 0. 0. 0. 0. -0.5 0. 0.5 0. ]\n", 487 | " [ 0. 0. 0. 0. 0. 0. 0. 0. -0.5 0. 0.5]\n", 488 | " [ 0. 0. 0. 0. 0. 0. 0. 0. 0. -1. 1. ]]\n" 489 | ] 490 | } 491 | ], 492 | "source": [ 493 | "nx = 11\n", 494 | "\n", 495 | "D = np.diag(0.5*np.ones(nx-1), k=1) - np.diag(0.5*np.ones(nx-1), k=-1) \n", 496 | "D[0, 0] = D[-1, -2] = -1\n", 497 | "D[0, 1] = D[-1, -1] = 1\n", 498 | "print('D:\\n', D)" 499 | ] 500 | }, 501 | { 502 | "cell_type": "markdown", 503 | "metadata": {}, 504 | "source": [ 505 | "### Linear operator definition" 506 | ] 507 | }, 508 | { 509 | "cell_type": "markdown", 510 | "metadata": {}, 511 | "source": [ 512 | "**EX:** try writing the operator" 513 | ] 514 | }, 515 | { 516 | "cell_type": "code", 517 | "execution_count": null, 518 | "metadata": {}, 519 | "outputs": [], 520 | "source": [ 521 | "# class FirstDerivative(LinearOperator):\n", 522 | " def __init__(self, ...):\n", 523 | " # fill here\n", 524 | "\n", 525 | " def _matvec(self, x):\n", 526 | " # fill here\n", 527 | "\n", 528 | " def _rmatvec(self, x):\n", 529 | " # fill here" 530 | ] 531 | }, 532 | { 533 | "cell_type": "markdown", 534 | "metadata": {}, 535 | "source": [ 536 | "**SOLUTION**" 537 | ] 538 | }, 539 | { 540 | "cell_type": "code", 541 | "execution_count": null, 542 | "metadata": {}, 543 | "outputs": [], 544 | "source": [ 545 | "# %load -s FirstDerivative solutions/intro_sol.py" 546 | ] 547 | }, 548 | { 549 | "cell_type": "markdown", 550 | "metadata": {}, 551 | "source": [ 552 | "Define the operator" 553 | ] 554 | }, 555 | { 556 | "cell_type": "code", 557 | "execution_count": null, 558 | "metadata": {}, 559 | "outputs": [], 560 | "source": [ 561 | "Dop = FirstDerivative(nx)\n", 562 | "print('Dop: ', Dop)" 563 | ] 564 | }, 565 | { 566 | "cell_type": "markdown", 567 | "metadata": {}, 568 | "source": [ 569 | "Perform the dot test" 570 | ] 571 | }, 572 | { 573 | "cell_type": "code", 574 | "execution_count": null, 575 | "metadata": {}, 576 | "outputs": [], 577 | "source": [ 578 | "dottest(Dop, nx, nx, verb=True);" 579 | ] 580 | }, 581 | { 582 | "cell_type": "markdown", 583 | "metadata": {}, 584 | "source": [ 585 | "Now that you understand, you can use PyLops implementation of this operator (see https://pylops.readthedocs.io/en/latest/api/generated/pylops.FirstDerivative.html for details)" 586 | ] 587 | }, 588 | { 589 | "cell_type": "code", 590 | "execution_count": 11, 591 | "metadata": {}, 592 | "outputs": [ 593 | { 594 | "name": "stdout", 595 | "output_type": "stream", 596 | "text": [ 597 | "Dop: <11x11 FirstDerivative with dtype=float64>\n" 598 | ] 599 | } 600 | ], 601 | "source": [ 602 | "Dop = pylops.FirstDerivative(nx, edge=True)\n", 603 | "print('Dop: ', Dop)" 604 | ] 605 | }, 606 | { 607 | "cell_type": "code", 608 | "execution_count": 12, 609 | "metadata": {}, 610 | "outputs": [ 611 | { 612 | "name": "stdout", 613 | "output_type": "stream", 614 | "text": [ 615 | "Dot test passed, v^T(Opu)=4.022456 - u^T(Op^Tv)=4.022456\n" 616 | ] 617 | } 618 | ], 619 | "source": [ 620 | "dottest(Dop, nx, nx, verb=True);" 621 | ] 622 | }, 623 | { 624 | "cell_type": "markdown", 625 | "metadata": {}, 626 | "source": [ 627 | "### Linear operator application" 628 | ] 629 | }, 630 | { 631 | "cell_type": "code", 632 | "execution_count": 13, 633 | "metadata": {}, 634 | "outputs": [ 635 | { 636 | "name": "stdout", 637 | "output_type": "stream", 638 | "text": [ 639 | "x: [-5. -4. -3. -2. -1. 0. 1. 2. 3. 4. 5.]\n" 640 | ] 641 | } 642 | ], 643 | "source": [ 644 | "x = np.arange(nx) - (nx-1)/2\n", 645 | "print('x: ', x)" 646 | ] 647 | }, 648 | { 649 | "cell_type": "markdown", 650 | "metadata": {}, 651 | "source": [ 652 | "Forward" 653 | ] 654 | }, 655 | { 656 | "cell_type": "code", 657 | "execution_count": 14, 658 | "metadata": {}, 659 | "outputs": [ 660 | { 661 | "name": "stdout", 662 | "output_type": "stream", 663 | "text": [ 664 | "y: [1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1.]\n", 665 | "y: [1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1.]\n" 666 | ] 667 | } 668 | ], 669 | "source": [ 670 | "y = np.dot(D, x)\n", 671 | "print('y: ', y)\n", 672 | "\n", 673 | "y = Dop * x\n", 674 | "print('y: ', y)" 675 | ] 676 | }, 677 | { 678 | "cell_type": "markdown", 679 | "metadata": {}, 680 | "source": [ 681 | "Adjoint" 682 | ] 683 | }, 684 | { 685 | "cell_type": "code", 686 | "execution_count": 15, 687 | "metadata": {}, 688 | "outputs": [ 689 | { 690 | "name": "stdout", 691 | "output_type": "stream", 692 | "text": [ 693 | "xadj: [-1.5 0.5 0. 0. 0. 0. 0. 0. 0. -0.5 1.5]\n", 694 | "xadj: [-1.5 0.5 0. 0. 0. 0. 0. 0. 0. -0.5 1.5]\n" 695 | ] 696 | } 697 | ], 698 | "source": [ 699 | "xadj = np.dot(D.T, y)\n", 700 | "print('xadj: ', xadj)\n", 701 | "\n", 702 | "xadj = Dop.H * y\n", 703 | "print('xadj: ', xadj)" 704 | ] 705 | }, 706 | { 707 | "cell_type": "markdown", 708 | "metadata": {}, 709 | "source": [ 710 | "**EX:** Same as before, let's time our two implementations" 711 | ] 712 | }, 713 | { 714 | "cell_type": "code", 715 | "execution_count": null, 716 | "metadata": {}, 717 | "outputs": [], 718 | "source": [ 719 | "# %load -s FirstDerivative_timing solutions/intro_sol.py" 720 | ] 721 | }, 722 | { 723 | "cell_type": "markdown", 724 | "metadata": {}, 725 | "source": [ 726 | "**EX:** try to compare the memory footprint of the matrix $\\mathbf{D}$ compared to its equivalent linear operator. Hint: install ``pympler`` and use ``pympler.asizeof``" 727 | ] 728 | }, 729 | { 730 | "cell_type": "code", 731 | "execution_count": null, 732 | "metadata": {}, 733 | "outputs": [], 734 | "source": [ 735 | "# %load -s FirstDerivative_memory solutions/intro_sol.py" 736 | ] 737 | }, 738 | { 739 | "cell_type": "markdown", 740 | "metadata": {}, 741 | "source": [ 742 | "Finally, let's try to move on step further and try to solve the inverse problem. \n", 743 | "\n", 744 | "For the dense matrix, we will use `scipy.linalg.lstsq`. For operator PyLops this can be very easily done by using the '/' which will call `scipy.sparse.linalg.lsqr` solver (you can also use this solver directly if you want to fine tune some of its input parameters" 745 | ] 746 | }, 747 | { 748 | "cell_type": "code", 749 | "execution_count": 16, 750 | "metadata": {}, 751 | "outputs": [ 752 | { 753 | "name": "stdout", 754 | "output_type": "stream", 755 | "text": [ 756 | "xinv: [-5.00000000e+00 -4.00000000e+00 -3.00000000e+00 -2.00000000e+00\n", 757 | " -1.00000000e+00 6.96836792e-16 1.00000000e+00 2.00000000e+00\n", 758 | " 3.00000000e+00 4.00000000e+00 5.00000000e+00]\n", 759 | "xinv: [-5. -4. -3. -2. -1. 0. 1. 2. 3. 4. 5.]\n" 760 | ] 761 | } 762 | ], 763 | "source": [ 764 | "xinv = lstsq(D, y)[0]\n", 765 | "print('xinv: ', xinv)\n", 766 | "\n", 767 | "xinv = Dop / y\n", 768 | "print('xinv: ', xinv)" 769 | ] 770 | }, 771 | { 772 | "cell_type": "markdown", 773 | "metadata": {}, 774 | "source": [ 775 | "In both cases we have retrieved the correct solution!" 776 | ] 777 | }, 778 | { 779 | "cell_type": "markdown", 780 | "metadata": {}, 781 | "source": [ 782 | "## Chaining operators\n", 783 | "\n", 784 | "Up until now, we have discussed how brand new operators can be created in few systematic steps. This sounds cool, but it may look like we would need to do this every time we need to solve a new problem.\n", 785 | "\n", 786 | "This is where **PyLops** comes in. Alongside providing users with an extensive collection of operators, the library allows such operators to be combined via basic algebraic operations (eg summed, subtracted, multiplied) or chained together (vertical and horizontal stacking, block and block diagonal).\n", 787 | "\n", 788 | "We will see more of this in the following. For now let's imagine to have a modelling operator that is a smooth first-order derivative. To do so we can chain the ``FirstDerivative`` operator ($\\mathbf{D}$) that we have just created with a smoothing operator ($\\mathbf{S}$)(https://pylops.readthedocs.io/en/latest/api/generated/pylops.Smoothing1D.html#pylops.Smoothing1D) and write the following problem:\n", 789 | "\n", 790 | "$$\\mathbf{y} = \\mathbf{S} \\mathbf{D} \\mathbf{x}$$\n", 791 | "\n", 792 | "\n", 793 | "\n", 794 | "Let's create it first and attempt to invert it afterwards." 795 | ] 796 | }, 797 | { 798 | "cell_type": "code", 799 | "execution_count": 17, 800 | "metadata": {}, 801 | "outputs": [ 802 | { 803 | "name": "stdout", 804 | "output_type": "stream", 805 | "text": [ 806 | "<51x51 LinearOperator with dtype=float64>\n" 807 | ] 808 | }, 809 | { 810 | "data": { 811 | "image/png": "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\n", 812 | "text/plain": [ 813 | "
" 814 | ] 815 | }, 816 | "metadata": { 817 | "needs_background": "light" 818 | }, 819 | "output_type": "display_data" 820 | } 821 | ], 822 | "source": [ 823 | "nx = 51\n", 824 | "x = np.ones(nx)\n", 825 | "x[:nx//2] = -1\n", 826 | "\n", 827 | "Dop = pylops.FirstDerivative(nx, edge=True, kind='forward')\n", 828 | "Sop = pylops.Smoothing1D(5, nx)\n", 829 | "\n", 830 | "# Chain the two operators\n", 831 | "Op = Sop * Dop\n", 832 | "print(Op)\n", 833 | "\n", 834 | "# Create data\n", 835 | "y = Op * x\n", 836 | "\n", 837 | "# Invert\n", 838 | "xinv = Op / y\n", 839 | "xinv = pylops.optimization.leastsquares.NormalEquationsInversion(Op, [pylops.Identity(nx)], y, epsRs=[1e-3,])\n", 840 | "\n", 841 | "fig, (ax1, ax2) = plt.subplots(2, 1, figsize=(8, 5))\n", 842 | "ax1.plot(y, '.-k')\n", 843 | "ax1.set_title(r\"Data $y$\")\n", 844 | "ax2.plot(x, 'k', label='x')\n", 845 | "ax2.plot(xinv, '--r', label='xinv')\n", 846 | "ax2.legend()\n", 847 | "ax2.set_title(r\"Model $x$\")\n", 848 | "plt.tight_layout()" 849 | ] 850 | }, 851 | { 852 | "cell_type": "markdown", 853 | "metadata": {}, 854 | "source": [ 855 | "## Recap\n", 856 | "\n", 857 | "In this first tutorial we have learned to:\n", 858 | "\n", 859 | "- translate a linear operator from pen and paper to computer code\n", 860 | "- write our own linear operators\n", 861 | "- use PyLops linear operators to perform forward, adjoint and inverse\n", 862 | "- combine PyLops linear operators." 863 | ] 864 | }, 865 | { 866 | "cell_type": "code", 867 | "execution_count": 18, 868 | "metadata": {}, 869 | "outputs": [ 870 | { 871 | "data": { 872 | "text/html": [ 873 | "\n", 874 | " \n", 875 | " \n", 876 | " \n", 877 | " \n", 878 | " \n", 879 | " \n", 880 | " \n", 881 | " \n", 882 | " \n", 883 | " \n", 884 | " \n", 885 | " \n", 886 | " \n", 887 | " \n", 888 | " \n", 889 | " \n", 890 | " \n", 891 | " \n", 892 | " \n", 893 | " \n", 894 | " \n", 896 | " \n", 897 | " \n", 898 | " \n", 899 | " \n", 900 | " \n", 901 | " \n", 902 | " \n", 903 | " \n", 904 | " \n", 905 | " \n", 906 | " \n", 907 | " \n", 908 | " \n", 909 | " \n", 910 | " \n", 911 | " \n", 912 | " \n", 913 | " \n", 914 | " \n", 915 | " \n", 916 | "
Sun Oct 04 12:05:57 2020 CEST
DarwinOS4CPU(s)x86_64Machine
64bitArchitecture8.0 GBRAMJupyterEnvironment
Python 3.7.2 (default, Dec 29 2018, 00:00:04) \n", 895 | "[Clang 4.0.1 (tags/RELEASE_401/final)]
1.7.1.dev32+g712d179.d20200111pylops1.18.1numpy1.5.0scipy
7.2.0IPython3.3.1matplotlib0.4.1scooby
Intel(R) Math Kernel Library Version 2019.0.4 Product Build 20190411 for Intel(R) 64 architecture applications
" 917 | ], 918 | "text/plain": [ 919 | "\n", 920 | "--------------------------------------------------------------------------------\n", 921 | " Date: Sun Oct 04 12:05:57 2020 CEST\n", 922 | "\n", 923 | " Darwin : OS\n", 924 | " 4 : CPU(s)\n", 925 | " x86_64 : Machine\n", 926 | " 64bit : Architecture\n", 927 | " 8.0 GB : RAM\n", 928 | " Jupyter : Environment\n", 929 | "\n", 930 | " Python 3.7.2 (default, Dec 29 2018, 00:00:04) [Clang 4.0.1\n", 931 | " (tags/RELEASE_401/final)]\n", 932 | "\n", 933 | "1.7.1.dev32+g712d179.d20200111 : pylops\n", 934 | " 1.18.1 : numpy\n", 935 | " 1.5.0 : scipy\n", 936 | " 7.2.0 : IPython\n", 937 | " 3.3.1 : matplotlib\n", 938 | " 0.4.1 : scooby\n", 939 | "\n", 940 | " Intel(R) Math Kernel Library Version 2019.0.4 Product Build 20190411 for\n", 941 | " Intel(R) 64 architecture applications\n", 942 | "--------------------------------------------------------------------------------" 943 | ] 944 | }, 945 | "execution_count": 18, 946 | "metadata": {}, 947 | "output_type": "execute_result" 948 | } 949 | ], 950 | "source": [ 951 | "scooby.Report(core='pylops')" 952 | ] 953 | }, 954 | { 955 | "cell_type": "code", 956 | "execution_count": null, 957 | "metadata": {}, 958 | "outputs": [], 959 | "source": [] 960 | } 961 | ], 962 | "metadata": { 963 | "kernelspec": { 964 | "display_name": "Python 3", 965 | "language": "python", 966 | "name": "python3" 967 | }, 968 | "language_info": { 969 | "codemirror_mode": { 970 | "name": "ipython", 971 | "version": 3 972 | }, 973 | "file_extension": ".py", 974 | "mimetype": "text/x-python", 975 | "name": "python", 976 | "nbconvert_exporter": "python", 977 | "pygments_lexer": "ipython3", 978 | "version": "3.7.2" 979 | }, 980 | "toc": { 981 | "base_numbering": 1, 982 | "nav_menu": {}, 983 | "number_sections": true, 984 | "sideBar": true, 985 | "skip_h1_title": false, 986 | "title_cell": "Table of Contents", 987 | "title_sidebar": "Contents", 988 | "toc_cell": false, 989 | "toc_position": {}, 990 | "toc_section_display": true, 991 | "toc_window_display": false 992 | } 993 | }, 994 | "nbformat": 4, 995 | "nbformat_minor": 1 996 | } 997 | --------------------------------------------------------------------------------