Draw interactive mandelbrot with pyglet - python

I have a function that calculates the Mandelbrot set in an numpy array of size (500,500).
I can draw it in matplotlib. Now i want to zoom into the image via mouse click. I want to use pyglet for that but struggle with rendering. I know this (not working) solution is inelegant, so I am happy for advice how to do this better!
PS: The zoom level is set arbitrary right now, my problem I ask for help is the refreshing.
The functions I replaced with a docstring can be found here
from pyglet.window import mouse
import pyglet
import numpy as np
from matplotlib import pyplot as plt
from matplotlib import cm
from pyglet import image
"""
def calc_pixel_array(x_center: float = 0, y_center: float = 0,
scale: float = 1.0, width: int = 500,
height: int = 500, threshold: int = 200):
Returns numpy array with shape (500,500) with color values
def plot_pixel_array(pixel_array, cmap: str = 'gnuplot', show_plot: bool = False):
Draws pixel_array via matplotlib and saves in curent_mandelbrot.png
"""
pixel_array = calc_pixel_array()
plot_pixel_array(pixel_array)
scale = 1
i = 1
window = pyglet.window.Window(width = 500, height = 500)
image = pyglet.resource.image('curent_mandelbrot.png')
pic = image.load('curent_mandelbrot.png')
#window.event
def on_draw():
window.clear()
pic.blit(0, 0)
#window.event
def on_mouse_press(x, y, button, modifiers):
if button == mouse.LEFT:
i = np.random.randint(5)+1
pixel_array = calc_pixel_array(scale/(10*i))
plot_pixel_array(pixel_array)
window.clear()
image = pyglet.resource.image('curent_mandelbrot.png')
image.blit(0,0)
pyglet.app.run()

Hi after a lot of trying around I found this
def draw_pixel_array(self, window):
main_batch = pyglet.graphics.Batch()
shape = self.pixel_array.shape
for x in range(shape[0]):
for y in range(shape[1]):
color = int(self.pixel_array[x, y])
pyglet.graphics.draw(1, pyglet.gl.GL_POINTS,
('v2i', (x, y)),
('c3B', (color, color, 0)))
main_batch.draw()
Got it running by removing the on draw and putting everything (calculating and drawing) in the on_mouse_press function.
You will find the complete solution here. It is still very slow in drawing, any suggestions there are warmly welcome!

Related

creating a spring in pygame [duplicate]

I am trying to create a pendulum with oscillating string and mass which is expected to oscillate for a period of time and then stop. From what I wrote, the mass could not stop oscillating. Kindly help me out.
import pyglet
import pymunk
import chipmunk
from pymunk import Vec2d
from pymunk.pyglet_util import DrawOptions
window = pyglet.window.Window(1300,700,"Oscillation",resizable=False)#
W,H
options=DrawOptions()
space=pymunk.Space()
space.gravity= 0,-100
b0= space.static_body
p0= 100,200
body= pymunk.Body(mass=0.5, moment =1)
body.position = (100,50)
circle= pymunk.Circle(body, radius=20)
joint= pymunk.constraint.DampedSpring(b0, body,p0, (0,0), 100, 10, 0)
space.add(body, circle, joint)
#window.event
def on_draw():
window.clear()
space.debug_draw(options)
def update(dt):
space.step(dt)
if __name__=="__main__":
pyglet.clock.schedule_interval(update,1.0/60)
pyglet.app.run()
You did not provide damping in the last parameter of the call:
joint= pymunk.constraint.DampedSpring(b0, body,p0, (0,0), 100, 10, 0)
So it will oscillate forever. Put some amount of damping and it should slow down.

How to embed skia python surface inside pysdl2

