Evaluation of radial distortion with triangulation

Author: Yannick Copin y.copin@ipnl.in2p3.fr

Abstract: The radial distortion tends to alter more the positions in the outer part of the field, while it has little impact close to the center of distortion. Triangulation of distorted positions generated from a regular grid can be used to estimate the center of distortion, as well as undistorted step and rotation angle.

[1]:
import numpy as N
import scipy as S
import matplotlib.pyplot as P
%matplotlib inline

Create a distorted grid

[2]:
def create_grid(nx, ny, scale=1., rotation=0.):
    """Create a (npts, 2) array of coordinates."""

    x = N.arange(nx) - (nx-1)/2.
    y = N.arange(ny) - (ny-1)/2.
    xy = N.array(N.meshgrid(x, y)).T.reshape(-1, 2).astype(float)  # npts × [x, y]
    xy *= scale
    if rotation:
        # Convert to complex and rotate
        xy = (xy[:, 0] + 1j * xy[:, 1]) * N.exp(1j*rotation)
        xy = N.vstack((xy.real, xy.imag)).T  # npts × [x, y]

    return xy

Undistorted positions:

[3]:
nx, ny = 20, 15
scale = 2
rotation = N.deg2rad(5.)

_xy = create_grid(nx, ny, scale=scale, rotation=rotation)  # Undistorted positions npts × [x, y]
print("Nb of points:", len(_xy))
Nb of points: 300

Add r² radial distortion:

[4]:
xy0 = [3, 5]  # True center of distortion
K1 = 0.005  # Radial distortion r²-coeff
r2 = N.sum((_xy - xy0)**2, axis=1)
t = N.arctan2(_xy[:, 1], _xy[:, 0])
xy = _xy + K1 * N.vstack((r2 * N.cos(t), r2 * N.sin(t))).T  # Distorted positions
[5]:
P.plot(_xy[:, 0], _xy[:, 1], '.k', label='Undistorted')
P.plot(xy[:, 0], xy[:, 1], 'o', label='Distorted')
P.legend(loc='lower left', numpoints=1)
[5]:
<matplotlib.legend.Legend at 0x7fa09a8fc6a0>
_images/distTriangulate_8_1.png

Triangulation

Using scipy.spatial.Delaunay

[6]:
from scipy.spatial import Delaunay, delaunay_plot_2d
tess = Delaunay(xy)
[7]:
fig = delaunay_plot_2d(tess)  # plot + triplot
_images/distTriangulate_11_0.png

`scipy.spatial.Delaunay <http://docs.scipy.org/doc/scipy/reference/generated/scipy.spatial.Delaunay.html>`__ lacks of helper methods/functions to post-process the triangulation. We rather use `matplotlib.tri.Triangulation <http://matplotlib.org/api/tri_api.html#matplotlib.tri.Triangulation>`__.

Using matplotlib.tri.Triangulation

[8]:
tri = P.matplotlib.tri.Triangulation(xy[:, 0], xy[:, 1])

Remove quasi-degenerate triangles on the borders:

[9]:
# See http://matplotlib.org/mpl_examples/pylab_examples/tricontour_smooth_delaunay.py
min_circle_ratio = 0.1
mask = P.matplotlib.tri.TriAnalyzer(tri).get_flat_tri_mask(min_circle_ratio)
tri.set_mask(mask)
print("After selection: {} triangles, {} edges".format(tri.triangles.shape[0], tri.edges.shape[0]))
After selection: 594 triangles, 831 edges
[10]:
P.plot(xy[:, 0], xy[:, 1], 'o')
P.triplot(tri);
_images/distTriangulate_17_0.png

Triangulation analysis

Compute length of all (selected) edges:

[11]:
edges = xy[tri.edges]  # nedges × [start, end] × [x, y]
dxy = edges[:, 1] - edges[:, 0]  # nedges × [x, y]
lengths = N.sum(dxy**2, axis=1)**0.5  # nedges
[12]:
P.hist(lengths, bins=30, histtype='stepfilled')

cut = N.percentile(lengths, 66)
P.axvline(cut, c='k')
medinf = N.median(lengths[lengths <= cut])
medsup = N.median(lengths[lengths >= cut])
P.axvline(medinf, c='k', ls='--')
P.axvline(medsup, c='k', ls='--')
med = (medinf + medsup) / 2
P.axvline(med, c='k', lw=2)
P.xlabel("Length");
_images/distTriangulate_20_0.png

