diff --git a/README.md b/README.md index dc802b83ffe..fc1e97c5c25 100644 --- a/README.md +++ b/README.md @@ -56,6 +56,7 @@ where FOO, BAR, etc are the dependencies. Others require more complicated instal | [FFTW](https://github.com/pyFFTW/pyFFTW) | Accelerated [FourierTransform](http://odl.readthedocs.io/generated/odl.trafos.fourier.FourierTransform.html) | fftw | | [PyWavelets](https://github.com/PyWavelets/pywt) | Computation of the [WaveletTransform](http://odl.readthedocs.io/generated/odl.trafos.wavelet.WaveletTransform.html) | pywavelets | | [matplotlib](http://matplotlib.org/) | Visualization through the [show](http://odl.readthedocs.io/generated/odl.discr.lp_discr.DiscreteLpVector.show.html) command | show | +| [proximal](http://github.com/comp-imaging/ProxImaL) | Solution of some convex optimization problems | proximal | | [pytest](http://pytest.org/latest/) | Unit tests | testing | diff --git a/doc/source/guide/glossary.rst b/doc/source/guide/glossary.rst index d6e4b679b07..7f84fd3cf9b 100644 --- a/doc/source/guide/glossary.rst +++ b/doc/source/guide/glossary.rst @@ -76,17 +76,20 @@ Glossary proximal Given a closed proper convex function :math:`f`, the proximal operator is defined by - + .. math:: - + \text{prox}_f(v) = \arg\min_x f(x) + (1/2)||x - v||_2^2 - + + proximal is also occationally used instead of ProxImaL, then refering to the proximal + modelling language for the solution of convex optimization problems. + proximal factory - A proximal factory associated with a function :math:`f` is a `callable`, + A proximal factory associated with a function :math:`f` is a `callable`, which returns the proximal of the scaled function :math:`\sigma f` when called with a - scalar :math:`\sigma`. This is used due to the fact that optimization methods often use + scalar :math:`\sigma`. This is used due to the fact that optimization methods often use :math:`\text{prox}_{\sigma f}` for varying :math:`\sigma`. - + range Set of elements to which an operator maps, i.e. in which the result of an operator evaluation lies. diff --git a/doc/source/guide/in_depth/index.rst b/doc/source/guide/in_depth/index.rst index d5d421888df..3792e2f0d17 100644 --- a/doc/source/guide/in_depth/index.rst +++ b/doc/source/guide/in_depth/index.rst @@ -11,4 +11,5 @@ This is a more in depth guide to the different parts of ODL. linearspace_guide vectorization_guide numpy_guide + proximal_lang_guide chambolle_pock_guide diff --git a/doc/source/guide/in_depth/proximal_lang_guide.rst b/doc/source/guide/in_depth/proximal_lang_guide.rst new file mode 100644 index 00000000000..3c34002eeff --- /dev/null +++ b/doc/source/guide/in_depth/proximal_lang_guide.rst @@ -0,0 +1,52 @@ +.. _proximal_lang_in_depth: + +####################### +Using ODL with ProxImaL +####################### + +`Proximal +`_ is a Python-embedded modeling language for image optimization problems and can be used with ODL to solve typical inverse problems phrased as optimization problems. The package is especially suited for non-differentiable problems such as total variance denoising. + +Here is a minimal example of solving Poisson's equation equation on an interval with a TV type regularizer (:math:`\min_x \ 10||-\Delta x - rhs||_2^2 + ||\nabla x||_1`):: + + >>> space = odl.uniform_discr(0, 1, 5) + >>> op = -odl.Laplacian(space) + >>> proximal_lang_op = odl.as_proximal_lang_operator(op) + >>> rhs = space.element(lambda x: (x>0.4) & (x<0.6)) # indicator function on [0.4, 0.6] + >>> x = proximal.Variable(space.shape) + >>> prob = proximal.Problem([10 * proximal.sum_squares(x - rhs.asarray()), + >>> proximal.norm1(proximal.grad(x))]) + >>> prob.solve() + >>> x.value + array([ 0.02352054, 0.02647946, 0.9 , 0.02647946, 0.02352054]) + +Notable differences between ODL and ProxImaL +============================================ + +It may be tempting to try to convert an arbitrary problem from ODL into ProxImaL, but some differences exist. + +Norms +----- +Norms in ODL are scaled according to the underlying function space. Hence a sequence of statements converging discretizations give rise to a converging norm:: + + >>> for n in range(2, 10000): + ... X = odl.uniform_discr(0, 1, n) + ... print(X.element(lambda x: x).norm()) + 0.559016994375 + 0.576628129734 + 0.577343052266 + 0.577350268468 + >>> 1 / np.sqrt(3) # exact result + 0.577350269189 + +this is not the case in ProxImaL, where the norm depends on the number of discretization points. Hence a scaling that is correct for a problem in ODL needs not be correct in proximal. This also changes the definition of things like the operator norm. + +This also has the added effect of changing the definition of derived features, like the spectral norm of operators. + +Spaces +------ +ODL can represent some complicated spaces, like :math:`\mathbb{R}^3 \times \mathbb{C}^2` through the `ProductSpace` class:: + + >>> space = odl.ProductSpace(odl.rn(3), odl.cn(2)) + +This can then be used in solvers and other structures. ProxImaL currently lacks an equivalent structure. \ No newline at end of file diff --git a/doc/source/prs.inc b/doc/source/prs.inc index 4f1b6afc016..bae88c4a593 100644 --- a/doc/source/prs.inc +++ b/doc/source/prs.inc @@ -58,6 +58,7 @@ .. _PR 491: https://github.com/odlgroup/odl/pull/491 .. _PR 492: https://github.com/odlgroup/odl/pull/492 .. _PR 493: https://github.com/odlgroup/odl/pull/493 +.. _PR 494: https://github.com/odlgroup/odl/pull/494 .. _PR 495: https://github.com/odlgroup/odl/pull/495 .. _PR 500: https://github.com/odlgroup/odl/pull/500 .. _PR 502: https://github.com/odlgroup/odl/pull/502 diff --git a/doc/source/refs.rst b/doc/source/refs.rst index ed57397b4cb..260de325481 100644 --- a/doc/source/refs.rst +++ b/doc/source/refs.rst @@ -42,6 +42,9 @@ References .. [GNS2009] Griva, I, Nash, S G, and Sofer, A. *Linear and nonlinear optimization*. Siam, 2009. +.. [Hei+2016] Heide, F et al. *ProxImaL: Efficient Image Optimization using + Proximal Algorithms*. ACM Transactions on Graphics (TOG), 2016. + .. [Kva1991] Kvaalen, E. *A faster Broyden method*. BIT Numerical Mathematics 31 (1991), pp 369--372. diff --git a/doc/source/release_notes.rst b/doc/source/release_notes.rst index 89772bf0d6a..715fa82ded6 100644 --- a/doc/source/release_notes.rst +++ b/doc/source/release_notes.rst @@ -11,8 +11,7 @@ Next release New features ------------ -- First implementation of a deformation model based on linearized deformations using displacement - vector fields. (`PR 488`) +- Add option to interface with ProxImaL solvers using ODL operators. (`PR 494`) ODL 0.3.1 Release Notes (2016-08-15) ==================================== @@ -23,7 +22,7 @@ splitting. New features ------------ -- New solvers based on the Douglas-Rachford and forward-backward splitting schemes. (`PR 478`_, +- New solvers based on the Douglas-Rachford and forward-backward splitting schemes. (`PR 478`_, `PR 480`_) - ``NormOperator`` and ``DistOperator`` added. (`PR 487`_) - Single-element ``NtuplesBase`` vectors can now be converted to ``float``, ``complex`` etc. @@ -38,7 +37,7 @@ Improvements (`PR 489`_, `PR 482`_, `PR 491`_) - Clearer separation between attributes that are intended as part of the subclassing API and those that are not. (`PR 471`_) -- Chambolle-Pock solver accepts also non-linear operators and has better documentation now. +- Chambolle-Pock solver accepts also non-linear operators and has better documentation now. (`PR 490`_) - Clean-up of imports. (`PR 492`_) - All solvers now check that the given start value ``x`` is in ``op.domain``. (`PR 502`_) diff --git a/examples/solvers/proximal_lang_poisson.py b/examples/solvers/proximal_lang_poisson.py new file mode 100644 index 00000000000..f100953aee9 --- /dev/null +++ b/examples/solvers/proximal_lang_poisson.py @@ -0,0 +1,63 @@ +# Copyright 2014-2016 The ODL development group +# +# This file is part of ODL. +# +# ODL is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# ODL is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with ODL. If not, see . + +"""Poisson's problem using the ProxImaL solver. + +Solves the optimization problem + + min_x 10 ||laplacian(x) - g||_2^2 + || |grad(x)| ||_1 + +Where ``laplacian`` is the spatial Laplacian, ``grad`` the spatial +gradient and ``g`` is given noisy data. +""" + +import numpy as np +import odl +import proximal + +# Create space defined on a square from [0, 0] to [100, 100] with (100 x 100) +# points +space = odl.uniform_discr([0, 0], [100, 100], [100, 100]) + +# Create ODL operator for the Laplacian +laplacian = odl.Laplacian(space) + +# Create right hand side +phantom = odl.phantom.shepp_logan(space, modified=True) +phantom.show('original image') +rhs = laplacian(phantom) +rhs += odl.phantom.white_noise(space) * np.std(rhs) * 0.1 +rhs.show('rhs') + +# Convert laplacian to ProxImaL operator +proximal_lang_laplacian = odl.as_proximal_lang_operator(laplacian) + +# Convert to array +rhs_arr = rhs.asarray() + +# Set up optimization problem +x = proximal.Variable(space.shape) +funcs = [10 * proximal.sum_squares(proximal_lang_laplacian(x) - rhs_arr), + proximal.norm1(proximal.grad(x))] + +# Solve the problem using ProxImaL +prob = proximal.Problem(funcs) +prob.solve(verbose=True) + +# Convert back to odl and display result +result_odl = space.element(x.value) +result_odl.show('result from ProxImaL') diff --git a/examples/solvers/proximal_lang_tomography.py b/examples/solvers/proximal_lang_tomography.py new file mode 100644 index 00000000000..0b90f69fc26 --- /dev/null +++ b/examples/solvers/proximal_lang_tomography.py @@ -0,0 +1,89 @@ +# Copyright 2014-2016 The ODL development group +# +# This file is part of ODL. +# +# ODL is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# ODL is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with ODL. If not, see . + +"""Tomography with TV regularization using the ProxImaL solver. + +Solves the optimization problem + + min_{0 <= x <= 1} ||A(x) - g||_2^2 + 0.2 || |grad(x)| ||_1 + +Where ``A`` is a parallel beam forward projector, ``grad`` the spatial +gradient and ``g`` is given noisy data. +""" + +import numpy as np +import odl +import proximal + + +# --- Set up the forward operator (ray transform) --- # + + +# Discrete reconstruction space: discretized functions on the rectangle +# [-20, 20]^2 with 300 samples per dimension. +reco_space = odl.uniform_discr( + min_corner=[-20, -20], max_corner=[20, 20], nsamples=[300, 300], + dtype='float32') + +# Make a parallel beam geometry with flat detector +# Angles: uniformly spaced, n = 360, min = 0, max = 2 * pi +angle_partition = odl.uniform_partition(0, 2 * np.pi, 360) +# Detector: uniformly sampled, n = 558, min = -30, max = 30 +detector_partition = odl.uniform_partition(-30, 30, 558) +geometry = odl.tomo.Parallel2dGeometry(angle_partition, detector_partition) + +# The implementation of the ray transform to use, options: +# 'scikit' Requires scikit-image (can be installed by +# running ``pip install scikit-image``). +# 'astra_cpu', 'astra_cuda' Requires astra tomography to be installed. +# Astra is much faster than scikit. Webpage: +# https://github.com/astra-toolbox/astra-toolbox +impl = 'astra_cuda' + +# Initialize the ray transform (forward projection). +ray_trafo = odl.tomo.RayTransform(reco_space, geometry, impl=impl) + +# Convert ray transform to proximal language operator +proximal_lang_ray_trafo = odl.as_proximal_lang_operator(ray_trafo) + +# Create sinogram of forward projected phantom with noise +phantom = odl.phantom.shepp_logan(reco_space, modified=True) +phantom.show('phantom') +data = ray_trafo(phantom) +data += odl.phantom.white_noise(ray_trafo.range) * np.mean(data) * 0.1 +data.show('noisy data') + +# Convert to array for ProxImaL +rhs_arr = data.asarray() + +# Set up optimization problem +# Note that proximal is not aware of the underlying space and only works with +# matrices. Hence the norm in proximal does not match the norm in the ODL space +# exactly. +x = proximal.Variable(reco_space.shape) +funcs = [proximal.sum_squares(proximal_lang_ray_trafo(x) - rhs_arr), + 0.2 * proximal.norm1(proximal.grad(x)), + proximal.nonneg(x), + proximal.nonneg(1 - x)] + +# Solve the problem using ProxImaL +prob = proximal.Problem(funcs) +prob.solve(verbose=True) + +# Convert back to odl and display result +result_odl = reco_space.element(x.value) +result_odl.show('ProxImaL result') diff --git a/odl/operator/operator.py b/odl/operator/operator.py index 19e134dbc09..a05057a8e0a 100644 --- a/odl/operator/operator.py +++ b/odl/operator/operator.py @@ -538,7 +538,7 @@ def _call(self, x, out=None, **kwargs): pattern. See the `documentation - `_ + `_ for more info on in-place vs. out-of-place evaluation. Parameters diff --git a/odl/operator/oputils.py b/odl/operator/oputils.py index a286676701e..cfa4610938b 100644 --- a/odl/operator/oputils.py +++ b/odl/operator/oputils.py @@ -28,7 +28,8 @@ from odl.space.base_ntuples import FnBase from odl.space import ProductSpace -__all__ = ('matrix_representation', 'power_method_opnorm', 'as_scipy_operator') +__all__ = ('matrix_representation', 'power_method_opnorm', 'as_scipy_operator', + 'as_proximal_lang_operator') def matrix_representation(op): @@ -238,7 +239,7 @@ def as_scipy_operator(op): ----- If the data representation of ``op``'s domain and range is of type `NumpyFn` this incurs no significant overhead. If the data type is `CudaFn` - or other nonlocal type, the overhead is significant. + or some other nonlocal type, the overhead is significant. """ if not op.is_linear: raise ValueError('`op` needs to be linear') @@ -268,6 +269,53 @@ def rmatvec(v): rmatvec=rmatvec, dtype=dtype) + +def as_proximal_lang_operator(op, norm_bound=None): + """Wrap ``op`` as a ``proximal.BlackBox``. + + This is intended to be used with the `ProxImaL language solvers. + `_ + + Parameters + ---------- + op : `Operator` + Linear operator to be wrapped. Its domain and range must implement + ``shape``, and elements in these need to implement ``asarray``. + norm_bound : float, optional + An upper bound on the spectral norm of the operator. Note that this is + the norm as defined by ProxImaL, and hence use the unweighted spaces. + + Returns + ------- + ``proximal.BlackBox`` : proximal_lang_operator + The wrapped operator. + + Notes + ----- + If the data representation of ``op``'s domain and range is of type + `NumpyFn` this incurs no significant overhead. If the data type is `CudaFn` + or some other nonlocal type, the overhead is significant. + + References + ---------- + For documentation on the proximal language (ProxImaL) see [Hei+2016]_. + """ + + # TODO: use out parameter once "as editable array" is added + + def forward(inp, out): + out[:] = op(inp).asarray() + + def adjoint(inp, out): + out[:] = op.adjoint(inp).asarray() + + import proximal + return proximal.LinOpFactory(input_shape=op.domain.shape, + output_shape=op.range.shape, + forward=forward, + adjoint=adjoint, + norm_bound=norm_bound) + if __name__ == '__main__': # pylint: disable=wrong-import-position from odl.util.testutils import run_doctests diff --git a/setup.py b/setup.py index cce869142da..aa387ed19c1 100644 --- a/setup.py +++ b/setup.py @@ -138,7 +138,8 @@ def run_tests(self): 'fftw': 'pyfftw', 'pywavelets': 'Pywavelets', 'scikit' : 'scikit-image', - 'all': test_requires + ['matplotlib', 'pyfftw', 'Pywavelets', 'scikit-image'] + 'proximal' : 'proximal', + 'all': test_requires + ['matplotlib', 'pyfftw', 'Pywavelets', 'scikit-image', 'proximal'] }, cmdclass={'test': PyTest},