Get points of a line that touch the edge of the image - python

I need help to get the real "start" and "end" points where a line is drawn in the image matrix. In the example below, a detected line is shown using y0 and y1.
import numpy as np
from skimage.transform import hough_line, hough_line_peaks
from skimage.feature import canny
from skimage import data
import matplotlib.pyplot as plt
from matplotlib import cm
# Constructing test image
image = np.zeros((200, 200))
idx = np.arange(0, 200)
image[idx, idx//2] = 255
imgOriginal = image.copy()
# Classic straight-line Hough transform
# Set a precision of 0.5 degree.
tested_angles = np.linspace(-np.pi / 2, np.pi / 2, 360)
h, theta, d = hough_line(image, theta=tested_angles)
# Generating figure 1
fig, axes = plt.subplots(1, 3, figsize=(15, 6))
ax = axes.ravel()
ax[0].imshow(image, cmap=cm.gray)
ax[0].set_title('Input image')
ax[0].set_axis_off()
ax[1].imshow(np.log(1 + h),
extent=[np.rad2deg(theta[-1]), np.rad2deg(theta[0]), d[-1], d[0]],
cmap=cm.gray, aspect=1/1.5)
ax[1].set_title('Hough transform')
ax[1].set_xlabel('Angles (degrees)')
ax[1].set_ylabel('Distance (pixels)')
ax[1].axis('image')
ax[2].imshow(image, cmap=cm.gray)
origin = np.array((0, image.shape[1]))
for _, angle, dist in zip(*hough_line_peaks(h, theta, d)):
y0, y1 = (dist - origin * np.cos(angle)) / np.sin(angle)
print('y0 = {} y1 = {}'.format(y0, y1))
ax[2].plot(origin, (y0, y1), '-r')
ax[2].set_xlim(origin)
ax[2].set_ylim((image.shape[0], 0))
ax[2].set_axis_off()
ax[2].set_title('Detected lines')
plt.tight_layout()
plt.show()
This code results in:
What I want is to get the following points in the real image matrix:
Which is likely to be (0,0) and (199, 100)
In summary, I want to transform y0 and y1 into real points in my numpy matrix.

Your problem is to find the point of intersection between two lines essentially. One is your given line and the other is the line defined by the edge of the image.
That can be done as explained here. I am borrowing the code in the first answer. Define these functions -
def line_intersection(line1, line2):
xdiff = (line1[0][0] - line1[1][0], line2[0][0] - line2[1][0])
ydiff = (line1[0][1] - line1[1][1], line2[0][1] - line2[1][1])
def det(a, b):
return a[0] * b[1] - a[1] * b[0]
div = det(xdiff, ydiff)
if div == 0:
raise Exception('lines do not intersect')
d = (det(*line1), det(*line2))
x = det(d, xdiff) / div
y = det(d, ydiff) / div
return x, y
Change your loop as follows -
for _, angle, dist in zip(*hough_line_peaks(h, theta, d)):
y0, y1 = (dist - origin * np.cos(angle)) / np.sin(angle)
print('y0 = {} y1 = {}'.format(y0, y1))
ax[2].plot(origin, (y0, y1), '-r')
l1 = ((origin[0], y0), (origin[1], y1))
l2 = ((0, 200), (200, 200))
print(line_intersection(l1, l2))
This is obviously assuming that the line of interest always intersects with the lower edge of the image. If the line intersects with the right edge, l2 will have to be modified accordingly. In practice, I would suggest finding the intersect with the two edges and picking the "closest" intersect.
This also assumes that the the line of interest always starts from the top left corner of the image (as you have defined your problem) . If that's not the case, you would need to do this for all four edges of the image and pick the first two intersects.

Related

Drawing an arc tangent to two lines segments in Python

I'm trying to draw an arc of n number of steps between two points so that I can bevel a 2D shape. This image illustrates what I'm looking to create (the blue arc) and how I'm trying to go about it:
move by the radius away from the target point (red)
get the normals of those lines
get the intersections of the normals to find the center of the circle
Draw an arc between those points from the circle's center
This is what I have so far:
As you can see, the circle is not tangent to the line segments. I think my approach may be flawed thinking that the two points used for the normal lines should be moved by the circle's radius. Can anyone please tell me where I am going wrong and how I might be able to find this arc of points? Here is my code:
import matplotlib.pyplot as plt
import numpy as np
#https://stackoverflow.com/questions/51223685/create-circle-tangent-to-two-lines-with-radius-r-geometry
def travel(dx, x1, y1, x2, y2):
a = {"x": x2 - x1, "y": y2 - y1}
mag = np.sqrt(a["x"]*a["x"] + a["y"]*a["y"])
if (mag == 0):
a["x"] = a["y"] = 0;
else:
a["x"] = a["x"]/mag*dx
a["y"] = a["y"]/mag*dx
return [x1 + a["x"], y1 + a["y"]]
def plot_line(line,color="go-",label=""):
plt.plot([p[0] for p in line],
[p[1] for p in line],color,label=label)
def line_intersection(line1, line2):
xdiff = (line1[0][0] - line1[1][0], line2[0][0] - line2[1][0])
ydiff = (line1[0][1] - line1[1][1], line2[0][1] - line2[1][1])
def det(a, b):
return a[0] * b[1] - a[1] * b[0]
div = det(xdiff, ydiff)
if div == 0:
raise Exception('lines do not intersect')
d = (det(*line1), det(*line2))
x = det(d, xdiff) / div
y = det(d, ydiff) / div
return x, y
line_segment1 = [[1,1],[4,8]]
line_segment2 = [[4,8],[8,8]]
line = line_segment1 + line_segment2
plot_line(line,'k-')
radius = 2
l1_x1 = line_segment1[0][0]
l1_y1 = line_segment1[0][1]
l1_x2 = line_segment1[1][0]
l1_y2 = line_segment1[1][1]
new_point1 = travel(radius, l1_x2, l1_y2, l1_x1, l1_y1)
l2_x1 = line_segment2[0][0]
l2_y1 = line_segment2[0][1]
l2_x2 = line_segment2[1][0]
l2_y2 = line_segment2[1][1]
new_point2 = travel(radius, l2_x1, l2_y1, l2_x2, l2_y2)
plt.plot(line_segment1[1][0], line_segment1[1][1],'ro',label="Point 1")
plt.plot(new_point2[0], new_point2[1],'go',label="radius from Point 1")
plt.plot(new_point1[0], new_point1[1],'mo',label="radius from Point 1")
# normal 1
dx = l1_x2 - l1_x1
dy = l1_y2 - l1_y1
normal_line1 = [[new_point1[0]+-dy, new_point1[1]+dx],[new_point1[0]+dy, new_point1[1]+-dx]]
plot_line(normal_line1,'m',label="normal 1")
# normal 2
dx2 = l2_x2 - l2_x1
dy2 = l2_y2 - l2_y1
normal_line2 = [[new_point2[0]+-dy2, new_point2[1]+dx2],[new_point2[0]+dy2, new_point2[1]+-dx2]]
plot_line(normal_line2,'g',label="normal 2")
x, y = line_intersection(normal_line1,normal_line2)
plt.plot(x, y,'bo',label="intersection") #'blue'
theta = np.linspace( 0 , 2 * np.pi , 150 )
a = x + radius * np.cos( theta )
b = y + radius * np.sin( theta )
plt.plot(a, b)
plt.legend()
plt.axis('square')
plt.show()
Thanks a lot!
You could try making a Bezier curve, like in this example. A basic implementation might be:
import matplotlib.path as mpath
import matplotlib.patches as mpatches
import matplotlib.pyplot as plt
Path = mpath.Path
fig, ax = plt.subplots()
# roughly equivalent of your purple, red and green points
points = [(3, 6.146), (4, 8), (6, 8.25)]
pp1 = mpatches.PathPatch(
Path(points, [Path.MOVETO, Path.CURVE3, Path.CURVE3]),
fc="none",
transform=ax.transData
)
ax.add_patch(pp1)
# lines between points
ax.plot([points[0][0], points[1][0]], [points[0][1], points[1][1]], 'b')
ax.plot([points[1][0], points[2][0]], [points[1][1], points[2][1]], 'b')
# plot points
for point in points:
ax.plot(point[0], point[1], 'o')
ax.set_aspect("equal")
plt.show()
which gives:
To do this without using a Matplotlib PathPatch object, you can calculate the Bezier points as, for example, in this answer, which I'll use below to do the same as above (note to avoid using scipy's comb function, as in that answer, I've used the comb function from here):
import numpy as np
from math import factorial
from matplotlib import pyplot as plt
def comb(n, k):
"""
N choose k
"""
return factorial(n) / factorial(k) / factorial(n - k)
def bernstein_poly(i, n, t):
"""
The Bernstein polynomial of n, i as a function of t
"""
return comb(n, i) * ( t**(n-i) ) * (1 - t)**i
def bezier_curve(points, n=1000):
"""
Given a set of control points, return the
bezier curve defined by the control points.
points should be a list of lists, or list of tuples
such as [ [1,1],
[2,3],
[4,5], ..[Xn, Yn] ]
n is the number of points at which to return the curve, defaults to 1000
See http://processingjs.nihongoresources.com/bezierinfo/
"""
nPoints = len(points)
xPoints = np.array([p[0] for p in points])
yPoints = np.array([p[1] for p in points])
t = np.linspace(0.0, 1.0, n)
polynomial_array = np.array(
[bernstein_poly(i, nPoints-1, t) for i in range(0, nPoints)]
)
xvals = np.dot(xPoints, polynomial_array)
yvals = np.dot(yPoints, polynomial_array)
return xvals, yvals
# set control points (as in the first example)
points = [(3, 6.146), (4, 8), (6, 8.25)]
# get the Bezier curve points at 100 points
xvals, yvals = bezier_curve(points, n=100)
# make the plot
fig, ax = plt.subplots()
# lines between control points
ax.plot([points[0][0], points[1][0]], [points[0][1], points[1][1]], 'b')
ax.plot([points[1][0], points[2][0]], [points[1][1], points[2][1]], 'b')
# plot control points
for point in points:
ax.plot(point[0], point[1], 'o')
# plot the Bezier curve
ax.plot(xvals, yvals, "k--")
ax.set_aspect("equal")
fig.show()
This gives:
If you are not just interested in the solution but in better understanding of this problem, you should read the article on Curved Paths that Amit Patel wrote in his 'Red Blob Games' blog.
https://www.redblobgames.com/articles/curved-paths/

Drawing an ellipse at an angle between two points in Python

I'm trying to draw an ellipse between two points. So far, I have it mostly working:
The issue comes with setting the ellipse height (ellipse_h below).
x = center_x + radius*np.cos(theta+deg)
y = center_y - ellipse_h * radius*np.sin(theta+deg)
In this example, it's set to -0.5:
Can anyone please help me rotate the ellipse height with the ellipse? Thank you!
import numpy as np
import matplotlib.pyplot as plt
def distance(x1, y1, x2, y2):
return np.sqrt(np.power(x2 - x1, 2) + np.power(y2 - y1, 2) * 1.0)
def midpoint(x1, y1, x2, y2):
return [(x1 + x2) / 2,(y1 + y2) / 2]
def angle(x1, y1, x2, y2):
#radians
return np.arctan2(y2 - y1, x2 - x1)
x1 = 100
y1 = 150
x2 = 200
y2 = 190
ellipse_h = -1
x_coords = []
y_coords = []
mid = midpoint(x1, y1, x2, y2)
center_x = mid[0]
center_y = mid[1]
ellipse_resolution = 40
step = 2*np.pi/ellipse_resolution
radius = distance(x1, y1, x2, y2) * 0.5
deg = angle(x1, y1, x2, y2)
cos = np.cos(deg * np.pi /180)
sin = np.sin(deg * np.pi /180)
for theta in np.arange(0, np.pi+step, step):
x = center_x + radius*np.cos(theta+deg)
y = center_y - ellipse_h * radius*np.sin(theta+deg)
x_coords.append(x)
y_coords.append(y)
plt.xlabel("X")
plt.ylabel("Y")
plt.title("Arc between 2 Points")
plt.plot(x_coords,y_coords)
plt.scatter([x1,x2],[y1,y2])
plt.axis('equal')
plt.show()
A simple solution is to describe the ellipse by its standard parametric equation, as you effectively did. However, under the assumption that it is centered on the origin of the coordinate system, it becomes straightforward to then apply a rotation to its points using a 2d rotation matrix and finally apply a translation to position it on its true center. This gives the following:
import numpy as np
import matplotlib.pyplot as plt
# extreme points along the major axis
x1, y1 = 100, 150
x2, y2 = 200, 190
# along minor axis
height = 15
# number of points
n = 100
# center
x_c, y_c = (x1 + x2)/2, (y1 + y2)/2
# width (major axis) and height (minor) of the ellipse halved
a, b = np.sqrt((x2 - x1)**2 + (y2 - y1)**2)/2, height/2
# rotation angle
angle = np.arctan2(y2 - y1, x2 - x1)
# standard parametric equation of an ellipse
t = np.linspace(0, 2*np.pi, n)
ellipse = np.array([a*np.cos(t), b*np.sin(t)])
# 2d rotation matrix
R = np.array([[np.cos(angle), -np.sin(angle)],
[np.sin(angle), np.cos(angle)]])
# apply the rotation to the ellipse
ellipse_rot = R # ellipse
plt.plot(x_c + ellipse_rot[0], y_c + ellipse_rot[1], 'r' )
plt.scatter([x1, x2], [y1, y2], color='k')
plt.axis('equal')
plt.show()
See the output for different heights:
Following your comment, for the limiting case of the circle, you need to specify height = np.sqrt((x2 - x1)**2 + (y2 - y1)**2), so that a = b.
Hope this helps !

How to draw lines on an image by giving the end point and an angle with respect to the vertical axis in Python

I want to draw a line on an image. I have only to give the angle and the end point of the line. How can I do this with Python?
I think it is easy by identifying the vertical line passing through that given point and ploting the line according to the angle. The line should ends with the given point.
I tried it with this code. But didn't work.
import math
def get_coords(x, y, angle, imwidth, imheight):
#img = cv2.imread('contours_none_image2.jpg', 1)
x1_length = (x-imwidth) / math.cos(angle)
y1_length = (y-imheight) / math.sin(angle)
length = max(abs(x1_length), abs(y1_length))
endx1 = x + length * math.cos(math.radians(angle))
endy1 = y + length * math.sin(math.radians(angle))
x2_length = (x-imwidth) / math.cos(angle+45)
y2_length = (y-imheight) / math.sin(angle+45)
length = max(abs(x2_length), abs(y2_length))
endx2 = x + length * math.cos(math.radians(angle+45))
endy2 = y + length * math.sin(math.radians(angle+45))
cv2.line(img, (int(endx1),int(endy1)), (int(endx2),int(endy2)), (0, 255, 255), 3)
cv2.imshow("contours_none_image2.jpg", img)
#cv2.imshow("contours_none_image2.jpg", result)
cv2.waitKey(0)
cv2.destroyAllWindows()
return endx1, endy1, endx2, endy2
An interesting way for finding the intersection point between the Y axis and the line is by using three cross products with homogeneous coordinates.
Ways for finding lines intersections are described in Wikipedia.
The cross products solution using homogeneous coordinates is described here.
Start by finding a very "far" origin point (x, y) - outside the image:
length = cv2.norm(np.array([imwidth, imheight])) # Apply maximum possible length: length = sqrt(imwidth**2 + imheight**2)
x0 = x - length * math.cos(math.radians(angle))
y0 = y + length * math.sin(math.radians(angle)) # Reverse sings because y axis in image goes down
Finding intersection with the Y axis:
The Y axis may be described as a line from (0,0) to (0, imheight-1).
We may find the line representation in homogeneous coordinates using cross product:
p0 = np.array([0, 0, 1])
p1 = np.array([0, imheight-1, 1])
l0 = np.cross(p0, p1) # [-107, 0, 0]
In the same way we may find the representation of the line from (x0, y0) to (x, y):
p0 = np.array([x0, y0, 1])
p1 = np.array([x, y, 1])
l1 = np.cross(p0, p1)
Finding the intersection point using cross product between the lines, and "normalizing" the homogeneous coordinate:
p = np.cross(l0, l1)
p = p / p[2]
Code sample:
import math
import cv2
import numpy as np
img = np.zeros((108, 192, 3), np.uint8)
x, y, angle = 150, 20, 80
imheight, imwidth = img.shape[0], img.shape[1]
angle = 90 - angle # Usualy the angle is relative to the horizontal axis - use 90 - angle for swaping axes
length = cv2.norm(np.array([imwidth, imheight])) # Apply maximum possible length: length = sqrt(imwidth**2 + imheight**2)
x0 = x - length * math.cos(math.radians(angle))
y0 = y + length * math.sin(math.radians(angle)) # Reverse sings because y axis in image goes down
# http://robotics.stanford.edu/~birch/projective/node4.html
# Find lines in homogeneous coordinates (using cross product):
# l0 represents a line of Y axis.
p0 = np.array([0, 0, 1])
p1 = np.array([0, imheight-1, 1])
l0 = np.cross(p0, p1) # [-107, 0, 0]
# l1 represents
p0 = np.array([x0, y0, 1])
p1 = np.array([x, y, 1])
l1 = np.cross(p0, p1)
# https://en.wikipedia.org/wiki/Line%E2%80%93line_intersection
# Lines intersection in homogeneous coordinates (using cross product):
p = np.cross(l0, l1)
p = p / p[2]
x0, y0 = p[0], p[1]
# Convert from homogeneous coordinate to euclidean coordinate (divide by last element).
cv2.line(img, (int(x0),int(y0)), (int(x),int(y)), (0, 255, 255), 3)
cv2.imshow("img", img)
cv2.waitKey(0)
cv2.destroyAllWindows()
Sample output:
More conventional solution:
We may simply assign x0 = 0, and find length:
x0 = x - length * cos(alpha)
y0 = y + length * sin(alpha)
Assign x0 = 0:
x - length * cos(alpha) = 0
=> x = length * cos(alpha)
=> length = x/cos(alpha)
Code:
length = x / math.cos(math.radians(angle)) # We better verify that math.cos(math.radians(angle)) != 0
x0 = 0
y0 = y + length * math.sin(math.radians(angle))
cv2.line(img, (int(x0),int(y0)), (int(x),int(y)), (255, 0, 0), 3)
cv2.imshow("img", img)
cv2.waitKey(0)
cv2.destroyAllWindows()
Output:

How do you calculate spherical camera position from point correspondence?

I have 4 points marked in an equirectangular image. [Red dots]
I also have the 4 corresponding points marked in an overhead image [ Red dots ]
How do I calculate where on the overhead image the camera was positioned?
So far I see there are 4 rays (R1, R2, R3, R4) extending from the unknown camera center C = (Cx, Cy, Cz) through the points in the equirectangular image and ending at the pixel coordinates of the overhead image (P1, P2, P3, P4). So 4 vector equations of the form:
[Cx, Cy, Cz] + [Rx, Ry, Rz]*t = [x, y, 0]
for each correspondence. So
C + R1*t1 = P1 = [x1, y1, 0]
C + R2*t2 = P2 = [x2, y2, 0]
C + R3*t3 = P3 = [x3, y3, 0]
C + R4*t4 = P4 = [x4, y4, 0]
So 7 unknowns and 12 equations? This was my attempt but doesn't seem to give a reasonable answer:
import numpy as np
def equi2sphere(x, y):
width = 2000
height = 1000
theta = 2 * np.pi * x / width - np.pi
phi = np.pi * y / height
return theta, phi
HEIGHT = 1000
MAP_HEIGHT = 788
#
# HEIGHT = 0
# MAP_HEIGHT = 0
# Point in equirectangular image, bottom left = (0, 0)
xs = [1190, 1325, 1178, 1333]
ys = [HEIGHT - 730, HEIGHT - 730, HEIGHT - 756, HEIGHT - 760]
# import cv2
# img = cv2.imread('equirectangular.jpg')
# for x, y in zip(xs, ys):
# img = cv2.circle(img, (x, y), 15, (255, 0, 0), -1)
# cv2.imwrite("debug_equirectangular.png", img)
# Corresponding points in overhead map, bottom left = (0, 0)
px = [269, 382, 269, 383]
py = [778, 778, 736, 737]
# import cv2
# img = cv2.imread('map.png')
# for x, y in zip(px, py):
# img = cv2.circle(img, (x, y), 15, (255, 0, 0), -1)
# cv2.imwrite("debug_map.png", img)
As = []
bs = []
for i in range(4):
x, y = xs[i], ys[i]
theta, phi = equi2sphere(x, y)
# convert to spherical
p = 1
sx = p * np.sin(phi) * np.cos(theta)
sy = p * np.sin(phi) * np.sin(theta)
sz = p * np.cos(phi)
print(x, y, '->', np.degrees(theta), np.degrees(phi), '->', round(sx, 2), round(sy, 2), round(sz, 2))
block = np.array([
[1, 0, 0, sx],
[0, 1, 0, sy],
[1, 0, 1, sz],
])
y = np.array([px[i], py[i], 0])
As.append(block)
bs.append(y)
A = np.vstack(As)
b = np.hstack(bs).T
solution = np.linalg.lstsq(A, b)
Cx, Cy, Cz, t = solution[0]
import cv2
img = cv2.imread('map_overhead.png')
for i in range(4):
x, y = xs[i], ys[i]
theta, phi = equi2sphere(x, y)
# convert to spherical
p = 1
sx = p * np.sin(phi) * np.cos(theta)
sy = p * np.sin(phi) * np.sin(theta)
sz = p * np.cos(phi)
pixel_x = Cx + sx * t
pixel_y = Cy + sy * t
pixel_z = Cz + sz * t
print(pixel_x, pixel_y, pixel_z)
img = cv2.circle(img, (int(pixel_x), img.shape[0] - int(pixel_y)), 15, (255,255, 0), -1)
img = cv2.circle(img, (int(Cx), img.shape[0] - int(Cy)), 15, (0,255, 0), -1)
cv2.imwrite("solution.png", img)
# print(A.dot(solution[0]))
# print(b)
Resulting camera position (Green) and projected points (Teal)
EDIT: One bug fixed is that the longitude offset in the equirectangular images in PI/4 which fixes the rotation issue but the scale is still off somehow.
EDIT: using the MAP picture width/length for spherical conversion gives way better results for camera center. Points positions are still a bit messy.
Map with a better solution for camera center: , points are somewhat flattened
I took the liberty of rewriting a bit of the code, adding points identification using variables and colors (In your original code, some points were in different order in the various lists).
This is preferable if one wants to work with more data points. yeah, I chose a dict for debug purposes, but a list of N points would indeed be preferrable, provided that theyare correctly index paired between the different projections.
I also adapted the coordinates to match the pictures I had. And the x,y variables usage/naming for my understanding.
It is still incorrect, but there is some sort of consistency between each found position.
Possible cause
OpenCV images put the [0,0] in the TOPLEFT corner. The code below is consistent with that convention for points coordinates, but I did not change any math formula.
Maybe there is an error or inconsistencies in some of the formulas.
You may want to check again your conventions : signs, [0,0] location etc.
I don't see any input related to camera location and altitude in the formulas, which may be a source of error.
You may have a look to this project that performs equirectangular projections: https://github.com/NitishMutha/equirectangular-toolbox
from typing import Dict
import cv2
import numpy as np
def equi2sphere(x, y, width, height):
theta = (2 * np.pi * x / width) - np.pi
phi = (np.pi * y / height)
return theta, phi
WIDTH = 805
HEIGHT = 374 # using stackoverflow PNG
MAP_WIDTH = 662
MAP_HEIGHT = 1056 # using stackoverflow PNG
BLUE = (255, 0, 0)
GREEN = (0, 255, 0)
RED = (0, 0, 255)
CYAN = (255, 255, 0)
points_colors = [BLUE, GREEN, RED, CYAN]
TOP_LEFT = "TOP_LEFT"
TOP_RIGHT = "TOP_RIGHT"
BOTTOM_LEFT = "BOTTOM_LEFT"
BOTTOM_RIGHT = "BOTTOM_RIGHT"
class Point:
def __init__(self, x, y, color):
self.x = x
self.y = y
self.c = color
#property
def coords(self):
return (self.x, self.y)
# coords using GIMP which uses upperleft [0,0]
POINTS_ON_SPHERICAL_MAP: Dict[str, Point] = {TOP_LEFT : Point(480, 263, BLUE),
TOP_RIGHT : Point(532, 265, GREEN),
BOTTOM_LEFT : Point(473, 274, RED),
BOTTOM_RIGHT: Point(535, 275, CYAN),
}
# xs = [480, 532, 473, 535, ]
# ys = [263, 265, 274, 275, ]
img = cv2.imread('equirectangular.png')
for p in POINTS_ON_SPHERICAL_MAP.values():
img = cv2.circle(img, p.coords, 5, p.c, -1)
cv2.imwrite("debug_equirectangular.png", img)
# coords using GIMP which uses upperleft [0,0]
# px = [269, 382, 269, 383]
# py = [278, 278, 320, 319]
POINTS_ON_OVERHEAD_MAP: Dict[str, Point] = {TOP_LEFT : Point(269, 278, BLUE),
TOP_RIGHT : Point(382, 278, GREEN),
BOTTOM_LEFT : Point(269, 320, RED),
BOTTOM_RIGHT: Point(383, 319, CYAN),
}
img = cv2.imread('map.png')
for p in POINTS_ON_OVERHEAD_MAP.values():
img = cv2.circle(img, p.coords, 5, p.c, -1)
cv2.imwrite("debug_map.png", img)
As = []
bs = []
for point_location in [TOP_LEFT, TOP_RIGHT, BOTTOM_LEFT, BOTTOM_RIGHT]:
x_spherical, y_spherical = POINTS_ON_SPHERICAL_MAP[point_location].coords
theta, phi = equi2sphere(x=x_spherical, y=y_spherical, width=MAP_WIDTH, height=MAP_HEIGHT) # using the overhead map data for conversions
# convert to spherical
p = 1
sx = p * np.sin(phi) * np.cos(theta)
sy = p * np.sin(phi) * np.sin(theta)
sz = p * np.cos(phi)
print(f"{x_spherical}, {y_spherical} -> {np.degrees(theta):+.3f}, {np.degrees(phi):+.3f} -> {sx:+.3f}, {sy:+.3f}, {sz:+.3f}")
block = np.array([[1, 0, 0, sx],
[0, 1, 0, sy],
[1, 0, 1, sz], ])
x_map, y_map = POINTS_ON_OVERHEAD_MAP[point_location].coords
vector = np.array([x_map, y_map, 0])
As.append(block)
bs.append(vector)
A = np.vstack(As)
b = np.hstack(bs).T
solution = np.linalg.lstsq(A, b)
Cx, Cy, Cz, t = solution[0]
img = cv2.imread("debug_map.png")
for point_location in [TOP_LEFT, TOP_RIGHT, BOTTOM_LEFT, BOTTOM_RIGHT]:
x_spherical, y_spherical = POINTS_ON_SPHERICAL_MAP[point_location].coords
theta, phi = equi2sphere(x=x_spherical, y=y_spherical, width=MAP_WIDTH, height=MAP_HEIGHT) # using the overhead map data for conversions
# convert to spherical
p = 1
sx = p * np.sin(phi) * np.cos(theta)
sy = p * np.sin(phi) * np.sin(theta)
sz = p * np.cos(phi)
pixel_x = Cx + sx * t
pixel_y = Cy + sy * t
pixel_z = Cz + sz * t
print(f"{pixel_x:+0.0f}, {pixel_y:+0.0f}, {pixel_z:+0.0f}")
img = cv2.circle(img, (int(pixel_x), int(pixel_y)), 5, POINTS_ON_SPHERICAL_MAP[point_location].c, -1)
img = cv2.circle(img, (int(Cx), int(Cy)), 4, (200, 200, 127), 3)
cv2.imwrite("solution.png", img)
Map with my initial solution:
Debug map:
Equirectangular image:
Debug equirectangular:
To expand on my comment, here's the method I use to first calculate Cx and Cy. Cz will be determined afterwards using Cx and Cy.
On this overhead view, the circle is the cylinder that unrolls into the equirectangular image; A' , B' , C' and D' are the points that represent A, B, C, D on this image; the horizontal distances between A' and B', ... are proportional to the angles A-Camera-B, ... . Hence A'B'/ circle-perimeter = A-Camera-B / 2pi
and thus A-Camera-B = A'B'/ circle-perimeter * 2pi (the circle's perimeter being the width of the equirectangular image). Let's call this angle alpha.
This figure illustrates how we can determine the possible positions of the camera from the angle alpha, using the properties of angles in circles : the 3 marked angles are equal to alpha, thus tan(alpha) = AH/O1H, hence O1H = AH / tan(alpha) . We now have the coordinates of O1 (AB/2 , AB/(2 tan(alpha)) . (in a cartesian coordinate system with A as origin).
By doing the same for segment [AD], we get a 2nd circle of possible positions for the camera. The intersection points of the 2 circles are A and the actual camera position.
Of course the precision of the determined position is dependent on the precision of the coordinates of A', B'... on the equirectangular picture; here A' and D' are (horizontally) only 6-7 pixels apart, so there's some fluctuation.
Now to calculate Cz : on this side view, the half-circle unfolds into the pixel column containing A' in the equirectangular image ; similar to the calculation of alpha earlier, the ratio of A'I / length of the half-circle (which is the height of the image) is equal to tilt angle / pi, so tilt = A'I / height * pi ; on the equirectangular image, A'I is the vertical pixel coordinate of A'.
Basic trigonometry yields : tan(tilt) = -AH/OH, so Cz = OH = -AH/tan(tilt).
AH is calculated from the coordinates of H computed before.
---------------------------------------------------
Here's the Python code for the calculations; for the intersections of the circles, I've used the code from this post ; note that since we know that A is one of the intersections, the code could be simplified (CamPos is actually the symmetrical reflection of A in relation to (O1 O2)).
The results are (Cx, Cy) relative to A, in pixels, then Cz, also in pixels.
Note that the calculations only make sense if the overhead picture's dimensions are proportional to the real dimensions (since calculating distances only make sense in an orthonormal coordinate system).
import math
# Equirectangular info
A_eq = (472,274)
B_eq = (542,274)
C_eq = (535,260)
D_eq = (479,260)
width = 805
height = 374
# Overhead info
A = (267,321)
B = (377,321)
C = (377,274)
D = (267,274)
Rect_width = C[0] - A[0]
Rect_height = A[1] - C[1]
# Angle of view of edge [AB]
alpha = (B_eq[0] - A_eq[0]) / width * 2 * math.pi
# Center and squared radius of the circle of camera positions related to edge [AB]
x0 = Rect_width / 2
y0 = Rect_width / (2* math.tan(alpha))
r02 = x0**2 + y0**2
# Angle of view of edge [AD]
beta = (D_eq[0] - A_eq[0]) / width * 2 * math.pi
# Center and squared radius of the circle of camera positions related to edge [AD]
x1 = Rect_height / (2* math.tan(beta))
y1 = -Rect_height / 2
r12 = x1**2 + y1**2
def get_intersections(x0, y0, r02, x1, y1, r12):
# circle 1: (x0, y0), sq_radius r02
# circle 2: (x1, y1), sq_radius r12
d=math.sqrt((x1-x0)**2 + (y1-y0)**2)
a=(r02-r12+d**2)/(2*d)
h=math.sqrt(r02-a**2)
x2=x0+a*(x1-x0)/d
y2=y0+a*(y1-y0)/d
x3=x2+h*(y1-y0)/d
y3=y2-h*(x1-x0)/d
x4=x2-h*(y1-y0)/d
y4=y2+h*(x1-x0)/d
return (round(x3,2), round(y3,2), round(x4,2), round(y4,2))
# The intersection of these 2 circles are A and Camera_Base_Position (noted H)
inters = get_intersections(x0, y0, r02, x1, y1, r12)
H = (Cx, Cy) = (inters[2], inters[3])
print(H)
def get_elevation(camera_base, overhead_point, equirect_point):
tilt = (equirect_point[1])/height * math.pi
x , y = overhead_point[0] - A[0] , overhead_point[1] - A[1]
base_distance = math.sqrt((camera_base[0] - x)**2 + (camera_base[1] - y)**2 )
Cz = -base_distance / math.tan(tilt)
return Cz
print(get_elevation(H, A, A_eq))
print(get_elevation(H, B, B_eq))
print(get_elevation(H, C, C_eq))
print(get_elevation(H, D, D_eq))
# (59.66, 196.19) # These are (Cx, Cy) relative to point A
# 185.36640516274633 # These are the values of the elevation Cz
# 183.09278981601847 # when using A and A', B and B' ...
# 176.32257112738986
# 177.7819910650333

Numpy mask from cylinder coordinates

I generated the coordinates of a cylinder. Its two faces connect two arbitrary points already given.
Is it possible to build a 3D numpy mask of the filled cylinder from the coordinates with standard Python libraries? Creating a 2D mask seems simple enough, but I'm encountering some difficulties with 3D.
Here the code for generating the cylinder, taken from here and here:
import scipy
import scipy.linalg
import numpy as np
import nibabel as nib
import matplotlib
matplotlib.use('TkAgg')
import matplotlib.pyplot as plt
from mpl_toolkits.mplot3d import Axes3D
# defining mask
shape = (100, 100, 100)
image = np.zeros(shape=shape)
# set radius and centres values
r = 3
start = [30, 45, 60]
end = [40, 58, 70]
p1 = np.array(start)
p2 = np.array(end)
# # calculate p2-p1 distance
# dx = p2[0] - p1[0]
# dy = p2[1] - p1[1]
# dz = p2[2] - p1[2]
# dist = math.sqrt(dx**2 + dy**2 + dz**2)
# vector in direction of axis
v = p2 - p1
# find magnitude of vector
mag = scipy.linalg.norm(v)
# unit vector in direction of axis
v = v / mag
# make some vector not in the same direction as v
not_v = np.array([1, 0, 0])
if (v == not_v).all():
not_v = np.array([0, 1, 0])
# make vector perpendicular to v
n1 = np.cross(v, not_v)
# normalize n1
n1 /= scipy.linalg.norm(n1)
# make unit vector perpendicular to v and n1
n2 = np.cross(v, n1)
#surface ranges over t from 0 to length of axis and 0 to 2*pi
t = np.linspace(0, mag, 100)
theta = np.linspace(0, 2 * np.pi, 100)
rsample = np.linspace(0, r, 2)
#use meshgrid to make 2d arrays
t, theta2 = np.meshgrid(t, theta)
rsample, theta = np.meshgrid(rsample, theta)
# generate coordinates for surface
# "Tube"
X, Y, Z = [p1[i] + v[i] * t + r * np.sin(theta2) * n1[i] + r * np.cos(theta2) * n2[i] for i in [0, 1, 2]]
# "Bottom"
X2, Y2, Z2 = [p1[i] + rsample[i] * np.sin(theta) * n1[i] + rsample[i] * np.cos(theta) * n2[i] for i in [0, 1, 2]]
# "Top"
X3, Y3, Z3 = [p1[i] + v[i] * mag + rsample[i] * np.sin(theta) * n1[i] + rsample[i] * np.cos(theta) * n2[i] for i in [0, 1, 2]]
fig = plt.figure()
ax = fig.add_subplot(111, projection='3d')
ax.plot_surface(X, Y, Z)
ax.plot_surface(X2, Y2, Z2)
ax.plot_surface(X3, Y3, Z3)
plt.show()
I need the 3D numpy mask to select all the values inside the cylinder of a 3D image. The shape of mask and image is the same.
In the end I looped through the coordinates of tube and faces.
I got the coordinates following this link: 3D points from Numpy meshgrid coordinates
tube = np.stack((X.ravel(), Y.ravel(), Z.ravel()), axis=1)
face1 = np.stack((X2.ravel(), Y2.ravel(), Z2.ravel()), axis=1)
face2 = np.stack((X3.ravel(), Y3.ravel(), Z3.ravel()), axis=1)
# filling numpy mask
for i in range(len(tube)):
image[int(tube[i][0]), int(tube[i][1]), int(tube[i][2])] = 255
for j in range(len(face1)):
image[int(face1[j][0]), int(face1[j][1]), int(face1[j][2])] = 255
for k in range(len(face2)):
image[int(face2[k][0]), int(face2[k][1]), int(face2[k][2])] = 255
mask_new = nib.Nifti1Image(image.astype(np.float32), ctsurg_file.affine)
nib.save(mask_new, os.path.join(currdir, 'mask_cyl.nii.gz'))

Categories