Normalized lengths:

[13]:
diags = lengths > med  # Diagonals
lengths[diags] /= 2**0.5
[14]:
P.hist([lengths[~diags], lengths[diags]], bins=30, stacked=True, histtype='stepfilled',
       label=['Short sides', r'Diagonals/$\sqrt{2}$'])
P.legend(loc='upper right')
P.xlabel("Normalized length");
_images/distTriangulate_23_0.png

Compute angles (modulo π/4):

[15]:
angles = N.rad2deg(N.arctan2(dxy[:, 1], dxy[:, 0])) + 180.
angles = (4 * angles - 180 * N.around(4*angles/180)) / 4
[16]:
P.hist([angles[~diags], angles[diags]], bins=30, stacked=True, histtype='stepfilled',
       label=['Short sides', u'Diagonals - 45°'])
P.legend(loc='upper right')
P.xlabel("Warp angle [deg]");
_images/distTriangulate_26_0.png

Sort edges by increasing length:

[17]:
iedges = N.argsort(lengths)

Compute elements of distortions from smallest edges:

[18]:
frac_cen = 0.05
ncen = int(len(edges) * frac_cen)
print("Nb of undistorted edges:", ncen)
shortests = edges[iedges[:ncen]]   # ncen × [start, end] × [x, y]
xyc = N.mean(shortests, axis=(0, 1))  # Center of distortion
print("Center of distortion:", xyc)
length = N.median(lengths[iedges[:ncen]])  # Undistorted step
print("Undistorted step:", length)
angle = N.median(angles[iedges[:ncen]])  # Undistorted angle
print("Undistorted angle [deg]:", angle)
Nb of undistorted edges: 41
Center of distortion: [2.86707356 4.30324354]
Undistorted step: 2.006053823135479
Undistorted angle [deg]: 5.03934704597188

Reference grid

Modeled positions (undistorted):

[19]:
xyu = create_grid(nx, ny, scale=length, rotation=N.deg2rad(angle))  # Modeled (undistorted) positions
[20]:
P.plot(xy[:, 0], xy[:, 1], 'o', label='Distorted')
P.plot(shortests[..., 0].T, shortests[..., 1].T, c='0.3')
P.plot(_xy[:, 0], _xy[:, 1], '.c', label='Undistorted (truth)')
P.plot(xyu[:, 0], xyu[:, 1], '.k', label='Undistorted (model)')
P.plot([xy0[0]], [xy0[1]], marker='*', ms=10, ls='none', label='Center (truth)')
P.plot([xyc[0]], [xyc[1]], marker='*', ms=10, ls='none', label='Center (model)')
P.legend(loc='lower left', numpoints=1)
P.gcf().set_size_inches((12, 10))
_images/distTriangulate_33_0.png

Radial distortion:

[21]:
ru = N.sum((_xy - xyc)**2, axis=1)**0.5
offsets = xy - xyu  # npts × [x, y]
dr = N.hypot(offsets[:, 0], offsets[:, 1])
[22]:
P.plot(ru, dr, '.', label='Observed')
sru = N.sort(ru)
P.plot(sru, K1 * sru**2, marker='', ls='-', label='Truth')
P.xlabel("Radius")
P.ylabel("Offset")
P.legend(loc='upper left', numpoints=1);
_images/distTriangulate_36_0.png

spectrogrism.distortion implementation

The aforementioned procedure is implemented in the spectrogrism.distortion module. The rescaling of long edges is not trustworthy, and is not applied.

[23]:
from spectrogrism import distortion as SD
[24]:
grid = SD.StructuredGrid.create(15, 15, step=1, rotation=N.deg2rad(5.))
print(grid)

# Add distortions
gdist = SD.GeometricDistortion(3 + 4j,
                               Kcoeffs=[1e-3], Pcoeffs=[1e-3, 0, 1e-3])
print(gdist)

grid.xy = gdist.forward(grid.xy)

fig = P.figure()
length, angle, offset, center = grid.estimate_parameters(fig=fig)
fig.set_size_inches((12, 10))
Structured grid: 15 × 15 = 225 positions, x+y+
Geometric distortion: center=(+3, +4), K-coeffs=[0.001], P-coeffs=[0.001 0.    0.001]
_images/distTriangulate_39_1.png

This page was generated from distTriangulate.ipynb.