Order of vertex for drawing quad - python

I need to draw n planes in 3D space. The quads are planes created from two points, and with an algorithm I get 4 vertex for drawing the quad. The problem I have is that the order of the vertices affect to the result, obviously. Here it is what I mean:
But when the plane is horizontal instead of vertical:
I can imagine two possible solutions. Using triangles and combining them or ordering the vertex properly. I dont know how to do the second idea. I have tried using triangles but I get the same problem.
# self.planos = [('A', (500, 500, 10), (-500, 500, 10), (-500, -500, 10), (500, -500, 10))] for horizontal
# self.planos = [('A', (-500, 10, 500), (500, 10, 500), (-500, 10, -500), (500, 10, -500))] for vertical
glEnable(GL_BLEND)
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA)
glDepthMask(GL_FALSE)
glBegin(GL_QUADS)
glColor(0.5, 0.5, 0.1, 0.5)
for i in range(len(self.planos)):
glVertex(self.planos[i][1][0], self.planos[i][1][2], self.planos[i][1][1])
glVertex(self.planos[i][2][0], self.planos[i][2][2], self.planos[i][2][1])
glVertex(self.planos[i][3][0], self.planos[i][3][2], self.planos[i][3][1])
glVertex(self.planos[i][4][0], self.planos[i][4][2], self.planos[i][4][1])
glEnd()
glDepthMask(GL_TRUE)
glDisable(GL_BLEND)
Intersection code for getting four vertex to draw planes:
In init method:
#Vertices of the cube
self.v = (Point3D(500, 500, 500), Point3D(-500, 500, 500), Point3D(-500, -500, 500),
Point3D(500, -500, 500), Point3D(500, 500, -500), Point3D(-500, 500, -500),
Point3D(-500, -500, -500), Point3D(500, -500, -500))
# Edges of the cube
self.a = (Segment3D(self.v[0], self.v[1]), Segment3D(self.v[1], self.v[2]),
Segment3D(self.v[2], self.v[3]), Segment3D(self.v[3], self.v[0]),
Segment3D(self.v[0], self.v[4]), Segment3D(self.v[1], self.v[5]),
Segment3D(self.v[2], self.v[6]), Segment3D(self.v[3], self.v[7]),
Segment3D(self.v[4], self.v[5]), Segment3D(self.v[5], self.v[6]),
Segment3D(self.v[6], self.v[7]), Segment3D(self.v[7], self.v[4]))
# Algorithm for getting 4 points
def plano_limites(self, point1, point2, point3):
plano = Plane(Point3D(point1), Point3D(point2), Point3D(point3))
good = []
for i in range(12):
a = intersection(plano, self.a[i]) # Sympy intersection
if a:
good.append(a[0])
return good

First of all, be aware that your intersection can result in more or less than four vertices. But since the region will always be convex, you can simply draw it with a triangle fan.
To sort your vertices, you need the normal n of the plane and the centroid c of all the vertices v_i:
c = 1/(number of vertices) * (v_1 + v_2 + v3 + ...)
Then, we need a coordinate system whose z-axis is the normal. For this, we can simply define an arbitrary other direction vector d and define x = normalize(cross(d, normal)), y = cross(normal, x). For cases where d conincides with normal, we need an alternative d.
We can then calculate a representative angle of any vertex in this coordinate system:
angle_i = atan2(dot(x, v_i - c), dot(y, v_i - c))
Sort by this angle and you are done.

