{
"cells": [
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Moebius transformation applied to a discrete image ##"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
" The aim of this notebook is to illustrate how a Moebius transformation, defined by its action on three points, distorts a discrete image.\n",
"A Mobius transformation acts as a geometric transformation of an image, defined via the function\n",
"`geometric_transform` from `scipy.ndimage.interpolation`.\n",
"\n",
"In image processing, a geometric transformation of an image consists in modifying\n",
"the positions of pixels in that image, but keeping\n",
"their colors unchanged.\n",
"\n",
"In order to define a geometric transformation, as it is implemented in `scipy.ndimage`, we fix some background:\n",
"\n",
"A 3 or 4 channels image of resolution, $m\\times n$, is interpreted as a mapping\n",
"$$img:\\{0,1, \\ldots, m-1\\}\\times \\{0,1, \\ldots, n-1\\}\\to\\mathbb{R}^3 (\\mathbb{R}^4),$$\n",
"defined by $img(k,l)=(r,g,b)$ or (r, g, b, a), i.e. to the pixel in the row k, column l, one \n",
"associates its color code (r, g, b) or (r, g, b, a). \n",
" \n",
"The position of a pixel, $(row=k, col=l)$, is also given by its coordinates $(x=l, y=k)$,\n",
" with respect to the image system of axes, $Oxy$, where $O$ is the upper-left corner, $Ox$ points horizontally to the right, and $Oy$ downwards.\n",
"\n",
"The geometric transformation, as a mapping from image to a planar region, is given in the world coordinate system, XO'Y, with O' usually chosen as being the lower left corner of the image.\n",
"\n",
"We have the following relationship between a pixel world coordinates, $(X,Y)$, and image coordinates, $(x,y)$:\n",
" \n",
"$$ \\begin{array}{lll} \n",
"X&=&x\\\\\n",
"Y&=&m-1-y\n",
"\\end{array}\n",
"$$\n",
"\n",
"Denoting by $D$ the rectangular region that covers our image, \n",
"a geometric transformation is an invertible map $T:D\\to D'\\subset \\mathbb{R}^2$. The transformation $T$ is chosen such that the transformed image has common regions with the original one, i.e. $T(D)\\cap D\\neq \\emptyset$.\n",
" "
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
" The geometric transformation implemented in `scipy.ndimage` works as [follows](https://docs.scipy.org/doc/scipy-0.18.1/reference/tutorial/ndimage.html): theoretically one defines the output image (i.e. the deformed image) as an image of the same resolution (or not) with the input image. For each point P in the output image one evaluates $T^{-1}(P)$. \n",
" - If $T^{-1}(P)$ is a pixel in the input image, one assigns to P the color of $T^{-1}(P)$.\n",
" - If $T^{-1}(P)$ is not just a pixel ( a point of integer coordinates), one assigns a color to P, through a spline interpolation of the colors of neighbouring pixels of $T^{-1}(P)$. \n",
" - If $T^{-1}(P)$ is outside the rectangle that covers the input image, then $P$ is filled by a prescribed method. \n",
" "
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"The Python function that implements $T^{-1}$, let us call it `inverse_map`, has as a first mandatory parameter, a tuple of length equal to the output array (image) rank, and returns a tuple of length equal to the input array (image) rank (recall that the rank of a `ndarray` is the length of its shape)."
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"The `scipy.ndimage.interpolation.geometric_transform` that applies the transformation $T$ to an image, `img`, is defined as follows:\n",
"\n",
"`geometric_transform(img, inverse_mapping, output_shape=None, output=None, order=3, mode='constant',\n",
" cval=0.0, prefilter=True, extra_arguments=(),extra_keywords={})` \n",
"\n",
"- `order` sets the order of the spline interpolation;\n",
"- `mode` is a key that sets the method of filling the points P, for which $T^{-1}(P)$ is not in img.\n",
" The options are: 'constant' (all such points are colored with the same color), 'nearest', 'reflect' or 'wrap';\n",
" default is 'constant'.\n",
"- `cval` has effect when mode='constant'. It gives the grey color code, between 0 and 255 (for jpg images), to fill the regions consisting in points that are not mapped by $T^{-1}$ in img.\n",
"- `extra_arguments=()` is a tuple defining the arguments of inverse_mapping, other than the mandatory one, defined above;\n",
"- for other keywords see [scipy docs](https://docs.scipy.org/doc/scipy-0.15.1/reference/generated/scipy.ndimage.interpolation.geometric_transform.html)."
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Next we show how to apply a Moebius transform, $T(z)=(az+b)/(cz+d)$, $ad-bc\\neq0$, to a color image.\n",
"The inverse map is defined by $T^{-1}(z)=(dz-b)/(-cz+a)$."
]
},
{
"cell_type": "code",
"execution_count": 1,
"metadata": {},
"outputs": [
{
"data": {
"text/html": [
" \n",
" "
]
},
"metadata": {},
"output_type": "display_data"
}
],
"source": [
"import skimage.io as sio\n",
"import numpy as np\n",
"from scipy.ndimage import geometric_transform\n",
"import plotly.express as px\n",
"from plotly.offline import download_plotlyjs, init_notebook_mode, iplot\n",
"init_notebook_mode(connected=True)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Our color image has the shape $(m, n, 3)$. Hence the `inverse_map` has as a mandatory first parameter, a tuple of len(3):"
]
},
{
"cell_type": "code",
"execution_count": 2,
"metadata": {},
"outputs": [],
"source": [
"def inv_Moebius_transform(index, a, b, c, d, img): \n",
" #index[0] gives the row of a pixel in the output image, \n",
" #index[1] the column, and index[2], the color channel\n",
" #a, b, c, d are the Moebius transform coeffcients and img is the output image (ndarray) \n",
" \n",
" z = index[1] + 1j*(img.shape[0]-1-index[0])#the complex number associated to a pixel (to the corresponding \n",
" # point expressed in coordinates X,Y)\n",
" w = (d*z-b)/(-c*z+a) #T^{-1}(z)\n",
" return img.shape[0]-1-np.imag(w), np.real(w), index[2] #returns the \"approx\" row, and column \n",
" # for T^{-1}(z)"
]
},
{
"cell_type": "markdown",
"metadata": {
"collapsed": true
},
"source": [
"Read the image, img:"
]
},
{
"cell_type": "code",
"execution_count": 3,
"metadata": {},
"outputs": [
{
"data": {
"application/vnd.plotly.v1+json": {
"config": {
"linkText": "Export to plotly.com/",
"plotlyServerURL": "https://plotly.com/",
"showLink": false
},
"data": [
{
"hovertemplate": "x: %{x}
y: %{y}
color: [%{z[0]}, %{z[1]}, %{z[2]}]