I have two 2D rotated rectangles, defined as an (center x,center y, height, width) and an angle of rotation (0-360°). How would I calculate the area of intersection of these two rotated rectangles.
Such tasks are solved using computational geometry packages, e.g. Shapely:
import shapely.geometry
import shapely.affinity
class RotatedRect:
def __init__(self, cx, cy, w, h, angle):
self.cx = cx
self.cy = cy
self.w = w
self.h = h
self.angle = angle
def get_contour(self):
w = self.w
h = self.h
c = shapely.geometry.box(-w/2.0, -h/2.0, w/2.0, h/2.0)
rc = shapely.affinity.rotate(c, self.angle)
return shapely.affinity.translate(rc, self.cx, self.cy)
def intersection(self, other):
return self.get_contour().intersection(other.get_contour())
r1 = RotatedRect(10, 15, 15, 10, 30)
r2 = RotatedRect(15, 15, 20, 10, 0)
from matplotlib import pyplot
from descartes import PolygonPatch
fig = pyplot.figure(1, figsize=(10, 4))
ax = fig.add_subplot(121)
ax.set_xlim(0, 30)
ax.set_ylim(0, 30)
ax.add_patch(PolygonPatch(r1.get_contour(), fc='#990000', alpha=0.7))
ax.add_patch(PolygonPatch(r2.get_contour(), fc='#000099', alpha=0.7))
ax.add_patch(PolygonPatch(r1.intersection(r2), fc='#009900', alpha=1))
pyplot.show()
Here is a solution that does not use any libraries outside of Python's standard library.
Determining the area of the intersection of two rectangles can be divided in two subproblems:
Finding the intersection polygon, if any;
Determine the area of the intersection polygon.
Both problems are relatively easy when you work with the
vertices (corners) of the rectangles. So first you have to determine
these vertices. Assuming the coordinate origin is in the center
of the rectangle, the vertices are,
starting from the lower left in a counter-clockwise direction:
(-w/2, -h/2), (w/2, -h/2), (w/2, h/2), and (-w/2, h/2).
Rotating this over the angle a, and translating them
to the proper position of the rectangle's center, these become:
(cx + (-w/2)cos(a) - (-h/2)sin(a), cy + (-w/2)sin(a) + (-h/2)cos(a)), and similar for the other corner points.
A simple way to determine the intersection polygon is the following:
you start with one rectangle as the candidate intersection polygon.
Then you apply the process of sequential cutting (as described here.
In short: you take each edges of the second rectangle in turn,
and remove all parts from the candidate intersection polygon that are on the "outer" half plane defined by the edge
(extended in both directions).
Doing this for all edges leaves the candidate intersection polygon
with only the parts that are inside the second rectangle or on its boundary.
The area of the resulting polygon (defined by a series of vertices) can be calculated
from the coordinates of the vertices.
You sum the cross products of the vertices
of each edge (again in counter-clockwise order),
and divide that by two. See e.g. www.mathopenref.com/coordpolygonarea.html
Enough theory and explanation. Here is the code:
from math import pi, cos, sin
class Vector:
def __init__(self, x, y):
self.x = x
self.y = y
def __add__(self, v):
if not isinstance(v, Vector):
return NotImplemented
return Vector(self.x + v.x, self.y + v.y)
def __sub__(self, v):
if not isinstance(v, Vector):
return NotImplemented
return Vector(self.x - v.x, self.y - v.y)
def cross(self, v):
if not isinstance(v, Vector):
return NotImplemented
return self.x*v.y - self.y*v.x
class Line:
# ax + by + c = 0
def __init__(self, v1, v2):
self.a = v2.y - v1.y
self.b = v1.x - v2.x
self.c = v2.cross(v1)
def __call__(self, p):
return self.a*p.x + self.b*p.y + self.c
def intersection(self, other):
# See e.g. https://en.wikipedia.org/wiki/Line%E2%80%93line_intersection#Using_homogeneous_coordinates
if not isinstance(other, Line):
return NotImplemented
w = self.a*other.b - self.b*other.a
return Vector(
(self.b*other.c - self.c*other.b)/w,
(self.c*other.a - self.a*other.c)/w
)
def rectangle_vertices(cx, cy, w, h, r):
angle = pi*r/180
dx = w/2
dy = h/2
dxcos = dx*cos(angle)
dxsin = dx*sin(angle)
dycos = dy*cos(angle)
dysin = dy*sin(angle)
return (
Vector(cx, cy) + Vector(-dxcos - -dysin, -dxsin + -dycos),
Vector(cx, cy) + Vector( dxcos - -dysin, dxsin + -dycos),
Vector(cx, cy) + Vector( dxcos - dysin, dxsin + dycos),
Vector(cx, cy) + Vector(-dxcos - dysin, -dxsin + dycos)
)
def intersection_area(r1, r2):
# r1 and r2 are in (center, width, height, rotation) representation
# First convert these into a sequence of vertices
rect1 = rectangle_vertices(*r1)
rect2 = rectangle_vertices(*r2)
# Use the vertices of the first rectangle as
# starting vertices of the intersection polygon.
intersection = rect1
# Loop over the edges of the second rectangle
for p, q in zip(rect2, rect2[1:] + rect2[:1]):
if len(intersection) <= 2:
break # No intersection
line = Line(p, q)
# Any point p with line(p) <= 0 is on the "inside" (or on the boundary),
# any point p with line(p) > 0 is on the "outside".
# Loop over the edges of the intersection polygon,
# and determine which part is inside and which is outside.
new_intersection = []
line_values = [line(t) for t in intersection]
for s, t, s_value, t_value in zip(
intersection, intersection[1:] + intersection[:1],
line_values, line_values[1:] + line_values[:1]):
if s_value <= 0:
new_intersection.append(s)
if s_value * t_value < 0:
# Points are on opposite sides.
# Add the intersection of the lines to new_intersection.
intersection_point = line.intersection(Line(s, t))
new_intersection.append(intersection_point)
intersection = new_intersection
# Calculate area
if len(intersection) <= 2:
return 0
return 0.5 * sum(p.x*q.y - p.y*q.x for p, q in
zip(intersection, intersection[1:] + intersection[:1]))
if __name__ == '__main__':
r1 = (10, 15, 15, 10, 30)
r2 = (15, 15, 20, 10, 0)
print(intersection_area(r1, r2))
intersection, pnt = contourIntersection(rect1, rect2)
After looking at the possible duplicate page for this problem I couldn't find a completed answer for python so here is my solution using masking. This function will work with complex shapes on any angle, not just rectangles
You pass in the 2 contours of your rotated rectangles as parameters and it returns 'None' if no intersection occurs or an image of the intersected area and the left/top position of that image in relation to the original image the contours were taken from
Uses python, cv2 and numpy
import cv2
import math
import numpy as np
def contourIntersection(con1, con2, showContours=False):
# skip if no bounding rect intersection
leftmost1 = tuple(con1[con1[:, :, 0].argmin()][0])
topmost1 = tuple(con1[con1[:, :, 1].argmin()][0])
leftmost2 = tuple(con2[con2[:, :, 0].argmin()][0])
topmost2 = tuple(con2[con2[:, :, 1].argmin()][0])
rightmost1 = tuple(con1[con1[:, :, 0].argmax()][0])
bottommost1 = tuple(con1[con1[:, :, 1].argmax()][0])
rightmost2 = tuple(con2[con2[:, :, 0].argmax()][0])
bottommost2 = tuple(con2[con2[:, :, 1].argmax()][0])
if rightmost1[0] < leftmost2[0] or rightmost2[0] < leftmost1[0] or bottommost1[1] < topmost2[1] or bottommost2[1] < topmost1[1]:
return None, None
# reset top / left to 0
left = leftmost1[0] if leftmost1[0] < leftmost2[0] else leftmost2[0]
top = topmost1[1] if topmost1[1] < topmost2[1] else topmost2[1]
newCon1 = []
for pnt in con1:
newLeft = pnt[0][0] - left
newTop = pnt[0][1] - top
newCon1.append([newLeft, newTop])
# next
con1_new = np.array([newCon1], dtype=np.int32)
newCon2 = []
for pnt in con2:
newLeft = pnt[0][0] - left
newTop = pnt[0][1] - top
newCon2.append([newLeft, newTop])
# next
con2_new = np.array([newCon2], dtype=np.int32)
# width / height
right1 = rightmost1[0] - left
bottom1 = bottommost1[1] - top
right2 = rightmost2[0] - left
bottom2 = bottommost2[1] - top
width = right1 if right1 > right2 else right2
height = bottom1 if bottom1 > bottom2 else bottom2
# create images
img1 = np.zeros([height, width], np.uint8)
cv2.drawContours(img1, con1_new, -1, (255, 255, 255), -1)
img2 = np.zeros([height, width], np.uint8)
cv2.drawContours(img2, con2_new, -1, (255, 255, 255), -1)
# mask images together using AND
imgIntersection = cv2.bitwise_and(img1, img2)
if showContours:
img1[img1 > 254] = 128
img2[img2 > 254] = 100
imgAll = cv2.bitwise_or(img1, img2)
cv2.imshow('Merged Images', imgAll)
# end if
if not imgIntersection.sum():
return None, None
# trim
while not imgIntersection[0].sum():
imgIntersection = np.delete(imgIntersection, (0), axis=0)
top += 1
while not imgIntersection[-1].sum():
imgIntersection = np.delete(imgIntersection, (-1), axis=0)
while not imgIntersection[:, 0].sum():
imgIntersection = np.delete(imgIntersection, (0), axis=1)
left += 1
while not imgIntersection[:, -1].sum():
imgIntersection = np.delete(imgIntersection, (-1), axis=1)
return imgIntersection, (left, top)
# end function
To complete the answer so you can use the above function with the values of CenterX, CenterY, Width, Height and Angle of 2 rotated rectangles I have added the below functions. Simple change the Rect1 and Rect2 properties at the bottom of the code to your own
def pixelsBetweenPoints(xy1, xy2):
X = abs(xy1[0] - xy2[0])
Y = abs(xy1[1] - xy2[1])
return int(math.sqrt((X ** 2) + (Y ** 2)))
# end function
def rotatePoint(angle, centerPoint, dist):
xRatio = math.cos(math.radians(angle))
yRatio = math.sin(math.radians(angle))
xPotted = int(centerPoint[0] + (dist * xRatio))
yPlotted = int(centerPoint[1] + (dist * yRatio))
newPoint = [xPotted, yPlotted]
return newPoint
# end function
def angleBetweenPoints(pnt1, pnt2):
A_B = pixelsBetweenPoints(pnt1, pnt2)
pnt3 = (pnt1[0] + A_B, pnt1[1])
C = pixelsBetweenPoints(pnt2, pnt3)
angle = math.degrees(math.acos((A_B * A_B + A_B * A_B - C * C) / (2.0 * A_B * A_B)))
# reverse if above horizon
if pnt2[1] < pnt1[1]:
angle = angle * -1
# end if
return angle
# end function
def rotateRectContour(xCenter, yCenter, height, width, angle):
# calc positions
top = int(yCenter - (height / 2))
left = int(xCenter - (width / 2))
right = left + width
rightTop = (right, top)
centerPoint = (xCenter, yCenter)
# new right / top point
rectAngle = angleBetweenPoints(centerPoint, rightTop)
angleRightTop = angle + rectAngle
angleRightBottom = angle + 180 - rectAngle
angleLeftBottom = angle + 180 + rectAngle
angleLeftTop = angle - rectAngle
distance = pixelsBetweenPoints(centerPoint, rightTop)
rightTop_new = rotatePoint(angleRightTop, centerPoint, distance)
rightBottom_new = rotatePoint(angleRightBottom, centerPoint, distance)
leftBottom_new = rotatePoint(angleLeftBottom, centerPoint, distance)
leftTop_new = rotatePoint(angleLeftTop, centerPoint, distance)
contourList = [[leftTop_new], [rightTop_new], [rightBottom_new], [leftBottom_new]]
contour = np.array(contourList, dtype=np.int32)
return contour
# end function
# rect1
xCenter_1 = 40
yCenter_1 = 20
height_1 = 200
width_1 = 80
angle_1 = 45
rect1 = rotateRectContour(xCenter_1, yCenter_1, height_1, width_1, angle_1)
# rect2
xCenter_2 = 80
yCenter_2 = 25
height_2 = 180
width_2 = 50
angle_2 = 123
rect2 = rotateRectContour(xCenter_2, yCenter_2, height_2, width_2, angle_2)
intersection, pnt = contourIntersection(rect1, rect2, True)
if intersection is None:
print('No intersection')
else:
print('Area of intersection = ' + str(int(intersection.sum() / 255)))
cv2.imshow('Intersection', intersection)
# end if
cv2.waitKey(0)
Related
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:
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
Given a unit circle and two polygons inscribed, how can I go about calculating the Intersection over Union(IoU) of the two polygons?
Assume I have a Numpy array of the polygon coordinates with respect to the unit circle as:
Polygon 1: [
(-0.708, 0.707),
(0.309, -0.951),
(0.587, -0.809),
]
Polygon 2: [
(1, 0),
(0, 1),
(-1, 0),
(0, -1),
(0.708, -0.708),
]
The expected IoU output should be: 0.124
Three Approaches
Shapely
Redo RaffleBuffle approach in Python rather than Java (check his/her post for description)
Monte Carlo Simulation
Shapely
from shapely.geometry import Polygon
p1 = [ (-0.708, 0.707),
(0.309, -0.951),
(0.587, -0.809)]
p2 = [ (1, 0),
(0, 1),
(-1, 0),
(0, -1),
(0.708, -0.708)]
# Create polygons from coordinates
poly1 = Polygon(p1)
poly2 = Polygon(p2)
# ratio of intersection area to union area
print(poly1.intersection(poly2).area/poly1.union(poly2).area)
# Output: 0.12403616470027264
RaffleBuffle Approach in Python
import math
import numpy as np
class Point:
def __init__(self, x, y, id = None):
self.x = x
self.y = y
self.id = id
self.prev = None
self.next = None
def __repr__(self):
result = f"{self.x} {self.y} {self.id}"
result += f" Prev: {self.prev.x} {self.prev.y} {self.prev.id}" if self.prev else ""
result += f" Next: {self.next.x} {self.next.y} {self.next.id}" if self.next else ""
return result
class Poly:
def __init__(self, pts):
self.pts = [p for p in pts]
' Sort polynomial coordinates based upon angle and radius in clockwize direction'
# Origin is the centroid of points
origin = Point(*[sum(pt.x for pt in self.pts)/len(self.pts), sum(pt.y for pt in self.pts)/len(self.pts)])
# Sort points by incresing angle around centroid based upon angle and distance to centroid
self.pts.sort(key=lambda p: clockwiseangle_and_distance(p, origin))
def __add__(self, other):
' Overload for adding two polynomials '
return Poly(self.pts + other.pts)
def assign_chain(self):
' Assign prev and next '
n = len(self.pts)
for i in range(n):
self.pts[i].next = self.pts[ (i + 1) % n]
self.pts[i].prev = self.pts[(i -1) % n]
return self
def area(self):
'''
area enclosed by polynomial coordinates '
Source: https://stackoverflow.com/questions/24467972/calculate-area-of-polygon-given-x-y-coordinates
'''
x = np.array([p.x for p in self.pts])
y = np.array([p.y for p in self.pts])
return 0.5*np.abs(np.dot(x,np.roll(y,1))-np.dot(y,np.roll(x,1)))
def intersection(self):
' Intersection of coordinates with different ids '
pts = self.pts
n = len(pts)
intersections = []
for i in range(n):
j = (i -1) % n
if pts[j].id != pts[i].id:
get_j = [pts[j], pts[j].next]
get_i = [pts[i].prev, pts[i]]
pt_intersect = line_intersect(get_j, get_i)
if pt_intersect:
intersections.append(pt_intersect)
return intersections
def __str__(self):
return '\n'.join(str(pt) for pt in self.pts)
def __repr__(self):
return '\n'.join(str(pt) for pt in self.pts)
def clockwiseangle_and_distance(point, origin):
# Source: https://stackoverflow.com/questions/41855695/sorting-list-of-two-dimensional-coordinates-by-clockwise-angle-using-python
# Vector between point and the origin: v = p - o
vector = [point.x-origin.x, point.y-origin.y]
refvec = [0, 1]
# Length of vector: ||v||
lenvector = math.hypot(vector[0], vector[1])
# If length is zero there is no angle
if lenvector == 0:
return -math.pi, 0
# Normalize vector: v/||v||
normalized = [vector[0]/lenvector, vector[1]/lenvector]
dotprod = normalized[0]*refvec[0] + normalized[1]*refvec[1] # x1*x2 + y1*y2
diffprod = refvec[1]*normalized[0] - refvec[0]*normalized[1] # x1*y2 - y1*x2
angle = math.atan2(diffprod, dotprod)
# Negative angles represent counter-clockwise angles so we need to subtract them
# from 2*pi (360 degrees)
if angle < 0:
return 2*math.pi+angle, lenvector
# I return first the angle because that's the primary sorting criterium
# but if two vectors have the same angle then the shorter distance should come first.
return angle, lenvector
def line_intersect(segment1, segment2):
""" returns a (x, y) tuple or None if there is no intersection
segment1 and segment2 are two line segements
specified by their starting/ending points
Source: https://rosettacode.org/wiki/Find_the_intersection_of_two_lines#Python
"""
Ax1, Ay1 = segment1[0].x, segment1[0].y # starting point in line segment 1
Ax2, Ay2 = segment1[1].x, segment1[1].y # ending point in line segment 1
Bx1, By1 = segment2[0].x, segment2[0].y # starting point in line segment 2
Bx2, By2 = segment2[1].x, segment2[1].y # ending point in line segment 2
d = (By2 - By1) * (Ax2 - Ax1) - (Bx2 - Bx1) * (Ay2 - Ay1)
if d:
uA = ((Bx2 - Bx1) * (Ay1 - By1) - (By2 - By1) * (Ax1 - Bx1)) / d
uB = ((Ax2 - Ax1) * (Ay1 - By1) - (Ay2 - Ay1) * (Ax1 - Bx1)) / d
else:
return
if not(0 <= uA <= 1 and 0 <= uB <= 1):
return
x = Ax1 + uA * (Ax2 - Ax1)
y = Ay1 + uA * (Ay2 - Ay1)
return Point(x, y, None)
def polygon_iou(coords1, coords2):
'''
Calculates IoU of two 2D polygons based upon coordinates
'''
# Make polynomials ordered clockwise and assign ID (0 and 1)
poly1 = Poly(Point(*p, 0) for p in coords1) # counter clockwise with ID 0
poly2 = Poly(Point(*p, 1) for p in coords2) # counter clockwise with ID 1
# Assign previous and next coordinates in polynomial chain
poly1.assign_chain()
poly2.assign_chain()
# Polygon areas
area1 = poly1.area()
area2 = poly2.area()
# Combine both polygons into one counter clockwise
poly = poly1 + poly2
# Get interesections
intersections = poly.intersection()
# IoU based upon intersection and sum of areas
if intersections:
area_intersection = Poly(intersections).area()
result = area_intersection/(area1 + area2 - area_intersection)
else:
result = 0.0
return result
print(polygon_iou(p1, p2))
Test
p1 = [ (-0.708, 0.707),
(0.309, -0.951),
(0.587, -0.809)]
p2 = [ (1, 0),
(0, 1),
(-1, 0),
(0, -1),
(0.708, -0.708)]
print(polygon_iou(p1, p2))
# Output: 0.12403616470027277
Monte Carlo Simulation
Generate random points in the min/max x and y range of the points
Count the number of points in either polygon (i.e. union)
Count the number of points in both polygon (i.e. intersection)
Ratio of the number of points in the intersection to the number in the union is the answer
Code
import math
from random import uniform
def ray_tracing_method(x, y, poly):
'''
Determines if point x, y is inside polygon poly
Source: "What's the fastest way of checking if a point is inside a polygon in python"
at URL: https://stackoverflow.com/questions/36399381/whats-the-fastest-way-of-checking-if-a-point-is-inside-a-polygon-in-python
'''
n = len(poly)
inside = False
p1x,p1y = poly[0]
for i in range(n+1):
p2x,p2y = poly[i % n]
if y > min(p1y,p2y):
if y <= max(p1y,p2y):
if x <= max(p1x,p2x):
if p1y != p2y:
xints = (y-p1y)*(p2x-p1x)/(p2y-p1y)+p1x
if p1x == p2x or x <= xints:
inside = not inside
p1x,p1y = p2x,p2y
return inside
def intersection_union(p1, p2, num_throws = 1000000):
'''
Computes the intersection untion for polygons p1, p2
(assuming that p1, and p2 are inside at polygon of radius 1)
'''
# Range of values
p = p1 + p2
xmin = min(x[0] for x in p)
xmax = max(x[0] for x in p)
ymin = min(x[1] for x in p)
ymax = max(x[1] for x in p)
# Init counts
in_union = 0
in_intersect = 0
throws = 0
while (throws < num_throws):
# Choose random x, y position in rectangle
x_pos = uniform (xmin, xmax)
y_pos = uniform (ymin, ymax)
# Only test points inside unit circle
# Check if points are inside p1 & p2
in_p1 = ray_tracing_method(x_pos, y_pos, p1)
in_p2 = ray_tracing_method(x_pos, y_pos, p2)
if in_p1 or in_p2:
in_union += 1 # in union
if in_p1 and in_p2:
in_intersect += 1 # in intersetion
throws += 1
return in_intersect/in_union
print(intersection_union(p1, p2))
Test
p1 = [ (-0.708, 0.707),
(0.309, -0.951),
(0.587, -0.809)]
p2 = [ (1, 0),
(0, 1),
(-1, 0),
(0, -1),
(0.708, -0.708)]
intersection_union(p1, p2)
# Out: 0.12418051627698147
Assuming polygons are non self-intersecting, i.e. the ordering of the points around the circle is monotonic, then I believe there is a relatively simple way to determine the IoU value without requiring a general shape package.
Assume the points of each polygon are ordered clockwise around the circle. We can ensure this by sorting by increasing angle w.r.t the x-axis or reversing the points if we find the signed area is negative.
Combine points from both polygons into a single list, L, keeping track of which polygon each point belongs to. We also need to be able to determine the previous and next point in the original polygon for each point.
Sort L by increasing angle w.r.t the x-axis.
If the input polygons intersect the number of transitions from one polygon to the other while iterating through L will be greater than two.
Iterate through L. If successive points are encountered belonging to different polygons then the intersection of the lines between the 1st point and its next point and the 2nd point and its previous point will belong to the intersection between the two polygons.
Add each point identified in step 4 to a new polygon, I. Points of I will be encountered in order.
The sum of the areas of each polygon will be equal to their union plus the area of the intersection, since this will be counted twice.
The value of IoU will therefore be given by the area of I divided by the sum of the areas of the two polygons minus the area of I.
The only geometry required is to calculate the area of a simple polygon using the Shoelace formula, and to determine the point of intersection between two line segments, required by step 5.
Here's some Java code (Ideone) to illustrate - you can probably make it a lot more compact in Python.
double[][] coords = {{-0.708, 0.707, 0.309, -0.951, 0.587, -0.809},
{1, 0, 0, 1, -1, 0, 0, -1, 0.708, -0.708}};
double areaSum = 0;
List<CPoint> pts = new ArrayList<>();
for(int p=0; p<coords.length; p++)
{
List<CPoint> poly = new ArrayList<>();
for(int j=0; j<coords[p].length; j+=2)
{
poly.add(new CPoint(p, coords[p][j], coords[p][j+1]));
}
double area = area(poly);
if(area < 0)
{
area = -area;
Collections.reverse(poly);
}
areaSum += area;
pts.addAll(poly);
int n = poly.size();
for(int i=0, j=n-1; i<n; j=i++)
{
poly.get(i).prev = poly.get(j);
poly.get(j).next = poly.get(i);
}
}
pts.sort((a, b) -> Double.compare(a.theta, b.theta));
List<Point2D> intersections = new ArrayList<>();
int n = pts.size();
for(int i=0, j=n-1; i<n; j=i++)
{
if(pts.get(j).id != pts.get(i).id)
{
intersections.add(intersect(pts.get(j), pts.get(j).next, pts.get(i).prev, pts.get(i)));
}
}
double areaInt = area(intersections);
double iou = areaInt/(areaSum - areaInt);
System.out.println(iou);
Output:
0.12403616470027268
And supporting code:
static class CPoint extends Point2D.Double
{
int id;
double theta;
CPoint prev, next;
public CPoint(int id, double x, double y)
{
super(x, y);
this.id = id;
theta = Math.atan2(y, x);
if(theta < 0) theta = 2*Math.PI + theta;
}
}
static double area(List<? extends Point2D> poly)
{
double area = 0;
for(int i=0, j=poly.size()-1; i<poly.size(); j=i++)
area += (poly.get(j).getX() * poly.get(i).getY()) - (poly.get(i).getX() * poly.get(j).getY());
return Math.abs(area)/2;
}
// https://rosettacode.org/wiki/Find_the_intersection_of_two_lines#Java
static Point2D intersect(Point2D p1, Point2D p2, Point2D p3, Point2D p4)
{
double a1 = p2.getY() - p1.getY();
double b1 = p1.getX() - p2.getX();
double c1 = a1 * p1.getX() + b1 * p1.getY();
double a2 = p4.getY() - p3.getY();
double b2 = p3.getX() - p4.getX();
double c2 = a2 * p3.getX() + b2 * p3.getY();
double delta = a1 * b2 - a2 * b1;
return new Point2D.Double((b2 * c1 - b1 * c2) / delta, (a1 * c2 - a2 * c1) / delta);
}
I need to find the area of intersection and union of two rectangles, as well as the ratio of the intersection area to the union area. What is the best way to do this?
Picture
def intersection_over_union(a, b):
ax1 = a['left']
ay1 = a['top']
aw = a['width']
ah = a['height']
ax2 = ax1 + aw
ay2 = ay1 + ah
bx1 = b['left']
by1 = b['top']
bw = b['width']
bh = b['height']
bx2 = bx1 + bw
by2 = by1 + bh
delta_x = max(ax1, bx1) - min(ax2, bx2)
delta_y = max(ay1, by1) - min(ay2, by2)
overlap = delta_x * delta_y
union = aw * ah + bw * bh - overlap
print(overlap, union)
return overlap / union
Assuming you are dealing with AABB (axis aligned bounding box) this little class defines everything you need:
class Rect:
def __init__(self, x, y, w, h):
self.x = x
self.y = y
self.w = w
self.h = h
def bottom(self):
return self.y + self.h
def right(self):
return self.x + self.w
def area(self):
return self.w * self.h
def union(self, b):
posX = min(self.x, b.x)
posY = min(self.y, b.y)
return Rect(posX, posY, max(self.right(), b.right()) - posX, max(self.bottom(), b.bottom()) - posY)
def intersection(self, b):
posX = max(self.x, b.x)
posY = max(self.y, b.y)
candidate = Rect(posX, posY, min(self.right(), b.right()) - posX, min(self.bottom(), b.bottom()) - posY)
if candidate.w > 0 and candidate.h > 0:
return candidate
return Rect(0, 0, 0, 0)
def ratio(self, b):
return self.intersection(b).area() / self.union(b).area()
if __name__ == "__main__":
assert Rect(1, 1, 1, 1).ratio(Rect(1, 1, 1, 1)) == 1
assert round(Rect(1, 1, 2, 2).ratio(Rect(2, 2, 2, 2)), 4) == 0.1111
One way I believe you could do this (although I am not sure it is the best solution as you've asked for), is to develop a function that creates a rectangle in a plane, and stores all of its corners' coordinates in a list.
Every time you create a rectangle (let's say you create it from the bottom left, increasing its x and y coordinates as you go), check for each coordinate if the x coordinate is within the other rectangle's x-range i.e. x_min < x < x_max. If so, check the y-coordinate. If the coordinate is within the x and y ranges of another rectangle, it's part of the intersection.
Repeat for all coordinates as you create them and save the intersection coordinates, and find the intersection's corners. With that, you can calculate the area of intersection and union between the two triangles, and their ratio to one another.
Given an image, I draw a line inside it in the following way:
def slope(x1,y1,x2,y2):
###finding slope
if x2!=x1:
return((y2-y1)/(x2-x1))
else:
return 'NA'
def draw_line(image,lineThickness, colors, points):
x1,y1, x2, y2=points
m=slope(x1,y1,x2,y2)
h,w=image.shape[:2]
if m!='NA':
px=0
py=-(x1-0)*m+y1
##ending point
qx=w
qy=-(x2-w)*m+y2
else:
### if slope is zero, draw a line with x=x1 and y=0 and y=height
px,py=x1,0
qx,qy=x1,h
cv2.line(image, (int(px), int(py)), (int(qx), int(qy)), colors, lineThickness)
return (px, py), (qx, qy)
This code (coming from SA) ensure that given a line (defined by two points), it covers the whole image.
Now, I would like to draw multiple parallel lines over a single image, with a parameters that specify how much the lines need to be distant from each other
My current attempt is this:
#Utility to find if two segments intersect
def ccw(A,B,C):
Ax, Ay = A
Bx, By = B
Cx, Cy = C
return (Cy-Ay) * (Bx-Ax) > (By-Ay) * (Cx-Ax)
#Utility to find if two segments intersect
def intersect(A,B,C,D):
return ccw(A,C,D) != ccw(B,C,D) and ccw(A,B,C) != ccw(A,B,D)
#Utility to find if a segment intersect a square
def intersect_square(A,B, s_size):
C = (1,1)
D = (1, s_size-1)
E = (s_size-1, +1)
F = (s_size-1, s_size-1)
return (ccw(A,C,D) != ccw(B,C,D) and ccw(A,B,C) != ccw(A,B,D)) or \
(ccw(A,E,F) != ccw(B,E,F) and ccw(A,B,E) != ccw(A,B,F)) or \
(ccw(A,C,E) != ccw(B,C,E) and ccw(A,B,C) != ccw(A,B,E)) or \
(ccw(A,D,F) != ccw(B,D,F) and ccw(A,B,D) != ccw(A,B,F))
#Utility to draw a white canvas
def draw_white_image(x_size,y_size):
img = np.zeros([x_size,y_size,3],dtype=np.uint8)
img.fill(255) # or img[:] = 255
return img
def draw_parallel_lines(img, thickness, colors, points, space):
#Draw the first line
draw_line(img, thickness, colors, points)
x1, y1, x2, y2 = points
flag = True
while(flag):
y1 += space
y2 += space
new_points = draw_line(img, thickness, colors, (x1,y1,x2,y2))
flag = intersect_square(new_points[0], new_points[1], h)
This code move the y coordinates of my initial points, until the new lines I generate are outside the square. Sadly, this code gives this result:
The lines are not equidistant. I spent a few hours on this and I'm kinda sad now. Please help
If line segment is defined by points (x1,y1) and (x2,y2), it has direction vector:
(dx, dy) = (x2-x1, y2-y1)
Normalized (unit length) vector:
len = sqrt((x2-x1)^2 + (y2-y1)^2)
(udx, udy) = (dx / len, dy / len)
Perpendicular vector:
(px, py) = (-udy, udx)
Base point for parallel line at distance dist:
(x1', y1') = (x1 + px * dist, y1 + py * dist)
or
(x1', y1') = (x1 - udy * dist, y1 + udx * dist)
Another end point:
(x2', y2') = (x1' + dx, y1' + dy)
To get parallel line in another direction, negate signs of px,py