Thanks to Nico's answer I was able to learn how to do this for myself but I thought it would be useful to do a full write up as it still took me further learning to understand what was going on here.
We essentiality need to find an x- and y-axis where the z-axis is aligned with the normal, we do this by taking the cross product of an arbitrary vector other. If the other vector coincides with the normal we will need to use a different vector. Two vectors coincide if their dot product is equal to 1.
By using the other vector of (0, 1, 0) (or (0, 0, -1) in the case they coincide) in a right-hand coordinate system we will produce easier to understand x- and y-axis vectors. This does not matter for the sake of the algorithm but I found this was the most crucial part initially missing in my own understanding.
By using the other vector of (0, 1, 0) when the normal of the plane is (0, 0, 1) the x-axis will be (1, 0, 0) and the y-axis will be (0, 1, 0).
By using the other vector of (0, 0, -1) when the normal of the plane is (0, 1, 0) the x-axis will be (1, 0, 0) and the y-axis will be (-1, 0, 0).
This can easily be modeled by using your right hand and turning your finger that is the normal to point up to positive z-axis (towards yourself).
def order_on_plane(vertices, normal):
# Find the centroid of the vertices
centroid = (0, 0, 0)
for v in vertices:
centroid = add(centroid, v)
centroid = scale(centroid, 1 / len(vertices))
# Determine the 'other' vector, used to create the axis vectors
other = (0, 1, 0)
# If the other vector coincides with the normal vector, we need to use a different other
if math.fabs(math.fabs(dot(other, normal)) - 1.0) < 0.0001:
other = (0, 0, -1)
# Create the axis vectors
x_axis = normalize(cross(other, normal))
y_axis = normalize(cross(normal, x_axis))
# Sort by angle as a vector from the centroid
angles = []
for v in vertices:
vector = sub(v, centroid)
x_pos = dot(vector, x_axis)
y_pos = dot(vector, y_axis)
# y_pos is passed in first for east counter clockwise convention
angle = math.atan2(y_pos, x_pos)
angles.append(angle)
# Sort vertices by angle
vertices = sorted(zip(angles, vertices), key=lambda x: x[0])
vertices = list(map(lambda x: x[1], vertices))
return vertices

Related

Extrude a concave, complex polygon in PyVista

I wish to take a concave and complex (containing holes) polygon and extrude it 'vertically' into a polyhedron, purely for visualisation. I begin with a shapely Polygon, like below:
poly = Polygon(
[(0,0), (10,0), (10,10), (5,8), (0,10), (1,7), (0,5), (1,3)],
holes=[
[(2,2),(4,2),(4,4),(2,4)],
[(6,6), (7,6), (6.5,6.5), (7,7), (6,7), (6.2,6.5)]])
which I correctly plot (reorientating the exterior coordinates to be clockwise, and the hole coordinates to be counterclockwise) in matplotlib as:
I then seek to render this polygon extruded out-of-the-page (along z), using PyVista. There are a few hurdles; PyVista doesn't directly support concave (nor complex) input to its PolyData type. So we first create an extrusion of simple (hole-free) concave polygons, as per this discussion.
def extrude_simple_polygon(xy, z0, z1):
# force counter-clockwise ordering, so PyVista interprets polygon correctly
xy = _reorient_coords(xy, clockwise=False)
# remove duplication of first & last vertex
xyz0 = [(x,y,z0) for x,y in xy]
if (xyz0[0] == xyz0[-1]):
xyz0.pop()
# explicitly set edge_source
base_vert = [len(xyz0)] + list(range(len(xyz0)))
base_data = pyvista.PolyData(xyz0, base_vert)
base_mesh = base_data.delaunay_2d(edge_source=base_data)
vol_mesh = base_mesh.extrude((0, 0, z1-z0), capping=True)
# force triangulation, so PyVista allows boolean_difference
return vol_mesh.triangulate()
Observe this works when extruding the outer polygon and each of its internal polygons in-turn:
extrude_simple_polygon(list(poly.exterior.coords), 0, 5).plot()
extrude_simple_polygon(list(poly.interiors[0].coords), 0, 5).plot()
extrude_simple_polygon(list(poly.interiors[1].coords), 0, 5).plot()
I reasoned that to create an extrusion of the original complex polygon, I could compute the boolean_difference. Alas, the result of
outer_vol = extrude_simple_polygon(list(poly.exterior.coords), 0, 5)
for hole in poly.interiors:
hole_vol = extrude_simple_polygon(list(hole.coords), 0, 5)
outer_vol = outer_vol.boolean_difference(hole_vol)
outer_vol.plot()
is erroneous:
The doc advises to inspect the normals via plot_normals, revealing that all extruded volumes have inward-pointing (or else, unexpected) normals:
The extrude doc mentions nothing of the extruded surface normals nor the original object (in this case, a polygon) orientation.
We could be forgiven for expecting our polygons must be clockwise, so we set clockwise=True in the first line of extrude_simple_polygon and try again. Alas, PolyData now misinterprets our base polygon; calling base_mesh.plot() reveals (what should look like our original blue outer polygon):
with extrusion
Does PyVista always expect counter-clockwise polygons?
Why does extrude create volumes with inward-pointing surface normals?
How can I correct the extruded surface normals?
Otherwise, how can I make PyVista correctly visualise what should be an incredibly simply-extruded concave complex polygon??
You're very close. What you have to do is use a single call to delaunay_2d() with all three polygons (i.e. the enclosing one and the two holes) as edge source (loop source?). It's also important to have faces (rather than lines) from each polygon; this is what makes it possible to enforce the holeyness of the holes.
Here's a complete example for your input (where I manually flipped the orientation of the holes; you seem to have a _reorient_coords() helper that you should use instead):
import pyvista as pv
# coordinates of enclosing polygon
poly_points = [
(0, 0), (10, 0), (10, 10), (5, 8), (0, 10), (1, 7), (0, 5), (1, 3),
]
# hole point order hard-coded here; use your _reorient_coords() function
holes = [
[(2, 2), (4, 2), (4, 4), (2, 4)][::-1],
[(6, 6), (7, 6), (6.5, 6.5), (7, 7), (6, 7), (6.2, 6.5)][::-1],
]
z0, z1 = 0.0, 5.0
def is_clockwise(xy):
value = 0
for i in range(len(xy)):
x1, y1 = xy[i]
x2, y2 = xy[(i+1)%len(coords)]
value += (x2-x1)*(y2+y1)
return (value > 0)
def reorient_coords(xy, clockwise):
if is_clockwise(xy) == clockwise:
return xy
return xy[::-1]
def points_2d_to_poly(xy, z, clockwise):
"""Convert a sequence of 2d coordinates to a polydata with a polygon."""
# ensure vertices are currently ordered without repeats
xy = reorient_coords(xy, clockwise)
if xy[0] == xy[-1]:
xy = xy[:-1]
xyz = [(x,y,z) for x,y in xy]
faces = [len(xyz), *range(len(xyz))]
data = pv.PolyData(xyz, faces=faces)
return data
# bounding polygon
polygon = points_2d_to_poly(poly_points, z0)
# add all holes
for hole_points in holes:
polygon += points_2d_to_poly(hole_points, z0)
# triangulate poly with all three subpolygons supplying edges
# (relative face orientation is critical here)
polygon_with_holes = polygon.delaunay_2d(edge_source=polygon)
# extrude
holey_solid = polygon_with_holes.extrude((0, 0, z1 - z0), capping=True)
holey_solid.plot()
Here's the top view of the polygon pre-extrusion:
plotter = pv.Plotter()
plotter.add_mesh(polygon_with_holes, show_edges=True, color='cyan')
plotter.view_xy()
plotter.show()

