├── imreg ├── tests │ ├── __init__.py │ └── test_register.py ├── __init__.py ├── setup.py ├── sampler.py ├── metric.py ├── _interpolation.pyx ├── model.py └── register.py ├── .gitignore ├── CONTRIBUTORS.txt ├── pytest.ini ├── DEPENDS.txt ├── LICENSE.txt ├── .travis.yml ├── README.md ├── setup.py └── notebooks ├── math.ipynb └── linreg-equivalence.ipynb /imreg/tests/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | build/* 2 | *.so 3 | *.pyc 4 | *~ 5 | *sublime* 6 | 7 | -------------------------------------------------------------------------------- /imreg/__init__.py: -------------------------------------------------------------------------------- 1 | # http://semver.org/ 2 | 3 | __version__ = '0.1.0' 4 | -------------------------------------------------------------------------------- /CONTRIBUTORS.txt: -------------------------------------------------------------------------------- 1 | - Nathan Faggian 2 | 3 | - Stefan Van Der Walt 4 | 5 | - Riaan Van Den Dool 6 | 7 | -------------------------------------------------------------------------------- /pytest.ini: -------------------------------------------------------------------------------- 1 | [pytest] 2 | addopts = -v --doctest-modules --tb=short imreg/tests 3 | python_files = *.py 4 | 5 | -------------------------------------------------------------------------------- /DEPENDS.txt: -------------------------------------------------------------------------------- 1 | Build Requirements 2 | ------------------ 3 | * `Python >= 2.7 `_ 4 | * `Numpy >= 1.5 `_ 5 | * `Scipy >- 0.9 '_ 6 | 7 | Usage Requirements 8 | ------------------ 9 | * `Scipy `_ 10 | 11 | -------------------------------------------------------------------------------- /imreg/setup.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | 4 | def configuration(parent_package='', top_path=None): 5 | from numpy.distutils.misc_util import Configuration 6 | config = Configuration('imreg', parent_package, top_path) 7 | return config 8 | 9 | if __name__ == "__main__": 10 | from numpy.distutils.core import setup 11 | 12 | config = configuration(top_path='').todict() 13 | setup(**config) 14 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright 2013, 2 | Nathan Faggian, Stefan Van Der Walt, Riaan Van Den Dool. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | 16 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | # vim ft=yaml 2 | # travis-ci.org definition for skimage build 3 | # 4 | # We pretend to be erlang because we need can't use the python support in 5 | # travis-ci; it uses virtualenvs, they do not have numpy, scipy, matplotlib, 6 | # and it is impractical to build them 7 | 8 | language: erlang 9 | 10 | env: 11 | - PYTHON=python PYSUF='' PYVER=2.7 12 | 13 | install: 14 | - sudo apt-get update 15 | - sudo apt-get install $PYTHON-dev 16 | - sudo apt-get install $PYTHON-numpy 17 | - sudo apt-get install $PYTHON-scipy 18 | - sudo apt-get install $PYTHON-setuptools 19 | - sudo pip-$PYVER install cython 20 | - sudo pip-$PYVER install pytest 21 | - sudo apt-get install libfreeimage3 22 | - $PYTHON setup.py build_ext 23 | - sudo $PYTHON setup.py install 24 | 25 | script: 26 | - py.test 27 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Image Registration (imreg) 2 | ========================== 3 | 4 | The "imreg" package implements fast image registration methods using Python, Cython and numerical tools (scipy, numpy). 5 | 6 | This package uses `semantic versioning `. 7 | 8 | References: 9 | ----------- 10 | 11 | S. Baker and I. Matthews. Equivalence and efficiency of image alignment algorithms. 12 | In Proceedings of the 2001 IEEE Conference on Computer Vision and Pattern Recognition, 13 | Volume 1, Pages 1090 – 1097, December 2001. 14 | 15 | A very comprehensive 3 part series with Matlab implementations): 16 | 17 | http://www.ri.cmu.edu/research_project_detail.html?project_id=515&menu_id=261 18 | 19 | Maintainers 20 | ----------- 21 | 22 | - Nathan Faggian 23 | - Riaan Van Den Dool 24 | - Stefan Van Der Walt 25 | 26 | Testing 27 | ------- 28 | 29 | [![Build Status](https://travis-ci.org/pyimreg/imreg.png?branch=master)](https://travis-ci.org/pyimreg/imreg) 30 | 31 | Dependencies 32 | ------------ 33 | 34 | The required dependencies to build the software are: 35 | 36 | - python 37 | - numpy 38 | - scipy 39 | - cython 40 | - py.test 41 | 42 | Install 43 | ------- 44 | 45 | This packages uses distutils, which is the default way of installing python modules. To install in your home directory, use: 46 | 47 | python setup.py install --home 48 | 49 | To install for all users on Unix/Linux: 50 | 51 | python setup.py build 52 | sudo python setup.py install 53 | 54 | Development 55 | ----------- 56 | 57 | Follow: Fork + Pull Model:: 58 | 59 | http://help.github.com/send-pull-requests/ 60 | 61 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env python 2 | 3 | """ imreg package configuration """ 4 | import numpy 5 | import imreg 6 | 7 | from setuptools import setup, find_packages 8 | from distutils.extension import Extension 9 | from Cython.Distutils import build_ext 10 | 11 | DISTNAME = 'imreg' 12 | DESCRIPTION = 'Image registration toolkit' 13 | LONG_DESCRIPTION = """ 14 | "imreg" is an image registration package for python that makes it easy to 15 | automatically align image data. 16 | """ 17 | MAINTAINER = 'Nathan Faggian, Riaan Van Den Dool, Stefan Van Der Walt' 18 | MAINTAINER_EMAIL = 'nathan.faggian@gmail.com' 19 | URL = 'pyimreg.github.com' 20 | LICENSE = 'Apache License (2.0)' 21 | DOWNLOAD_URL = '' 22 | 23 | VERSION = imreg.__version__ 24 | 25 | CLASSIFIERS = [ 26 | 'Development Status :: 4 - Beta', 27 | 'Environment :: Console', 28 | 'Intended Audience :: Developers', 29 | 'Intended Audience :: Science/Research', 30 | 'License :: OSI Approved :: Apache Software License', 31 | 'Topic :: Scientific/Engineering' 32 | ] 33 | 34 | setup( 35 | name=DISTNAME, 36 | description=DESCRIPTION, 37 | long_description=LONG_DESCRIPTION, 38 | maintainer=MAINTAINER, 39 | maintainer_email=MAINTAINER_EMAIL, 40 | url=URL, 41 | license=LICENSE, 42 | download_url=DOWNLOAD_URL, 43 | version=VERSION, 44 | classifiers=CLASSIFIERS, 45 | packages=find_packages(), 46 | cmdclass={ 47 | 'build_ext': build_ext 48 | }, 49 | ext_modules=[Extension("imreg.interpolation", ["imreg/_interpolation.pyx"], )], 50 | include_dirs=[numpy.get_include(), ], 51 | package_data={}, 52 | install_requires=[ 53 | 'numpy', 54 | 'scipy' 55 | ], 56 | zip_safe=False 57 | ) 58 | -------------------------------------------------------------------------------- /imreg/sampler.py: -------------------------------------------------------------------------------- 1 | """ A collection of samplers""" 2 | 3 | import numpy as np 4 | import scipy.ndimage as nd 5 | 6 | try: 7 | import interpolation 8 | except ImportError as error: 9 | # Attempt autocompilation. 10 | import pyximport 11 | pyximport.install() 12 | import _interpolation as interpolation 13 | 14 | 15 | def nearest(image, warp): 16 | """ 17 | Nearest-neighbour interpolation. 18 | 19 | Parameters 20 | ---------- 21 | array: nd-array 22 | Input array for sampling. 23 | warp: nd-array 24 | Deformation coordinates. 25 | 26 | Returns 27 | ------- 28 | sample: nd-array 29 | Sampled array data. 30 | """ 31 | 32 | result = np.zeros_like(warp[0], dtype=np.float64) 33 | 34 | interpolation.nearest( 35 | warp.astype(np.float64), 36 | image.astype(np.float64), 37 | result 38 | ) 39 | 40 | return result 41 | 42 | 43 | def bilinear(image, warp): 44 | """ 45 | Bilinear interpolation. 46 | 47 | Parameters 48 | ---------- 49 | array: nd-array 50 | Input array for sampling. 51 | warp: nd-array 52 | Deformation coordinates. 53 | 54 | Returns 55 | ------- 56 | sample: nd-array 57 | Sampled array data. 58 | """ 59 | 60 | result = np.zeros_like(warp[0], dtype=np.float64) 61 | 62 | interpolation.bilinear( 63 | warp.astype(np.float64), 64 | image.astype(np.float64), 65 | result 66 | ) 67 | 68 | return result 69 | 70 | 71 | def spline(image, warp): 72 | """ 73 | Spline interpolation. 74 | 75 | Parameters 76 | ---------- 77 | array: nd-array 78 | Input array for sampling. 79 | warp: nd-array 80 | Deformation coordinates. 81 | 82 | Returns 83 | ------- 84 | sample: nd-array 85 | Sampled array data. 86 | """ 87 | 88 | return nd.map_coordinates( 89 | image, 90 | warp, 91 | order=3, 92 | mode='nearest' 93 | ) 94 | -------------------------------------------------------------------------------- /imreg/tests/test_register.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | 3 | import scipy.ndimage as nd 4 | import scipy.misc as misc 5 | 6 | from imreg import model, register, sampler, metric 7 | 8 | 9 | def deform(image, p, deformationModel): 10 | """ 11 | Warps an image. 12 | """ 13 | 14 | coords = model.Coordinates([0, image.shape[0], 0, image.shape[1]]) 15 | 16 | return sampler.bilinear(image, deformationModel(p, coords).tensor) 17 | 18 | 19 | def pytest_generate_tests(metafunc): 20 | """ 21 | Generates a set of test for the registration methods. 22 | """ 23 | methods = [ 24 | ('additive', metric.forwardsAdditive), 25 | ('compositional', metric.forwardsCompositional), 26 | ('inverse-compositional', metric.inverseCompositional) 27 | ] 28 | 29 | image = misc.lena() 30 | image = nd.zoom(image, 0.25) 31 | 32 | if metafunc.function is test_shift: 33 | 34 | for (method_name, method) in methods: 35 | 36 | for displacement in np.arange(-10., 11., 2.): 37 | p = np.array([displacement, displacement]) 38 | template = deform(image, p, model.Shift()) 39 | 40 | metafunc.addcall( 41 | id='method={}, dx={}, dy={}'.format(method_name, p[0], p[1]), 42 | funcargs=dict( 43 | image=image, 44 | template=template, 45 | method=method, 46 | p=p 47 | ) 48 | ) 49 | 50 | if metafunc.function is test_affine: 51 | 52 | for (method_name, method) in methods: 53 | # test the displacement component 54 | for displacement in np.arange(-10., 10., 2.): 55 | p = np.array([0., 0., 0., 0., displacement, displacement]) 56 | template = deform(image, p, model.Affine()) 57 | metafunc.addcall( 58 | id='method={}, dx={}, dy={}'.format(method_name, p[4], p[5]), 59 | funcargs=dict( 60 | image=image, 61 | template=template, 62 | method=method, 63 | p=p 64 | ) 65 | ) 66 | 67 | 68 | def test_shift(image, template, method, p): 69 | """ 70 | Tests image registration using a shift deformation model. 71 | """ 72 | 73 | shift = register.Register() 74 | 75 | # Coerce the image data into RegisterData. 76 | image = register.RegisterData(image) 77 | template = register.RegisterData(template) 78 | 79 | step, _search = shift.register(image, template, model.Shift(), method=method) 80 | 81 | assert np.allclose(p, step.p, atol=0.5), \ 82 | "Estimated p: {} not equal to p: {}".format( 83 | step.p, 84 | p 85 | ) 86 | 87 | 88 | def test_affine(image, template, method, p): 89 | """ 90 | Tests image registration using a affine deformation model. 91 | """ 92 | 93 | affine = register.Register() 94 | 95 | # Coerce the image data into RegisterData. 96 | image = register.RegisterData(image) 97 | template = register.RegisterData(template) 98 | 99 | step, _search = affine.register(image, template, model.Affine(), method=method) 100 | 101 | assert np.allclose(p, step.p, atol=0.5), \ 102 | "Estimated p: {} not equal to p: {}".format( 103 | step.p, 104 | p 105 | ) 106 | -------------------------------------------------------------------------------- /imreg/metric.py: -------------------------------------------------------------------------------- 1 | """ A collection of image similarity metrics. """ 2 | 3 | import collections 4 | 5 | import numpy as np 6 | from scipy import ndimage 7 | 8 | # Container for registration methods: 9 | 10 | Method = collections.namedtuple('method', 'jacobian error update') 11 | 12 | 13 | def gradient(image, variance=0.1): 14 | """ Computes the image gradient """ 15 | grad = np.gradient(image) 16 | 17 | dIx = ndimage.gaussian_filter(grad[1], variance).flatten() 18 | dIy = ndimage.gaussian_filter(grad[0], variance).flatten() 19 | 20 | return dIx, dIy 21 | 22 | # ============================================================================== 23 | # Forwards additive: 24 | # ============================================================================== 25 | 26 | 27 | def forwardsAdditiveJacobian(image, template, model, p): 28 | """ 29 | Computes the jacobian dP/dE. 30 | 31 | Parameters 32 | ---------- 33 | model: deformation model 34 | A particular deformation model. 35 | warpedImage: nd-array 36 | Input image after warping. 37 | p : optional list 38 | Current warp parameters 39 | 40 | Returns 41 | ------- 42 | jacobian: nd-array 43 | A jacobain matrix. (m x n) 44 | | where: m = number of image pixels, 45 | | p = number of parameters. 46 | """ 47 | 48 | dIx, dIy = gradient(image) 49 | 50 | dPx, dPy = model.jacobian(template.coords, p) 51 | 52 | J = np.zeros_like(dPx) 53 | for index in range(0, dPx.shape[1]): 54 | J[:, index] = (dPx[:, index] * dIx) + (dPy[:, index] * dIy) 55 | return J 56 | 57 | 58 | def forwardsAdditiveError(image, template): 59 | """ Compute the forwards additive error """ 60 | return (template - image).flatten() 61 | 62 | 63 | def forwardsAdditiveUpdate(p, deltaP, model=None): 64 | """ Compute the forwards additive error """ 65 | return p + deltaP 66 | 67 | # Define the forwards additive approach: 68 | 69 | forwardsAdditive = Method( 70 | forwardsAdditiveJacobian, 71 | forwardsAdditiveError, 72 | forwardsAdditiveUpdate 73 | ) 74 | 75 | # ============================================================================== 76 | # Forwards compositional. 77 | # ============================================================================== 78 | 79 | 80 | def forwardsCompositionalError(image, template): 81 | """ Compute the forwards additive error """ 82 | return (template - image).flatten() 83 | 84 | 85 | def forwardsCompositionalUpdate(p, deltaP, tform): 86 | """ Compute the forwards additive error """ 87 | return tform.vector(np.dot(tform.matrix(p), tform.matrix(deltaP))) 88 | 89 | 90 | forwardsCompositional = Method( 91 | forwardsAdditiveJacobian, 92 | forwardsCompositionalError, 93 | forwardsCompositionalUpdate 94 | ) 95 | 96 | 97 | # ============================================================================== 98 | # Inverse compositional. 99 | # ============================================================================== 100 | 101 | 102 | def inverseCompositionalJacobian(image, template, model, p): 103 | """ 104 | Computes the jacobian dP/dE. 105 | 106 | Parameters 107 | ---------- 108 | model: deformation model 109 | A particular deformation model. 110 | warpedImage: nd-array 111 | Input image after warping. 112 | p : optional list 113 | Current warp parameters 114 | 115 | Returns 116 | ------- 117 | jacobian: nd-array 118 | A jacobain matrix. (m x n) 119 | | where: m = number of image pixels, 120 | | p = number of parameters. 121 | """ 122 | 123 | dIx, dIy = gradient(template.data, variance=1.5) 124 | 125 | dPx, dPy = model.jacobian(template.coords, p) 126 | 127 | J = np.zeros_like(dPx) 128 | for index in range(0, dPx.shape[1]): 129 | J[:, index] = (dPx[:, index] * dIx) + (dPy[:, index] * dIy) 130 | return J 131 | 132 | 133 | def inverseCompositionalError(image, template): 134 | """ Compute the inverse additive error """ 135 | return (image - template).flatten() 136 | 137 | 138 | def inverseCompositionalUpdate(p, deltaP, tform): 139 | """ Compute the inverse compositional error """ 140 | 141 | return tform.vector( 142 | np.dot(tform.matrix(p), np.linalg.inv(tform.matrix(deltaP))) 143 | ) 144 | 145 | 146 | inverseCompositional = Method( 147 | inverseCompositionalJacobian, 148 | inverseCompositionalError, 149 | inverseCompositionalUpdate 150 | ) 151 | 152 | -------------------------------------------------------------------------------- /notebooks/math.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "metadata": { 3 | "name": "math" 4 | }, 5 | "nbformat": 3, 6 | "nbformat_minor": 0, 7 | "worksheets": [ 8 | { 9 | "cells": [ 10 | { 11 | "cell_type": "markdown", 12 | "metadata": {}, 13 | "source": [ 14 | "Sympy to calculate partial derivatives\n", 15 | "======================================" 16 | ] 17 | }, 18 | { 19 | "cell_type": "code", 20 | "collapsed": false, 21 | "input": [ 22 | "from sympy import Symbol, symbols, Matrix, diff, expand" 23 | ], 24 | "language": "python", 25 | "metadata": {}, 26 | "outputs": [], 27 | "prompt_number": 1 28 | }, 29 | { 30 | "cell_type": "code", 31 | "collapsed": false, 32 | "input": [ 33 | "x, y = symbols('x,y')" 34 | ], 35 | "language": "python", 36 | "metadata": {}, 37 | "outputs": [], 38 | "prompt_number": 2 39 | }, 40 | { 41 | "cell_type": "code", 42 | "collapsed": false, 43 | "input": [ 44 | "p0,p1,p2,p3,p4,p5,p6,p7 = symbols('p0,p1,p2,p3,p4,p5,p6,p7')" 45 | ], 46 | "language": "python", 47 | "metadata": {}, 48 | "outputs": [], 49 | "prompt_number": 3 50 | }, 51 | { 52 | "cell_type": "code", 53 | "collapsed": false, 54 | "input": [ 55 | "H = Matrix([(p0+1,p3,p6), (p1,p4+1,p7), (p2,p5,1.0)])\n", 56 | "P = Matrix([(x),(y),(1.0)])\n", 57 | "H" 58 | ], 59 | "language": "python", 60 | "metadata": {}, 61 | "outputs": [ 62 | { 63 | "output_type": "pyout", 64 | "prompt_number": 20, 65 | "text": [ 66 | "[p0 + 1, p3, p6]\n", 67 | "[ p1, p4 + 1, p7]\n", 68 | "[ p2, p5, 1.0]" 69 | ] 70 | } 71 | ], 72 | "prompt_number": 20 73 | }, 74 | { 75 | "cell_type": "code", 76 | "collapsed": false, 77 | "input": [ 78 | "equations = H*P" 79 | ], 80 | "language": "python", 81 | "metadata": {}, 82 | "outputs": [], 83 | "prompt_number": 21 84 | }, 85 | { 86 | "cell_type": "code", 87 | "collapsed": false, 88 | "input": [ 89 | "equations" 90 | ], 91 | "language": "python", 92 | "metadata": {}, 93 | "outputs": [ 94 | { 95 | "output_type": "pyout", 96 | "prompt_number": 22, 97 | "text": [ 98 | "[p3*y + 1.0*p6 + x*(p0 + 1)]\n", 99 | "[p1*x + 1.0*p7 + y*(p4 + 1)]\n", 100 | "[ p2*x + p5*y + 1.0]" 101 | ] 102 | } 103 | ], 104 | "prompt_number": 22 105 | }, 106 | { 107 | "cell_type": "code", 108 | "collapsed": false, 109 | "input": [ 110 | "xequation = equations[0] / equations[2]" 111 | ], 112 | "language": "python", 113 | "metadata": {}, 114 | "outputs": [], 115 | "prompt_number": 23 116 | }, 117 | { 118 | "cell_type": "code", 119 | "collapsed": false, 120 | "input": [ 121 | "yequation = equations[1] / equations[2]" 122 | ], 123 | "language": "python", 124 | "metadata": {}, 125 | "outputs": [], 126 | "prompt_number": 25 127 | }, 128 | { 129 | "cell_type": "markdown", 130 | "metadata": {}, 131 | "source": [ 132 | "Formulate equations for partial derivatives:" 133 | ] 134 | }, 135 | { 136 | "cell_type": "code", 137 | "collapsed": false, 138 | "input": [ 139 | "for var in (p0,p1,p2,p3,p4,p5,p6,p7):\n", 140 | " eq = diff(xequation, var)\n", 141 | " print '{}: {}'.format(var, eq)" 142 | ], 143 | "language": "python", 144 | "metadata": {}, 145 | "outputs": [ 146 | { 147 | "output_type": "stream", 148 | "stream": "stdout", 149 | "text": [ 150 | "p0: x/(p2*x + p5*y + 1.0)\n", 151 | "p1: 0\n", 152 | "p2: -x*(p3*y + 1.0*p6 + x*(p0 + 1))/(p2*x + p5*y + 1.0)**2\n", 153 | "p3: y/(p2*x + p5*y + 1.0)\n", 154 | "p4: 0\n", 155 | "p5: -y*(p3*y + 1.0*p6 + x*(p0 + 1))/(p2*x + p5*y + 1.0)**2\n", 156 | "p6: 1.0/(p2*x + p5*y + 1.0)\n", 157 | "p7: 0\n" 158 | ] 159 | } 160 | ], 161 | "prompt_number": 29 162 | }, 163 | { 164 | "cell_type": "code", 165 | "collapsed": false, 166 | "input": [ 167 | "for var in (p0,p1,p2,p3,p4,p5,p6,p7):\n", 168 | " eq = diff(yequation, var)\n", 169 | " print '{}: {}'.format(var, eq)" 170 | ], 171 | "language": "python", 172 | "metadata": {}, 173 | "outputs": [ 174 | { 175 | "output_type": "stream", 176 | "stream": "stdout", 177 | "text": [ 178 | "p0: 0\n", 179 | "p1: x/(p2*x + p5*y + 1.0)\n", 180 | "p2: -x*(p1*x + 1.0*p7 + y*(p4 + 1))/(p2*x + p5*y + 1.0)**2\n", 181 | "p3: 0\n", 182 | "p4: y/(p2*x + p5*y + 1.0)\n", 183 | "p5: -y*(p1*x + 1.0*p7 + y*(p4 + 1))/(p2*x + p5*y + 1.0)**2\n", 184 | "p6: 0\n", 185 | "p7: 1.0/(p2*x + p5*y + 1.0)\n" 186 | ] 187 | } 188 | ], 189 | "prompt_number": 30 190 | }, 191 | { 192 | "cell_type": "code", 193 | "collapsed": false, 194 | "input": [], 195 | "language": "python", 196 | "metadata": {}, 197 | "outputs": [] 198 | } 199 | ], 200 | "metadata": {} 201 | } 202 | ] 203 | } -------------------------------------------------------------------------------- /imreg/_interpolation.pyx: -------------------------------------------------------------------------------- 1 | #cython: cdivision=True 2 | #cython: boundscheck=False 3 | #cython: nonecheck=False 4 | #cython: wraparound=False 5 | 6 | cimport numpy as cnp 7 | 8 | from libc.math cimport ceil, floor 9 | 10 | def nearest( 11 | cnp.ndarray[double, ndim=3, mode="c"] warp, 12 | cnp.ndarray[double, ndim=2, mode="c"] image, 13 | cnp.ndarray[double, ndim=2, mode="c"] output 14 | ): 15 | """ Computes a nearest neighbor sample """ 16 | 17 | cdef int out_rows = warp[0].shape[1] 18 | cdef int out_cols = warp[0].shape[0] 19 | 20 | cdef int img_rows = image.shape[1] 21 | cdef int img_cols = image.shape[0] 22 | 23 | cdef double rhat, chat 24 | cdef int r, c 25 | 26 | for r in xrange(out_rows): 27 | for c in xrange(out_cols): 28 | output[r][c] = nearest_neighbour_interpolation( 29 | image.data, 30 | img_rows, 31 | img_cols, 32 | warp[0, r, c], 33 | warp[1, r, c], 34 | 'C', 35 | 0.0 36 | ) 37 | return 0 38 | 39 | 40 | def bilinear( 41 | cnp.ndarray[double, ndim=3, mode="c"] warp, 42 | cnp.ndarray[double, ndim=2, mode="c"] image, 43 | cnp.ndarray[double, ndim=2, mode="c"] output 44 | ): 45 | """ Computes a bilinear sample """ 46 | 47 | cdef int out_rows = warp[0].shape[1] 48 | cdef int out_cols = warp[0].shape[0] 49 | 50 | cdef int img_rows = image.shape[1] 51 | cdef int img_cols = image.shape[0] 52 | 53 | cdef double rhat, chat 54 | cdef int r, c 55 | 56 | 57 | for r in xrange(out_rows): 58 | for c in xrange(out_cols): 59 | output[r][c] = bilinear_interpolation( 60 | image.data, 61 | img_rows, 62 | img_cols, 63 | warp[0, r, c], 64 | warp[1, r, c], 65 | 'C', 66 | 0.0 67 | ) 68 | return 0 69 | 70 | 71 | # Code below is directly from skimage - 72 | # - latest commit @ 4ff97da8b15c987b5eb5b2944395cff812715e0e 73 | 74 | 75 | cdef inline int round(double r): 76 | return ((r + 0.5) if (r > 0.0) else (r - 0.5)) 77 | 78 | 79 | cdef inline double nearest_neighbour_interpolation(double* image, int rows, 80 | int cols, double r, 81 | double c, char mode, 82 | double cval): 83 | """Nearest neighbour interpolation at a given position in the image. 84 | 85 | Parameters 86 | ---------- 87 | image : double array 88 | Input image. 89 | rows, cols : int 90 | Shape of image. 91 | r, c : double 92 | Position at which to interpolate. 93 | mode : {'C', 'W', 'R', 'N'} 94 | Wrapping mode. Constant, Wrap, Reflect or Nearest. 95 | cval : double 96 | Constant value to use for constant mode. 97 | 98 | Returns 99 | ------- 100 | value : double 101 | Interpolated value. 102 | 103 | """ 104 | 105 | return get_pixel2d(image, rows, cols, round(r), round(c), 106 | mode, cval) 107 | 108 | 109 | cdef inline double bilinear_interpolation(double* image, int rows, int cols, 110 | double r, double c, char mode, 111 | double cval): 112 | """Bilinear interpolation at a given position in the image. 113 | 114 | Parameters 115 | ---------- 116 | image : double array 117 | Input image. 118 | rows, cols : int 119 | Shape of image. 120 | r, c : double 121 | Position at which to interpolate. 122 | mode : {'C', 'W', 'R', 'N'} 123 | Wrapping mode. Constant, Wrap, Reflect or Nearest. 124 | cval : double 125 | Constant value to use for constant mode. 126 | 127 | Returns 128 | ------- 129 | value : double 130 | Interpolated value. 131 | 132 | """ 133 | cdef double dr, dc 134 | cdef int minr, minc, maxr, maxc 135 | 136 | minr = floor(r) 137 | minc = floor(c) 138 | maxr = ceil(r) 139 | maxc = ceil(c) 140 | dr = r - minr 141 | dc = c - minc 142 | top = (1 - dc) * get_pixel2d(image, rows, cols, minr, minc, mode, cval) \ 143 | + dc * get_pixel2d(image, rows, cols, minr, maxc, mode, cval) 144 | bottom = (1 - dc) * get_pixel2d(image, rows, cols, maxr, minc, mode, cval) \ 145 | + dc * get_pixel2d(image, rows, cols, maxr, maxc, mode, cval) 146 | return (1 - dr) * top + dr * bottom 147 | 148 | 149 | cdef inline double get_pixel2d(double* image, int rows, int cols, int r, int c, 150 | char mode, double cval): 151 | """Get a pixel from the image, taking wrapping mode into consideration. 152 | 153 | Parameters 154 | ---------- 155 | image : double array 156 | Input image. 157 | rows, cols : int 158 | Shape of image. 159 | r, c : int 160 | Position at which to get the pixel. 161 | mode : {'C', 'W', 'R', 'N'} 162 | Wrapping mode. Constant, Wrap, Reflect or Nearest. 163 | cval : double 164 | Constant value to use for constant mode. 165 | 166 | Returns 167 | ------- 168 | value : double 169 | Pixel value at given position. 170 | 171 | """ 172 | if mode == 'C': 173 | if (r < 0) or (r > rows - 1) or (c < 0) or (c > cols - 1): 174 | return cval 175 | else: 176 | return image[r * cols + c] 177 | else: 178 | return image[coord_map(rows, r, mode) * cols + coord_map(cols, c, mode)] 179 | 180 | 181 | cdef inline int coord_map(int dim, int coord, char mode): 182 | """ 183 | Wrap a coordinate, according to a given mode. 184 | 185 | Parameters 186 | ---------- 187 | dim : int 188 | Maximum coordinate. 189 | coord : int 190 | Coord provided by user. May be < 0 or > dim. 191 | mode : {'W', 'R', 'N'} 192 | Whether to wrap or reflect the coordinate if it 193 | falls outside [0, dim). 194 | 195 | """ 196 | dim = dim - 1 197 | if mode == 'R': # reflect 198 | if coord < 0: 199 | # How many times times does the coordinate wrap? 200 | if (-coord / dim) % 2 != 0: 201 | return dim - (-coord % dim) 202 | else: 203 | return (-coord % dim) 204 | elif coord > dim: 205 | if (coord / dim) % 2 != 0: 206 | return (dim - (coord % dim)) 207 | else: 208 | return (coord % dim) 209 | elif mode == 'W': # wrap 210 | if coord < 0: 211 | return (dim - (-coord % dim)) 212 | elif coord > dim: 213 | return (coord % dim) 214 | elif mode == 'N': # nearest 215 | if coord < 0: 216 | return 0 217 | elif coord > dim: 218 | return dim 219 | 220 | return coord 221 | -------------------------------------------------------------------------------- /imreg/model.py: -------------------------------------------------------------------------------- 1 | """ A collection of deformation models. """ 2 | 3 | import numpy as np 4 | 5 | 6 | class Coordinates(object): 7 | """ 8 | Container for grid coordinates. 9 | 10 | Attributes 11 | ---------- 12 | domain : nd-array 13 | Domain of the coordinate system. 14 | tensor : nd-array 15 | Grid coordinates. 16 | homogenous : nd-array 17 | `Homogenous` coordinate system representation of grid coordinates. 18 | """ 19 | 20 | def __init__(self, domain, tensor=None): 21 | 22 | self.domain = domain 23 | 24 | if tensor is None: 25 | self.tensor = np.mgrid[domain[0]:domain[1], domain[2]:domain[3]] 26 | else: 27 | self.tensor = tensor 28 | 29 | self.homogenous = np.zeros((3, self.tensor[0].size)) 30 | self.homogenous[0] = self.tensor[1].flatten() 31 | self.homogenous[1] = self.tensor[0].flatten() 32 | self.homogenous[2] = 1.0 33 | 34 | @staticmethod 35 | def fromTensor(tensor): 36 | domain = [tensor[1].min(), tensor[1].max(), tensor[0].min(), tensor[0].max()] 37 | return Coordinates(domain, tensor) 38 | 39 | @property 40 | def xy(self): 41 | return self.homogenous[0], self.homogenous[1] 42 | 43 | 44 | # ============================================================================== 45 | # 2 - DOF 46 | # ============================================================================== 47 | 48 | 49 | class Shift(object): 50 | """ 51 | Applies the shift coordinate transformation. Follows the derivations 52 | shown in: 53 | 54 | S. Baker and I. Matthews. 2004. Lucas-Kanade 20 Years On: A 55 | Unifying Framework. Int. J. Comput. Vision 56, 3 (February 2004). 56 | """ 57 | 58 | def __call__(self, p, coords): 59 | return Coordinates.fromTensor(self.transform(p, coords)) 60 | 61 | @property 62 | def identity(self): 63 | return np.zeros(2) 64 | 65 | def matrix(self, p): 66 | return np.array([ 67 | (1.0, 0.0, p[0]), 68 | (0.0, 1.0, p[1]), 69 | (0.0, 0.0, 1.0) 70 | ]) 71 | 72 | def vector(self, H): 73 | return H[0:2, 2] 74 | 75 | def transform(self, p, coords): 76 | """ 77 | A "shift" transformation of coordinates. 78 | """ 79 | 80 | displacement = np.dot(self.matrix(p), coords.homogenous) 81 | 82 | shape = coords.tensor[0].shape 83 | 84 | return np.array( 85 | [displacement[1].reshape(shape), displacement[0].reshape(shape)] 86 | ) 87 | 88 | def jacobian(self, coords, p=None): 89 | """ 90 | Evaluates the derivative of deformation model with respect to the 91 | coordinates. 92 | """ 93 | 94 | dx = np.zeros((coords.tensor[0].size, 2)) 95 | dy = np.zeros((coords.tensor[0].size, 2)) 96 | 97 | dx[:, 0] = 1.0 98 | dy[:, 1] = 1.0 99 | 100 | return (dx, dy) 101 | 102 | # ============================================================================== 103 | # 6 - DOF 104 | # ============================================================================== 105 | 106 | 107 | class Affine(object): 108 | """ 109 | Applies the affine coordinate transformation. Follows the derivations 110 | shown in: 111 | 112 | S. Baker and I. Matthews. 2004. Lucas-Kanade 20 Years On: A 113 | Unifying Framework. Int. J. Comput. Vision 56, 3 (February 2004). 114 | """ 115 | 116 | def __call__(self, p, coords): 117 | return Coordinates.fromTensor(self.transform(p, coords)) 118 | 119 | @property 120 | def identity(self): 121 | return np.zeros(6) 122 | 123 | def matrix(self, p): 124 | return np.array([ 125 | [p[0] + 1.0, p[2], p[4]], 126 | [p[1], p[3] + 1.0, p[5]], 127 | [0.0, 0.0, 1.0] 128 | ]) 129 | 130 | def vector(self, H): 131 | return np.array( 132 | [H[0, 0] - 1.0, 133 | H[1, 0], 134 | H[0, 1], 135 | H[1, 1] - 1.0, 136 | H[0, 2], 137 | H[1, 2], 138 | ]) 139 | 140 | def transform(self, p, coords): 141 | """ 142 | An "affine" transformation of coordinates. 143 | """ 144 | 145 | displacement = np.dot(self.matrix(p), coords.homogenous) 146 | shape = coords.tensor[0].shape 147 | 148 | return np.array( 149 | [displacement[1].reshape(shape), displacement[0].reshape(shape)] 150 | ) 151 | 152 | def jacobian(self, coords, p=None): 153 | """" 154 | Evaluates the derivative of deformation model with respect to the 155 | coordinates. 156 | """ 157 | 158 | x, y = coords.xy 159 | 160 | dx = np.zeros((x.size, 6)) 161 | dy = np.zeros((x.size, 6)) 162 | 163 | dx[:, 0] = x 164 | dx[:, 2] = y 165 | dx[:, 4] = 1.0 166 | 167 | dy[:, 1] = x 168 | dy[:, 3] = y 169 | dy[:, 5] = 1.0 170 | 171 | return (dx, dy) 172 | 173 | # ============================================================================== 174 | # 8 - DOF 175 | # ============================================================================== 176 | 177 | 178 | class Homography(object): 179 | """ 180 | Applies the projective coordinate transformation. Follows the derivations 181 | shown in: 182 | 183 | S. Baker and I. Matthews. 2004. Lucas-Kanade 20 Years On: A 184 | Unifying Framework. Int. J. Comput. Vision 56, 3 (February 2004). 185 | """ 186 | 187 | def __call__(self, p, coords): 188 | return Coordinates.fromTensor(self.transform(p, coords)) 189 | 190 | @property 191 | def identity(self): 192 | return np.zeros(8) 193 | 194 | def matrix(self, p): 195 | return np.array([ 196 | [p[0] + 1.0, p[3], p[6]], 197 | [p[1], p[4] + 1.0, p[7]], 198 | [p[2], p[5], 1.0] 199 | ]) 200 | 201 | def vector(self, H): 202 | return np.array( 203 | [H[0, 0] - 1.0, 204 | H[1, 0], 205 | H[2, 0], 206 | H[0, 1], 207 | H[1, 1] - 1.0, 208 | H[2, 1], 209 | H[0, 2], 210 | H[1, 2], 211 | ]) 212 | 213 | def transform(self, p, coords): 214 | """ 215 | An "projective" transformation of coordinates. 216 | """ 217 | 218 | displacement = np.dot(self.matrix(p), coords.homogenous) 219 | shape = coords.tensor[0].shape 220 | return np.array( 221 | [displacement[1].reshape(shape), displacement[0].reshape(shape)] 222 | ) 223 | 224 | def jacobian(self, coords, p): 225 | """" 226 | Evaluates the derivative of deformation model with respect to the 227 | coordinates. 228 | """ 229 | x, y = coords.xy 230 | 231 | p0, p1, p2, p3, p4, p5, p6, p7 = p 232 | 233 | dx = np.zeros((x.size, 8)) 234 | dy = np.zeros((x.size, 8)) 235 | 236 | dx[:, 0] = x/(p2*x + p5*y + 1.0) 237 | dx[:, 1] = 0.0 238 | dx[:, 2] = -x*(p3*y + 1.0*p6 + x*(p0 + 1))/(p2*x + p5*y + 1.0)**2 239 | dx[:, 3] = y/(p2*x + p5*y + 1.0) 240 | dx[:, 4] = 0.0 241 | dx[:, 5] = -y*(p3*y + 1.0*p6 + x*(p0 + 1))/(p2*x + p5*y + 1.0)**2 242 | dx[:, 6] = 1.0/(p2*x + p5*y + 1.0) 243 | dx[:, 7] = 0.0 244 | 245 | dy[:, 0] = 0.0 246 | dy[:, 1] = x/(p2*x + p5*y + 1.0) 247 | dy[:, 2] = -x*(p1*x + 1.0*p7 + y*(p4 + 1))/(p2*x + p5*y + 1.0)**2 248 | dy[:, 3] = 0.0 249 | dy[:, 4] = y/(p2*x + p5*y + 1.0) 250 | dy[:, 5] = -y*(p1*x + 1.0*p7 + y*(p4 + 1))/(p2*x + p5*y + 1.0)**2 251 | dy[:, 6] = 0.0 252 | dy[:, 7] = 1.0/(p2*x + p5*y + 1.0) 253 | 254 | return (dx, dy) 255 | -------------------------------------------------------------------------------- /imreg/register.py: -------------------------------------------------------------------------------- 1 | """ A top level registration module """ 2 | 3 | import numpy as np 4 | 5 | import logging 6 | 7 | import metric 8 | import model 9 | import sampler 10 | 11 | # Setup a loger. 12 | log = logging.getLogger('imreg.register') 13 | 14 | REGISTRATION_STEP = """ 15 | ================================================================================ 16 | iteration : {0} 17 | parameters : {1} 18 | error : {2} 19 | ================================================================================ 20 | """ 21 | 22 | REGISTRATION_STOP = """ 23 | ================================================================================ 24 | Optimization break, maximum number of bad iterations exceeded. 25 | ================================================================================ 26 | """ 27 | 28 | 29 | class RegisterData(object): 30 | """ 31 | Container for registration data. 32 | 33 | Attributes 34 | ---------- 35 | data : nd-array 36 | The image registration image values. 37 | coords : nd-array, optional 38 | The grid coordinates. 39 | """ 40 | 41 | def __init__(self, data, coords=None, features=None, spacing=1.0): 42 | 43 | self.data = data.astype(np.double) 44 | 45 | if not coords: 46 | self.coords = model.Coordinates( 47 | [0, data.shape[0], 0, data.shape[1]], 48 | ) 49 | else: 50 | self.coords = coords 51 | 52 | 53 | class optStep(): 54 | """ 55 | A container class for optimization steps. 56 | 57 | Attributes 58 | ---------- 59 | error: float 60 | Normalised fitting error. 61 | p: nd-array 62 | Model parameters. 63 | deltaP: nd-array 64 | Model parameter update vector. 65 | decreasing: boolean. 66 | State of the error function at this point. 67 | """ 68 | 69 | def __init__(self, error=None, p=None, deltaP=None, decreasing=None): 70 | self.error = error 71 | self.p = p 72 | self.deltaP = deltaP 73 | self.decreasing = decreasing 74 | 75 | 76 | class Register(object): 77 | """ 78 | A registration class for estimating the deformation model parameters that 79 | best solve: 80 | 81 | | :math:`f( W(I;p), T )` 82 | | 83 | | where: 84 | | :math:`f` : is a similarity metric. 85 | | :math:`W(x;p)`: is a deformation model (defined by the parameter set p). 86 | | :math:`I` : is an input image (to be deformed). 87 | | :math:`T` : is a template (which is a deformed version of the input). 88 | 89 | Notes: 90 | ------ 91 | 92 | Solved using a modified gradient descent algorithm. 93 | 94 | .. [0] Levernberg-Marquardt algorithm, 95 | http://en.wikipedia.org/wiki/Levenberg-Marquardt_algorithm 96 | 97 | Attributes 98 | ---------- 99 | model: class 100 | A `deformation` model class definition. 101 | """ 102 | 103 | MAX_ITER = 200 104 | MAX_BAD = 5 105 | 106 | def __deltaP(self, J, e, alpha, p=None): 107 | """ 108 | Computes the parameter update. 109 | 110 | Parameters 111 | ---------- 112 | J: nd-array 113 | The (dE/dP) the relationship between image differences and model 114 | parameters. 115 | e: float 116 | The evaluated similarity metric. 117 | alpha: float 118 | A dampening factor. 119 | p: nd-array or list of floats, optional 120 | 121 | Returns 122 | ------- 123 | deltaP: nd-array 124 | The parameter update vector. 125 | """ 126 | 127 | H = np.dot(J.T, J) 128 | 129 | H += np.diag(alpha * np.diagonal(H)) 130 | 131 | return np.dot(np.linalg.inv(H), np.dot(J.T, e)) 132 | 133 | def __dampening(self, alpha, decreasing): 134 | """ 135 | Computes the adjusted dampening factor. 136 | 137 | Parameters 138 | ---------- 139 | alpha: float 140 | The current dampening factor. 141 | decreasing: boolean 142 | Conditional on the decreasing error function. 143 | 144 | Returns 145 | ------- 146 | alpha: float 147 | The adjusted dampening factor. 148 | """ 149 | return alpha / 10. if decreasing else alpha * 10. 150 | 151 | def register(self, 152 | image, 153 | template, 154 | tform, 155 | sampler=sampler.bilinear, 156 | method=metric.forwardsAdditive, 157 | p=None, 158 | alpha=None, 159 | verbose=False 160 | ): 161 | """ 162 | Computes the registration between the image and template. 163 | 164 | Parameters 165 | ---------- 166 | image: nd-array 167 | The floating image. 168 | template: nd-array 169 | The target image. 170 | tform: deformation (class) 171 | The deformation model (shift, affine, projective) 172 | method: collection, optional. 173 | The registration method (defaults to FrowardsAdditive) 174 | p: list (or nd-array), optional. 175 | First guess at fitting parameters. 176 | alpha: float 177 | The dampening factor. 178 | verbose: boolean 179 | A debug flag for text status updates. 180 | 181 | Returns 182 | ------- 183 | step: optimization step. 184 | The best optimization step (after convergence). 185 | search: list (of optimization steps) 186 | The set of optimization steps (good and bad) 187 | """ 188 | 189 | p = tform.identity if p is None else p 190 | deltaP = np.zeros_like(p) 191 | 192 | # Dampening factor. 193 | alpha = alpha if alpha is not None else 1e-4 194 | 195 | # Variables used to implement a back-tracking algorithm. 196 | search = [] 197 | badSteps = 0 198 | bestStep = None 199 | 200 | for itteration in range(0, self.MAX_ITER): 201 | 202 | # Compute the transformed coordinates. 203 | coords = tform(p, template.coords) 204 | 205 | # Sample to the template frame using the transformed coordinates. 206 | warpedImage = sampler(image.data, coords.tensor) 207 | 208 | # Evaluate the error metric. 209 | e = method.error(warpedImage, template.data) 210 | 211 | # Cache the optimization step. 212 | searchStep = optStep( 213 | error=np.abs(e).sum() / np.prod(image.data.shape), 214 | p=p.copy(), 215 | deltaP=deltaP.copy(), 216 | decreasing=True 217 | ) 218 | 219 | # Update the current best step. 220 | bestStep = searchStep if bestStep is None else bestStep 221 | 222 | if verbose: 223 | log.warn( 224 | REGISTRATION_STEP.format( 225 | itteration, 226 | ' '.join('{0:3.2f}'.format(param) for param in searchStep.p), 227 | searchStep.error 228 | ) 229 | ) 230 | 231 | # Append the search step to the search. 232 | search.append(searchStep) 233 | 234 | if len(search) > 1: 235 | 236 | searchStep.decreasing = (searchStep.error < bestStep.error) 237 | 238 | alpha = self.__dampening(alpha, searchStep.decreasing) 239 | 240 | if searchStep.decreasing: 241 | bestStep = searchStep 242 | else: 243 | badSteps += 1 244 | if badSteps > self.MAX_BAD: 245 | if verbose: 246 | log.warn(REGISTRATION_STOP) 247 | break 248 | 249 | # Restore the parameters from the previous best iteration. 250 | p = bestStep.p.copy() 251 | 252 | # Computes the derivative of the error with respect to model 253 | # parameters. 254 | 255 | J = method.jacobian(warpedImage, template, tform, p) 256 | 257 | # Compute the parameter update vector. 258 | deltaP = self.__deltaP(J, e, alpha, p) 259 | 260 | # Evaluate stopping condition: 261 | if np.dot(deltaP.T, deltaP) < 1e-4: 262 | break 263 | 264 | # Update the estimated parameters. 265 | p = method.update(p, deltaP, tform) 266 | 267 | return bestStep, search 268 | 269 | -------------------------------------------------------------------------------- /notebooks/linreg-equivalence.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "metadata": { 3 | "name": "linreg-equivalence" 4 | }, 5 | "nbformat": 3, 6 | "nbformat_minor": 0, 7 | "worksheets": [ 8 | { 9 | "cells": [ 10 | { 11 | "cell_type": "markdown", 12 | "metadata": {}, 13 | "source": [ 14 | "Linear Registration Equivalence of Methods\n", 15 | "==========================================\n", 16 | "\n", 17 | "Author: Nathan Faggian" 18 | ] 19 | }, 20 | { 21 | "cell_type": "code", 22 | "collapsed": false, 23 | "input": [ 24 | "%pylab inline" 25 | ], 26 | "language": "python", 27 | "metadata": {}, 28 | "outputs": [ 29 | { 30 | "output_type": "stream", 31 | "stream": "stdout", 32 | "text": [ 33 | "\n", 34 | "Welcome to pylab, a matplotlib-based Python environment [backend: module://IPython.zmq.pylab.backend_inline].\n", 35 | "For more information, type 'help(pylab)'.\n" 36 | ] 37 | } 38 | ], 39 | "prompt_number": 1 40 | }, 41 | { 42 | "cell_type": "code", 43 | "collapsed": false, 44 | "input": [ 45 | "from imreg import register, sampler, model, metric" 46 | ], 47 | "language": "python", 48 | "metadata": {}, 49 | "outputs": [], 50 | "prompt_number": 2 51 | }, 52 | { 53 | "cell_type": "code", 54 | "collapsed": false, 55 | "input": [ 56 | "from scipy.misc import lena\n", 57 | "from scipy import ndimage" 58 | ], 59 | "language": "python", 60 | "metadata": {}, 61 | "outputs": [], 62 | "prompt_number": 3 63 | }, 64 | { 65 | "cell_type": "code", 66 | "collapsed": false, 67 | "input": [ 68 | "image = ndimage.zoom(lena(), 0.1)\n", 69 | "template = ndimage.rotate(image, 30, reshape=False)" 70 | ], 71 | "language": "python", 72 | "metadata": {}, 73 | "outputs": [], 74 | "prompt_number": 4 75 | }, 76 | { 77 | "cell_type": "code", 78 | "collapsed": false, 79 | "input": [ 80 | "registrator = register.Register()" 81 | ], 82 | "language": "python", 83 | "metadata": {}, 84 | "outputs": [], 85 | "prompt_number": 5 86 | }, 87 | { 88 | "cell_type": "code", 89 | "collapsed": false, 90 | "input": [ 91 | "image = register.RegisterData(image)\n", 92 | "\n", 93 | "template = register.RegisterData(template)\n", 94 | "\n", 95 | "tform = model.Affine()" 96 | ], 97 | "language": "python", 98 | "metadata": {}, 99 | "outputs": [], 100 | "prompt_number": 6 101 | }, 102 | { 103 | "cell_type": "code", 104 | "collapsed": false, 105 | "input": [ 106 | "step, additive_search = registrator.register(image, template, tform, sampler=sampler.bilinear, method=metric.forwardsAdditive)\n", 107 | "\n", 108 | "step, compositional_search = registrator.register(image, template, tform, sampler=sampler.bilinear, method=metric.forwardsCompositional)\n", 109 | "\n", 110 | "step, inverse_compositional_search = registrator.register(image, template, tform, sampler=sampler.bilinear, method=metric.inverseCompositional)\n" 111 | ], 112 | "language": "python", 113 | "metadata": {}, 114 | "outputs": [], 115 | "prompt_number": 7 116 | }, 117 | { 118 | "cell_type": "code", 119 | "collapsed": false, 120 | "input": [ 121 | "fig, ax = plt.subplots(1, 1, figsize=(8, 5))\n", 122 | "\n", 123 | "ax.plot( \n", 124 | " [x.error for x in additive_search if x.decreasing], 's-y', \n", 125 | " [x.error for x in compositional_search if x.decreasing], '^-r',\n", 126 | " [x.error for x in inverse_compositional_search if x.decreasing], '^-g',\n", 127 | " )\n", 128 | "\n", 129 | "ax.grid()\n", 130 | "_ = ax.legend(['forwards-additive', 'forwards-compositional', 'inverse-compositional'])\n", 131 | " \n" 132 | ], 133 | "language": "python", 134 | "metadata": {}, 135 | "outputs": [ 136 | { 137 | "output_type": "display_data", 138 | "png": "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\nlmlt8+wzKUzeubm5adalpKQIhUIhHj58mOfn5uPjI77//nshROkVwuW2OvoZpULJ0u5LCfwjkBuP\nbzB8ZNHbgIqLvpEdVoOXYP57PE8v7yW1oRkZn39AhkNVkj/zIfv6JdLSwvHxOYSTxWGahj/AyfIw\nPj6HSEsLL9PYC6OitClKeSvv+SeE4Le5c+mYqh6i9h1gT/PmCJWqUEWwUKn4rXlznk1++k5qKnvm\nzCnydeHx48dkZmbi7Oysec/JyYmoqCjN8r97ybZp04YjR47w4MEDsrOz6d27N0ePHiUiIoKEhAQa\nNmwIQFhYGN26dcPOzg5TU1MCAgJ48uSJVlo5046OjtZadnJy0tp2+/btBAcH4+Ligre3NydOqCeo\n6dy5M8bGxhgbG7Nhwwbu3r2Ls7MzyjymdI2Ojs51rllZWTx8+FDzno2Njeb3KlWq5FpOSUnRLNeo\nUUMrfWdnZ6Kjo4mOjsbCwoKqVavm+bkuX76csLAw6tatS7Nmzdi9e3euWJ/Jr124MHlna2ur+d3I\nyAiA5ORkAEJCQmjevDmWlpaYm5sTHBycK39KWrkvhAFcLVyZ3GYyPjN92Hp1Kz/vKlobUnFTKBQY\nO7en+leHMbqWTNLG6Tx9fIHsFg0Yufk4Nrvh6sa/n0HeJJ9BliRQt+V2unhRq0PkOxcvFrpt92X3\n/zcrKyv09PS0HnmJjIzEwcFBs/zvQsDNzQ0jIyN++OEHvLy8MDY2xtbWliVLltC6dWvNdiNGjMDT\n05ObN2+SkJDAzJkzUalUWmnlTNvOzo7IyEitOHJq0qQJQUFBxMTE4OPjg6+vL6AuTJKSkkhKSqJ/\n//44OjoSGRmZZycye3v7XOeqq6urVdC+iJwFHqg7vNnb22Nvb09sbKymwHt2rGefq5ubGxs2bCAm\nJobx48fTq1cvnj59miv9gjpmFSbv8pOens7777/Pl19+yaNHj4iLi6NLly6lfpNXIQphgJFNR3Lv\n3D2SvJOYs6bo33pLilJpiKX3l1ivDUcVHsbvDcy4tBN631BfJHregnOHyzrKwpHPmVZs5T3/QoOD\nOda0KdO8vDSv402bcrCAO6Hi3P/fdHR08PX1JSAggOTkZCIiIvjuu+8YOHBggft5eXnx448/4uWl\nnvnN29tbaxnUd1zGxsYYGRlx/fp1Fi1aVGCavr6+rFq1imvXrpGamspXX32lWZeZmcn69etJSEhA\n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\nYII=\n" 139 | } 140 | ], 141 | "prompt_number": 9 142 | }, 143 | { 144 | "cell_type": "code", 145 | "collapsed": false, 146 | "input": [], 147 | "language": "python", 148 | "metadata": {}, 149 | "outputs": [], 150 | "prompt_number": 8 151 | } 152 | ], 153 | "metadata": {} 154 | } 155 | ] 156 | } --------------------------------------------------------------------------------