I would like to implement Koch Koch snow flake using pygame.
I am working with the following series of images from http://en.wikipedia.org/wiki/File:KochFlake.svg
My algorithm is this
Draw a triangle
Calculate the points of a triangle one-third its size and delete the centre line
Find out the outer points (as show in the second figure of the above image)
Make a list of all end points
Using polygon join all points
I've done up to the second step. But I'm struggling on the third step - as I can't work out how to find the outer points - any tips?
Here is my code up to second step
import pygame
from pygame.locals import *
pygame.init()
fpsClock = pygame.time.Clock()
screen = pygame.display.set_mode((600,600))
pygame.display.set_caption('Koch snowflake')
white = (255, 255, 255)
black = (0, 0 ,0)
def midpoints(pt1 , pt2):
(x1, y1) = pt1
(x2, y2) = pt2
return ((x1+x2)/2, (y1 + y2)/2)
def midline(pt1, pt2):
(x1, y1) = pt1
(x2, y2) = pt2
return [(x1 + float(x2-x1)/3.0,y1 + float(y2-y1)/3.0), (x1 + float(x2-x1)*2.0/3,y1+ float(y2-y1)*2.0/3)]
def drawline(pt1, pt2):
pygame.draw.line(screen, white, pt1, pt2)
def clearline(pt1,pt2):
pygame.draw.line(screen, black, pt1, pt2, 4)
a = [(150,150), (450,150), (300,410), (150,150)]
pygame.draw.polygon(screen, white ,(a[0], a[1], a[2]), 1)
i = 0
order = 0
length = len(a)
while order < length - 1:
pts = midline(a[i], a[i+1])
clearline(pts[0], pts[1])
a = a[:i+1] + pts + a[i+1:]
print a
if order < 3:
i = i+3
order = order + 1
#pygame.draw.polygon(screen, white ,Tup, 1)
pygame.display.update()
Not exactly an answer but something relevant to your larger question.
L-system fractals (like what you're trying to draw here) are best done using a rudimentary L-system parser. For a Koch snowflake, the 'axiom' (which is a description of the initial shape is something like this) D++D++D++. The D stands for "move forward by one unit" and the + for "turn clockwise by 30 degrees". The instructions will be "interpreted" by a turtle like cursor. It's not very hard to do this.
Once the axiom is drawn, you have a segment that replaces the D. For the koch flake, it is D-D++D-D meaning "move forward one unit, turn anticlockwise 30 degrees, forward, clockwise 60, forward, anticlockwise 30 and forward". This gives you the _/\_ shape that replaces the sides of the initial triangle. One "unit" reduces by to one third of the original length on every iteration.
Now, repeat this as many times as you want and you're looking for. This was one of my earliest Python programs and I have a crude parser/interpreter for it on github. It doesn't use pygame but you should be able to swap that part out quite easily.
To calculate the points, I'd use a vector approach. If the triangle's corners are a1, a2 and a3, then you can get an equation for all points on the line a1 to a2. Using that equation, you can find the points at 1/3 and 2/3 between a1 and a2. The distance between those points gives you the side of the new triangle you're going to create. Using that information, and the point at 1/2 between a1 and a2 you can work out the coordinates of the third new point.
Related
I'm trying to draw any regular polygon, so from triangles to polygons with so many corners, it looks like a circle. to make it easier, they must be regular, so a normal pentagon/hexagon/octagon etc. I want to be able to rotate them. What ive tried is to draw a circle and divide 360 by the amount of points i want then, create a point every nth degrees around the circle, putting these points in pygame.draw.polygon() then creates the shape i want, the problem is it isn't the right size, i also want to be able to have stretch the shapes, so have a different width and height.
def regular_polygon(hapi, x, y, w, h, n, rotation, angle_offset = 0):
#angle_offset is the starting angle in the circle where rotation is rotating the circle
#so when its an oval, rotation rotates the oval and angle_offset is where on the oval to start from
if n < 3:
n = 3
midpoint = pygame.Vector2(x + w//2, y + h//2)
r = sqrt(w**2 + h**2)
#if angle_offset != 0:
#w = (w//2)//cos(angle_offset)
#if angle_offset != 90:
#h = (h//2)//sin(angle_offset)
w,h = r,r
points = []
for angle in range(0, 360, 360//n):
angle = radians(angle + angle_offset)
d = pygame.Vector2(-sin(angle)*w//2, -cos(angle)*h//2).rotate(rotation) #the negative sign is because it was drawing upside down
points.append(midpoint + d)
#draws the circle for debugging
for angle in range(0, 360, 1):
angle = radians(angle + angle_offset)
d = pygame.Vector2(-sin(angle)*w//2, -cos(angle)*h//2).rotate(rotation)
pygame.draw.rect(screen, (0,255,0), (midpoint[0] + d[0], midpoint[1] + d[1], 5, 5))
pygame.draw.polygon(screen,(255,0,0),points)
the red square is the the function above is making, the blue one behind is what it should be
as you can see the circle does line up with the edges of the rect, but because the angles of the circle are not even, the rectangle the func makes is not right.
i think i need to change the circle to an oval but cannot find out how to find the width radius and height radius of it. currently ive found the radius by using pythag.
this is what happens when i dont change the width or height
I found the solution
doing w *= math.sqrt(2) and h *= math.sqrt(2) works perfectly. dont quite understand the math, but after trial and error, this works. You can probable find the maths here but i just multiplied the width and height by a number and printed that number when it lined up which was very close to the sqrt(2)
Here is a test program. I started with two random dots and the line connecting them. Now, I want to take a given image (with x,y dimensions of 79 x 1080) and blit it on top of the guide line. I understand that arctan will give me the angle between the points on a cartesian grid, but because y is backwards the screen (x,y), I have to invert some values. I'm confused about the negating step.
If you run this repeatedly, you'll see the image is always parallel to the line, and sometimes on top, but not consistently.
import math
import pygame
import random
pygame.init()
screen = pygame.display.set_mode((600,600))
#target = (126, 270)
#start = (234, 54)
target = (random.randrange(600), random.randrange(600))
start = (random.randrange(600), random.randrange(600))
BLACK = (0,0,0)
BLUE = (0,0,128)
GREEN = (0,128,0)
pygame.draw.circle(screen, GREEN, start, 15)
pygame.draw.circle(screen, BLUE, target, 15)
pygame.draw.line(screen, BLUE, start, target, 5)
route = pygame.Surface((79,1080))
route.set_colorkey(BLACK)
BMP = pygame.image.load('art/trade_route00.png').convert()
(bx, by, bwidth, bheight) = route.get_rect()
route.blit(BMP, (0,0), area=route.get_rect())
# get distance within screen in pixels
dist = math.sqrt((start[0] - target[0])**2 + (start[1] - target[1])**2)
# scale to fit: use distance between points, and make width extra skinny.
route = pygame.transform.scale(route, (int(bwidth * dist/bwidth * 0.05), int( bheight * dist/bheight)))
# and rotate... (invert, as negative is for clockwise)
angle = math.degrees(math.atan2(-1*(target[1]-start[1]), target[0]-start[0]))
route = pygame.transform.rotate(route, angle + 90 )
position = route.get_rect()
HERE = (abs(target[0] - position[2]), target[1]) # - position[3]/2)
print(HERE)
screen.blit(route, HERE)
pygame.display.update()
print(start, target, dist, angle, position)
The main problem
The error is not due to the inverse y coordinates (0 at top, max at bottom) while rotating as you seems to think. That part is correct. The error is here:
HERE = (abs(target[0] - position[2]), target[1]) # - position[3]/2)
HERE must be the coordinates of the top-left corner of the rectangle inscribing your green and blue dots connected by the blue line. At those coordinates, you need to place the Surface route after rescaling.
You can get this vertex by doing:
HERE = (min(start[0], target[0]), min(start[1], target[1]))
This should solve the problem, and your colored dots should lay on the blue line.
A side note
Another thing you might wish to fix is the scaling parameter of route:
route = pygame.transform.scale(route, (int(bwidth * dist/bwidth * 0.05), int( bheight * dist/bheight)))
If my guess is correct and you want to preserve the original widht/height ratio in the rescaled route (since your original image is not a square) this should be:
route = pygame.transform.scale(route, (int(dist* bwidth/bheight), int(dist)))
assuming that you want height (the greater size in the original) be scaled to dist. So you may not need the 0.05, or maybe you can use a different shrinking parameter (probably 0.05 will shrink it too much).
I graphed a fractal shape in Python using turtle, and am trying to get the area of this fractal after a sufficiently high iteration. This fractal is related to the Koch snowflake, for those interested.
I was able to fill in the fractal with black using begin_fill() and end_fill(). I then used this answer to get the color of each pixel in a valid range. If it wasn't equal to white, then I added one to the count. This solution works for a small iteration of the fractal. However, it takes an exorbitant amount of time when trying to go to a higher iteration.
Here is my code for the fractal.
def realSnowflake(length, n, s, show = False):
#n: after n iterations
#s: number of sides (in Koch snowflake, it is 3)
#length: starting side length
turtle.begin_fill()
a = 360/s
for i in range(s):
snowflake(length, n, s)
turtle.right(a)
turtle.end_fill()
Here is my code for finding the area.
count = 0
canvas = turtle.getcanvas()
for x in range(x1, x2+1): #limits calculated through math
for y in range(y2, y1+1):
if get_pixel_color(x, y, canvas) != "white":
count += 1
I want to be able to find the area of this fractal faster. It takes the most amount of time not in graphing the fractal, but in the double for loop of x and y. I think if there is a way to find the area while turtle is filling, this would be optimal.
the complexity of the image drawn shouldn't affect the time it takes
to count black pixels
Unfortunately, in this case it does. If we lookup an earlier source of the get_pixel_color() code, we find the telling text, "is slow". But it's worse than that, it actually slows down!
This code is built atop canvas.find_overlapping() which is looking for high level objects that sit over X,Y. In the case of tkinter filling an object for turtle, there is overlap, up to three layers in the code below. This increases as the factal gets more complex. Here's my code to demonstrate this:
from turtle import Screen, Turtle
from math import floor, ceil
from time import time
def koch_curve(turtle, iterations, length):
if iterations == 0:
turtle.forward(length)
else:
for angle in [60, -120, 60, 0]:
koch_curve(turtle, iterations - 1, length / 3)
turtle.left(angle)
def koch_snowflake(turtle, iterations, length):
turtle.begin_poly()
turtle.begin_fill()
for _ in range(3):
koch_curve(turtle, iterations, length)
turtle.right(120)
turtle.end_fill()
turtle.end_poly()
return turtle.get_poly()
def bounding_box(points):
x_coordinates, y_coordinates = zip(*points)
return [(min(x_coordinates), min(y_coordinates)), (max(x_coordinates), max(y_coordinates))]
def get_pixel_color(x, y):
ids = canvas.find_overlapping(x, y, x, y) # This is our bottleneck!
if ids: # if list is not empty
index = ids[-1]
return canvas.itemcget(index, 'fill')
return 'white' # default color
screen = Screen()
screen.setup(500, 500)
turtle = Turtle(visible=False)
turtle.color('red')
canvas = screen.getcanvas()
width, height = screen.window_width(), screen.window_height()
for iterations in range(1, 7):
screen.clear()
turtle.clear()
screen.tracer(False)
polygon_start_time = time()
polygon = koch_snowflake(turtle, iterations, 200)
polygon_elapsed = round((time() - polygon_start_time) * 1000) # milliseconds
screen.tracer(True)
((x_min, y_min), (x_max, y_max)) = bounding_box(polygon)
screen.update()
# Convert from turtle coordinates to tkinter coordinates
x1, y1 = floor(x_min), floor(-y_max)
x2, y2 = ceil(x_max), ceil(-y_min)
canvas.create_rectangle((x1, y1, x2, y2))
count = 0
pixel_count_start_time = time()
for x in range(x1, x2 + 1):
for y in range(y1, y2 + 1):
if get_pixel_color(x, y) == 'red':
count += 1
pixel_count_elapsed = round((time() - pixel_count_start_time) * 1000)
print(iterations, count, polygon_elapsed, pixel_count_elapsed, ((x1, y1), (x2, y2)))
screen.exitonclick()
CONSOLE OUTPUT
> python3 test.py
1 23165 1 493 ((-1, -58), (201, 174))
2 26064 4 1058 ((-1, -58), (201, 174))
3 27358 9 1347 ((-1, -58), (201, 174))
4 28159 29 2262 ((0, -58), (201, 174))
5 28712 104 5925 ((0, -58), (201, 174))
6 28881 449 19759 ((0, -58), (200, 174))
>
The fields are as follows:
Iterations
Pixels counted
Time to draw image in ms
Time to count pixels in ms
Computed bounding box (in tkinter coordinates)
FINAL ITERATION SCREEN OUTPUT
Note that the fractal is drawn by turtle but the bounding box is drawn by the underlying tkinter to verify that we converted coordinates correctly.
POSSIBLE SOLUTION
Find an approach that doesn't rely on find_overlapping(). I think the next thing to investigate would be either to convert the canvas to a bitmap image, and pixel count it, or draw a bitmap image in the first place. There are several discusions on SO about converting a canvas to a bitmap, either directly or indirectly via Postscript. You could then load in that image and use one of the Python image libraries to count the pixels. Although more complicated, it should provide a constant time way of counting pixels. Alternately, there are libraries to draw bitmaps, which you could load into tkinter for visual verfication, but could then pixel count directly. Good luck!
In my image I have a triangle (representing an arrow). This arrow defines the direction and area under consideration for further search in same image. For example if I have a triangle rotated at 30 degree w.r.t x-axis and it's tip is located at (250,150) in the image. I would want to find and draw a line normal the tip of triangle, as shown in the image below.
In the image above, I have the angle of blue line in the triangle, to which the normal is to be drawn. Also the tip of the triangle is known which is the only point known on the normal line.
My code for the python function is given below. This code draws a line, passing through the tip but not necessarily NORMAL to the blue line.
def draw_intercepts(img,triangle):
tip=triangle["tip"]
x1=tip[0]
y1=tip[1]
arrow_angle=triangle["arrow_angle"]
y_intercept=int(y1+((1/np.tan(arrow_angle))*x1))
x_intercept=int(x1+(np.tan(arrow_angle)*y1))
cv2.line(img,(x_intercept,0),(0,y_intercept),[0,0,255],3,cv2.LINE_AA)
This code is written by following an answer to this post:
https://math.stackexchange.com/questions/2381119/how-to-find-slope-x-and-y-intercepts-given-angle-to-the-normal-vector-and-a-poi
Note:
I updated the code as recommended:
def draw_intercepts(img,triangle):
tip=triangle["tip"]
x1=tip[0]
y1=tip[1]
arrow_angle=triangle["arrow_angle"]
arrow_angle_rad=np.radians(arrow_angle)
y_intercept=int(y1+((1/np.tan(arrow_angle_rad))*x1))
x_intercept=int(x1+(np.tan(arrow_angle_rad)*y1))
cv2.line(img, (x_intercept, 0), (0, y_intercept), [0, 0, 255], 3, cv2.LINE_AA)
This resolved the issue.
Now the line is drawn perfectly when the arrow_angle is in 1st or 3rd quadrant e.g. 0 < arrow_angle < 90 and 180 < arrow_angle < 270 but in 2nd and 4th quadrant ( 90 < arrow_angle < 180 and 270 < arrow_angle < 360) line angle is not drawn as correct angle or position. Even I don't know it line is drawn somewhere because it is not visible in image.
Note that used line equation "in intercept segments" is not universal - does not work for horizontal and vertical lines. With intercept approach cases 0, Pi/2, Pi, 3*Pi/2 or -Pi/2 must be treated separately.
If you arrow_angle is for blue line:
c = -Sin(arrow_angle)
s = Cos(arrow_angle)
If you arrow_angle is for red line:
c = Cos(arrow_angle)
s = Sin(arrow_angle)
Then draw line through points
(x1 - c * 4096, y1 - s * 4096) and (x1 + c * 4096, y1 + s * 4096)
(I used arbitrary large constant comparable with screen size)
I have created a triangle positioned in the centre of the screen.
from PIL import Image, ImageDraw
GRAY = (190, 190, 190)
im = Image.new('RGBA', (400, 400), WHITE)
points = (250, 250), (100, 250), (250, 100)
draw = ImageDraw.Draw(im)
draw.polygon(points, GRAY)
How do I duplicate this image and reflect it along each sides of the triangle at different random points. For example...
Plan: First find a random point on the edge of the big triangle where to put a smaller one, and then rotate it so it fits properly on the edge.
Suppose we can access the points of the triangle with something like this
triangle.edges[0].x,
triangle.edges[0].y,
triangle.edges[1].x,
etc
We can then find an arbitrary point by first selecting an edge, and "walk a random distance to the next edge":
r = randInt(3) # random integer between 0 and 2
first_edge = triangle.edges[r]
second_edge = r == 2 ? triangle.edges[0] : triangle.edges[r + 1]
## The next lines is kind of pseudo-code
r = randFloat(1)
random_point = (second_edge - first_edge)*r + first_edge
Our next problem is how to rotate a triangle. If you have done some algebra you might recognise this:
def rotatePointAroundOrigin(point, angle):
new_point = Point()
new_point.x = cos(angle)*point.x - sin(angle)*point.y
new_point.y = sin(angle).point.x + cos(angle)*point.y
return new_point
(see https://en.wikipedia.org/wiki/Rotation_matrix)
In addition to this you need to determine just how much to rotate the triangle, and then apply the function above to all of the points.