I am making a langton's ant cellular automata program, and I want the user to be able to pan and zoom. Right now, I have all my rectangles (grid squares) stored as a dictionary, and to move/zoom, I iterate through all of them and apply the transformation needed.
def zoom(self, factor, center_x, center_y):
for x in range(WIDTH):
for y in range(HEIGHT):
rect = self.rects[x][y]
self.rects[x][y].x = (rect.x - center_x)*factor + center_x
self.rects[x][y].y = (rect.y - center_y)*factor + center_y
self.rects[x][y].width = rect.width * factor
self.rects[x][y].height = rect.height * factor
However, with the amount of rectangles (32,000), it takes a second or to do pan and zoom. Is there any better way of doing it than this? Thanks!
Here is the full code
Yes. Use OpenGL transformation matrices to apply transformations. These will be calculated on the GPU for performance gain.
pyglet.graphics.Group lets you group together such transformations in order to apply them automatically to Pyglet primitives when drawing them.
Example
We create a CameraGroup that pans and zooms objects into view.
import pyglet.gl as gl
import pyglet.shapes
class CameraGroup(Group):
def __init__(self, window, *args, **kwargs):
super().__init__(*args, **kwargs)
self.win = window
def set_state(self):
gl.glPushMatrix()
x = -(self.win.x - self.win.width // 2)
y = -(self.win.y - self.win.height // 2)
gl.glTranslatef(x, y, 0.0)
gl.glScalef(self.win.factor, self.win.factor, 1.0)
def unset_state(self):
gl.glPopMatrix()
The example assumes you have the properties center_x, center_y and factor on your window.
Apply the group by attaching it to Pyglet objects.
cam_group = CameraGroup(main_win)
rect = pyglet.shapes.Rectangle(250, 300, 400, 200, color=(255, 22, 20), batch=batch, group=cam_group)
When rect gets rendered the group transformations area applied automatically.
You can also construct more complex groups if needed.
There is a camera example for pyglet in the examples folder.
https://github.com/pyglet/pyglet/blob/pyglet-1.5-maintenance/examples/camera.py
Related
I am trying to fill the image area outside of a custom curved shape in Pycairo, however am struggling to achieve this. I have managed to get the result I require by stroking the shape with a large thickness and drawing multiple shapes of increasing size on top of each other, however this solution is inefficient (I care about efficiency as I will be needing to draw 1200 shapes quickly, which currently takes 1 minute). I think there might be a way to use a mask or clip or something similar, but can't find anything online that helps. If there is a way to specify that the stroke is drawn only outside the path, not on both sides, that could also be a solution.
Anyone out there no of a better way to achieve this?
Here's the code I use to draw a curved shape, the calculate_curve_handles function just returns two curve handles between the two sides of the shape based on the curve_point_1 and 2 offsets. The polygon function returns the vertex locations for an N sided polygon
vertices = polygon(num_sides, shape_radius + (scale * (line_thickness-20)), rotation, [x + offset[0], y + offset[1]])
for i in range(len(vertices)):
start_point = [vertices[i][0], vertices[i][1]]
cr.move_to(start_point[0], start_point[1])
if i == len(vertices)-1:
end_point = [vertices[0][0], vertices[0][1]]
else:
end_point = [vertices[i+1][0], vertices[i+1][1]]
point_1, point_2 = calculate_curve_handles(start_point, end_point, curve_point_1_offset, curve_point_2_offset)
cr.curve_to(point_1[0], point_1[1], point_2[0], point_2[1], end_point[0], end_point[1])
cr.set_line_cap(cairo.LINE_CAP_ROUND)
cr.fill()
This is the desired result, achieved with many stroked objects layered on top of each other:
This is what I get when I try to use cr.fill() on the curved path:
Ok, I just figured out that if I move the move_to() function outside of the for loop for the vertices, it draws the shape properly.
Then by setting the fill rule to cr.set_fill_rule(cairo.FILL_RULE_EVEN_ODD) and drawing a large rectangle behind the shape, I can get the desired effect int even less time.
cr.move_to(vertices[0][0], vertices[0][1])
for i in range(0, len(vertices)):
start_point = [vertices[i][0], vertices[i][1]]
if i == len(vertices)-1:
end_point = [vertices[0][0], vertices[0][1]]
else:
end_point = [vertices[i+1][0], vertices[i+1][1]]
point_1, point_2 = calculate_curve_handles(start_point, end_point, curve_point_1_offset, curve_point_2_offset)
cr.curve_to(point_1[0], point_1[1], point_2[0], point_2[1], end_point[0], end_point[1])
I found a solution that works for now. Basically for every side of the shape, I find a point that extends from the vector between the centre of the object and the vertex, well outside the drawing area. Then I fill each line segment as a separate shape
def calculate_bounds(start_point, end_point, centre_point):
direction = np.subtract(start_point, centre_point)
normalised_dir = direction / np.sqrt(np.sum(direction ** 2))
bound_1 = start_point + normalised_dir * 5000
direction = np.subtract(end_point, centre_point)
normalised_dir = direction / np.sqrt(np.sum(direction ** 2))
bound_2 = end_point + normalised_dir * 5000
return bound_1, bound_2
Then the code for drawing the polygon is:
for i in range(0, len(vertices)):
start_point = [vertices[i][0], vertices[i][1]]
cr.move_to(start_point[0], start_point[1])
if i == len(vertices)-1:
end_point = [vertices[0][0], vertices[0][1]]
else:
end_point = [vertices[i+1][0], vertices[i+1][1]]
point_1, point_2 = calculate_curve_handles(start_point, end_point, curve_point_1_offset, curve_point_2_offset)
cr.curve_to(point_1[0], point_1[1], point_2[0], point_2[1], end_point[0], end_point[1])
bound_1, bound_2 = calculate_bounds(start_point, end_point, [x + offset[0], y + offset[1]])
cr.line_to(bound_2[0], bound_2[1])
cr.line_to(bound_1[0], bound_1[1])
cr.fill_preserve()
cr.stroke()
I am trying to draw this simple tree fractal without success...
I have tried many combinations to get the recursive working propperly but never seemed to succeed to get the shape I wanted.
Here is the code I got at the end.
from PyQt5 import QtGui, QtWidgets, QtCore, Qt
import sys
class Arena(QtWidgets.QWidget):
def __init__(self):
super(Arena, self).__init__()
self.angle = 45
self.transform = QtGui.QTransform()
self.translate2 = 0
self.recursions = 10
self.setGeometry(2500, 400, 500, 500)
self.origin = (self.width()/2, self.height())
self.pal = QtGui.QPalette()
self.pal.setColor(QtGui.QPalette.Background, QtGui.QColor(0, 0, 0))
self.setPalette(self.pal)
self.pen_branch = QtGui.QPen()
self.pen_branch.setColor(QtGui.QColor(255, 255, 255))
initializes class
self.init_UI()
def init_UI(self):
self.slider_angle = QtWidgets.QSlider(QtCore.Qt.Horizontal, self)
self.slider_angle.setMinimum(-8000)
self.slider_angle.setMaximum(8000)
self.slider_angle.setGeometry(0, 50, self.width(), 50)
self.slider_angle.valueChanged.connect(
lambda value, x=0 : self.setAngleValue(value))
self.setAngleValue(4500)
self.slider_angle.setValue(4500)
builds the UI
def setAngleValue(self, value):
self.angle = value/100
self.update()
sets the angle
def branch(self, p, len):
p.drawLine(0, 0, 0, -len)
p.translate(0,-len)
if len > 3:
len *= 0.66
self.transform = p.transform()
p.rotate(self.angle)
self.branch(p, len)
p.setTransform(self.transform)
self.transform = p.transform()
p.rotate(-self.angle)
p.translate(0, len)
self.branch(p, len)
p.setTransform(self.transform)
Here is where the magic of the fractal should happen: the recursive method (AFAIK ...)
I use 'p.transform' to grab the transformation matrix and restore it after I create the line.
def paintEvent(self, e):
p = QtGui.QPainter(self)
p.setRenderHint(QtGui.QPainter.Antialiasing)
p.setPen(self.pen_branch)
p.drawText(20, 20, 'angle: ' + str(self.angle))
p.drawText(100, 20, 'trans2: ' + str(self.translate2))
# Trunk
p.translate(self.width()/2, self.height())
p.drawLine(0, 0, 0, -100)
self.branch(p, 100)
The paint event that paints the lines on the canvas.
app = QtWidgets.QApplication(sys.argv)
Arena = Arena()
Arena.show()
app.exec_()
The application runner.
After some fiddling I have reached a close result to what I am looking for but still not the goal yet. I'd like to create a perfectly symetrical tree, but this is what I got:
The new code is like this:
def branch(self, p, x, r, len):
p.drawLine(0, 0, 0, -len)
if len > 1 :
p.translate(0, -len)
p.rotate(r)
self.branch(p, 10, self.angle, len * 0.66)
p.rotate(-r)
self.branch(p, 10, -self.angle, len * 0.66)
p.translate(0, len)
p.rotate(-r)
def paintEvent(self, e):
p = QtGui.QPainter(self)
p.setRenderHint(QtGui.QPainter.Antialiasing)
p.setPen(self.pen_branch)
p.drawText(20, 20, 'angle: ' + str(self.angle))
p.drawText(100, 20, 'trans2: ' + str(self.translate2))
p.translate (self.width()/2, self.height())
self.branch(p, 10, self.angle, 200)
The problem resides in how the branch function is implemented: after you call rotate the first time, the other branch needs to use a doubled angle in order to go in the opposite direction.
Consider this: assuming the angle is 30°, if you call rotate(r) for the right branch, then you have to rotate(-r * 2) for the left one; in this way the angle is "reset" to the original value and then rotated on the opposite side.
This is the modified version of your function:
def branch(self, p, x, r, length):
p.drawLine(0, 0, 0, -length)
if length > 1 :
p.translate(0, -length)
p.rotate(r)
self.branch(p, 10, r, length * 0.66)
# rotate to the opposite side
p.rotate(-r * 2)
self.branch(p, 10, -r, length * 0.66)
# note that translation and rotation are inverted, opposed to your
# example, this is because you translated first and *then* applied
# rotation, so you should "reset" those states in the opposite way
p.rotate(r)
p.translate(0, length)
To avoid this kind of problems, it's usually better to use the save() and restore() functions, which allow you to use multiple levels of painter states: you save a state, apply all modifications you want (pen, brush and any kind of transformation), then you can restore the previous state automatically; this makes development easier, and get a much more readable and understandable code; just remember that the states must always be restored up to the original saved "level".
Here is how a better branch function could look:
def branch(self, p, x, r, length):
p.drawLine(0, 0, 0, -length)
if length > 3:
# save state, first level (for the current function)
p.save()
p.translate(0, -length)
# save state for the right branch
p.save()
p.rotate(r)
self.branch(p, 10, r, length * .66)
# restore to the previous "first" level
p.restore()
# again, for the left branch
p.save()
p.rotate(-r)
self.branch(p, 10, -r, length * .66)
# restore again
p.restore()
# restore the previous state
p.restore()
And here's the result:
Some suggestions:
use descriptive variables names (p, x and r are not very meaningful);
never use built-ins for variable names (in your case, len);
the lambda connection for valueChanged seems useless; what's the use of the x variable? Please try to keep your examples as minimal as possible, avoiding unnecessary variables or functions that just create confusion to the reader;
use separated widgets for each task; create a main container (which might be the "top level window"), set a layout manager, then add the slider and the custom widget to it; avoid setting fixed geometries as much as possible: the UI should be able to adjust itself to the window size ("responsive", as we call it nowadays);
As shown in the documentation of Open3D, you can use the get_view_control.rotate() function to rotate the object inside the viewer. But it does not specify the type (degree, radian etc.). If I use a value of around 2100 it looks like a full turn, but after putting those in a loop, it turns out this is not the exact value for turning 360 degrees. Also I don't see it mentioned anywhere in the documentation of Open3D.
I want to capture depth images at different angles for a full 360 degree (x,y,z). This is a piece of my code:
class Viewer:
def __init__(self, on, of, fd): #objectname, objectFile and folderdirectory
self.index = 0
self.objectName = on
self.objectFile = of
self.folderDirectory = fd
self.vis = o3d.visualization.Visualizer()
self.view = o3d.visualization.ViewControl()
self.pcd = o3d.io.read_triangle_mesh(self.folderDirectory + self.objectFile)
def depthFullCapture(self, times):
self.numberOfTimes = times
def captureDepth(vis):
print('Capturing')
self.depth = vis.capture_depth_float_buffer(False)
plt.imsave((self.folderDirectory + 'images/' + self.objectName + '_{:05d}.png'.format(self.index)),np.asarray(self.depth), dpi = 1)
np.savetxt((self.folderDirectory + 'text/' + self.objectName + '_{:05d}.txt'.format(self.index)),self.depth,fmt='%.2f',delimiter=',')
vis.register_animation_callback(rotate)
def rotate(vis):
print('Rotating')
ctr = vis.get_view_control()
if(self.index % 25 == 0):
self.vis.reset_view_point(True)
ctr.rotate(0,((2100/25)*(self.index/25)))
else:
ctr.rotate(84, 0)
ctr.set_zoom(0.75)
self.index += 1
if not (self.index == 625):
vis.register_animation_callback(captureDepth)
else:
vis.register_animation_callback(None)
vis.destroy_window()
self.vis.create_window(width = 200, height = 200)
self.vis.add_geometry(self.pcd)
self.vis.register_animation_callback(captureDepth)
self.vis.run()
So can anyone explain the correct value/type for turning a certain degrees? Or is there another/better way to do this? Thanks in advance! If anything is not clear, please ask :)
The actual answer can be found in the C documentation:
const double open3d::visualization::ViewControl::ROTATION_RADIAN_PER_PIXEL = 0.003
the rotation units are pixels:
x and y are the distances the mouse cursor has moved. xo and yo are the original point coordinate the mouse cursor started to move from. Coordinates are measured in screen coordinates relative to the top-left corner of the window client area.
You were very close.
0.003 [radian/pixel] * (180/pi) [degrees/radian] = 0.1719 [degrees/pixel]
OR
5.8178 [pixels/degree]
Taking
360 [degrees/rotation] * 5.8178 [pixels/degree] = 2094.3951 [pixels/rotation]
As I know from the example in Open3D Docs (see also this link), get_view_control.rotate() takes 4 arguments: x, y, xo, and yo, all of them float values in degrees.
Surely this answer comes too late and can be expanded, maybe you can tell us what you learnt!
I need to rotate an image around its x-axis (or y-axis). I can easily create such an animation with avisynth, but now I need to implement that effect with Python's moviepy module. I can easily rotate an image with the following script but need some clues how to rotate it in 2D or 3D.
from moviepy.editor import *
clip = ImageClip('my_image.jpg')
rotated_clip = (clip.add_mask()
.fx(vfx.resize, width=300, height=300)
.fx(vfx.rotate, lambda t: 90*t, expand=False)
.set_duration(5))
final_clip = CompositeVideoClip([rotated_clip.set_pos("center")], size=(800,800), bg_color=3*[255])
final_clip.write_videofile("test.mp4", fps=25, codec="libx264")
Here is the avisynth script that actually generated that example image. Please note, it does require "QUAD" plugin.
function stars(clip c, int r) {
c.Overlay(x=rand(c.width),y=rand(c.height),BlankClip(c,width=1,height=1,color=$030301*rand(85)))
(r==0)? last : stars(r-1)
Trim(0,-1).Loop(c.Framecount, 0, 0)
}
width= 800
height=600
length=100000
Tcolor=$000040
Bcolor=$000018
StackVertical(BlankClip(length=length,width=2,height=1,color=TColor,pixel_type="RGB32"),BlankClip(length=length,width=2,height=1,color=BColor)).BilinearResize(width,2*height,src_top=0,src_height=2).Crop(0,height/2,0,-height/2).Stars(width*height/3072)
ImageSource("path_to_image.png", start=0, end=total_time, fps=300, pixel_type="RGB32")
#BlankClip(length=length,FPS=25,width=640,height=480,color=$000018,pixel_type="RGB32")
#ColorBars()
HALFCYCLE=10 # Frames in 1 HALF rotation (spinning clip)
NSPIN = 1 # Number of HALF rotations in spinning clip
NSTILL = 10 # Frames in STILL clip
V = 0.2 # Tilt/Yaw
tim = PI / HALFCYCLE
ScriptClip("""
c=last
t=tim*current_frame
t1x= 0.5 - 0.5 * cos(t) # BOTH Left
t2x= 0.5 + 0.5 * cos(t) # BOTH Right
#
t1y= 0.0 + V * sin(t) # ] both Top's opposite sign
t2y= 0.0 - V * sin(t) # ]
t3y= 1.0 + V * sin(t) # [ both Bottoms opposite sign
t4y= 1.0 - V * sin(t) # [
ResetMask
quad(t1x,t1y, t2x,t2y, t2x,t3y, t1x,t4y, normal=true)
#Overlay(c,last,mask=last.ShowAlpha())
""")
SPIN=Trim(0,-(NSPIN*HALFCYCLE +1)) # Spinning clip, + 1 to complete last spin
STILL=SPIN.Trim(SPIN.FrameCount-1,-1).Loop(NSTILL,0,0)
SPIN2=Trim((NSPIN%2 ==0)?0:HALFCYCLE,-(NSPIN*HALFCYCLE +1))
SPIN ++ STILL ++ SPIN2
Return Last
One way to do this is to use Vapory, another library by the author of MoviePy, which facilitates the operation of POV-Ray via Python. You can create a rectangle within a 3D scene and rotate it around whatever axis you like, saving frames into a MoviePy clip at intervals.
MoviePy + Vapory code
from moviepy.editor import concatenate, ImageClip, VideoClip
from vapory import *
img_path = './baseball.png'
img_clip = ImageClip(img_path)
W, H = img_clip.w, img_clip.h
AR = 1.0*W/H
# Set rotation rate by defining the period (in seconds) for 360 deg. revolution
t_rev = 2.0
t_half = t_rev/2.0 # The time required for a half revolution
t_still = 0.8 # How long (in seconds) to hold the half rotated image still
# Static POV-Ray objects
cam = Camera('location', [ 0, 0, -1],
'look_at', [ 0, 0, 0])
light = LightSource([0, 0, -1]) # Light at camera location
bg = Background('color', [0, 0, 0]) # Black background
def scene(t):
""" Returns the scene at time 't' (in seconds) """
s = Scene(camera = cam, objects = [light, bg])
# Add POV-Ray box with image textured on it
s = s.add_objects([
Box([0, 0, 0],
[W, H, 0],
Texture(Pigment(ImageMap('"{}"'.format(img_path), 'once')),
Finish('ambient', 1.0)),
'translate', [-0.5, -0.5, 0],
'scale', [AR, 1, 0],
'rotate', [0, (360/t_rev)*t, 0])]) # Can change axis of rotation here
return s
def make_frame(t):
return scene(t).render(width=W, height=H, antialiasing=0.1)
still_1 = VideoClip(make_frame).to_ImageClip(t=0).set_duration(t_still)
half_1 = VideoClip(make_frame).subclip(0, t_half)
still_2 = VideoClip(make_frame).to_ImageClip(t=t_half).set_duration(t_still)
half_2 = VideoClip(make_frame).subclip(t_half, t_rev)
final_clip = concatenate([still_1, half_1, still_2, half_2])
final_clip.write_gif("./baseball_rot.gif", fps=15)
Output GIF
Other thoughts:
The main things you might want to change are img_path, t_rev (the time for a full 360 degree revolution), t_still, and the output frame rate.
I removed one column of pixels from your example image to get it down to an even width (150 px). It's not important if you only want to make GIFs, but if you want to produce an x264-encoded MP4, you should probably use mod2 dimensions.
It seems like overkill to use a ray tracer for this problem, but it's the first working solution I came up with. I wanted to represent the image as a 2D rectangle in a 3D scene, where I could simply specify an angle of rotation and the 3D library would handle the rest.
It should be possible to solve this problem using a projective transform from scikit-image, as in this MoviePy example. Note specifically, the trapzWarp function near the middle of that code listing.
pdb.gimp_paintbrush_default seems to be very slow (several seconds, for 500 dots using a standard brush. Lines are worse, obviously). Is this the way it is? Is there a way to speed things up when drawing straight lines using the user selected brush?
pythonfu console code:
from random import randint
img=gimp.image_list()[0]
drw = pdb.gimp_image_active_drawable(img)
width = pdb.gimp_image_width(img)
height = pdb.gimp_image_height(img)
point_number = 500
while (point_number > 0):
x = randint(0, width)
y = randint(0, height)
pdb.gimp_paintbrush_default(drw,2,[x,y])
point_number -= 1
I've been working on something very similar and ran into this problem also. Here's one technique that I found that made my function about 5 times faster:
Create a temporary image
Copy the layer you are working with to the temporary image
Do the drawing on the temporary layer
Copy the temporary layer on top of the original layer
I believe this speeds stuff up because GIMP doesn't have to draw the edits to the screen, but I'm not 100% sure. Here's my function:
def splotches(img, layer, size, variability, quantity):
gimp.context_push()
img.undo_group_start()
width = layer.width
height = layer.height
temp_img = pdb.gimp_image_new(width, height, img.base_type)
temp_img.disable_undo()
temp_layer = pdb.gimp_layer_new_from_drawable(layer, temp_img)
temp_img.insert_layer(temp_layer)
brush = pdb.gimp_brush_new("Splotch")
pdb.gimp_brush_set_hardness(brush, 1.0)
pdb.gimp_brush_set_shape(brush, BRUSH_GENERATED_CIRCLE)
pdb.gimp_brush_set_spacing(brush, 1000)
pdb.gimp_context_set_brush(brush)
for i in range(quantity):
random_size = size + random.randrange(variability)
x = random.randrange(width)
y = random.randrange(height)
pdb.gimp_context_set_brush_size(random_size)
pdb.gimp_paintbrush(temp_layer, 0.0, 2, [x, y, x, y], PAINT_CONSTANT, 0.0)
gimp.progress_update(float(i) / float(quantity))
temp_layer.flush()
temp_layer.merge_shadow(True)
# Delete the original layer and copy the new layer in its place
new_layer = pdb.gimp_layer_new_from_drawable(temp_layer, img)
name = layer.name
img.remove_layer(layer)
pdb.gimp_item_set_name(new_layer, name)
img.insert_layer(new_layer)
gimp.delete(temp_img)
img.undo_group_end()
gimp.context_pop()