Python: Depth First Search 'maximum recursion depth exceeded'

I have a recursive depth first search algorithm which takes in a black/white mask image, e.g.:
And outputs the x largest clusters of white pixels in that mask, e.g. (for x = 2):
The function is below:
def split_mask_into_x_biggest_clusters(input_mask, x):
def generate_neighbours(point):
neighbours = [
(1, -1), (1, 0), (1, 1),
(0, -1), (0, 1),
(1, -1), (1, 0), (-1, 1)
]
for neigh in neighbours:
yield tuple(map(sum, zip(point, neigh)))
def find_regions(p, points):
reg = []
seen = set()
def dfs(point):
if point not in seen:
seen.add(point)
if point in points:
reg.append(point)
points.remove(point)
for n in generate_neighbours(point):
dfs(n)
dfs(p)
return reg
region = []
data = np.array(input_mask)[:, :, 0]
wpoint = np.where(data == 255)
points = set((x, y) for x, y in zip(*wpoint))
while points:
cur = next(iter(points))
reg = find_regions(cur, points)
region.append(reg.copy())
areas = {idx: area for idx, area in enumerate(map(len, region))}
areas = sorted(areas.items(), key=lambda x: x[1], reverse=True)
num = x
masks = []
for idx, area in enumerate(areas[:num]):
input_mask = np.zeros((512, 512, 3))
for x, y in region[area[0]]:
input_mask[x, y] = [255, 255, 255]
input_mask = input_mask.astype(np.uint8)
masks.append(Image.fromarray(input_mask))
return masks
My problem is that when I run it I get the following error: maximum recursion depth exceeded. Experimentally, I have tried increasing the recursion limit to 2000 then to 10000(!):
sys.setrecursionlimit(10000)
This fixes the problem sometimes but not all the time (i.e. not when the clusters of white pixels are bigger).
What can I do to fix this problem? Thank you for any help.
For big images you will always end up with this error.
You can either change you implementation to iterative DFS (which doesn't use recursion), or use BFS.
UPD
Implementation can be found here (for iterative DFS)
BFS implementation

Flip coordinates as if they were a grid

I have been at it for a long time but can't finish what I'm trying to do. I am trying to find a way to flip coordinates based on a list of objects and a list of their coordinates.
objects = [1, 2, 3, 4, 5]
grid_placement = [(0,0), (32, 0), (64, 0), (0, -32), (0, -64)]
I want to get the flipped axis coordinates of where they are
So the ideal flipped Y output would essentially be:
placement = [(0, -64), (32, -64), (64, -64), (0, -32), (0, 0)]
And the ideal flipped X output would be:
placement = [(64, 0), (32, -64), (0,0), (64, -32), (64, -64)]
So essentially if something is in the top right, it would be on the top left after flipping on X. Their index positions would remain the same in the list, but with the altered coordinates.
The code I have cobbled together works just fine for the Y axis, but I cannot get the X axis working. My brain is a little fried from messing with this for a few hours, any outside advice would be highly appreciated.
Here is my code (messy)
gridSize = 32
if axis == "x":
columns = defaultdict(dict)
for gridOffset in self.offsetList:
row = gridOffset[1] // gridSize
col = gridOffset[0] // gridSize
columns[col][gridOffset] = row
new_order = []
order = list(reversed(list(columns.keys())))
for col in order:
for offset in self.offsetList:
if offset in columns[col]:
new_order.append((col * gridSize, columns[col][offset] * gridSize))
elif axis == "y":
rows = defaultdict(dict)
for gridOffset in self.offsetList:
row = gridOffset[1] // gridSize
col = gridOffset[0] // gridSize
rows[row][gridOffset] = col
new_order = []
order = list(reversed(list(rows.keys())))
for offset in self.offsetList:
for row in order:
if offset in rows[row]:
new_order.append((rows[row][offset] * gridSize, row * -gridSize))
self.offsetList = new_order
Your examples suggest that you want to swap object coordinates rather than "flip" them. To actually flip on a Cartesian plane you would need a pivot point. If that point is (0,0) flipping would amount to inverting the sign of the X or Y coordinate you want to flip.
On the other hand if you want to swap coordinates between objects, use zip:
# vertical swap
grid_placement = [(F[0],B[1]) for F,B in zip(grid_placement, grid_placement[::-1])
# horizontal swap
grid_placement = [(F[1],B[0]) for F,B in zip(grid_placement, grid_placement[::-1])

Get shortest path to a cell in a 2D array in Python

I have a 2D array, arr, where each cell in it has a value 1, 2 or 3, for example, arr[0][0] = 3, arr[2][1] = 2, and arr[0][4] = 1.
I want to know the shortest path from a given certain cell, for example, arr[5][5] to the closest cell which has value 2 where the path shouldn't contain any cells that have the value 1. How can I do this?
Below is a script for the BFS, but how can I make it accept a 2D array as a graph and starting point as a certain cell location in the array and then go to the nearest two from this cell avoiding cells with 1s, so that it looks like bfs(2darray, starting location, 2)?
def bfs(graph, start, end):
# Maintain a queue of paths
queue = []
# Push the first path into the queue
queue.append([start])
while queue:
# Get the first path from the queue
path = queue.pop(0)
# Get the last node from the path
node = path[-1]
# Path found
if node == end:
return path
# Enumerate all adjacent nodes, construct a new path and push it into the queue
for adjacent in graph.get(node, []):
new_path = list(path)
new_path.append(adjacent)
queue.append(new_path)
print bfs(graph, '1', '11')
You can use a simple breadth first search for this. Basically, each cell in your grid corresponds to a node in the graph, with edges between adjacent cells. Start at the starting position, and keep expanding passable cells until you find a goal cell.
def bfs(grid, start):
queue = collections.deque([[start]])
seen = set([start])
while queue:
path = queue.popleft()
x, y = path[-1]
if grid[y][x] == goal:
return path
for x2, y2 in ((x+1,y), (x-1,y), (x,y+1), (x,y-1)):
if 0 <= x2 < width and 0 <= y2 < height and grid[y2][x2] != wall and (x2, y2) not in seen:
queue.append(path + [(x2, y2)])
seen.add((x2, y2))
Grid setup and results: (Note that I'm using symbols instead of numbers, simply for the reason that it's easier to visually parse the grid this way and to verify the solution.)
wall, clear, goal = "#", ".", "*"
width, height = 10, 5
grid = ["..........",
"..*#...##.",
"..##...#*.",
".....###..",
"......*..."]
path = bfs(grid, (5, 2))
# [(5, 2), (4, 2), (4, 3), (4, 4), (5, 4), (6, 4)]
If the list is not too big, the easiest solution I find is using the where function of the NumPy library to find the cells which have the value you are looking for. So, you will need to convert your list into a NumPy array.
The code below might be simplified to make it shorter and more efficient, but in this way it will be clearer. By the way, you can compute two kind of distances: the typical Euclidean and the Manhattan.
If there is more than one target cell at the same distance of the origin cell, min_coords corresponds to the first cell found (first by rows, then by columns).
import numpy as np
# The list needs to be transformed into an array in order to use the np.where method
# arr = np.random.randint(5, size=(6, 6))
arr = np.array([[0, 0, 0, 1, 1, 3],
[0, 0, 2, 1, 1, 0],
[0, 0, 1, 1, 1, 1],
[3, 0, 3, 1, 1, 1], ])
# Origin cell to make the search
x0, y0 = (1, 1)
targetValue = 3
# This is the keypoint of the problem: find the positions of the cells containing the searched value
positions = np.where(arr == targetValue)
x, y = positions
dx = abs(x0 - x) # Horizontal distance
dy = abs(y0 - y) # Vertical distance
# There are different criteria to compute distances
euclidean_distance = np.sqrt(dx ** 2 + dy ** 2)
manhattan_distance = abs(dx + dy)
my_distance = euclidean_distance # Criterion choice
min_dist = min(my_distance)
print(min_dist)
min_pos = np.argmin(my_distance) # This method will only return the first occurrence (!)
min_coords = x[min_pos], y[min_pos]
print(min_coords)

How to convert python list of points to numpy image array?

I have a python list of points (x/y coordinates):
[(200, 245), (344, 248), (125, 34), ...]
It represents a contour on a 2d plane. I would like to use some numpy/scipy algorithms for smoothing, interpolation etc. They normally require numpy array as input. For example scipy.ndimage.interpolation.zoom.
What is the simplest way to get the right numpy array from my list of points?
EDIT: I added the word "image" to my question, hope it is clear now, I am really sorry, if it was somehow misleading. Example of what I meant (points to binary image array).
Input:
[(0, 0), (2, 0), (2, 1)]
Output:
[[0, 0, 1],
[1, 0, 1]]
Rounding the accepted answer here is the working sample:
import numpy as np
coordinates = [(0, 0), (2, 0), (2, 1)]
x, y = [i[0] for i in coordinates], [i[1] for i in coordinates]
max_x, max_y = max(x), max(y)
image = np.zeros((max_y + 1, max_x + 1))
for i in range(len(coordinates)):
image[max_y - y[i], x[i]] = 1
Ah, better now, so you do have all the points you want to fill... then its very simple:
image = np.zeros((max_x, max_y))
image[coordinates] = 1
You could create an array first, but its not necessary.
numpy.array(your_list)
numpy has very extensive documentation that you should try reading. You can find it online or by typing help(obj_you_want_help_with) (eg. help(numpy)) on the REPL.
Building on what Jon Clements and Dunes said, after doing
new_array = numpy.array([(200, 245), (344, 248), (125, 34), ...])
you will get a two-dimensional array where the first column contains the x coordinates and the second column contains the y coordinates. The array can be further split into separate x and y arrays like this:
x_coords = new_array[:,0]
y_coords = new_array[:,1]

Categories