I am trying to embed skia-python's surface inside a window rather than output to a image file. I am using pysdl2 to create the window using the following code from the documentation:
import sys
import sdl2.ext
RESOURCES = sdl2.ext.Resources(__file__, "resources")
sdl2.ext.init()
window = sdl2.ext.Window("Hello World!", size=(640, 480))
window.show()
factory = sdl2.ext.SpriteFactory(sdl2.ext.SOFTWARE)
sprite = factory.from_image(RESOURCES.get_path("hello.bmp"))
spriterenderer = factory.create_sprite_render_system(window)
spriterenderer.render(sprite)
processor = sdl2.ext.TestEventProcessor()
processor.run(window)
sdl2.ext.quit()
And this code to create the surface from skia's documentation:
import skia
surface = skia.Surface(128, 128)
with surface as canvas:
rect = skia.Rect(32, 32, 96, 96)
paint = skia.Paint(
Color=skia.ColorBLUE,
Style=skia.Paint.kFill_Style)
canvas.drawRect(rect, paint)
image = surface.makeImageSnapshot()
image.save('output.png', skia.kPNG)
Now what I want to achieve is to take the image (or surface whichever applicable) object from the skia portion and plug it into pysdl2 so that I can draw with skia but handle window's event loop with pysdl2 and I'd like to avoid ctypes right now because I am not so familiar with it.
I gave up on creating it without ctypes as all we need from it is ctypes.byref and I am now importing sdl2 instead of sdl2.ext which was more pythonic, but also restricted a bit of functionality that is required here.
Now to answer the question, I followed this guide here (if you are not building a browser it might go a little off topic for you)
So I have also implemented a general enough version from the above guide, you can draw to Window.skia_surface and then call Window.update to copy skia surface to the window screen:
import skia
import sdl2 as sdl
from ctypes import byref
def sdl_event_loop():
event = sdl.SDL_Event()
running = True
while running:
pending_events = sdl.SDL_PollEvent(byref(event))
while pending_events != 0:
# QUIT HANDLER
if event.type == sdl.SDL_QUIT:
running = False
sdl.SDL_Quit()
break
# UPDATE PENDING EVENTS
pending_events = sdl.SDL_PollEvent(byref(event))
class Window:
DEFAULT_FLAGS = sdl.SDL_WINDOW_SHOWN
BYTE_ORDER = {
# ---------- -> RED GREEN BLUE ALPHA
"BIG_ENDIAN": (0xff000000, 0x00ff0000, 0x0000ff00, 0x000000ff),
"LIL_ENDIAN": (0x000000ff, 0x0000ff00, 0x00ff0000, 0xff000000)
}
PIXEL_DEPTH = 32 # BITS PER PIXEL
PIXEL_PITCH_FACTOR = 4 # Multiplied by Width to get BYTES PER ROW
def __init__(self, title, width, height, x=None, y=None, flags=None):
self.title = bytes(title, "utf8")
self.width = width
self.height = height
# Center Window By default
self.x, self.y = x, y
if x is None:
self.x = sdl.SDL_WINDOWPOS_CENTERED
if y is None:
self.y = sdl.SDL_WINDOWPOS_CENTERED
# Override flags
self.flags = flags
if flags is None:
self.flags = self.DEFAULT_FLAGS
# SET RGBA MASKS BASED ON BYTE_ORDER
is_big_endian = sdl.SDL_BYTEORDER == sdl.SDL_BIG_ENDIAN
if is_big_endian:
self.RGBA_MASKS = self.BYTE_ORDER["BIG_ENDIAN"]
else:
self.RGBA_MASKS = self.BYTE_ORDER["LIL_ENDIAN"]
# CALCULATE PIXEL PITCH
self.PIXEL_PITCH = self.PIXEL_PITCH_FACTOR * self.width
# SKIA INIT
self.skia_surface = self.__create_skia_surface()
# SDL INIT
sdl.SDL_Init(sdl.SDL_INIT_EVENTS) # INITIALIZE SDL EVENTS
self.sdl_window = self.__create_SDL_Window()
sdl_event_loop()
def __create_SDL_Window(self):
window = sdl.SDL_CreateWindow(
self.title,
self.x, self.y,
self.width, self.height,
self.flags
)
return window
def __create_skia_surface(self):
surface_blueprint = skia.ImageInfo.Make(
self.width, self.height,
ct=skia.kRGBA_8888_ColorType,
at=skia.kUnpremul_AlphaType
)
surface = skia.Surface.MakeRaster(surface_blueprint)
return surface
def __pixels_from_skia_surface(self):
image = self.skia_surface.makeImageSnapshot()
pixels = image.tobytes()
return pixels
def __transform_skia_surface_to_SDL_surface(self):
pixels = self.__pixels_from_skia_surface()
sdl_surface = sdl.SDL_CreateRGBSurfaceFrom(
pixels,
self.width, self.height,
self.PIXEL_DEPTH, self.PIXEL_PITCH,
*self.RGBA_MASKS
)
return sdl_surface
def update(self):
rect = sdl.SDL_Rect(0, 0, self.width, self.height)
window_surface = sdl.SDL_GetWindowSurface(self.sdl_window) # the SDL surface associated with the window
transformed_skia_surface = self.__transform_skia_surface_to_SDL_surface()
# Transfer skia surface to SDL window's surface
sdl.SDL_BlitSurface(
transformed_skia_surface, rect,
window_surface, rect
)
# Update window with new copied data
sdl.SDL_UpdateWindowSurface(self.sdl_window)
if __name__ == "__main__":
window = Window("Browser Test", 500, 500, flags=sdl.SDL_WINDOW_SHOWN | sdl.SDL_WINDOW_RESIZABLE)
Explanation: Primarily the above code does four things create a skia surface that you will draw on, create an SDL window, convert the skia surface to a SDL surface and at last copy the data in the newly created surface to the SDL surface associated with the window and update it. For a bit more explanation I recommend you look into the above guide and also check out skia-python docs and SDL2's API Reference.

