Hey so I'm just starting to learn manim but I encountered some strange behavior. If I get the coordinates of a square and draw them, they align with the square. But, if I try to move / shift / align_to / next_to the square, then draw the corners again, they no longer align with the square. It works in all directions, for all scales, and what's quite weird is that the offset seems to be proportional to how far the square is shifted.
Here is my (distilled) code:
def square_and_corners(square, color):
# Calculate the corners of the square
DL = square.get_left() + square.get_bottom()
DR = square.get_right() + square.get_bottom()
UL = square.get_left() + square.get_top()
UR = square.get_right() + square.get_top()
# Add corners and square
self.add(Dot(DL, color=color), Dot(DR, color=color),
Dot(UL, color=color), Dot(UR, color=color))
self.add(square)
# No moving
square1 = Square(2, color=WHITE)
square_and_corners(square1, WHITE)
# Move to RIGHT
square2 = Square(2, color=RED)
square2.move_to(RIGHT)
square_and_corners(square2, RED)
# align to the left of the central square
square3 = Square(2, color=GREEN)
square3.next_to(square1, LEFT)
square_and_corners(square3, GREEN)
Solution Code
class SquareScene(Scene):
def construct(self):
def square_and_corners(square, color):
DL = square.get_left() + square.get_bottom()
DR = square.get_right() + square.get_bottom()
UL = square.get_left() + square.get_top()
UR = square.get_right() + square.get_top()
g = [DL,DR,UL,UR]
G = VGroup()
for s in g:
dot = Dot(point = s,color = color)
G.add(dot)
G.add(square)
return G
# No moving
square1 = Square(side_length = 2)
self.add(square_and_corners(square1, WHITE))
# Move to RIGHT
square2 = Square(side_length = 2).set_stroke(color = RED)
a = square_and_corners(square2, RED)
self.add(a.move_to(RIGHT))
# align to the left of the central square
square3 = Square(side_length = 2, color=GREEN)
b = square_and_corners(square3, GREEN)
self.add(b.next_to(square1, LEFT))
# You can move only the square if you want (or any mobject in the VGroup)
square4 = Square(side_length = 2, color=BLUE)
b = square_and_corners(square4, BLUE)
self.add(b.next_to(square1, UP))
self.add(square4.next_to(square1,DOWN))
Details
The best way to deal with a group of mobjects in manim is by grouping them in a VGroup() this way enables you to animate all the mobjects in the group or any mobject you want separately and reduces the number of errors while rendering and problems that appear in the animation and reduces the lines you write to make your animation and it is pretty easy to deal with in iterations.
The Output
Related
I am trying to animate a particle and a vector that is attached to its center as a random motion is applied to the particle. The particle behaves as intended, but the vector always has a offset from the particle. I tried to set the random seed of each scene, but it didn't work.
from manim import *
import numpy as np
class Particles(ThreeDScene):
def construct(self):
self.camera.background_color = WHITE
coordinate = np.array([ (0,0,0) ])
angle = 90
radius = 1.25
#Polar coordinates
def polar2cart(theta_degrees, rho):
theta = theta_degrees*(np.pi/180)
x = rho * np.cos(theta)
y = rho * np.sin(theta)
return(x, y, 0)
start_arrow = np.array(coordinate) - np.array( [polar2cart(angle,radius )] )
end_arrow = np.array(coordinate) + np.array( [polar2cart(angle,radius )] )
arrow = Arrow( start= start_arrow, end = end_arrow, color = '#ff0000')
dot = Dot(point=coordinate, radius=0.2, color = '#000000')
#Display
self.add(arrow, dot)
##############################################################################################################################
#Random motion
valor = ValueTracker(0)
dot.add_updater(lambda m: m.move_to( m.get_center() + (np.random.normal(0,0.05),np.random.normal(0,0.05),np.random.normal(0,0.05)) ) )
arrow.add_updater(lambda m: m.move_to( dot.get_center() + (np.random.normal(0,0.05),np.random.normal(0,0.05),np.random.normal(0,0.05)) ) )
self.play(valor.animate.set_value(10),rate_func=smooth, run_time=5)
self.wait()
Particle and vector
How can I make that the vector and the particle remain fixed in all frames?
Here is some things that you need to know:
The order in which updaters attached to mobjects are executed is the order in which their mobjects have been added to the scene. In your code, the updater attached to arrow runs before the updater attached to dot (which will make the arrow always lag behind).
Even if you fix the seed, subsequent calls to np.random.norm will not yield the same numbers (fortunately, otherwise your dot would move in a straight line out of the scene). If you want to fix the arrow to the center of the dot, then there is no need to add the random offset to that as well.
Here are possible solutions:
Add the objects in the other way around, self.add(dot, arrow), and remove the random offset from the updater attached to the arrow, arrow.add_updater(lambda m: m.move_to(dot.get_center())). If the order of mobjects on the screen is important to you, you can also run dot.set_z_index(1).
Alternatively, you could also simply create a group of your two objects and attach an updater to that; particle_group = VGroup(arrow, dot) followed by particle_group.add_updater(lambda m: m.shift(...)).
I'd personally go for the group, but it really depends on what else you intend to do in this scene.
I'm attempting to extend the 'tail' of an arrow. So far I've been able to draw a line through the center of the arrow, but this line extends 'both' ways, rather than in just one direction. The script below shows my progress. Ideally I would be able to extend the tail of the arrow regardless of the orientation of the arrow image. Any suggestions on how to accomplish this. Image examples below, L:R start, progress, goal.
# import image and grayscale
image = cv2.imread("image path")
image = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
cv2.imshow("original",image)
# inverts black and white
gray = 255 - image
cv2.imshow("Inverted", gray)
# Extend the borders for the line
extended = cv2.copyMakeBorder(gray, 20, 20, 10, 10, cv2.BORDER_CONSTANT)
cv2.imshow("extended borders", extended)
# contour finding
contours, hierarchy = cv2.findContours(extended, 1, 2)
cont = contours[0]
rows,cols = extended.shape[:2]
[vx,vy,x,y] = cv2.fitLine(cont, cv2.DIST_L2,0,0.01,0.01)
leftish = int((-x*vy/vx) + y)
rightish = int(((cols-x)*vy/vx)+y)
line = cv2.line(extended,(cols-1,rightish),(0,leftish),(255,255,255), 6)
cv2.imshow("drawn line", line)
"Moments" can be strange things. They're building blocks and show up most often in statistics.
It helps to have a little background in statistics, and see the application of those calculations to image data, which can be considered a set of points. If you've ever calculated the weighted average or "centroid" of something, you'll recognize some of the sums that show up in "moments".
Higher order moments can be building blocks to higher statistical measures such as covariance and skewness.
Using covariance, you can calculate the major axis of your set of points, or your arrow in this case.
Using skewness, you can figure out which side of a distribution is heavier than the other... i.e. which side is the arrow's tip and which is its tail.
This should give you a very precise angle. The scale/radius however is best estimated using other ways. You'll notice that the radius estimated from the area of the arrow fluctuates a little. You could find the points belonging to the arrow that are furthest away from the center, and take that as a somewhat stable length.
Here's a longish program that implements the two ideas above and shows the direction of an arrow:
#!/usr/bin/env python3
import os
import sys
import numpy as np
import cv2 as cv
# utilities to convert between 2D vectors and complex numbers
# complex numbers are handy for rotating stuff
def to_complex(vec):
assert vec.shape[-1] == 2
if vec.dtype == np.float32:
return vec.view(np.complex64)
elif vec.dtype == np.float64:
return vec.view(np.complex128)
else:
assert False, vec.dtype
def from_complex(cplx):
if cplx.dtype == np.complex64:
return cplx.view(np.float32)
elif cplx.dtype == np.complex128:
return cplx.view(np.float64)
else:
assert False, cplx.dtype
# utilities for drawing with fractional bits of position
# just to make a pretty picture
def iround(val):
return int(round(val))
def ipt(vec, shift=0):
if isinstance(vec, (int, float)):
return iround(vec * 2**shift)
elif isinstance(vec, (tuple, list, np.ndarray)):
return tuple(iround(el * 2**shift) for el in vec)
else:
assert False, type(vec)
# utilities for affine transformation
# just to make a pretty picture
def rotate(degrees=0):
# we want positive rotation
# meaning move +x towards +y
# getRotationMatrix2D does it differently
result = np.eye(3).astype(np.float32)
result[0:2, 0:3] = cv.getRotationMatrix2D(center=(0,0), angle=-degrees, scale=1.0)
return result
def translate(dx=0, dy=0):
result = np.eye(3).astype(np.float32)
result[0:2,2] = [dx, dy]
return result
# main logic
def calculate_direction(im):
# using "nonzero" (default behavior) is a little noisy
mask = (im >= 128)
m = cv.moments(mask.astype(np.uint8), binaryImage=True)
# easier access... see below for details
m00 = m['m00']
m10 = m['m10']
m01 = m['m01']
mu00 = m00
mu20 = m['mu20']
mu11 = m['mu11']
mu02 = m['mu02']
nu30 = m['nu30']
nu03 = m['nu03']
# that's just the centroid
cx = m10 / m00
cy = m01 / m00
centroid = np.array([cx, cy]) # as a vector
# and that's the size in pixels:
size = m00
# and that's an approximate "radius", if it were a circle which it isn't
radius = (size / np.pi) ** 0.5
# (since the "size" in pixels can fluctuate due to resampling, so will the "radius")
# wikipedia helpfully mentions "image orientation" as an example:
# https://en.wikipedia.org/wiki/Image_moment#Examples_2
# we'll use that for the major axis
mup20 = mu20 / mu00
mup02 = mu02 / mu00
mup11 = mu11 / mu00
theta = 0.5 * np.arctan2(2 * mup11, mup20 - mup02)
#print(f"angle: {theta / np.pi * 180:+6.1f} degrees")
# we only have the axis, not yet the direction
# we will assess "skewness" now
# https://en.wikipedia.org/wiki/Skewness#Definition
# note how "positive" skewness appears in a distribution:
# it points away from the heavy side, towards the light side
# fortunately, cv.moments() also calculates those "standardized moments"
# https://en.wikipedia.org/wiki/Standardized_moment#Standard_normalization
skew = np.array([nu30, nu03])
#print("skew:", skew)
# we'll have to *rotate* that so it *roughly* lies along the x axis
# then assess which end is the heavy/light end
# then use that information to maybe flip the axis,
# so it points in the direction of the arrow
skew_complex = to_complex(skew) # reinterpret two reals as one complex number
rotated_skew_complex = skew_complex * np.exp(1j * -theta) # rotation
rotated_skew = from_complex(rotated_skew_complex)
#print("rotated skew:", rotated_skew)
if rotated_skew[0] > 0: # pointing towards tail
theta = (theta + np.pi) % (2*np.pi) # flip direction 180 degrees
else: # pointing towards head
pass
print(f"angle: {theta / np.pi * 180:+6.1f} degrees")
# construct a vector that points like the arrow in the picture
direction = np.exp([1j * theta])
direction = from_complex(direction)
return (radius, centroid, direction)
def draw_a_picture(im, radius, centroid, direction):
height, width = im.shape[:2]
# take the source at half brightness
canvas = cv.cvtColor(im // 2, cv.COLOR_GRAY2BGR)
shift = 4 # prettier drawing
cv.circle(canvas,
center=ipt(centroid, shift),
radius=ipt(radius, shift),
thickness=iround(radius * 0.1),
color=(0,0,255),
lineType=cv.LINE_AA,
shift=shift)
# (-direction) meaning point the *opposite* of the arrow's direction, i.e. towards tail
cv.line(canvas,
pt1=ipt(centroid + direction * radius * -3.0, shift),
pt2=ipt(centroid + direction * radius * +3.0, shift),
thickness=iround(radius * 0.05),
color=(0,255,255),
lineType=cv.LINE_AA,
shift=shift)
cv.line(canvas,
pt1=ipt(centroid + (-direction) * radius * 3.5, shift),
pt2=ipt(centroid + (-direction) * radius * 4.5, shift),
thickness=iround(radius * 0.15),
color=(0,255,255),
lineType=cv.LINE_AA,
shift=shift)
return canvas
if __name__ == '__main__':
imfile = sys.argv[1] if len(sys.argv) >= 2 else "p7cmR.png"
src = cv.imread(imfile, cv.IMREAD_GRAYSCALE)
src = 255 - src # invert (white arrow on black background)
height, width = src.shape[:2]
diagonal = np.hypot(height, width)
outsize = int(np.ceil(diagonal * 1.3)) # fudge factor
cv.namedWindow("arrow", cv.WINDOW_NORMAL)
cv.resizeWindow("arrow", 5*outsize, 5*outsize)
angle = 0 # degrees
increment = +1
do_spin = True
while True:
print(f"{angle:+.0f} degrees")
M = translate(dx=+outsize/2, dy=+outsize/2) # rotate(degrees=angle) # translate(dx=-width/2, dy=-height/2)
im = cv.warpAffine(src, M=M[:2], dsize=(outsize, outsize), flags=cv.INTER_CUBIC, borderMode=cv.BORDER_REPLICATE)
# resampling introduces blur... except when it's an even number like 0 degrees, 90 degrees, ...
# so at even rotations, things will jump a little.
# this rotation is only for demo purposes
(radius, centroid, direction) = calculate_direction(im)
canvas = draw_a_picture(im, radius, centroid, direction)
cv.imshow("arrow", canvas)
if do_spin:
angle = (angle + increment) % 360
print()
key = cv.waitKeyEx(30 if do_spin else -1)
if key == -1:
continue
elif key in (0x0D, 0x20): # ENTER (CR), SPACE
do_spin = not do_spin # toggle spinning
elif key == 27: # ESC
break # end program
elif key == 0x250000: # VK_LEFT
increment = -abs(increment)
angle += increment
elif key == 0x270000: # VK_RIGHT
increment = +abs(increment)
angle += increment
else:
print(f"key 0x{key:02x}")
cv.destroyAllWindows()
I'm having a hard time cutting the code and making it into a loop so that it would make the code of the program, neater.
Although my code works as it suppose to be, I think there is a right way of creating it, adding a for loop rather than writing all of these codes, I know there is an easy way to do this, I just couldn't figure how to do it properly. I know I'm suppose to create a for loop.
squares
from graphics import *
def main():
win = GraphWin("Squares", 500, 500)
rect = Rectangle(Point(0,500), Point(500,0))
rect.setFill("Red")
rect.draw(win)
rect2 = Rectangle(Point(20,480), Point(480,20))
rect2.setFill("white")
rect2.draw(win)
rect3 = Rectangle(Point(40,460), Point(460,40))
rect3.setFill("red")
rect3.draw(win)
rect4 = Rectangle(Point(60,440), Point(440,60))
rect4.setFill("white")
rect4.draw(win)
rect5 = Rectangle(Point(80,420), Point(420,80))
rect5.setFill("red")
rect5.draw(win)
rect6 = Rectangle(Point(100,400), Point(400,100))
rect6.setFill("white")
rect6.draw(win)
rect7 = Rectangle(Point(120,380), Point(380,120))
rect7.setFill("red")
rect7.draw(win)
rect8 = Rectangle(Point(140,360), Point(360,140))
rect8.setFill("white")
rect8.draw(win)
rect9 = Rectangle(Point(160,340), Point(340,160))
rect9.setFill("red")
rect9.draw(win)
rect10 = Rectangle(Point(180,320), Point(320,180))
rect10.setFill("white")
rect10.draw(win)
rect11 = Rectangle(Point(200,300), Point(300,200))
rect11.setFill("red")
rect11.draw(win)
rect12 = Rectangle(Point(220,280), Point(280,220))
rect12.setFill("white")
rect12.draw(win)
The results shows squares into some sort of a patchwork
Try the following:
from graphics import *
def main():
win = GraphWin("Squares", 500, 500)
# create all rects
rects = [Rectangle(Point(0 + 20*i,500 - 20*i), Point(500 - 20*i, 0 + 20*i)) for i in range(12)]
# draw all rects
for idx, rect in enumerate(rects):
rect.fill("red" if idx % 2 == 0 else "white")
rect.draw(win)
If the patchwork is just a background and you don't plan on modifying it you could use this:
from graphics import *
def main():
win = GraphWin("Squares", 500, 500)
i = 1
for x in range(0, 221, 20):
rect = Rectangle(Point(x, 500 - x), Point(500 - x,x))
rect.setFill("red" if i % 2 else "white")
rect.draw(win)
i += 1
An alternate approach that only needs to draw half as many rectangles due to using the rectangle's outline as the other color:
SQUARE, WIDTH = 500, 20
def main():
win = GraphWin("Squares", SQUARE, SQUARE)
save_config = dict(DEFAULT_CONFIG)
DEFAULT_CONFIG.update(dict(outline='red', fill='white', width=WIDTH))
for xy in range(WIDTH//2, SQUARE//2, WIDTH*2):
Rectangle(Point(xy, SQUARE - xy), Point(SQUARE - xy, xy)).draw(win)
DEFAULT_CONFIG.update(save_config)
It's fully parameterized so you can fit it to a different size square or have different width stripes by adjusting the SQUARE and WIDTH parameters. Rather than draw 12 rectangles in alternating colors, with the parameters as currently set, it draws 6 white rectangles with red outlines:
I've been trying to replicate this design using Python.
I am using the Graphics module, obtained here. I can't use any other Graphic Module at the moment.
This code here allows me to draw 5 circles in a line repeated by a loop.
def fdShape():
win = GraphWin(199,199)
centre = Point(20,100)
for y in range(5):
for x in range(5):
centre = Point(x * 40 + 20, y * 40 + 60)
circle = Circle(centre, 20)
circle.setOutline("red")
circle.draw(win)
One problem that I've found with this code is that it leaves a blank line at the top of the window and place the last circle line at the bottom, beyond the borders of the window. Thats the first problem.
The second is using the code to display semi circles displayed in red. As you can see in the image at the top of this page. I'm unsure of how to replicate this picture using Python.
Thank you!
This looks pretty close:
from graphic import *
def main():
repeat = 5
diameter = 40
radius = diameter // 2
offset = radius // 2
win = GraphWin("Graphic Design", diameter*repeat + offset, diameter*repeat)
win.setBackground('white')
for i in range(repeat):
for j in range(repeat):
draw_symbol(win, i % 2,
Point(i*diameter + offset, j*diameter), radius, 'red')
win.getMouse()
win.close()
def draw_symbol(win, kind, lower_left, radius, colour):
centre = Point(lower_left.x+radius, lower_left.y+radius)
circle = Circle(centre, radius)
circle.setOutline('')
circle.setFill(colour)
circle.draw(win)
if kind == 0:
rectangle = Rectangle(lower_left,
Point(lower_left.x+radius, lower_left.y+radius*2))
else:
rectangle = Rectangle(lower_left,
Point(lower_left.x+radius*2, lower_left.y+radius))
rectangle.setOutline('white')
rectangle.setFill('white')
rectangle.draw(win)
circle = Circle(centre, radius)
circle.setWidth(1)
circle.setOutline(colour)
circle.setFill('')
circle.draw(win)
main()
Two issues I see.
GraphWin should be initialized to 200x200, not 199x199.
This line:
centre = Point(x * 40 + 20, y * 40 + 60)
Should most likely be:
centre = Point(x * 40 + 20, y * 40 + 20)
I need to create a patchwork in Python 3. All I have left to do is create a loop which makes the design border the graphics window. I know I need a for loop however I am not sure how to do this.
This is what I have so far:
from graphics import *
def main():
height = eval(input("What is the height of the window"))
width = eval(input("What is the width of the window"))
colour = input("enter the colour of the patch")
win = GraphWin("Patch", 100*width, 100*height)
boat_x = 0
boat_y = 0
for x in range (4):
boat(win, boat_x, boat_y, colour)
boat_x = boat_x + 23
for i in range(height * 5):
boat(win, boat_x, boat_y, colour)
boat_x = boat_x + 24
for j in range(height * 5):
boat(win, boat_x, boat_y, colour)
boat_y = boat_y + 100
win.getMouse()
win.close()
def boat(win, x, y, colour):
body1 = Polygon(Point(1+x,95+y), Point(5+x,100+y),
Point(20+x,100+y), Point(24+x,95+y))
body1.draw(win)
line1 = Line(Point(13+x,95+y), Point(13+x,90+y))
line1.draw(win)
sail1 = Polygon(Point(1+x,90+y), Point(24+x,90+y), Point(13+x, 73+y))
sail1.setFill(colour)
sail1.draw(win)
body2 = Polygon(Point(1+x, 63), Point(5+x, 68),
Point(20+x,68), Point(24+x,63))
body2.draw(win)
line2 = Line(Point(13+x,63), Point(13+x,58))
line2.draw(win)
sail2 = Polygon(Point(1+x,58), Point(24+x, 58), Point(13+x,40))
sail2.setFill(colour)
sail2.draw(win)
body3 = Polygon(Point(1+x,28), Point(5+x,33),
Point(20+x,33), Point(24+x, 28))
body3.draw(win)
line3 = Polygon(Point(13+x,28), Point(13+x,23))
line3.draw(win)
sail3 = Polygon(Point(1+x,23), Point(24+x, 23), Point(13+x, 5))
sail3.setFill(colour)
sail3.draw(win)
main()
So far this creates the top border but nothing else.
I am also aware that the boat function isn't the most efficient way of drawing
When you say that you need to "make the design border the graphics window" I assume you want your boat design to be repeated several times along each edge of the window (that is, the top, bottom, left and right).
This should be doable in two loops. One will draw the top and bottom edges, the other two will draw the left and right edges. I'm not too sure how your drawing code works, so I'm guessing at some offsets here:
top = 0
bottom = (height-1) * 100
for x in range(0, width*100, 25):
boat(x, top, colour)
boat(x, bottom, colour)
left = 0
right = width * 100 - 25
for y in range(100, (height-1)*100, 100):
boat(left, y, colour)
boat(right, y, colour)
This should call your boat subroutine every 25 pixels across the top and bottom, and every 100 pixels along the left and right edges. Adjust the top, bottom, left and right values and the parameters in the range calls in the loops to make the spacing suit your needs (I just made it up). This code avoids drawing the corner items twice, though depending on how the drawing routine works that might not be necessary.