PIL's Image.transform has a perspective-mode which requires an 8-tuple of data but I can't figure out how to convert let's say a right tilt of 30 degrees to that tuple.
Can anyone explain it?
To apply a perspective transformation you first have to know four points in a plane A that will be mapped to four points in a plane B. With those points, you can derive the homographic transform. By doing this, you obtain your 8 coefficients and the transformation can take place.
The site http://xenia.media.mit.edu/~cwren/interpolator/ (mirror: WebArchive), as well as many other texts, describes how those coefficients can be determined. To make things easy, here is a direct implementation according from the mentioned link:
import numpy
def find_coeffs(pa, pb):
matrix = []
for p1, p2 in zip(pa, pb):
matrix.append([p1[0], p1[1], 1, 0, 0, 0, -p2[0]*p1[0], -p2[0]*p1[1]])
matrix.append([0, 0, 0, p1[0], p1[1], 1, -p2[1]*p1[0], -p2[1]*p1[1]])
A = numpy.matrix(matrix, dtype=numpy.float)
B = numpy.array(pb).reshape(8)
res = numpy.dot(numpy.linalg.inv(A.T * A) * A.T, B)
return numpy.array(res).reshape(8)
where pb is the four vertices in the current plane, and pa contains four vertices in the resulting plane.
So, suppose we transform an image as in:
import sys
from PIL import Image
img = Image.open(sys.argv[1])
width, height = img.size
m = -0.5
xshift = abs(m) * width
new_width = width + int(round(xshift))
img = img.transform((new_width, height), Image.AFFINE,
(1, m, -xshift if m > 0 else 0, 0, 1, 0), Image.BICUBIC)
img.save(sys.argv[2])
Here is a sample input and output with the code above:
We can continue on the last code and perform a perspective transformation to revert the shear:
coeffs = find_coeffs(
[(0, 0), (256, 0), (256, 256), (0, 256)],
[(0, 0), (256, 0), (new_width, height), (xshift, height)])
img.transform((width, height), Image.PERSPECTIVE, coeffs,
Image.BICUBIC).save(sys.argv[3])
Resulting in:
You can also have some fun with the destination points:
I'm going to hijack this question just a tiny bit because it's the only thing on Google pertaining to perspective transformations in Python. Here is some slightly more general code based on the above which creates a perspective transform matrix and generates a function which will run that transform on arbitrary points:
import numpy as np
def create_perspective_transform_matrix(src, dst):
""" Creates a perspective transformation matrix which transforms points
in quadrilateral ``src`` to the corresponding points on quadrilateral
``dst``.
Will raise a ``np.linalg.LinAlgError`` on invalid input.
"""
# See:
# * http://xenia.media.mit.edu/~cwren/interpolator/
# * http://stackoverflow.com/a/14178717/71522
in_matrix = []
for (x, y), (X, Y) in zip(src, dst):
in_matrix.extend([
[x, y, 1, 0, 0, 0, -X * x, -X * y],
[0, 0, 0, x, y, 1, -Y * x, -Y * y],
])
A = np.matrix(in_matrix, dtype=np.float)
B = np.array(dst).reshape(8)
af = np.dot(np.linalg.inv(A.T * A) * A.T, B)
return np.append(np.array(af).reshape(8), 1).reshape((3, 3))
def create_perspective_transform(src, dst, round=False, splat_args=False):
""" Returns a function which will transform points in quadrilateral
``src`` to the corresponding points on quadrilateral ``dst``::
>>> transform = create_perspective_transform(
... [(0, 0), (10, 0), (10, 10), (0, 10)],
... [(50, 50), (100, 50), (100, 100), (50, 100)],
... )
>>> transform((5, 5))
(74.99999999999639, 74.999999999999957)
If ``round`` is ``True`` then points will be rounded to the nearest
integer and integer values will be returned.
>>> transform = create_perspective_transform(
... [(0, 0), (10, 0), (10, 10), (0, 10)],
... [(50, 50), (100, 50), (100, 100), (50, 100)],
... round=True,
... )
>>> transform((5, 5))
(75, 75)
If ``splat_args`` is ``True`` the function will accept two arguments
instead of a tuple.
>>> transform = create_perspective_transform(
... [(0, 0), (10, 0), (10, 10), (0, 10)],
... [(50, 50), (100, 50), (100, 100), (50, 100)],
... splat_args=True,
... )
>>> transform(5, 5)
(74.99999999999639, 74.999999999999957)
If the input values yield an invalid transformation matrix an identity
function will be returned and the ``error`` attribute will be set to a
description of the error::
>>> tranform = create_perspective_transform(
... np.zeros((4, 2)),
... np.zeros((4, 2)),
... )
>>> transform((5, 5))
(5.0, 5.0)
>>> transform.error
'invalid input quads (...): Singular matrix
"""
try:
transform_matrix = create_perspective_transform_matrix(src, dst)
error = None
except np.linalg.LinAlgError as e:
transform_matrix = np.identity(3, dtype=np.float)
error = "invalid input quads (%s and %s): %s" %(src, dst, e)
error = error.replace("\n", "")
to_eval = "def perspective_transform(%s):\n" %(
splat_args and "*pt" or "pt",
)
to_eval += " res = np.dot(transform_matrix, ((pt[0], ), (pt[1], ), (1, )))\n"
to_eval += " res = res / res[2]\n"
if round:
to_eval += " return (int(round(res[0][0])), int(round(res[1][0])))\n"
else:
to_eval += " return (res[0][0], res[1][0])\n"
locals = {
"transform_matrix": transform_matrix,
}
locals.update(globals())
exec to_eval in locals, locals
res = locals["perspective_transform"]
res.matrix = transform_matrix
res.error = error
return res
The 8 transform coefficients (a, b, c, d, e, f, g, h) correspond to the following transformation:
x' = (ax + by + c) / (gx + hy + 1)
y' = (dx + ey + f) / (gx + hy + 1)
These 8 coefficients can in general be found from solving 8 (linear) equations that define how 4 points on the plane transform (4 points in 2D -> 8 equations), see the answer by mmgp for a code that solves this, although you might find it a tad more accurate to change the line
res = numpy.dot(numpy.linalg.inv(A.T * A) * A.T, B)
to
res = numpy.linalg.solve(A, B)
i.e., there is no real reason to actually invert the A matrix there, or to multiply it by its transpose and losing a bit of accuracy, in order to solve the equations.
As for your question, for a simple tilt of theta degrees around (x0, y0), the coefficients you are looking for are:
def find_rotation_coeffs(theta, x0, y0):
ct = cos(theta)
st = sin(theta)
return np.array([ct, -st, x0*(1-ct) + y0*st, st, ct, y0*(1-ct)-x0*st,0,0])
And in general any Affine transformation must have (g, h) equal to zero. Hope that helps!
Here is a pure-Python version of generating the transform coefficients (as I've seen this requested by several). I made and used it for making the PyDraw pure-Python image drawing package.
If using it for your own project, note that the calculations requires several advanced matrix operations which means that this function requires another, luckily pure-Python, matrix library called matfunc originally written by Raymond Hettinger and which you can download here or here.
import matfunc as mt
def perspective_coefficients(self, oldplane, newplane):
"""
Calculates and returns the transform coefficients needed for a perspective
transform, ie tilting an image in 3D.
Note: it is not very obvious how to set the oldplane and newplane arguments
in order to tilt an image the way one wants. Need to make the arguments more
user-friendly and handle the oldplane/newplane behind the scenes.
Some hints on how to do that at http://www.cs.utexas.edu/~fussell/courses/cs384g/lectures/lecture20-Z_buffer_pipeline.pdf
| **option** | **description**
| --- | ---
| oldplane | a list of four old xy coordinate pairs
| newplane | four points in the new plane corresponding to the old points
"""
# first find the transform coefficients, thanks to http://stackoverflow.com/questions/14177744/how-does-perspective-transformation-work-in-pil
pb,pa = oldplane,newplane
grid = []
for p1,p2 in zip(pa, pb):
grid.append([p1[0], p1[1], 1, 0, 0, 0, -p2[0]*p1[0], -p2[0]*p1[1]])
grid.append([0, 0, 0, p1[0], p1[1], 1, -p2[1]*p1[0], -p2[1]*p1[1]])
# then do some matrix magic
A = mt.Matrix(grid)
B = mt.Vec([xory for xy in pb for xory in xy])
AT = A.tr()
ATA = AT.mmul(A)
gridinv = ATA.inverse()
invAT = gridinv.mmul(AT)
res = invAT.mmul(B)
a,b,c,d,e,f,g,h = res.flatten()
# finito
return a,b,c,d,e,f,g,h
Related
I want to generate a random placement and a random trajectory inside of a polygon. To speed up computation I am looking to do it in vectorized way (using Numpy).
I created code for it if we have square boundaries:
import numpy as np
# coordinates of square
square_coords = [-255, -255, 256, 256] # [xMin, yMin, xMax, yMax]
# convert to np.array
square_boundaries = np.array([(square_coords[0], square_coords[2]), (square_coords[1], square_coords[3])])
# specify speeds with corresponding probabilities of each speed
ue_speed = [3, 4, 8, 25]
ue_speed_prob = [0.4, 0.2, 0.3, 0.1] # should sum up to 1
steps = 50
time_interval = 10
nwalks = 1
# calcultation without boundaries
v = np.random.choice(ue_speed, size=steps, p=ue_speed_prob)
R = np.expand_dims((v * time_interval), axis=-1)
theta = 2 * np.pi * np.random.rand(nwalks, steps)
xy = np.dstack((np.cos(theta), np.sin(theta))) * R
trajectory_no_boundaries = np.hstack((np.zeros((nwalks, 1, 2)), np.cumsum(xy, axis=1)))
# generete random placement inside of boundaries
start = (np.random.randint(square_coords[0], square_coords[2]), np.random.randint(square_coords[1], square_coords[3]))
# generate random trajectory inside of boundaries
size = np.diff(square_boundaries, axis=1).ravel()
trajectory = np.abs((trajectory_no_boundaries[0] + start - square_boundaries[:, 0] + size) % (2 * size) - size) + square_boundaries[:, 0]
I am trying to understand if it is possible to transform this code if we have polygon coordinates instead of square coordinates e.g.:
polygon_coords = [(100, 100), (80, 130), (90, 130), (90, 140), (70, 140), (150, 200), (120, 150), (100, 100)]
To give an idea of how this polygon looks like here is a visualization with Shapely:
from shapely import geometry
shapely_polygon_coords = [geometry.Point(coord) for coord in polygon_coords]
geometry.Polygon([[p.x, p.y] for p in shapely_polygon_coords])
output:
I'm trying to find the surface of points cloud using the 3D alpha shape algorithm. When I calculate the circumradius, the determinant a is equal to zero, leading to the error 'divide by zero encountered in double_scalars'. What should i do with it? Thanks a lot! Here is the code:
def alpha_shape_3D(points, alpha):
tetra = Delaunay(points)
edge = []
for i1, i2, i3, i4 in tetra.vertices:
pa = points[i1]
pb = points[i2]
pc = points[i3]
pd = points[i4]
pa2 = np.dot(pa, pa)
pb2 = np.dot(pb, pb)
pc2 = np.dot(pc, pc)
pd2 = np.dot(pd, pd)
a = np.linalg.det(np.array([np.append(pa, 1), np.append(pb, 1), np.append(pc, 1), np.append(pd, 1)]))
Dx = np.linalg.det(np.array([np.array([pa2, pa[1], pa[2], 1]),
np.array([pb2, pb[1], pb[2], 1]),
np.array([pc2, pc[1], pc[2], 1]),
np.array([pd2, pd[1], pd[2], 1])]))
Dy = - np.linalg.det(np.array([np.array([pa2, pa[0], pa[2], 1]),
np.array([pb2, pb[0], pb[2], 1]),
np.array([pc2, pc[0], pc[2], 1]),
np.array([pd2, pd[0], pd[2], 1])]))
Dz = np.linalg.det(np.array([np.array([pa2, pa[0], pa[1], 1]),
np.array([pb2, pb[0], pb[1], 1]),
np.array([pc2, pc[0], pc[1], 1]),
np.array([pd2, pd[0], pd[1], 1])]))
c = np.linalg.det(np.array([np.array([pa2, pa[0], pa[1], pa[2]]),
np.array([pb2, pb[0], pb[1], pb[2]]),
np.array([pc2, pc[0], pc[1], pc[2]]),
np.array([pd2, pd[0], pd[1], pd[2]])]))
r = math.sqrt(math.pow(Dx, 2) + math.pow(Dy, 2) + math.pow(Dz, 2) - 4 * a * c) / (2 * abs(a)) # error occurs here, since some value of a is zero.
if r<alpha:
edge.append([pa,pb,pc,pd])
tetras = np.array(edge)
# triangles
TriComb = np.array([(0, 1, 2), (0, 1, 3), (0, 2, 3), (1, 2, 3)])
Triangles = tetras[:,TriComb].reshape(-1,3)
Triangles = np.sort(Triangles,axis=1)
# Remove triangles that occurs twice, because they are within shapes
TrianglesDict = defaultdict(int)
for tri in Triangles:TrianglesDict[tuple(tri)] += 1
Triangles=np.array([tri for tri in TrianglesDict if TrianglesDict[tri] ==1])
#edges
EdgeComb=np.array([(0, 1), (0, 2), (1, 2)])
Edges=Triangles[:,EdgeComb].reshape(-1,2)
Edges=np.sort(Edges,axis=1)
Edges=np.unique(Edges,axis=0)
Vertices = np.unique(Edges)
return Vertices,Edges,Triangles
Without your points data it's hard to say for sure, but it looks like your data is degenerate. For example, if all your points are on the same plane you can get this result.
I want to implement a "row wise" matrix multiplication.
More specifically speaking, I want to plot a set of arrows whose directions range from (-pi, pi). The following code is how I implemented it.
scan_phi = np.linspace(-np.pi*0.5, np.pi*0.5, 450)
points = np.ones((450, 2), dtype=np.float)
points[..., 0] = 0.0
n_pts = len(points)
sin = np.sin(scan_phi)
cos = np.cos(scan_phi)
rot = np.append(np.expand_dims(np.vstack([cos, -sin]).T, axis=1),
np.expand_dims(np.vstack([sin, cos]).T, axis=1),
axis=1)
points_rot = []
for idx, p in enumerate(points):
points_rot.append(np.matmul(rot[idx], p.T))
points_rot = np.array(points_rot)
sample = points_rot[::10]
ax = plt.axes()
ax.set_xlim(-2, 2)
ax.set_ylim(-2, 2)
for idx, p in enumerate(sample):
if idx == 0:
ax.arrow(0, 0, p[0], p[1], head_width=0.05, head_length=0.1, color='red')
else:
ax.arrow(0, 0, p[0], p[1], head_width=0.05, head_length=0.1, fc='k', ec='k')
plt.show()
In my code, "rot" ends up being an array of (450, 2, 2) meaning for each arrow, I have created a corresponding rotation matrix to rotate it. I have 450 points stored in "points" (450, 2) that I want to draw arrows with. (Here the arrows are all initialized with [0, 1]. However, it can be initialized with different values which is why I want to have 450 individual points instead of just rotating a single point by 450 different angles)
The way I did is using a for-loop, i.e. for each arrow, I transform it individually.
points_rot = []
for idx, p in enumerate(points):
points_rot.append(np.matmul(rot[idx], p.T))
points_rot = np.array(points_rot)
However, I wonder if there's any nicer and easy way to do this completely through numpy, such as some operations that can perform matrix multiplication row-wise. Any idea will be grateful, thanks in advance!
This is a nice use-case for np.einsum:
aa = np.random.normal(size=(450, 2, 2))
bb = np.random.normal(size=(450, 2))
cc = np.einsum('ijk,ik->ij', aa, bb)
So that each row of cc is the product of corresponding rows of aa and bb:
np.allclose(aa[3].dot(bb[3]), cc) # returns True
Explanation: the Einstein notation ijk,ik->ij is saying:
cc[i,j] = sum(aa[i,j,k] * bb[i,k] for k in range(2))
I.e., all variables that do not appear in the right-hand side are summed away.
I have several points that I need to covert them from Cartesian to Polar Coordinates. But for some points, the results I got were negative values.
For example, the origin or the center of the system is (50,50), and the point I want to covert is (10, 43). The angle I got from my code is -170.07375449, but I wish the angle is 189.92624551. (I hope all of the angles after conversion are between 0~360 degree)
How I fix this?
Thanks!!!
import numpy as np
points = np.array([(10, 43), (10, 44), (10, 45), (10, 46), (10, 47)])
#Set the center (origin) at (50, 50). Not (0, 0)
def cart_to_pol(coords, center = [50,50], deg = True):
complex_format = np.array(coords, dtype = float).view(dtype = np.complex) -\
np.array(center, dtype = float).view(dtype = np.complex)
# return np.abs(complex_format).squeeze(), np.angle(complex_format, deg = deg).squeeze()
return np.angle(complex_format, deg=deg).squeeze()
print(cart_to_pol(points))
You could use np.where to add 360 to the negative angles:
import numpy as np
points = np.array([(10, 43), (10, 44), (10, 45), (10, 46), (10, 47)])
#Set the center (origin) at (50, 50). Not (0, 0)
def cart_to_pol(coords, center = [50,50], deg = True):
complex_format = np.array(coords, dtype = float).view(dtype = np.complex) -\
np.array(center, dtype = float).view(dtype = np.complex)
angles = np.angle(complex_format, deg=deg).squeeze()
summand = 360 if deg else 2*np.pi
return np.where(angles < 0, angles+summand, angles)
print(cart_to_pol(points))
Output:
[189.92624551 188.53076561 187.12501635 185.71059314 184.28915333]
Note that complex numbers are not needed here. arctan2(y, x) calculates the desired angle. To get the distances: np.linalg.norm(diff_with_center, axis=-1).
def cart_to_pol(coords, center=[50, 50], deg=True):
conversion = 180 / np.pi if deg else 1
diff_with_center = points - center
angles = np.arctan2(diff_with_center[:, 1], diff_with_center[:, 0])
return conversion * np.where(angles < 0, angles + 2*np.pi, angles)
If you need to convert [-180; 180] angle to [0; 360] you can use this code:
def convert_angle(angle):
return (angle + 360) % 360
Combining some of the other solutions here...
If you specify the origin and/or points as float, you can us a view to operate on them as complex numbers, and then simply return the angle modulo 360 degrees:
points = points - origin
np.angle(points.view(np.complex), deg=True) % 360
>>> array([[189.92624551],
[188.53076561],
[187.12501635],
[185.71059314],
[184.28915333]])
Or, with in-place operations, assuming that points is already floating point:
np.subtract(points, origin, out=points)
v = points.view(np.complex)
np.arctan2(v.imag, v.real, out=v.real)
np.degrees(v.real, out=v.real)
np.mod(v.real, 360, out=v.real)
print(points[0]) # or print(v.real)
>>> array([[189.92624551],
[188.53076561],
[187.12501635],
[185.71059314],
[184.28915333]])
In this case we can't use angle because there is no out option, but we can compute arctan2 in-place in points[0], via our view. This uses no more memory than the original (floating point) points array, and while I didn't time it, should not take more operations to compute.
I have a dictionary which maps XY tuples to RGB tuples. For example,
d = {
(0, 0): (0, 0, 0),
(0, 1): (0, 0, 200),
}
I wish to plot some sort of heatmap, which, at a given XY coordinate, will have color which is the average of the colors in the dict, weighted by their reciprocal distances; as if they were "sources of light" or so.
In the given example, the coordinate (0, 0.5) should be colored with (0, 0, 100) and the coordinate (0, 0.1) should be colored with (0, 0, 20).
My question is rather technical: how do I let pyplot plot a pixel image with colors retrieved from a function f(x, y) -> (r, g, b)?
If you have your X-Y grid:
import numpy
from matplotlib import pyplot as plt
width, height = 300, 500
xs = numpy.arange(width)
ys = numpy.arange(height)
data = numpy.dstack(numpy.meshgrid(xs, ys))
You should just map these to (r, g, b) tuples. The following is pretty slow, but how to speed it up depends on what your function does.
from colorsys import hsv_to_rgb
import random
def data_to_color(x, y):
return (
(x/width)**(0.5+random.random()*2),
(y/height)**3,
(x/width*y/height)*0.6 + random.random()*0.4
)
colors = [[data_to_color(x, y) for x, y in row] for row in data]
colors = numpy.array(colors)
colors.shape
#>>> (500, 300, 3)
Then imshow can give the wanted output:
plt.imshow(colors, origin='lower')
plt.show()
Now, if you want to interpolate from your points as you say, you can use scipy.interpolate. I'll make a dictionary to extrapolate from the function above:
from scipy.interpolate import griddata
gridpoints = data.reshape(width*height, 2)
d = {(x, y): data_to_color(x, y) for x, y in gridpoints if not random.randint(0, 1000)}
len(d)
#>>> 142
Extract the dictionary into numpy arrays, and separate the colours (it might be possible to avoid the separation, but you can test that yourself):
points, values = zip(*d.items())
points = numpy.array(points)
values = numpy.array(values)
reds = values[:, 0]
greens = values[:, 1]
blues = values[:, 2]
Then run griddata on the points:
new_reds = griddata(points, reds, (data[:, :, 0], data[:, :, 1]), method='linear')
new_greens = griddata(points, greens, (data[:, :, 0], data[:, :, 1]), method='linear')
new_blues = griddata(points, blues, (data[:, :, 0], data[:, :, 1]), method='linear')
new_colors = numpy.dstack([new_reds, new_greens, new_blues])
new_colors[numpy.isnan(new_colors)] = 0.5
And plot:
plt.triplot(points[:,0], points[:,1], 'k-', linewidth=1, alpha=0.5)
plt.imshow(new_colors, extent=(0, width, 0, height), origin='lower')
plt.show()
And, finally, if you want extrapolation too, I copied some code from here:
import scipy
def extrapolate_nans(x, y, v):
'''
Extrapolate the NaNs or masked values in a grid INPLACE using nearest
value.
.. warning:: Replaces the NaN or masked values of the original array!
Parameters:
* x, y : 1D arrays
Arrays with the x and y coordinates of the data points.
* v : 1D array
Array with the scalar value assigned to the data points.
Returns:
* v : 1D array
The array with NaNs or masked values extrapolated.
'''
if numpy.ma.is_masked(v):
nans = v.mask
else:
nans = numpy.isnan(v)
notnans = numpy.logical_not(nans)
v[nans] = scipy.interpolate.griddata((x[notnans], y[notnans]), v[notnans],
(x[nans], y[nans]), method='nearest').ravel()
return v
new_reds = extrapolate_nans(data[:, :, 0], data[:, :, 1], new_reds)
new_greens = extrapolate_nans(data[:, :, 0], data[:, :, 1], new_greens)
new_blues = extrapolate_nans(data[:, :, 0], data[:, :, 1], new_blues)
new_colors = numpy.dstack([new_reds, new_greens, new_blues])
plt.imshow(new_colors, extent=(0, width, 0, height), origin='lower')
plt.show()
EDIT: Maybe something more like
import numpy
from matplotlib import pyplot as plt
from numpy.core.umath_tests import inner1d
width, height = 300, 500
xs, ys = numpy.mgrid[:width, :height]
coordinates = numpy.dstack([xs, ys])
light_sources = {
(0, 0): (0, 0, 0),
(300, 0): (0, 0, 0),
(0, 0): (0, 0, 0),
(300, 500): (0, 0, 0),
(100, 0): (0, 0, 200),
(200, 150): (100, 70, 0),
(220, 400): (255, 255, 255),
(80, 220): (255, 0, 0),
}
weights = numpy.zeros([width, height])
values = numpy.zeros([width, height, 3])
For each light source:
for coordinate, value in light_sources.items():
Compute the (inverse) distances. Use +1e9 to prevent infinities, although this will have silly failures so a more rigorous fix would be important later:
shifted_coordinates = coordinates - coordinate + 1e-9
inverse_distances = (shifted_coordinates ** 2).sum(axis=-1) ** (-1/2)
Add it to the sum and the sum weighting:
weights += inverse_distances
values += inverse_distances[:, :, numpy.newaxis].repeat(3, axis=-1) * value / 255
Divide by the weighting to have the average:
values /= weights[..., numpy.newaxis]
And show...
plt.imshow(values, origin='lower')
plt.show()
For this:
The reason I didn't go for this originally is because the value at (0, 0.1) in your example is not (0, 0, 20) but:
distances = [0.9, 0.1]
inverse_distances = [10/9, 10]
sum_weighting = 100 / 9
blue_levels = 200 / (109/90) = 18
so it should be (0, 0, 18) by this definition.