I have a template in the form of a GeoPandas GeoDataFrame with several shapely geometries that correspond to areas on a page.
For this process, the pages are photographed and I need the geometries in my template with the corresponding areas on the photographed page. Right now, my "projection" of the template to the bounding box of the photographed page does not align with the photographed image. I'm also pretty sure there is a better way to do this.
I've created a simplified example to illustrate:
I have this document, with both axes scaled to 1.
I created template that matches the objects on the image
shapes = [
('line1', LineString([(0.146, 0.216), (0.476 , 0.216)])),
('line2', LineString([(0.498, 0.871), (0.838, 0.871)]))
]
shapes = gpd.GeoDataFrame(shapes, columns=['name', 'geometry'], geometry='geometry')
Plotting that on top of the image we can verify that the shapes closely align with the shapes in the image.
plt.imshow(img, extent=[0,1,1,0], aspect=1.4142)
ax = plt.gca()
shapes.plot(ax=ax)
ax.set_aspect(1.4142)
I take a photograph of the same document get the bounding box of the page.
photo = load_image('example_photo.jpg')
bbox = Polygon([(0.847429096698761, 0.047594085335731506),
(0.9442346692085266, 0.8787651062011719),
(0.05563090369105339, 0.8789013028144836),
(0.12740036845207214, 0.06380380690097809),
(0.847429096698761, 0.047594085335731506)])
plt.figure(figsize=(6,6))
plt.imshow(photo, extent=[0,1,1,0], aspect=1.4142)
plt.plot(*bbox.boundary.xy)
The problem is in the next step as I try to remap or project the original template into the shape of the bounding box. This is what I have tried, but I'm sure this isn't the most efficient way to do this. Also it doesn't work.
Summary of the method below is:
figure out which edge is top, bottom, right, left and orient them.
map each point into the new shape finding the intersection of the lines meeting each opposite side of the bounding box at proportionaly the same place as in the unit square.
def get_edges(bbox, visualize=True):
'''Takes shapely polygon with exactly 5 points'''
# check for 5 points
if len(bbox.boundary.coords) != 5:
raise('Polygon must have 5 points (4 sides)')
#find top/bottom edge
x, y = bbox.boundary.xy
# remove last point which must be the same as the first
x = np.array(x)[:-1]
y = np.array(y)[:-1]
# sort by y values. Higher is closer to top, lower closer to bottom
y_sorted = np.argsort(y)
# get the index of the top and bottom lines
top_points_idx = y_sorted[-2:]
bot_points_idx = y_sorted[:2]
# order the top point coords left to right
top_point_order = top_points_idx[
np.argsort(x[top_points_idx])
]
bot_point_order = bot_points_idx[
np.argsort(x[bot_points_idx])
]
top_points = np.array(bbox.boundary.coords)[top_point_order]
bot_points = np.array(bbox.boundary.coords)[bot_point_order]
left_points = LineString([bot_points[0], top_points[0]])
right_points = LineString([bot_points[1], top_points[1]])
top_points = LineString(top_points)
bot_points = LineString(bot_points)
return top_points, bot_points, left_points, right_points
def project_unit_square_point(point, bbox, visualize=False):
# check for 5 points
if len(bbox.boundary.coords) != 5:
raise('Polygon must have 5 points (4 sides)')
# use the position as the portion of the each side
x_scale, y_scale = point.coords[0]
top, bot, left, right = get_edges(bbox)
# find proportional intersections on edges
top_point = top.interpolate(x_scale*top.length)
bot_point = bot.interpolate(x_scale*bot.length)
left_point = left.interpolate(y_scale*left.length)
right_point = right.interpolate(y_scale*right.length)
# connect edge points
vline = LineString([top_point, bot_point])
hline = LineString([left_point, right_point])
# new point is intersection of vline and hline
new_point = vline.intersection(hline)
return new_point
def project_unit_square_geom(geom, bbox):
new_points = []
for point in geom.coords:
new_points.append(project_unit_square_point(Point(point), bbox))
new_geom = LineString(new_points)
return new_geom
# project geoms onto form
projected_shapes = []
for shape in shapes.geometry:
projected_shapes.append(
project_unit_square_geom(shape, bbox)
)
# create a new df for the mapped shapes
projected_shapes = gpd.GeoSeries(projected_shapes, name='geometry')
projected_shapes = gpd.GeoDataFrame({'name': shapes['name'],
'geometry': projected_shapes},
geometry='geometry')
Then when I visualize the result I get this:
plt.figure(figsize=(6,6))
plt.imshow(photo, extent=[0,1,1,0])
plt.plot(*bbox.boundary.xy)
ax = plt.gca()
projected_shapes.plot(ax=ax)
ax.set_aspect(1.4142)
Close but not close enough. Obvioulsy my approach is not working. How can I map the template shapes onto the new shape defined by the bounding box?
Here are the original images to work with.
Related
I use shapes to work with contours. I need to add to the contours of different sizes around the field by a given value. Do not scale the contour by a certain percentage, but expand the border by the same given value, regardless of the size of the contour itself.
I am trying to do it like this:
from shapely.geometry import Polygon, LinearRing
coords = [(30.3283020760901, 59.929340439331035), (30.32625283518726, 59.929669569762034), (30.326617897500824, 59.93065894162025), (30.328354001537814, 59.93056342794558), (30.329838363175877, 59.93089851628186), (30.330225213253033, 59.929729335995624), (30.3283020760901, 59.929340439331035)]
poly_B = Polygon(coords)
poly_A = s.buffer(0.005, quad_segs=10.0, cap_style=1, join_style=2, mitre_limit=10.0)
Or like this:
r = LinearRing(coords)
poly_B = Polygon(r)
poly_A = Polygon(s.buffer(0.005).exterior, [r])
But every time I get a contour in which the Y coordinate is doubled(see image).
Help me figure out where I'm wrong.
I need the fields of the larger contour to be uniform relative to the smaller one.
I was wondering if there is a way to access the symmetry table of the MRichSelection having as a result the positive, the seam and the negative side with the positive and the negative ordered by vertex id correspondence. ie: vertex id 15 is the symmetry correlated to vert id 350. They are both at index 5 in the positive and negative list.
I know I can achieve something similar using the filterXpand, but I believe the lists are not ordered in the way I can access the opposite vertex.
I don't know if you ever found a solution to this, but I will post mine for future TD's looking for a solution.
So let's assume you want to get the corresponding verts between left and right on the YZ plane. you have 2 different options. Using the MRichSelection to handle you symmetry table. Or calculate the vert yourself, by getting the smallest distance vector on the opposite side. Note: if you use the MRichSelection method, you will need to make sure that symmetry mode is enbaled in the viewport.
I will show both answers, so lets get started:
Also note: I will be calculating the YZ Plane, as mentioned earlier. So adjust to your liking if needed.
Solution 1(Calculating yourself):
#importing the OpenMaya Module
from maya.api import OpenMaya as om
#converting selected object into MObject and MFnMesh functionset
mSel=om.MSelectionList()
mSel.add(cmds.ls(sl=1)[0])
mObj=mSel.getDagPath(0)
mfnMesh=om.MFnMesh(mObj)
#getting our basePoints
baseShape = mfnMesh.getPoints()
#this function can be used to revert the object back to the baseShape
mfnMesh.setPoints(baseShape)
#getting l and r verts
mtol=0.02# this will be our mid tolerance, if the mesh is not completely symmetric on the mid
lVerts=[]#for storing left Verts
rVerts=[]#for storing right Verts
mVerts=[]#for storing mid Verts
corrVerts={} #for storing correspondign verts
for i in range(mfnMesh.numVertices): #iteratign through all the verts on the mesh
thisPoint = mfnMesh.getPoint(i) #getting current point position
if thisPoint.x>0+mtol: # if pointValue on x axis is bigger than 0+midTolerance
lVerts.append((i, thisPoint))#append to left vert storage list(i = vert index, thisPoint = vert Mpoint position)
elif thisPoint.x<0-mtol: #opposite of left vert calculation
rVerts.append((i, thisPoint))
else: #if none of the above, assign to mid verts
mVerts.append((i, thisPoint))
rVertspoints=[i for v,i in rVerts] #getting the vert mPoint positions of the right side
for vert, mp in lVerts: #going through our left points, unpacking our vert index and mPoint position()
nmp=om.MPoint(-mp.x, mp.y, mp.z) #storing the reversed mpoint of the left side vert
rp = mfnMesh.getClosestPoint(nmp)#getting the closest point on the mesh
if rp[0] in rVertspoints: #cheking if the point is in the right side
corrVerts[vert] = rVerts[rVertspoints.index(rp[0])][0] #adding it if it is true
else:#if it is not, calculate closest vert
#iterating through rVertspoints and find smallest distance
dList=[nmp.distanceTo(rVert) for rVert in rVertspoints]#distance list for each vert based on input point
mindist = min(dList)#getting the closest distance
corrVerts[vert] = rVerts[dList.index(mindist)][0]#adding the vert
#now the corrVerts will have stored the corresponding vertices from left to right
Solution 2(using MRichSelection):
#MAKE SURE SYMMETRY IN THE VIEWPORT IS TURNED ON TO WORK! (will also work with topological symmetry)
#importing the OpenMaya Module
from maya.api import OpenMaya as om
#converting selected object into MObject and MFnMesh functionset
mSel=om.MSelectionList()
mSel.add(cmds.ls(sl=1)[0])
mObj=mSel.getDagPath(0)
mfnMesh=om.MFnMesh(mObj)
#getting our basePoints
baseShape = mfnMesh.getPoints()
#this function can be used to revert the object back to the baseShape
mfnMesh.setPoints(baseShape)
#getting l and r verts
mtol=0.02# this will be our mid tolerance, if the mesh is not completely symmetric on the mid
lVerts=[]#for storing left Verts
corrVerts={} #for storing correspondign verts
for i in range(mfnMesh.numVertices): #iteratign through all the verts on the mesh
thisPoint = mfnMesh.getPoint(i) #getting current point position
if thisPoint.x>0+mtol: # if pointValue on x axis is bigger than 0+midTolerance
lVerts.append((i, thisPoint))#append to left vert storage list(i = vert index, thisPoint = vert Mpoint position)
#selecting our verts with symmetry on
SymSelection = cmds.select(["%s.vtx[%s]"%(mObj,i) for i,v in lVerts], sym=True)
#getting the rich selection. it will store the symmetry iformation for us
mRichBase = om.MGlobal.getRichSelection()
lCor = mRichBase.getSelection()#this will store our lSide verts as an MSelectionList
rCor = mRichBase.getSymmetry()#this will symmetry verts as an MSelectionList
mitL = om.MItSelectionList(lCor)#creating iterative lists so we can get the components
mitR = om.MItSelectionList(rCor)
while not mitL.isDone():#iterating through the left list
mitLComp = mitL.getComponent()#getting dag path and components of leftside
mitRComp = mitR.getComponent()#getting dag path and components of rightside
mitLCorVert = om.MItMeshVertex(mitLComp[0], mitLComp[1]) #creating our iterative vertex lists
mitRCorVert = om.MItMeshVertex(mitRComp[0], mitRComp[1])
while not mitLCorVert.isDone():#iterating through our verts
corrVerts[mitLCorVert.index()] = mitRCorVert.index()#adding corresponding verts to our dictionary
mitLCorVert.next()#go to next vert. needed to stop loop
mitRCorVert.next()#go to next vert. needed to stop loop
mitL.next()#go to next selection in list if more. needed to stop loop
mitR.next()#go to next selection in list if more. needed to stop loop
cmds.select(cl=1)#deseleting our verts
#now the corrVerts will have stored the corresponding vertices from left to right
Hope it will help you all, looking for a few solutions.
Cheers
Bjarke Rauff, Rigging TD.
The answer by #Bjarke Rauff was very helpful, wanted to add a note about speed.
MFnMesh.getClosestPoint() builds an octree to efficiently find the point, but it will do that on every call. A mesh with 100k points can take up to 45s to process.
Use a MMeshIntersector() to cache the data between lookups. This speeds up the table creation by 900x for 100k points to .05s.
mesh # MDagpath obj to poly
flip_matrix # MTransformMatrix to flop the point
itMesh = om.MItMeshPolygon(mesh)
mesh.extendToShape()
matrix = mesh.inclusiveMatrix()
node = mesh.node()
intersector = om.MMeshIntersector()
intersector.create(node, matrix)
if not (intersector.isCreated):
print("Failed to create mesh intersector")
return
flipped_ids={}
while not itMesh.isDone():
id = itMesh.index()
face_center = itMesh.center()
# flop the point across the axis
flipped_point = face_center*flip_matrix
MpointOnMesh = intersector.getClosestPoint(flipped_point)
if MpointOnMesh is not None:
# get face id property from MPointOnMesh
flipped_id = MpointOnMesh.face
flipped_ids[id] = flipped_id
else:
print("No intersection")
itMesh.next()
NOTE
I tried hash tables with a tuple of the point as the key, but the point positions had slight variations, even with rounding, which created different hashes.
I've tested the MRichSelection approach and it doesn't actually work consistently in practice. It seems like it works when you have a perfectly mirrored mesh, but that can't be assumed. The component lists are not necessarily in sync.
While trying to answer an old, unanswered question, I encountered a little problem concerning text annotations in matplotlib: When adding rotated text to a figure at a certain position, the text is aligned relative to the bounding box of the text, not the (imaginary) rotated box that holds the text itself. This is maybe best explained with a little example:
The figure shows pieces of text with different rotation angles and different alignment options. For each text object, the red point denotes the coordinate given to the ax.text() function. The blue box is the rotated frame around the text, and the black box is the approximate bounding box of the text (it's a bit too big, but one should get the idea). It's easily visible that, for the cases where alignment is at the edges (left, right, top, bottom), the red dot is on the sides or edges of the bounding box, not the text frame. The only alignment option, where the text is aligned in an intuitive way is if both horizontal and vertical alignments are set to 'center'. Now, this is not a bug, but intended behaviour as outlined here. However, in some situations, it's not very practical, as the position has to be adjusted 'manually' for the text to be in the desired place, and this adjustment changes if the rotation angle changes or if the figure is re-scaled.
The question is, is there a robust way to generate text that is aligned with the text frame rather with the bounding box. I already have a solution to the problem, but it was quite tedious to figure out, so I thought I'd share it.
New solution rotation_mode="anchor"
There is actually an argument rotation_mode to matplotlib.text.Text, which steers exactly the requested functionality. The default is rotation_mode="default" which recreates the unwanted behaviour from the question, while rotation_mode="anchor" anchors the point of revolution according to the text itself and not its bounding box.
ax.text(x,y,'test', rotation = deg, rotation_mode="anchor")
Also see the demo_text_rotation_mode example.
With this, the example from the question can be created easily without the need to subclass Text.
from matplotlib import pyplot as plt
import numpy as np
fig, axes = plt.subplots(3,3, figsize=(10,10),dpi=100)
aligns = [ (va,ha) for va in ('top', 'center', 'bottom')
for ha in ('left', 'center', 'right')]
xys = [[i,j] for j in np.linspace(0.9,0.1,5) for i in np.linspace(0.1,0.9,5)]
degs = np.linspace(0,360,25)
for ax, align in zip(axes.reshape(-1), aligns):
ax.set_xlim([-0.1,1.1])
ax.set_ylim([-0.1,1.1])
for deg,xy in zip(degs,xys):
x,y = xy
ax.plot(x,y,'r.')
text = ax.text(x,y,'test',
rotation = deg,
rotation_mode="anchor", ### <--- this is the key
va = align[0],
ha = align[1],
bbox=dict(facecolor='none', edgecolor='blue', pad=0.0),
)
ax.set_title('alignment = {}'.format(align))
fig.tight_layout()
plt.show()
old solution, subclassing Text
In case one is still interested, the solution given by #ThomasKühn is of course working fine, but has some drawbacks when text is used in a non-cartesian system, because it calculates the offset needed in Data coordinates.
The following would be a version of the code which offsets the text in display coordinates by using a transformation, which is temporarily attached while drawing the text. It can therefore also be used e.g. in polar plots.
from matplotlib import pyplot as plt
from matplotlib import patches, text
import matplotlib.transforms
import numpy as np
class TextTrueAlign(text.Text):
"""
A Text object that always aligns relative to the text, not
to the bounding box; also when the text is rotated.
"""
def __init__(self, x, y, text, **kwargs):
super(TextTrueAlign, self).__init__(x,y,text, **kwargs)
self.__Ha = self.get_ha()
self.__Va = self.get_va()
def draw(self, renderer, *args, **kwargs):
"""
Overload of the Text.draw() function
"""
trans = self.get_transform()
offset = self.update_position()
# while drawing, set a transform which is offset
self.set_transform(trans + offset)
super(TextTrueAlign, self).draw(renderer, *args, **kwargs)
# reset to original transform
self.set_transform(trans)
def update_position(self):
"""
As the (center/center) alignment always aligns to the center of the
text, even upon rotation, we make use of this here. The algorithm
first computes the (x,y) offset for the un-rotated text between
centered alignment and the alignment requested by the user. This offset
is then rotated by the given rotation angle.
Finally a translation of the negative offset is returned.
"""
#resetting to the original state:
rotation = self.get_rotation()
self.set_rotation(0)
self.set_va(self.__Va)
self.set_ha(self.__Ha)
##from https://stackoverflow.com/questions/5320205/matplotlib-text-dimensions
##getting the current renderer, so that
##get_window_extent() works
renderer = self.axes.figure.canvas.get_renderer()
##computing the bounding box for the un-rotated text
##aligned as requested by the user
bbox1 = self.get_window_extent(renderer=renderer)
##re-aligning text to (center,center) as here rotations
##do what is intuitively expected
self.set_va('center')
self.set_ha('center')
##computing the bounding box for the un-rotated text
##aligned to (center,center)
bbox2 = self.get_window_extent(renderer=renderer)
##computing the difference vector between the two alignments
dr = np.array(bbox2.get_points()[0]-bbox1.get_points()[0])
##computing the rotation matrix, which also accounts for
##the aspect ratio of the figure, to stretch squeeze
##dimensions as needed
rad = np.deg2rad(rotation)
rot_mat = np.array([
[np.cos(rad), np.sin(rad)],
[-np.sin(rad), np.cos(rad)]
])
##computing the offset vector
drp = np.dot(dr,rot_mat)
# transform to translate by the negative offset vector
offset = matplotlib.transforms.Affine2D().translate(-drp[0],-drp[1])
##setting rotation value back to the one requested by the user
self.set_rotation(rotation)
return offset
if __name__ == '__main__':
fig, axes = plt.subplots(3,3, figsize=(10,10),dpi=100)
aligns = [ (va,ha) for va in ('top', 'center', 'bottom')
for ha in ('left', 'center', 'right')]
xys = [[i,j] for j in np.linspace(0.9,0.1,5) for i in np.linspace(0.1,0.9,5)]
degs = np.linspace(0,360,25)
for ax, align in zip(axes.reshape(-1), aligns):
ax.set_xlim([-0.1,1.1])
ax.set_ylim([-0.1,1.1])
for deg,xy in zip(degs,xys):
x,y = xy
ax.plot(x,y,'r.')
text = TextTrueAlign(
x = x,
y = y,
text='test',
axes = ax,
rotation = deg,
va = align[0],
ha = align[1],
bbox=dict(facecolor='none', edgecolor='blue', pad=0.0),
)
ax.add_artist(text)
ax.set_title('alignment = {}'.format(align))
fig.tight_layout()
plt.show()
After some searching and digging into the matplotlib code itself, and with some inspiration from here and here, I have come up with the following solution:
from matplotlib import pyplot as plt
from matplotlib import patches, text
import numpy as np
import math
class TextTrueAlign(text.Text):
"""
A Text object that always aligns relative to the text, not
to the bounding box; also when the text is rotated.
"""
def __init__(self, x, y, text, **kwargs):
super().__init__(x,y,text, **kwargs)
self.__Ha = self.get_ha()
self.__Va = self.get_va()
self.__Rotation = self.get_rotation()
self.__Position = self.get_position()
def draw(self, renderer, *args, **kwargs):
"""
Overload of the Text.draw() function
"""
self.update_position()
super().draw(renderer, *args, **kwargs)
def update_position(self):
"""
As the (center/center) alignment always aligns to the center of the
text, even upon rotation, we make use of this here. The algorithm
first computes the (x,y) offset for the un-rotated text between
centered alignment and the alignment requested by the user. This offset
is then transformed according to the requested rotation angle and the
aspect ratio of the graph. Finally the transformed offset is used to
shift the text such that the alignment point coincides with the
requested coordinate also when the text is rotated.
"""
#resetting to the original state:
self.set_rotation(0)
self.set_va(self.__Va)
self.set_ha(self.__Ha)
self.set_position(self.__Position)
ax = self.axes
xy = self.__Position
##determining the aspect ratio:
##from https://stackoverflow.com/questions/41597177/get-aspect-ratio-of-axes
##data limits
xlim = ax.get_xlim()
ylim = ax.get_ylim()
## Axis size on figure
figW, figH = ax.get_figure().get_size_inches()
## Ratio of display units
_, _, w, h = ax.get_position().bounds
##final aspect ratio
aspect = ((figW * w)/(figH * h))*(ylim[1]-ylim[0])/(xlim[1]-xlim[0])
##from https://stackoverflow.com/questions/5320205/matplotlib-text-dimensions
##getting the current renderer, so that
##get_window_extent() works
renderer = ax.figure.canvas.get_renderer()
##computing the bounding box for the un-rotated text
##aligned as requested by the user
bbox1 = self.get_window_extent(renderer=renderer)
bbox1d = ax.transData.inverted().transform(bbox1)
width = bbox1d[1,0]-bbox1d[0,0]
height = bbox1d[1,1]-bbox1d[0,1]
##re-aligning text to (center,center) as here rotations
##do what is intuitively expected
self.set_va('center')
self.set_ha('center')
##computing the bounding box for the un-rotated text
##aligned to (center,center)
bbox2 = self.get_window_extent(renderer=renderer)
bbox2d = ax.transData.inverted().transform(bbox2)
##computing the difference vector between the two
##alignments
dr = np.array(bbox2d[0]-bbox1d[0])
##computing the rotation matrix, which also accounts for
##the aspect ratio of the figure, to stretch squeeze
##dimensions as needed
rad = np.deg2rad(self.__Rotation)
rot_mat = np.array([
[math.cos(rad), math.sin(rad)*aspect],
[-math.sin(rad)/aspect, math.cos(rad)]
])
##computing the offset vector
drp = np.dot(dr,rot_mat)
##setting new position
self.set_position((xy[0]-drp[0],xy[1]-drp[1]))
##setting rotation value back to the one requested by the user
self.set_rotation(self.__Rotation)
if __name__ == '__main__':
fig, axes = plt.subplots(3,3, figsize=(10,10),dpi=100)
aligns = [ (va,ha) for va in ('top', 'center', 'bottom')
for ha in ('left', 'center', 'right')]
xys = [[i,j] for j in np.linspace(0.9,0.1,5) for i in np.linspace(0.1,0.9,5)]
degs = np.linspace(0,360,25)
for ax, align in zip(axes.reshape(-1), aligns):
ax.set_xlim([-0.1,1.1])
ax.set_ylim([-0.1,1.1])
for deg,xy in zip(degs,xys):
ax.plot(*xy,'r.')
text = TextTrueAlign(
x = xy[0],
y = xy[1],
text='test',
axes = ax,
rotation = deg,
va = align[0],
ha = align[1],
bbox=dict(facecolor='none', edgecolor='blue', pad=0.0),
)
ax.add_artist(text)
ax.set_title('alignment = {}'.format(align))
fig.tight_layout()
plt.show()
The example is somewhat lengthy, because I had to write a class that is derived from the matplotlib.text.Text class in order to properly update the text object upon redraw (for instance if the figure is re-scaled). The code relies on the text always aligning to its center point, if both horizontal and vertical alignments are set to 'center'. It takes the difference between the bounding boxes of the text with center alignment and with requested alignment to predict an offset by which the text needs to be shifted after rotation. The output of the example looks like this:
As the aspect ratio of the graph, axes, and figure are taken into account, this approach is also robust to re-sizing of the figure.
I think that, by treating the methods set_ha(), set_va(), set_rotation(), and set_position() the way I do, I might have broken some of the original functionality of matplotlib.text.Text, but that should be relatively easy to fix by overloading these functions and replacing a few self with super().
Any comments or suggestions how to improve this would be highly appreciated. Also, if you happen to test this and find any bugs or flaws, please let me know and I will try to fix them. Hope this is useful to someone :)
I can compute the SLIC boundaries using skimage as follows:
def compute_superpixels(frame, num_pixels=100, std=5, iter_max=10,
connectivity=False, compactness=10.0):
return slic(frame, n_segments=num_pixels, sigma=std, max_iter=iter_max,
enforce_connectivity=connectivity, compactness=compactness)
Now, what I would like to do is get the index of pixels which form the boundary of each label. So my idea was to get all pixels belonging to a given segment and then check which pixels have a change in all two directions
def boundary_pixels(segments, index):
# Get all pixels having a given index
x, y = np.where(segments == index)
right = x + 1
# check we are in bounds
right_mask = right < segments.shape[0]
down = y + 1
down_mask = down < segments.shape[1]
left = x - 1
left_mask = left >= 0
up = y - 1
up_mask = up >= 0
neighbors_1 = np.union1d(right_n, down_n)
neighbors_2 = np.union1d(left_n, up_n)
neighbors = np.union1d(neighbors_1, neighbors_2)
# Not neighbours to ourselves
neighbors = np.delete(neighbors, np.where(neighbors == i))
However, with this all I managed to do was to get the neighbours in the 4 directions of a given label. Can someone suggest some way to actually get all pixels on the border of the label.
I found an answer to my own question. The mark_boundaries in the skimage.segmentation package does exactly what I needed.
Usage:
processed = mark_boundaries(frame, segments==some_segment)
Here frame is he current image frame and segments is the label array. some_segment is the label integer index whose boundaries we are interested in.
You can make use of the find_contours function available in skimage.measure module to find the co-ordinates of the pixels along the boundary. An example is available at find_contours.. Next, you can change for change in both directions as needed.
I want to create the following image patterns using Python.
For clarity: these are two separate image sequences (one on the top row, one on the bottom row).
They are related to each other as they are projected areas of stacked tetrahedra.
In a 3D environment it looks as follows:
Note that these 3D object are not scaled such that the total object dimension remains the same. This is the case with the projected areas shown above.
The four level structure (not shown) would have an additional 10 cells on top.
The total amount of cells C at level n is:
C = (n^3 + 3*n^2 + 2*n)/6
Now I'm creating the patterns by hand (make 3D object, render out projected area, repeat) but this is very tedious and not feasible for more subdivisions.
I managed to create a single polygon with the following code, but I can't figure out how to loop this such that the total edge length stays the same, but the polygon gets subdivided in the way visualised above.
import numpy as np
import matplotlib.pyplot as plt
from matplotlib.patches import Polygon
el = 1
dfv = 1/np.sqrt(3)*el
dfe = 1/(2*np.sqrt(3))*el
vertices1 = [(0,0),(0.5*el,-dfe),(0,dfv),(-0.5*el,-dfe)]
vertices2 = [(0.5*el,-dfe),(0,dfv),(-0.5*el,-dfe)]
fig = plt.figure()
ax1 = fig.add_subplot(121)
ax1.add_patch(Polygon(vertices1, closed=True, fill=True))
ax1.set_xlim([-1, 1])
ax1.set_ylim([-1, 1])
ax1.set_aspect('equal')
ax2 = fig.add_subplot(122)
ax2.add_patch(Polygon(vertices2, closed=True, fill=True))
ax2.set_xlim([-1, 1])
ax2.set_ylim([-1, 1])
ax2.set_aspect('equal')
plt.show()
I used matplotlib and the included Polygon patch, but I'm not sure if that is the most optimal method.
Also the orientation of the polygon or the color is of no importance.
Helper class:
class Custom_Polygon(object):
"""docstring for Polygon"""
def __init__(self, verts):
self.verticies = np.array(verts)
self.dims = self.verticies.shape[1]
def scale(self, scaleFactor):
scaleMatrix = np.zeros((self.dims, self.dims))
np.fill_diagonal(scaleMatrix, scaleFactor)
self.verticies = np.dot(self.verticies, scaleMatrix)
def scale_with_orgin(self, scaleFactor, origin):
origin = origin.copy()
self.translate([-i for i in origin])
self.scale(scaleFactor)
self.translate([i for i in origin])
def translate(self, shiftBy):
self.verticies += shiftBy
def get_width(self):
x_min = self.verticies[:,0].min()
x_max = self.verticies[:,0].max()
return abs(x_max - x_min)
def get_height(self):
y_min = self.verticies[:,1].min()
y_max = self.verticies[:,1].max()
return abs(y_max - y_min)
Made a helper class for scaling and translating the polygon around to make the pattern. And wrote the algorithm for drawing the first pattern. Should not be hard to make a similar algorithm for drawing the second pattern.
import numpy as np
import matplotlib.pyplot as plt
from matplotlib.patches import Polygon
def makeFirstPattern(ax, d, verticies, scale=True):
Pn = Custom_Polygon(verticies)
direction = 1
divisions = d
if scale: Pn.scale_with_orgin(2.0/(divisions+1), Pn.verticies[-1])
for i in range(divisions, 0, -2):
for _ in range(i):
ax.add_patch(Polygon(Pn.verticies, closed=True, fill=True, ec='none'))
Pn.translate([direction * Pn.get_width()/2, 0])
direction *= -1
Pn.translate([direction * Pn.get_width(), Pn.get_height()])
el = 1
dfv = 1/np.sqrt(3)*el
dfe = 1/(2*np.sqrt(3))*el
vertices1 = [(0,0),(0.5*el,-dfe),(0,dfv),(-0.5*el,-dfe)]
vertices2 = [(0.5*el,-dfe),(0,dfv),(-0.5*el,-dfe)]
fig = plt.figure()
ax1 = fig.add_subplot(111)
makeFirstPattern(ax1, 7, vertices2)
ax1.set_xlim([-1, 1])
ax1.set_ylim([-1, 1])
ax1.set_aspect('equal')
plt.show()
For first pattern you can use this code:
from __future__ import division
import numpy as np
import matplotlib.pyplot as plt
from matplotlib.patches import Polygon
nrows = 5
pat = np.array([[0,0],[0.5,np.sqrt(0.75)],[-0.5,np.sqrt(0.75)]])
fig = plt.figure()
ax = fig.add_subplot(111)
for base in range(nrows):
npat = 2*base + 1
for col in np.linspace(-base/2, base/2, npat):
pp = Polygon(pat + np.array([col, base*v]), fc='k', ec='none')
ax.add_patch(pp)
ax.autoscale_view(True,True,True)
ax.set_aspect('equal')
plt.show()
Instead of using matplotlib I'd offer a solution using SVGs, where your script only prints out the respective SVG commands.
Note: The SVG header created is missing same definitions, that's why the resulting image can not be handled by some programs. Inkscape though can open it without problems and save it again.
Defining the polygon in SVG
The solution is describe is based in the "arrow" example you showed, i.e. the lower one.
I coded the arrow as a SVG path with four points p0,p1,p2 and p3, where p0 is the upper tip, p1 is the lower right edge, p3 is the lower left one and p2 is the point underneath the tip. Every point has an x and y coordinate (p0x,p0y...).
Note: SVG coordinates unlike mathematical ones increases from left to right (x) and top-down (y), so the origin is the top-left corner.
The path is stored as string with the points being varibale. The final string is created using python's str.format() method.
Code
# width of the complete picture
width=600
# height of the complete picture
height=600
# desired recursion depth (>=1)
n=5
# "shape factor" of the arrow (=(p1y-p2y)/(p2y-p0y))
# a=0 would result in a triangle
a=0.3
def create_arrows(n, depth=1, width=600, height=600, refx=None, refy=None):
global a
if refx==None or refy==None:
# the first polygon instances defines it's reference point
# following instances are given a reference point by the caller
refx = (width - width/n)/2
refy = 0
if depth==1:
# the first polygon instance defines the size of all instancens
width=width/n
height=height/n
# the SVG definition of the polygon
polyg = r'<path d="M{p0x} {p0y} L{p1x} {p1y} L{p2x} {p2y} L{p3x} {p3y} Z" />'
points = {"p0x":refx+width/2, "p0y":refy, "p1x":refx+width, "p1y":refy+height, "p2x":refx+width/2, "p2y":refy+(1-a)*height, "p3x":refx, "p3y":refy+height}
# create the requested polygon
polygons = [polyg.format(**points)]
# if maximum recursion depth not reached call method recursively
if depth<n:
polygons.extend(myfunction(n, depth+1, width=width, height=height, refy=refy+(1-a)*height, refx=refx)) # down shifted
polygons.extend(myfunction(n, depth+1, width=width, height=height, refy=refy+height, refx=refx-width/2)) # bottom left
polygons.extend(myfunction(n, depth+1, width=width, height=height, refy=refy+height, refx=refx+width/2)) #bottom right
return(polygons)
print('<svg height="{height}" width="{width}">'.format(width=width, height=height))
# converting the list to a set is necessary since some polygons can be created by multiple recursion paths
print('\n'.join(set(create_arrows(4,width=width,height=height))))
print('</svg>')
Explanation
The arrows are created in a recursive fashion, i.e. you start with the top arrow. The dimensions of the arrows are width/n and height/n, where n is the desired recursion level (n>=1). The examples you showed would be n=1, n=2, and n=3. The below explained recursion pattern was empirically derived and is not directly based on your 3D example.
n=1: In level n=1 you're done after creating the top arrow.
n=2: In level n=2 this TOP arrow creates another three, one right below and each left and right below, respectively. The tips (p0) of these three arrows are at p2, p3 and p1 of the original arrow, respectively. You're done.
n=3: Repeat the above procedure for every arrow created in level n=2 and so on.
Below please find an example for level n=6.
For the triangles, your upper example, this idea can easily be adapted. You just have to change the polygon path and recursion pattern.
Btw. a rotated version of your trianlges are created using the given script with a=0. So if you're lazy just use that and turn the resulting SVG in inkscape.