Related
So I have some code in Python (3.9.13) to obtain a Delaunay triangulation of a set of points in real time and analyze the graph properties. First I use OpenCV (opencv-python 4.6.0.66) Subdiv2D method to obtain the triangulation. Then I convert it in a graph I can analyze with igraph (igraph 0.10.3). But I am not sure why, once every few frames the graph produced by igraph is messed up such as shown in this image:
graph messed up (Left is OpenCV and right is igraph):
Else it is working properly.
good graph (Left is OpenCV and right is igraph):
Here is my demo code:
import time
import numpy
import cv2
import igraph as ig
# Draw a point
def draw_point(img, p, color):
cv2.circle(img, (int(p[0]), int(p[1])), 2, color, 0)
# Get a triangulation
def get_delaunay(subdiv):
return subdiv.getTriangleList()
# Draw delaunay triangles
def draw_delaunay(img, subdiv, delaunay_color):
triangleList = subdiv.getTriangleList()
size = img.shape
r = (0, 0, size[1], size[0])
for t in triangleList:
pt1 = (int(t[0]), int(t[1]))
pt2 = (int(t[2]), int(t[3]))
pt3 = (int(t[4]), int(t[5]))
cv2.line(img, pt1, pt2, delaunay_color, 1, cv2.LINE_AA, 0)
cv2.line(img, pt2, pt3, delaunay_color, 1, cv2.LINE_AA, 0)
cv2.line(img, pt3, pt1, delaunay_color, 1, cv2.LINE_AA, 0)
if __name__ == '__main__':
NUM_PART = 500
SIZE = 1000
REPEAT = 10
for iteration in range(REPEAT):
positions = numpy.random.randint(0, SIZE, size=(NUM_PART, 2))
print("There is {p} positions. And {up} unique position".format(p=len(positions), up=len(numpy.unique(positions, axis=1))))
# Create an instance of Subdiv2D
rect = (0, 0, SIZE, SIZE)
subdiv = cv2.Subdiv2D(rect)
timer = time.time()
# Insert points into subdiv
print("There is {} points in subdiv".format(len(positions)))
for p in positions:
p = p.astype("float32")
subdiv.insert(p)
# get triangulation
trilist = get_delaunay(subdiv)
print("Took {}s".format(round(time.time() - timer, 12)))
print("there is {} triangles in trilist".format(len(trilist)))
# create image
opencv_image = numpy.zeros((SIZE, SIZE, 3))
# Draw delaunay triangles
draw_delaunay(opencv_image, subdiv, (255, 255, 255))
# Draw points
for p in positions:
draw_point(opencv_image, p, (0, 0, 255))
timer = time.time()
n_vertices = NUM_PART
# create graph
g = ig.Graph(n=n_vertices, )
g.vs["name"] = range(NUM_PART)
print("graph name vector of length {l}:\n{v}".format(l=len(g.vs["name"]), v=g.vs["name"]))
# Inversion x positions
positionsx = [SIZE - pos for pos in positions[:, 0]]
g.vs["x"] = positions[:, 0]
print("graph x vector of length {l}:\n{v}".format(l=len(g.vs["x"]), v=g.vs["x"]))
g.vs["y"] = positions[:, 1]
print("graph y vector of length {l}:\n{v}".format(l=len(g.vs["y"]), v=g.vs["y"]))
print("Graph took {}s".format(round(time.time() - timer, 12)))
list_vtx = []
for tri in trilist:
vertex1, _ = subdiv.findNearest((tri[0], tri[1]))
vertex2, _ = subdiv.findNearest((tri[2], tri[3]))
vertex3, _ = subdiv.findNearest((tri[4], tri[5]))
list_vtx.extend([vertex3, vertex2, vertex1])
list_cleared = list(dict.fromkeys(list_vtx))
list_cleared.sort()
print("list cleared of length {len}: {lst}".format(len=len(list_cleared), lst=list_cleared))
for tri in trilist:
vertex1, _ = subdiv.findNearest((tri[0], tri[1]))
vertex2, _ = subdiv.findNearest((tri[2], tri[3]))
vertex3, _ = subdiv.findNearest((tri[4], tri[5]))
#print("vertex 1: {v} of position {p}".format(v=vertex1, p=(tri[0], tri[1])))
#print("vertex 2: {v} of position {p}".format(v=vertex2, p=(tri[2], tri[3])))
#print("vertex 3: {v} of position {p}".format(v=vertex3, p=(tri[4], tri[5])))
# -4 because https://stackoverflow.com/a/52377891/18493005
g.add_edges([
(vertex1 - 4, vertex2 - 4),
(vertex2 - 4, vertex3 - 4),
(vertex3 - 4, vertex1 - 4),
])
# simplify graph
g.simplify()
nodes = g.vs.indices
print(nodes)
print(subdiv)
# create image
igraph_image = numpy.zeros((SIZE, SIZE, 3))
for point in g.vs:
draw_point(igraph_image, (point["x"], point["y"]), (0, 0, 255))
for edge in g.es:
# print(edge.tuple)
# print(g.vs["x"][edge.tuple[0]])
cv2.line(igraph_image, (int(g.vs["x"][edge.tuple[0]]), int(g.vs["y"][edge.tuple[0]])),
(int(g.vs["x"][edge.tuple[1]]), int(g.vs["y"][edge.tuple[1]])), (255, 255, 255), 1, cv2.LINE_AA, 0)
numpy_horizontal = numpy.hstack((opencv_image, igraph_image))
# Show results
cv2.imshow('L: opencv || R: igraph', numpy_horizontal)
cv2.waitKey(0)
I try to have a repeatable result of my graph in igraph. But it is only working 80% of the time which is pretty strange behavior. Any idea of what are my mistakes here?
Edit: it seems to be a variation in the length of the list generated by:
trilist = get_delaunay(subdiv)
list_vtx = []
for tri in trilist:
vertex1, _ = subdiv.findNearest((tri[0], tri[1]))
vertex2, _ = subdiv.findNearest((tri[2], tri[3]))
vertex3, _ = subdiv.findNearest((tri[4], tri[5]))
list_vtx.extend([vertex3, vertex2, vertex1])
list_cleared = list(dict.fromkeys(list_vtx))
list_cleared.sort()
but I am not sure why.
Edit2:
After the modification sugested by Markus. I do not get a messed up graph anymore. But now the graph is missing some edges
x_pos = [0] * NUM_PART # create 0-filled array of x-positions
y_pos = [0] * NUM_PART # create 0-filled array of y-positions
edges = [] # create empty array of edges
# for each triangle add vertex positions and edges
for tri in trilist:
vertex1 = subdiv.findNearest((tri[0], tri[1]))[0] - 4
vertex2 = subdiv.findNearest((tri[2], tri[3]))[0] - 4
vertex3 = subdiv.findNearest((tri[4], tri[5]))[0] - 4
x_pos[vertex1] = tri[0]
y_pos[vertex1] = tri[1]
x_pos[vertex2] = tri[2]
y_pos[vertex2] = tri[3]
x_pos[vertex3] = tri[4]
y_pos[vertex3] = tri[5]
edges.append((vertex1, vertex2))
edges.append((vertex2, vertex3))
edges.append((vertex2, vertex3))
# create graph
g = ig.Graph(NUM_PART, edges)
g.vs["name"] = range(NUM_PART)
g.vs["x"] = x_pos
g.vs["y"] = y_pos
g.simplify()
The following image shows an overlay between 3 type of drawing (White=opencv , Red=Markus suggestion, Green + Red = previous method used)
Overlay of Markus solution in case of no mess up
Overlay of Markus solution in case of mess up
So Markus solution indeed remove the mess up, but also some edges, even in the case that was working previously.
So in fact my test code was working as expected. The issue was not from Subdiv2D or igraph but from the generation of my position.
I made a mistake verifying the uniqueness of my position with
len(numpy.unique(positions, axis=1))
but should have been using
len(numpy.unique(positions, axis=0)).
So when I used subdiv.findNearest()[0] or subdiv.locate()[2] I was in fact finding several points at the same position, and only the first index was thrown back by the function and so the graph was being messed up.
In order to generate unique position I uses the following code and the graph messing disappeared:
rng = numpy.random.default_rng()
xx = rng.choice(range(SIZE), NUM_PART, replace=False).astype("float32")
yy = rng.choice(range(SIZE), NUM_PART, replace=False).astype("float32")
pp= numpy.stack((xx,yy), axis=1)
The fact that numpy.random.randint(0, 1000, size=(500, 2)) was providing a similar position every 10 or so frame is pretty strange to me as the probability of getting two identical positions seems intuitively to be lower than 0.1
I'm having a problem with the font scaling of TextItems in pyqtgraph, like you can see from the following code when I zoom in/zoom out in the main graph the font of the TextItems stays the same while I'm trying to make It scale in the same exact way (rate) of the QGraphicsRectItem. I've tried to look on all the forums I know but I haven't find an answer so I really hope someone has a solution for this.
import sys
import pyqtgraph as pg
from PyQt6.QtWidgets import QApplication, QGraphicsRectItem
from pyqtgraph.Qt import QtCore
app = QApplication(sys.argv)
view = pg.GraphicsView()
l = pg.GraphicsLayout()
view.setCentralItem(l)
view.show()
view.resize(800, 600)
p0 = l.addPlot(0, 0)
p0.showGrid(x=True, y=True, alpha=1.0)
# have no x-axis tickmark below the upper plot (coordinate 0,0)
# without these lines, there will be separate coordinate systems with a gap inbetween
ay0 = p0.getAxis('left') # get handle to y-axis 0
ay0.setStyle(showValues=False) # this will remove the tick labels and reduces gap b/w plots almost to zero
# there will be a double line separating the plot rows
# ay02 = p0.getAxis('right')
# ay02.setStyle(showValues=False)
p0.hideAxis('right')
ax02 = p0.getAxis('top')
ax02.setStyle(showValues=False)
p1 = l.addPlot(0, 1)
# p1.showGrid(x=True, y=True, alpha=1.0)
p1.setYLink(p0)
l.layout.setSpacing(0.5)
l.setContentsMargins(0., 0., 0., 0.)
p1.setFixedWidth(300)
# p1.setFixedHeight(h-451)
p1.setMouseEnabled(x=False)
# ay1 = p1.getAxis('left')
# ay1.setStyle(showValues=False)
ax12 = p1.getAxis('top')
ax12.setStyle(showValues=False)
# ax1 = p1.getAxis('bottom')
# ax1.setStyle(showValues=False)
p1.showAxis('right')
p1.hideAxis('left')
p1.setXRange(0, 6, padding=0) # Then add others like 1 pip
# p1.getAxis('bottom').setTextPen('black')
board = ['123456',
'abcdef',
'ghilmn']
def draw_board(board2):
for j, row in enumerate(board2):
for i, cell in enumerate(row):
rect_w = 1
rect_h = 1
r = QGraphicsRectItem(i, -j+2, rect_w, rect_h)
r.setPen(pg.mkPen((0, 0, 0, 100)))
r.setBrush(pg.mkBrush((50, 50, 200)))
p1.addItem(r)
t_up = pg.TextItem(cell, (255, 255, 255), anchor=(0, 0))
t_up.setPos(i, -j+1+2)
p1.addItem(t_up)
draw_board(board)
if __name__ == '__main__':
if (sys.flags.interactive != 1) or not hasattr(QtCore, 'PYQT_VERSION'):
QApplication.instance().exec()
Scaling of a text item is quite difficult, as you need to consider a constant aspect ratio of the base scale, and the problems related to the way fonts are positioned and drawn relative to the origin point.
Assuming that the displayed text will always be a single character and that the characters used are standard ascii letters and numbers, the only possibility is to cycle through all possible characters, and create properly aligned paths for each of them.
So, for every character:
construct a QPainterPath;
add the letter to the path;
get the max() of that path width and the others;
get the minimum Y and maximum bottom of the bounding rectangle;
translate the path based on all other values computed above (in a separate loop);
Then, you have to set a reference size for the letter (using the maximum width above and the font metrics' height) and get the aspect ratio for that size.
The last part is implemented in the paint() function of the QGraphicsRectItem subclass, which is required to get the proper geometry of the item (if any transformation is applied to a parent item, the item will not know it), and get the maximum rectangle for the reference size based on the current rectangle size.
class NumberRectItem(QGraphicsRectItem):
textSize = None
textPaths = {}
textPath = None
def __init__(self, x, y, width, height, letter=''):
super().__init__(x, y, width, height)
if letter:
if not self.textPaths:
self._buildTextPaths()
self.textPath = self.textPaths[letter]
def _buildTextPaths(self):
from string import ascii_letters, digits
font = QApplication.font()
fm = QFontMetricsF(font)
maxWidth = 0
minY = 1000
maxY = 0
for l in ascii_letters + digits:
path = QPainterPath()
path.addText(0, 0, font, l)
br = path.boundingRect()
maxWidth = max(maxWidth, br.width())
minY = min(minY, br.y())
maxY = max(maxY, br.bottom())
self.textPaths[l] = path
self.__class__.textSize = QSizeF(maxWidth, fm.height())
self.__class__.textRatio = self.textSize.height() / self.textSize.width()
middle = minY + (maxY - minY) / 2
for path in self.textPaths.values():
path.translate(
-path.boundingRect().center().x(),
-middle)
def paint(self, qp, opt, widget=None):
super().paint(qp, opt, widget)
if not self.textPath:
return
qp.save()
qp.resetTransform()
view = widget.parent()
sceneRect = self.mapToScene(self.rect())
viewRect = view.mapFromScene(sceneRect).boundingRect()
rectSize = QSizeF(viewRect.size())
newSize = self.textSize.scaled(rectSize, Qt.KeepAspectRatio)
if newSize.width() == rectSize.width():
# width is the maximum
ratio = newSize.width() / self.textSize.width()
else:
ratio = newSize.height() / self.textSize.height()
transform = QTransform().scale(ratio, ratio)
path = transform.map(self.textPath)
qp.setRenderHint(qp.Antialiasing)
qp.setPen(Qt.NoPen)
qp.setBrush(Qt.white)
qp.drawPath(path.translated(viewRect.center()))
qp.restore()
def draw_board(board2):
for j, row in enumerate(board2):
for i, cell in enumerate(row):
rect_w = 1
rect_h = 1
r = NumberRectItem(i, -j+2, rect_w, rect_h, letter=cell)
r.setPen(pg.mkPen((150, 0, 0, 255)))
r.setBrush(pg.mkBrush((50, 50, 200, 128)))
p1.addItem(r)
Note: for PyQt6 you need to use the full enum names: Qt.GlobalColor.white, etc.
TLDR:
Need help trying to calculate overlap region between 2 graphs.
So I'm trying to stitch these 2 images:
Since I know that the images I will be stitching definitely come from the same image, I feel that I should be able to code this up myself. Using libraries like OpenCV feels a little like overkill for me for this task.
My current idea is that I can simplify this task by doing the following steps for each image:
Load image using PIL
Convert image to black and white (PIL image mode āLā)
[Optional: crop images to overlapping region by inspection by eye]
Create vector row_sum, which is a sum of each row
[Optional: log row_sum, to reduce the size of values we're working with]
Plot row_sum.
This would reduce the (potentially) (3*2)-dimensional problem, with 3 RGB channels for each pixel on the 2D image to a (1*2)-D problem with the black and white pixel for the 2D image instead. Then, summing across the rows reduces this to a 1D problem.
I used the following code to implement the above:
import matplotlib.pyplot as plt
import numpy as np
from PIL import Image
class Stitcher():
def combine_2(self, img1, img2):
# thr1, thr2 = self.get_cropped_bw(img1, 115, img2, 80)
thr1, thr2 = self.get_cropped_bw(img1, 0, img2, 0)
row_sum1 = np.log(thr1.sum(1))
row_sum2 = np.log(thr2.sum(1))
self.plot_4x4(thr1, thr2, row_sum1, row_sum2)
def get_cropped_bw(self, img1, img1_keep_from, img2, img2_keep_till):
im1 = Image.open(img1).convert("L")
im2 = Image.open(img2).convert("L")
data1 = (np.array(im1)[img1_keep_from:]
if img1_keep_from != 0 else np.array(im1))
data2 = (np.array(im2)[:img2_keep_till]
if img2_keep_till != 0 else np.array(im2))
return data1, data2
def plot_4x4(self, thr1, thr2, row_sum1, row_sum2):
fig, ax = plt.subplots(2, 2, sharey="row", constrained_layout=True)
ax[0, 0].imshow(thr1, cmap="Greys")
ax[0, 1].imshow(thr2, cmap="Greys")
ax[1, 0].plot(row_sum1, "k.")
ax[1, 1].plot(row_sum2, "r.")
ax[1, 0].set(
xlabel="Index Value",
ylabel="Row Sum",
)
plt.show()
imgs = (r"combine\imgs\test_image_part_1.jpg",
r"combine\imgs\test_image_part_2.jpg")
s = Stitcher()
s.combine_2(*imgs)
This gave me this graph:
(I've added in those yellow boxes, to indicate the overlap regions.)
This is the bit I'm stuck at. I want to find exactly:
the index value of the left-side of the yellow box for the 1st image and
the index value of the right-side of the yellow box for the 2nd image.
I define the overlap region as the longest range for which the end of the 1st graph 'matches' the start of the 2nd graph. For the method to find the overlap region, what should I do if the row sum values aren't exactly the same (what if one is the other scaled by some factor)?
I feel like this could be a problem that could use dot products to find the similarity between the 2 graphs? But I can't think of how to implement this.
I had a lot more fun with this than I expected. I wrote this using opencv, but that's just to load and show the image. Everything else is done with numpy so swapping this to PIL shouldn't be too difficult.
I'm using a brute-force matcher. I also wrote a random-start hillclimber that runs in much less time, but I can't guarantee it'll find the correct answer since the gradient space isn't smooth. I won't include it in my code since it's long and janky, but if you really need the time efficiency I can add it back in later.
I added a random crop and some salt and pepper noise to the images to test for robustness.
The brute-force matcher operates on the idea that we don't know which section of the two images overlap, so we need to convolve the smaller image over the larger image from left to right, top to bottom. This means our search space is:
horizontal = small_width + big_width
vertical = small_height + big_height
area = horizontal * vertical
This will grow very quickly with image size. I motivate the algorithm by giving it points for having a larger overlap, but it loses more points for having differences in color for the overlapped area.
Here are some pictures from an execution of this program
import cv2
import numpy as np
import random
# randomly snips edges
def randCrop(image, maxMargin):
c = [random.randint(0,maxMargin) for a in range(4)];
return image[c[0]:-c[1], c[2]:-c[3]];
# adds noise to image
def saltPepper(image, minNoise, maxNoise):
h,w = image.shape;
randNum = random.randint(minNoise, maxNoise);
for a in range(randNum):
x = random.randint(0, w-1);
y = random.randint(0, h-1);
image[y,x] = random.randint(0, 255);
return image;
# evaluate layout
def getScore(one, two):
# do raw subtraction
left = one - two;
right = two - one;
sub = np.minimum(left, right);
return np.count_nonzero(sub);
# return 2d random position within range
def randPos(img, big_shape):
th,tw = big_shape;
h,w = img.shape;
x = random.randint(0, tw - w);
y = random.randint(0, th - h);
return [x,y];
# overlays small image onto big image
def overlay(small, big, pos):
# unpack
h,w = small.shape;
x,y = pos;
# copy and place
copy = big.copy();
copy[y:y+h, x:x+w] = small;
return copy;
# calculates overlap region
def overlap(one, two, pos_one, pos_two):
# unpack
h1,w1 = one.shape;
h2,w2 = two.shape;
x1,y1 = pos_one;
x2,y2 = pos_two;
# set edges
l1 = x1;
l2 = x2;
r1 = x1 + w1;
r2 = x2 + w2;
t1 = y1;
t2 = y2;
b1 = y1 + h1;
b2 = y2 + h2;
# go
left = max(l1, l2);
right = min(r1, r2);
top = max(t1, t2);
bottom = min(b1, b2);
return [left, right, top, bottom];
# wrapper for overlay + getScore
def fullScore(one, two, pos_one, pos_two, big_empty):
# check positions
x,y = pos_two;
h,w = two.shape;
th,tw = big_empty.shape;
if y+h > th or x+w > tw or x < 0 or y < 0:
return -99999999;
# overlay
temp_one = overlay(one, big_empty, pos_one);
temp_two = overlay(two, big_empty, pos_two);
# get overlap
l,r,t,b = overlap(one, two, pos_one, pos_two);
temp_one = temp_one[t:b, l:r];
temp_two = temp_two[t:b, l:r];
# score
diff = getScore(temp_one, temp_two);
score = (r-l) * (b-t);
score -= diff*2;
return score;
# do brute force
def bruteForce(one, two):
# calculate search space
# unpack size
h,w = one.shape;
one_size = h*w;
h,w = two.shape;
two_size = h*w;
# small and big
if one_size < two_size:
small = one;
big = two;
else:
small = two;
big = one;
# unpack size
sh, sw = small.shape;
bh, bw = big.shape;
total_width = bw + sw * 2;
total_height = bh + sh * 2;
# set up empty images
empty = np.zeros((total_height, total_width), np.uint8);
# set global best
best_score = -999999;
best_pos = None;
# start scrolling
ybound = total_height - sh;
xbound = total_width - sw;
for y in range(ybound):
print("y: " + str(y) + " || " + str(empty.shape));
for x in range(xbound):
# get score
score = fullScore(big, small, [sw,sh], [x,y], empty);
# show
# prog = overlay(big, empty, [sw,sh]);
# prog = overlay(small, prog, [x,y]);
# cv2.imshow("prog", prog);
# cv2.waitKey(1);
# compare
if score > best_score:
best_score = score;
best_pos = [x,y];
print("best_score: " + str(best_score));
return best_pos, [sw,sh], small, big, empty;
# do a step of hill climber
def hillStep(one, two, best_pos, big_empty, step):
# make a step
new_pos = best_pos[1][:];
new_pos[0] += step[0];
new_pos[1] += step[1];
# get score
return fullScore(one, two, best_pos[0], new_pos, big_empty), new_pos;
# hunt around for good position
# let's do a random-start hillclimber
def randHill(one, two, shape):
# set up empty images
big_empty = np.zeros(shape, np.uint8);
# set global best
g_best_score = -999999;
g_best_pos = None;
# lets do 200 iterations
iters = 200;
for a in range(iters):
# progress check
print(str(a) + " of " + str(iters));
# start with random position
h,w = two.shape[:2];
pos_one = [w,h];
pos_two = randPos(two, shape);
# get score
best_score = fullScore(one, two, pos_one, pos_two, big_empty);
best_pos = [pos_one, pos_two];
# hill climb (only on second image)
while True:
# end condition: no step improves score
end_flag = True;
# 8-way
for y in range(-1, 1+1):
for x in range(-1, 1+1):
if x != 0 or y != 0:
# get score and update
score, new_pos = hillStep(one, two, best_pos, big_empty, [x,y]);
if score > best_score:
best_score = score;
best_pos[1] = new_pos[:];
end_flag = False;
# end
if end_flag:
break;
else:
# show
# prog = overlay(one, big_empty, best_pos[0]);
# prog = overlay(two, prog, best_pos[1]);
# cv2.imshow("prog", prog);
# cv2.waitKey(1);
pass;
# check for new global best
if best_score > g_best_score:
g_best_score = best_score;
g_best_pos = best_pos[:];
print("top score: " + str(g_best_score));
return g_best_score, g_best_pos;
# load both images
top = cv2.imread("top.jpg");
bottom = cv2.imread("bottom.jpg");
top = cv2.cvtColor(top, cv2.COLOR_BGR2GRAY);
bottom = cv2.cvtColor(bottom, cv2.COLOR_BGR2GRAY);
# randomly crop
top = randCrop(top, 20);
bottom = randCrop(bottom, 20);
# randomly add noise
saltPepper(top, 200, 1000);
saltPepper(bottom, 200, 1000);
# set up max image (assume no overlap whatsoever)
tw = 0;
th = 0;
h, w = top.shape;
tw += w;
th += h;
h, w = bottom.shape;
tw += w*2;
th += h*2;
# do random-start hill climb
_, best_pos = randHill(top, bottom, (th, tw));
# show
empty = np.zeros((th, tw), np.uint8);
pos1, pos2 = best_pos;
image = overlay(top, empty, pos1);
image = overlay(bottom, image, pos2);
# do brute force
# small_pos, big_pos, small, big, empty = bruteForce(top, bottom);
# image = overlay(big, empty, big_pos);
# image = overlay(small, image, small_pos);
# recolor overlap
h,w = empty.shape;
color = np.zeros((h,w,3), np.uint8);
l,r,t,b = overlap(top, bottom, pos1, pos2);
color[:,:,0] = image;
color[:,:,1] = image;
color[:,:,2] = image;
color[t:b, l:r, 0] += 100;
# show images
cv2.imshow("top", top);
cv2.imshow("bottom", bottom);
cv2.imshow("overlayed", image);
cv2.imshow("Color", color);
cv2.waitKey(0);
Edit: I added in the random-start hillclimber
I have an oriented cylinder generated with vtkCylinderSource and some transformations are applied on it to get the orientation that i want. Here is the code for creating this oriented-cylinder:
def cylinder_object(startPoint, endPoint, radius, my_color="DarkRed", opacity=1):
colors = vtk.vtkNamedColors()
# Create a cylinder.
# Cylinder height vector is (0,1,0).
# Cylinder center is in the middle of the cylinder
cylinderSource = vtk.vtkCylinderSource()
cylinderSource.SetRadius(radius)
cylinderSource.SetResolution(50)
# Generate a random start and end point
# startPoint = [0] * 3
# endPoint = [0] * 3
rng = vtk.vtkMinimalStandardRandomSequence()
rng.SetSeed(8775070) # For testing.8775070
# Compute a basis
normalizedX = [0] * 3
normalizedY = [0] * 3
normalizedZ = [0] * 3
# The X axis is a vector from start to end
vtk.vtkMath.Subtract(endPoint, startPoint, normalizedX)
length = vtk.vtkMath.Norm(normalizedX)
vtk.vtkMath.Normalize(normalizedX)
# The Z axis is an arbitrary vector cross X
arbitrary = [0] * 3
for i in range(0, 3):
rng.Next()
arbitrary[i] = rng.GetRangeValue(-10, 10)
vtk.vtkMath.Cross(normalizedX, arbitrary, normalizedZ)
vtk.vtkMath.Normalize(normalizedZ)
# The Y axis is Z cross X
vtk.vtkMath.Cross(normalizedZ, normalizedX, normalizedY)
matrix = vtk.vtkMatrix4x4()
# Create the direction cosine matrix
matrix.Identity()
for i in range(0, 3):
matrix.SetElement(i, 0, normalizedX[i])
matrix.SetElement(i, 1, normalizedY[i])
matrix.SetElement(i, 2, normalizedZ[i])
# Apply the transforms
transform = vtk.vtkTransform()
transform.Translate(startPoint) # translate to starting point
transform.Concatenate(matrix) # apply direction cosines
transform.RotateZ(-90.0) # align cylinder to x axis
transform.Scale(1.0, length, 1.0) # scale along the height vector
transform.Translate(0, .5, 0) # translate to start of cylinder
# Transform the polydata
transformPD = vtk.vtkTransformPolyDataFilter()
transformPD.SetTransform(transform)
transformPD.SetInputConnection(cylinderSource.GetOutputPort())
cylinderSource.Update()
# Create a mapper and actor for the arrow
mapper = vtk.vtkPolyDataMapper()
actor = vtk.vtkActor()
if USER_MATRIX:
mapper.SetInputConnection(cylinderSource.GetOutputPort())
actor.SetUserMatrix(transform.GetMatrix())
else:
mapper.SetInputConnection(transformPD.GetOutputPort())
actor.SetMapper(mapper)
actor.GetProperty().SetColor(colors.GetColor3d(my_color))
actor.GetProperty().SetOpacity(opacity)
return actor, transformPD
Now i want to ray cast a line with this oriented cylinder. unfortunately, using the vtkCylinderSource as the dataset for vtkOBBTree produces the wrong points as the result. how can i use ray-casting with a PolyDataFilter?
I came up with a solution where i export my oriented-cylinder to a .stl file and then read it again to implement the ray-casting algorithm using IntersectWithLine. The problem is i have thousands of these oriented-cylinders and this method (exporting and reading) makes my code extremely slow.
def ray_cast(filename, p_source, p_target):
'''
:param filename: STL file to perform ray casting on.
:param p_source: first point
:param p_target: second point
:return: code --> 0 : No intersection.
:return: code --> +1 : p_source lies OUTSIDE the closed surface.
:return; code --> -1 : p_source lies INSIDE closed surface
'''
reader = vtk.vtkSTLReader()
reader.SetFileName(filename)
reader.Update()
mesh = reader.GetOutput()
obbtree = vtk.vtkOBBTree()
obbtree.SetDataSet(mesh)
obbtree.BuildLocator()
pointsVTKIntersection = vtk.vtkPoints()
code = obbtree.IntersectWithLine(p_source, p_target, pointsVTKIntersection, None)
# Extracting data
pointsVTKIntersectionData = pointsVTKIntersection.GetData()
noPointsVTKIntersection = pointsVTKIntersectionData.GetNumberOfTuples()
pointsIntersection = []
for idx in range(noPointsVTKIntersection):
_tup = pointsVTKIntersectionData.GetTuple3(idx)
pointsIntersection.append(_tup)
return code, pointsIntersection, noPointsVTKIntersection
Below image shows the desired result using export-stl method. (the green spheres are intersection points)
I would appreciate any suggestion and help..
With vedo:
from vedo import *
cyl = Cylinder() # vtkActor
cyl.alpha(0.5).pos(3,3,3).orientation([2,1,1])
p1, p2 = (0,0,0), (4,4,5)
ipts_coords = cyl.intersectWithLine(p1, p2)
print('hit coords are', ipts_coords)
pts = Points(ipts_coords, r=10).color("yellow")
# print(pts.polydata()) # is the vtkPolyData object
origin = Point()
ln = Line(p1,p2)
show(origin, cyl, ln, pts, axes=True)
TL;DR: Is there anyway I can get rid of my second for-loop?
I have a time series of points on a 2D-grid. To get rid of fast fluctuations of their position, I average the coordinates over a window of frames. Now in my case, it's possible for the points to cover a larger distance than usual. I don't want to include frames for a specific point, if it travels farther than the cut_off value.
In the first for-loop, I go over all frames and define the moving window. I then calculate the distances between the current frame and each frame in the moving window. After I grab only those positions from all frames, where both the x and y component did not travel farther than cut_off. Now I want to calculate the mean positions for every point from all these selected frames of the moving window (note: the number of selected frames can be smaller than n_window). This leads me to the second for-loop. Here I iterate over all points and actually grab the positions from the frames, in which the current point did not travel farther than cut_off. From these selected frames I calculate the mean value of the coordinates and use it as the new value for the current frame.
This very last for-loop slows down the whole processing. I can't come up with a better way to accomplish this calculation. Any suggestions?
MWE
Put in comments for clarification.
import numpy as np
# Generate a timeseries with 1000 frames, each
# containing 50 individual points defined by their
# x and y coordinates
n_frames = 1000
n_points = 50
n_coordinates = 2
timeseries = np.random.randint(-100, 100, [n_frames, n_points, n_coordinates])
# Set window size to 20 frames
n_window = 20
# Distance cut off
cut_off = 60
# Set up empty array to hold results
avg_data_store = np.zeros([n_frames, timeseries.shape[1], 2])
# Iterate over all frames
for frame in np.arange(0, n_frames):
# Set the frame according to the window size that we're looking at
t_before = int(frame - (n_window / 2))
t_after = int(frame + (n_window / 2))
# If we're trying to access frames below 0, set the lowest one to 0
if t_before < 0:
t_before = 0
# Trying to access frames that are not in the trajectory, set to last frame
if t_after > n_frames - 1:
t_after = n_frames - 1
# Grab x and y coordinates for all points in the corresponding window
pos_before = timeseries[t_before:frame]
pos_after = timeseries[frame + 1:t_after + 1]
pos_now = timeseries[frame]
# Calculate the distance between the current frame and the windows before/after
d_before = np.abs(pos_before - pos_now)
d_after = np.abs(pos_after - pos_now)
# Grab indices of frames+points, that are below the cut off
arg_before = np.argwhere(np.all(d_before < cut_off, axis=2))
arg_after = np.argwhere(np.all(d_after < cut_off, axis=2))
# Iterate over all points
for i in range(0, timeseries.shape[1]):
# Create temp array
temp_stack = pos_now[i]
# Grab all frames in which the current point did _not_
# travel farther than `cut_off`
all_before = arg_before[arg_before[:, 1] == i][:, 0]
all_after = arg_after[arg_after[:, 1] == i][:, 0]
# Grab the corresponding positions for this points in these frames
all_pos_before = pos_before[all_before, i]
all_pos_after = pos_after[all_after, i]
# If we have any frames for that point before / after
# stack them into the temp array
if all_pos_before.size > 0:
temp_stack = np.vstack([all_pos_before, temp_stack])
if all_pos_after.size > 0:
temp_stack = np.vstack([temp_stack, all_pos_after])
# Calculate the moving window average for the selection of frames
avg_data_store[frame, i] = temp_stack.mean(axis=0)
If you are fine with calculating the cutoff distance in x and y separately, you can use scipy.ndimage.generic_filter.
import numpy as np
from scipy.ndimage import generic_filter
def _mean(x, cutoff):
is_too_different = np.abs(x - x[len(x) / 2]) > cutoff
return np.mean(x[~is_too_different])
def _smooth(x, window_length=5, cutoff=1.):
return generic_filter(x, _mean, size=window_length, mode='nearest', extra_keywords=dict(cutoff=cutoff))
def smooth(arr, window_length=5, cutoff=1., axis=-1):
return np.apply_along_axis(_smooth, axis, arr, window_length=window_length, cutoff=cutoff)
# --------------------------------------------------------------------------------
def _simulate_movement_2d(T, fraction_is_jump=0.01):
# generate random velocities with a few "jumps"
velocity = np.random.randn(T, 2)
is_jump = np.random.rand(T) < fraction_is_jump
jump = 10 * np.random.randn(T, 2)
jump[~is_jump] = 0.
# pre-allocate position and momentum arrays
position = np.zeros((T,2))
momentum = np.zeros((T,2))
# initialise the first position
position[0] = np.random.randn(2)
# update position using velocity vector:
# smooth movement by not applying the velocity directly
# but rather by keeping track of the momentum
for ii in range(2,T):
momentum[ii] = 0.9 * momentum[ii-1] + 0.1 * velocity[ii-1]
position[ii] = position[ii-1] + momentum[ii] + jump[ii]
# add some measurement noise
noise = np.random.randn(T,2)
position += noise
return position
def demo(nframes=1000, npoints=3):
# create data
positions = np.array([_simulate_movement_2d(nframes) for ii in range(npoints)])
# format to (nframes, npoints, 2)
position = positions.transpose([1, 0, 2])
# smooth
smoothed = smooth(positions, window_length=11, cutoff=5., axis=1)
# plot
x, y = positions.T
xs, ys = smoothed.T
import matplotlib.pyplot as plt
fig, ax = plt.subplots(1,1)
ax.plot(x, y, 'o')
ax.plot(xs, ys, 'k-', alpha=0.3, lw=2)
plt.show()
demo()