PIL ImageDraw.Draw() doesn't work when used in a function

I made a program that renders the Mandelbrot set to an image. I put the draw.point() method in a function, but it doesn't seem to actually draw on the final image, but if I put im.save() in the function it does work. The full code actually uses multiprocessing and renders the image CineBench style, which is why I can't put the im.save() in the function, or pull the draw.point() out of it. Is there some other way I can solve the problem?
from PIL import Image, ImageDraw
im = Image.new("RGB", (hor_res, vert_res), (0, 0, 0))
draw = ImageDraw.Draw(im)
def mandelbrot():
# mandelbrot code
def box_renderer(x_start: int, x_end: int, y_start: int, y_end: int):
for y in range(y_start, y_end):
for x in range(x_start, x_end):
colour = 255 - int(255*mandelbrot(x, y)/iterations)
draw.point([x, y], (0, 0, colour))
if __name__ == "__main__":
box_renderer(args)
im.save("mandelbrot.png", "PNG")
This is not the entire program, but hopefully enough to make sense
I'm not sure that your sample code is representative, because this version works fine:
from PIL import Image, ImageDraw
hor_res, vert_res = 200, 200
im = Image.new("RGB", (hor_res, vert_res), (255, 0, 0))
draw = ImageDraw.Draw(im)
def box_renderer(x_start: int, x_end: int, y_start: int, y_end: int):
for y in range(y_start, y_end):
for x in range(x_start, x_end):
colour = 128
draw.point([x, y], (0, 0, colour))
if __name__ == "__main__":
box_renderer(x_start=50,x_end=100,y_start=20,y_end=180)
im.save("mandelbrot.png", "PNG")

PyGlet Shape Following The Mouse

So I try to make a "game" with PyGlet:
This is my code:
import pyglet
from pyglet import shapes
window = pyglet.window.Window(800, 600, "PyGlet Window")
circle = shapes.Circle(x = 100, y = 100, radius = 13, color=(255, 255, 255))
def callback(dt):
pass
pyglet.clock.schedule_interval(callback, 0.5)
#window.event
def on_draw():
window.clear()
circle.draw()
pyglet.app.run()
How to make the circle follow the mouse? Thanks!
Implement the on_mouse_motion event (see Working with the mouse) and change the position of the shape (see pyglet.shapes):
#window.event
def on_mouse_motion(x, y, dx, dy):
circle.x = x
circle.y = y

