Related
I have a set of 68 keypoints (size [68, 2]) that I am mapping to gaussian heatmaps. To do this, I have the following function:
def generate_gaussian(t, x, y, sigma=10):
"""
Generates a 2D Gaussian point at location x,y in tensor t.
x should be in range (-1, 1).
sigma is the standard deviation of the generated 2D Gaussian.
"""
h,w = t.shape
# Heatmap pixel per output pixel
mu_x = int(0.5 * (x + 1.) * w)
mu_y = int(0.5 * (y + 1.) * h)
tmp_size = sigma * 3
# Top-left
x1,y1 = int(mu_x - tmp_size), int(mu_y - tmp_size)
# Bottom right
x2, y2 = int(mu_x + tmp_size + 1), int(mu_y + tmp_size + 1)
if x1 >= w or y1 >= h or x2 < 0 or y2 < 0:
return t
size = 2 * tmp_size + 1
tx = np.arange(0, size, 1, np.float32)
ty = tx[:, np.newaxis]
x0 = y0 = size // 2
# The gaussian is not normalized, we want the center value to equal 1
g = torch.tensor(np.exp(- ((tx - x0) ** 2 + (ty - y0) ** 2) / (2 * sigma ** 2)))
# Determine the bounds of the source gaussian
g_x_min, g_x_max = max(0, -x1), min(x2, w) - x1
g_y_min, g_y_max = max(0, -y1), min(y2, h) - y1
# Image range
img_x_min, img_x_max = max(0, x1), min(x2, w)
img_y_min, img_y_max = max(0, y1), min(y2, h)
t[img_y_min:img_y_max, img_x_min:img_x_max] = \
g[g_y_min:g_y_max, g_x_min:g_x_max]
return t
def rescale(a, img_size):
# scale tensor to [-1, 1]
return 2 * a / img_size[0] - 1
My current code uses a for loop to compute the gaussian heatmap for each of the 68 keypoint coordinates, then stacks the resulting tensors to create a [68, H, W] tensor:
x_k1 = [generate_gaussian(torch.zeros(H, W), x, y) for x, y in rescale(kp1.numpy(), frame.shape)]
x_k1 = torch.stack(x_k1, dim=0)
However, this method is super slow. Is there some way that I can do this without a for loop?
Edit:
I tried #Cris Luengo's proposal to compute a 1D Gaussian:
def generate_gaussian1D(t, x, y, sigma=10):
h,w = t.shape
# Heatmap pixel per output pixel
mu_x = int(0.5 * (x + 1.) * w)
mu_y = int(0.5 * (y + 1.) * h)
tmp_size = sigma * 3
# Top-left
x1, y1 = int(mu_x - tmp_size), int(mu_y - tmp_size)
# Bottom right
x2, y2 = int(mu_x + tmp_size + 1), int(mu_y + tmp_size + 1)
if x1 >= w or y1 >= h or x2 < 0 or y2 < 0:
return t
size = 2 * tmp_size + 1
tx = np.arange(0, size, 1, np.float32)
ty = tx[:, np.newaxis]
x0 = y0 = size // 2
g = torch.tensor(np.exp(-np.power(tx - mu_x, 2.) / (2 * np.power(sigma, 2.))))
g = g * g[:, None]
g_x_min, g_x_max = max(0, -x1), min(x2, w) - x1
g_y_min, g_y_max = max(0, -y1), min(y2, h) - y1
img_x_min, img_x_max = max(0, x1), min(x2, w)
img_y_min, img_y_max = max(0, y1), min(y2, h)
t[img_y_min:img_y_max, img_x_min:img_x_max] = \
g[g_y_min:g_y_max, g_x_min:g_x_max]
return t
but my output ends up being an incomplete gaussian.
I'm not sure what I'm doing wrong. Any help would be appreciated.
You generate an NxN array g with a Gaussian centered on its center pixel. N is computed such that it extends by 3*sigma from that center pixel. This is the fastest way to build such an array:
tmp_size = sigma * 3
tx = np.arange(1, tmp_size + 1, 1, np.float32)
g = np.exp(-(tx**2) / (2 * sigma**2))
g = np.concatenate((np.flip(g), [1], g))
g = g * g[:, None]
What we're doing here is compute half a 1D Gaussian. We don't even bother computing the value of the Gaussian for the middle pixel, which we know will be 1. We then build the full 1D Gaussian by flipping our half-Gaussian and concatenating. Finally, the 2D Gaussian is built by the outer product of the 1D Gaussian with itself.
We could shave a bit of extra time by building a quarter of the 2D Gaussian, then concatenating four rotated copies of it. But the difference in computational cost is not very large, and this is much simpler. Note that np.exp is the most expensive operation here by far, so just minimizing how often we call it we significantly reduce the computational cost.
However, the best way to speed up the complete code is to compute the array g only once, rather than anew for each key point. Note how your sigma doesn't change, so all the arrays g that are computed are identical. If you compute it only once, it no longer matters which method you use to compute it, since this will be a minimal portion of the total program anyway.
You could, for example, have a global variable _gaussian to hold your array, and have your function compute it only the first time it is called. Or you could separate your function into two functions, one that constructs this array, and one that copies it into an image, and call them as follows:
g = create_gaussian(sigma=3)
x_k1 = [
copy_gaussian(torch.zeros(H, W), x, y, g)
for x, y in rescale(kp1.numpy(), frame.shape)
]
On the other hand, you're likely best off using existing functionality. For example, DIPlib has a function dip.DrawBandlimitedPoint() [disclosure: I'm an author] that adds a Gaussian blob to an image. Likely you'll find similar functions in other libraries.
I am trying to randomly generate points along the curved surface of a cylinder that has a y up-axis. Following a SO question of creating points along a 2D circle, I have
def point(h, k, r):
theta = random.random() * 2 * pi
global x
global y
x = h + cos(theta) * r
y = k + sin(theta) * r
given the cylinder's (h,k) origin point (0, -21.56462) and r (radius = 7.625). I then made these points 3D by generating a z point within my range (-2.35, 12.31). However, this got me half the way there because the final result was a cylinder but rotated 90 degrees clockwise.
Image of generated cylinder
What formula can I use that will generate the points in the correct direction? I am not that familiar with trigonometry, unfortunately. Thanks in advance!
THE SOLUTION:
def point(h, k, r):
theta = random.random() * 2 * pi
global x
global z
x = h + cos(theta) * r
z = k + sin(theta) * r
The new (h,k) origin is now (x,z) where x and z are the coordinates for the center of the cylinder and y is randomly generated within its appropriate height range. The vector is still (x,y,z).
Updated generated cylinder
THE SOLUTION:
(thanks to David Huculak)
def point(h, k, r):
theta = random.random() * 2 * pi
global x
global z
x = h + cos(theta) * r
z = k + sin(theta) * r
The new (h,k) origin is now (x,z) where x and z are the coordinates for the center of the cylinder and y is randomly generated within its appropriate height range. The vector is still (x,y,z).
Updated Generated cylinder
I'm using a python caller in fme to create polygons from points with aixm 4.5 data
Somes of the polygons contains arcs, and theirs direction clockwise (CWA) or anti-clock wise (CCA) matters, I don't know how to handle this.
here's the code I have so far:
import fme
import fmeobjects
from math import hypot
def replaceWithArc(feature):
coords = feature.getAllCoordinates()
x0, y0 = coords[0][0], coords[0][1] # coordinates of start of arc
xc, yc = coords[1][0], coords[1][1] # coordinates of cetner of arc
x2, y2 = coords[2][0], coords[2][1] # coordinates of end of arc
vx0, vy0 = (x0 - xc), (y0 - yc) # vector: center -> start
vx2, vy2 = (x2 - xc), (y2 - yc) # vector: center -> end
vx1, vy1 = (vx0 + vx2), (vy0 + vy2) # vector: center -> middle
len = hypot(vx1, vy1) # length of the vector
radius = (hypot(vx0, vy0) + hypot(vx2, vy2)) * 0.5
x1, y1 = xc + vx1 / len * radius, yc + vy1 / len * radius # coordinates of middle point on arc
threePoints = (
fmeobjects.FMEPoint(x0, y0),
fmeobjects.FMEPoint(x1, y1),
fmeobjects.FMEPoint(x2, y2)
)
feature.setGeometry(fmeobjects.FMEArc(threePoints))
This looks to me like there is something wrong with the three points.
Could you please paste the values?
From the image above it looks slightly asymmetric, but I could be wrong.
Another thing that you could try is to use a different function to initialize FMEArc, e.g.
init(twoPoints, bulge)
init(centerPoint, rotation, primaryRadius, secondaryRadius, startAngle, sweepAngle, startPoint, endPoint)
I used to draw lines (given some start and end points) at pygame like this: pygame.draw.line(window, color_L1, X0, X1, 2), where 2 was defining the thickness of the line.
As, anti-aliasing is not supported by .draw, so I moved to .gfxdraw and pygame.gfxdraw.line(window, X0[0], X0[1], X1[0], X1[1], color_L1).
However, this does not allow me to define the thickness of the line. How could I have thickness and anti-aliasing together?
After many trials and errors, the optimal way to do it would be the following:
First, we define the center point of the shape given the X0_{x,y} start and X1_{x,y} end points of the line:
center_L1 = (X0+X1) / 2.
Then find the slope (angle) of the line:
length = 10 # Total length of line
thickness = 2
angle = math.atan2(X0[1] - X1[1], X0[0] - X1[0])
Using the slope and the shape parameters you can calculate the following coordinates of the box ends:
UL = (center_L1[0] + (length/2.) * cos(angle) - (thickness/2.) * sin(angle),
center_L1[1] + (thickness/2.) * cos(angle) + (length/2.) * sin(angle))
UR = (center_L1[0] - (length/2.) * cos(angle) - (thickness/2.) * sin(angle),
center_L1[1] + (thickness/2.) * cos(angle) - (length/2.) * sin(angle))
BL = (center_L1[0] + (length/2.) * cos(angle) + (thickness/2.) * sin(angle),
center_L1[1] - (thickness/2.) * cos(angle) + (length/2.) * sin(angle))
BR = (center_L1[0] - (length/2.) * cos(angle) + (thickness/2.) * sin(angle),
center_L1[1] - (thickness/2.) * cos(angle) - (length/2.) * sin(angle))
Using the computed coordinates, we draw an unfilled anti-aliased polygon (thanks to #martineau) and then fill it as suggested in the documentation of pygame's gfxdraw module for drawing shapes.
pygame.gfxdraw.aapolygon(window, (UL, UR, BR, BL), color_L1)
pygame.gfxdraw.filled_polygon(window, (UL, UR, BR, BL), color_L1)
I would suggest a filled rectangle, as shown here: https://www.pygame.org/docs/ref/gfxdraw.html#pygame.gfxdraw.rectangle.
Your code would look something like:
thickLine = pygame.gfxdraw.rectangle(surface, rect, color)
and then remember to fill the surface. This is along the lines of:
thickLine.fill()
You can also do a bit of a hack with the pygame.draw.aalines() function by drawing copies of the line +/- 1-N pixels around the original line (yes, this isn't super efficient, but it works in a pinch). For example, assuming we have a list of line segments (self._segments) to draw and with a width (self._LINE_WIDTH):
for segment in self._segments:
if len(segment) > 2:
for i in xrange(self._LINE_WIDTH):
pygame.draw.aalines(self._display, self._LINE_COLOR, False,
((x,y+i) for x,y in segment))
pygame.draw.aalines(self._display, self._LINE_COLOR, False,
((x,y-i) for x,y in segment))
pygame.draw.aalines(self._display, self._LINE_COLOR, False,
((x+i,y) for x,y in segment))
pygame.draw.aalines(self._display, self._LINE_COLOR, False,
((x-i,y) for x,y in segment))
Your answer gets the job done but I think this would be a better/more readable way to do it. This is piggybacking off of your answer though so credit to you.
from math import atan2, cos, degrees, radians, sin
def Move(rotation, steps, position):
"""Return coordinate position of an amount of steps in a direction."""
xPosition = cos(radians(rotation)) * steps + position[0]
yPosition = sin(radians(rotation)) * steps + position[1]
return (xPosition, yPosition)
def DrawThickLine(surface, point1, point2, thickness, color):
angle = degrees(atan2(point1[1] - point2[1], point1[0] - point2[0]))
vertices = list()
vertices.append(Move(angle-90, thickness, point1))
vertices.append(Move(angle+90, thickness, point1))
vertices.append(Move(angle+90, thickness, point2))
vertices.append(Move(angle-90, thickness, point2))
pygame.gfxdraw.aapolygon(surface, vertices, color)
pygame.gfxdraw.filled_polygon(surface, vertices, color)
Keep in mind that this treats the thickness more as a radius than a diameter. If you want it to act more like a diameter you can divide each instance of the variable by 2.
So anyway, this calculates all the points of the rectangle and fills it in. It does this by going to each point and calculating the two adjacent points by turning 90 degrees and moving forward.
Here is a slightly faster and shorter solution:
def drawLineWidth(surface, color, p1, p2, width):
# delta vector
d = (p2[0] - p1[0], p2[1] - p1[1])
# distance between the points
dis = math.hypot(*d)
# normalized vector
n = (d[0]/dis, d[1]/dis)
# perpendicular vector
p = (-n[1], n[0])
# scaled perpendicular vector (vector from p1 & p2 to the polygon's points)
sp = (p[0]*width/2, p[1]*width/2)
# points
p1_1 = (p1[0] - sp[0], p1[1] - sp[1])
p1_2 = (p1[0] + sp[0], p1[1] + sp[1])
p2_1 = (p2[0] - sp[0], p2[1] - sp[1])
p2_2 = (p2[0] + sp[0], p2[1] + sp[1])
# draw the polygon
pygame.gfxdraw.aapolygon(surface, (p1_1, p1_2, p2_2, p2_1), color)
pygame.gfxdraw.filled_polygon(surface, (p1_1, p1_2, p2_2, p2_1), color)
The polygon's points here are calculated using vector math rather than trigonometry, which is much less costly.
If efficiency is of the essence, it's easy to further optimize this code - for instance the first few lines can be condensed to:
d = (p2[0] - p1[0], p2[1] - p1[1])
dis = math.hypot(*d)
sp = (-d[1]*width/(2*dis), d[0]*width/(2*dis))
Hope this helps someone.
This is a slightly longer code, but maybe will help someone.
It uses vectors and create a stroke on each side of the line connecting two points.
def make_vector(pointA,pointB): #vector between two points
x1,y1,x2,y2 = pointA[0],pointA[1],pointB[0],pointB[1]
x,y = x2-x1,y2-y1
return x,y
def normalize_vector(vector): #sel explanatory
x, y = vector[0], vector[1]
u = math.sqrt(x ** 2 + y ** 2)
try:
return x / u, y / u
except:
return 0,0
def perp_vectorCL(vector): #creates a vector perpendicular to the first clockwise
x, y = vector[0], vector[1]
return y, -x
def perp_vectorCC(vector): #creates a vector perpendicular to the first counterclockwise
x, y = vector[0], vector[1]
return -y, x
def add_thickness(point,vector,thickness): #offsets a point by the vector
return point[0] + vector[0] * thickness, point[1] + vector[1] * thickness
def draw_line(surface,fill,thickness, start,end): #all draw instructions
x,y = make_vector(start,end)
x,y = normalize_vector((x,y))
sx1,sy1 = add_thickness(start,perp_vectorCC((x,y)),thickness//2)
ex1,ey1 = add_thickness(end,perp_vectorCC((x,y)),thickness//2)
pygame.gfxdraw.aapolygon(surface,(start,end,(ex1,ey1),(sx1,sy1)),fill)
pygame.gfxdraw.filled_polygon(surface, (start, end, (ex1, ey1), (sx1, sy1)), fill)
sx2, sy2 = add_thickness(start, perp_vectorCL((x, y)), thickness // 2)
ex2, ey2 = add_thickness(end, perp_vectorCL((x, y)), thickness//2)
pygame.gfxdraw.aapolygon(surface, (start, end, (ex2, ey2), (sx2, sy2)), fill)
pygame.gfxdraw.filled_polygon(surface, (start, end, (ex2, ey2), (sx2, sy2)), fill)
I've been trying to rotate a bunch of lines by 90 degrees (that together form a polyline). Each line contains two vertices, say (x1, y1) and (x2, y2). What I'm currently trying to do is rotate around the center point of the line, given center points |x1 - x2| and |y1 - y2|. For some reason (I'm not very mathematically savvy) I can't get the lines to rotate correctly.
Could someone verify that the math here is correct? I'm thinking that it could be correct, however, when I set the line's vertices to the new rotated vertices, the next line may not be grabbing the new (x2, y2) vertex from the previous line, causing the lines to rotate incorrectly.
Here's what I've written:
def rotate_lines(self, deg=-90):
# Convert from degrees to radians
theta = math.radians(deg)
for pl in self.polylines:
self.curr_pl = pl
for line in pl.lines:
# Get the vertices of the line
# (px, py) = first vertex
# (ox, oy) = second vertex
px, ox = line.get_xdata()
py, oy = line.get_ydata()
# Get the center of the line
cx = math.fabs(px-ox)
cy = math.fabs(py-oy)
# Rotate line around center point
p1x = cx - ((px-cx) * math.cos(theta)) - ((py-cy) * math.sin(theta))
p1y = cy - ((px-cx) * math.sin(theta)) + ((py-cy) * math.cos(theta))
p2x = cx - ((ox-cx) * math.cos(theta)) - ((oy-cy) * math.sin(theta))
p2y = cy - ((ox-cx) * math.sin(theta)) + ((oy-cy) * math.cos(theta))
self.curr_pl.set_line(line, [p1x, p2x], [p1y, p2y])
The coordinates of the center point (cx,cy) of a line segment between points (x1,y1) and (x2,y2) are:
cx = (x1 + x2) / 2
cy = (y1 + y2) / 2
In other words it's just the average, or arithmetic mean, of the two pairs of x and y coordinate values.
For a multi-segmented line, or polyline, its logical center point's x and y coordinates are just the corresponding average of x and y values of all the points. An average is just the sum of the values divided by the number of them.
The general formulas to rotate a 2D point (x,y) θ radians around the origin (0,0) are:
x′ = x * cos(θ) - y * sin(θ)
y′ = x * sin(θ) + y * cos(θ)
To perform a rotation about a different center (cx, cy), the x and y values of the point need to be adjusted by first subtracting the coordinate of the desired center of rotation from the point's coordinate, which has the effect of moving (known in geometry as translating) it is expressed mathematically like this:
tx = x - cx
ty = y - cy
then rotating this intermediate point by the angle desired, and finally adding the x and y values of the point of rotation back to the x and y of each coordinate. In geometric terms, it's the following sequence of operations: Tʀᴀɴsʟᴀᴛᴇ ─► Rᴏᴛᴀᴛᴇ ─► Uɴᴛʀᴀɴsʟᴀᴛᴇ.
This concept can be extended to allow rotating a whole polyline about any arbitrary point—such as its own logical center—by just applying the math described to each point of each line segment within it.
To simplify implementation of this computation, the numerical result of all three sets of calculations can be combined and expressed with a pair of mathematical formulas which perform them all simultaneously. So a new point (x′,y′) can be obtained by rotating an existing point (x,y), θ radians around the point (cx, cy) by using:
x′ = ( (x - cx) * cos(θ) + (y - cy) * sin(θ) ) + cx
y′ = ( -(x - cx) * sin(θ) + (y - cy) * cos(θ) ) + cy
Incorporating this mathematical/geometrical concept into your function produces the following:
from math import sin, cos, radians
def rotate_lines(self, deg=-90):
""" Rotate self.polylines the given angle about their centers. """
theta = radians(deg) # Convert angle from degrees to radians
cosang, sinang = cos(theta), sin(theta)
for pl in self.polylines:
# Find logical center (avg x and avg y) of entire polyline
n = len(pl.lines)*2 # Total number of points in polyline
cx = sum(sum(line.get_xdata()) for line in pl.lines) / n
cy = sum(sum(line.get_ydata()) for line in pl.lines) / n
for line in pl.lines:
# Retrieve vertices of the line
x1, x2 = line.get_xdata()
y1, y2 = line.get_ydata()
# Rotate each around whole polyline's center point
tx1, ty1 = x1-cx, y1-cy
p1x = ( tx1*cosang + ty1*sinang) + cx
p1y = (-tx1*sinang + ty1*cosang) + cy
tx2, ty2 = x2-cx, y2-cy
p2x = ( tx2*cosang + ty2*sinang) + cx
p2y = (-tx2*sinang + ty2*cosang) + cy
# Replace vertices with updated values
pl.set_line(line, [p1x, p2x], [p1y, p2y])
Your center point is going to be:
centerX = (x2 - x1) / 2 + x1
centerY = (y2 - y1) / 2 + y1
because you take half the length (x2 - x1) / 2 and add it to where your line starts to get to the middle.
As an exercise, take two lines:
line1 = (0, 0) -> (5, 5)
then: |x1 - x2| = 5, when the center x value is at 2.5.
line2 = (2, 2) -> (7, 7)
then: |x1 - x2| = 5, which can't be right because that's the center for
the line that's parallel to it but shifted downwards and to the left