How to use glReadPixels in Python and OpenGL?

How can I get the value of pixel color using glReadPixels()? I did so many try but getting wrong value.
My background color is blue(0,1,1) and I have drawn a circle with boundary color red(1,0,0) and I want to get the color of any of boundary point. So it must give me red. but I am getting background color.
Here is my code in Python3 and OpenGL
from OpenGL.GLU import *
from OpenGL.GLUT import *
import time
from math import *
import numpy
import sys
def init():
glClearColor(0.0,1.0,1.0,0.0)
glClear(GL_COLOR_BUFFER_BIT)
glPointSize(3.0)
glMatrixMode(GL_PROJECTION)
glLoadIdentity()
gluOrtho2D(0.0,640.0,0.0,640.0)
def circle():
for i in range(361):
m=float(50*cos(i*pi/180.0))+320
n=float(50*sin(i*pi/180.0))+320
setpixc(m,n)
print(m,n)
redinput()
def redinput():
global x,y
x=int(input("enter x:"))
y=int(input("enter y:"))
setpixc(x,y)
pixel=[]
c=glReadPixels(x,y,1.0,1.0,GL_RGB,GL_UNSIGNED_BYTE,None)
print(c)
string_pixels=numpy_pixel.tolist()
print(string_pixels)
def setpixc(xcor,ycor):
glBegin(GL_POINTS)
glColor3f(1.0,0.0,0.0)
glVertex2f(xcor,ycor)
glEnd()
glFlush()
def Display():
circle()
print("hello")
def main():
glutInit(sys.argv)
glutInitDisplayMode(GLUT_SINGLE | GLUT_RGB)
glutInitWindowSize(600,600)
glutInitWindowPosition(10,10)
glutCreateWindow("line-dda")
glutDisplayFunc(Display)
init()
glutMainLoop()
main()
You are using an orthographic projection, which which projects the coordinates to the rectangle form (0, 0) to (640, 640):
gluOrtho2D(0.0,640.0,0.0,640.0)
But your window size is (600, 600):
glutInitWindowSize(600,600)
This causes that the coordinates in the range from (0, 0) to (640, 640) are drawn to the viewport from (0, 0) to (600, 600), by glVertex2f:
But when the coordinates are read by glReadPixels, then you would have to use viewport (pixel) coordinates.
To solve your is you can change the window size from (600, 600) to (640, 640):
glutInitWindowSize(640, 640)
Now e.g.
x=270
y=320
will return a red pixel.
Note, if you don't want to change the window size, then you would have to scale the input coordinates by 600/640.
scale = 600/640
c=glReadPixels(x*scale,y*scale,1.0,1.0,GL_RGB,GL_UNSIGNED_BYTE,None)
e.g.
x = 270 * 600 / 640 = 253
y = 320 * 600 / 640 = 300
Further note, that drawing by glBegin/glEnd sequences is deprecated since several years.
Read about Fixed Function Pipeline and see Vertex Specification and Shader for a state of the art way of rendering.
Anyway, I recommend to use double buffering
glutInitDisplayMode(GLUT_DOUBLE | GLUT_RGB)
and to do a single buffer swap, after the entire circle was drawn. Skip the glFlush call in setpixc and add a single glutSwapBuffers call to the Display function and don't forget to clear the display before rendering:
def Display():
glClear(GL_COLOR_BUFFER_BIT)
circle()
glutSwapBuffers()
glutPostRedisplay()
redinput()
print("hello")
It is up to you if you want to draw the circle by single points
def circle():
glPointSize(3.0)
glColor3f(1.0,0.0,0.0)
glBegin(GL_POINTS)
for i in range(360):
m=float(50*cos(i*pi/180.0))+320
n=float(50*sin(i*pi/180.0))+320
glVertex2f(m,n)
glEnd()
or a coherent line:
def circle():
glLineWidth(3.0)
glColor3f(1.0,0.0,0.0)
glBegin(GL_LINE_LOOP)
for i in range(360):
m=float(50*cos(i*pi/180.0))+320
n=float(50*sin(i*pi/180.0))+320
glVertex2f(m,n)
glEnd()
If you want to get the color of a pixel by a mouse click, the you can set a mouse call back by glutMouseFunc:
from OpenGL.GL import *
from OpenGL.GLU import *
from OpenGL.GLUT import *
from math import *
def init():
global width, height
glClearColor(0.0, 1.0, 1.0, 0.0)
glMatrixMode(GL_PROJECTION)
glLoadIdentity()
gluOrtho2D(0.0, width, 0.0, height)
def circle():
glLineWidth(3.0)
glColor3f(1.0, 0.0, 0.0)
glBegin(GL_LINE_LOOP)
for i in range(360):
m=float(50*cos(i*pi/180.0))+320
n=float(50*sin(i*pi/180.0))+320
glVertex2f(m, n)
glEnd()
def Mouse(button, state, x, y):
global mouse_x, mouse_y, get_input
if button == GLUT_LEFT_BUTTON and state == GLUT_DOWN:
mouse_x = x
mouse_y = height - y # the y coordinate of the mouse has to be flipped
get_input = True
def redinput(x, y):
c = glReadPixels(x, y, 1.0, 1.0, GL_RGB,GL_UNSIGNED_BYTE, None)
print(c)
def Display():
global mouse_x, mouse_y, get_input
glClear(GL_COLOR_BUFFER_BIT)
circle()
glutSwapBuffers()
glutPostRedisplay()
if get_input:
redinput(mouse_x, mouse_y)
get_input=False
def main():
global width, height
glutInit(sys.argv)
glutInitDisplayMode(GLUT_DOUBLE | GLUT_RGB)
glutInitWindowSize(width, height)
glutInitWindowPosition(10, 10)
glutCreateWindow("line-dda")
glutDisplayFunc(Display)
glutMouseFunc(Mouse)
init()
glutMainLoop()
width = 640
height = 640
mouse_x = 0
mouse_y = 0
get_input = False
main()
There are couple of problems with your code.
your window size is different from data you are using in gluOrtho2D
you are relying on pixel exact rasterization, and OpenGL does not guarantee it.
The answer you are searching for is described in "Red Book" (i.e. OpenGL Programming Guide), specifically in Appendix G, Programming Tips. I'd also suggest you to read Appendix H, Invariance. Online version can be found on following link: https://www.glprogramming.com/red/
Also,
you do not need to call glFlush after every single point drawn, call it once just before glReadPixels...
you are using glBegin/glEnd pair for every single point, which is huge waste of
resources. you can draw complete circle using one glBegin/glEnd pair:
glBegin(GL_POINTS)
glColor3f(1.0, 0.0, 0.0)
for i in range(361):
x=float(50*cos(i*pi/180.0))+320
y=float(50*sin(i*pi/180.0))+320
glVertex2f(x,y)
glEnd()
you are using very dense set of GL_POINTS to draw circle, but this will not prodice correct circle. If radius is smaller, you will have multiple rasterization of same window pixel. If you increase radius enough, it will result in set of unconnected points. In your situation, I would use GL_LINE_LOOP:
glBegin(GL_LINE_LOOP)
glColor3f(1.0, 0.0, 0.0)
for i in range(0, 360, 5):
x=float(50*cos(i*pi/180.0))+320
y=float(50*sin(i*pi/180.0))+320
glVertex2f(x,y)
glEnd()
last but not least, this is ancient way of OpenGL usage. Unless you have very good reason, I'd suggest to move to some newer OpenGL version.

Categories

Resources