Python3 Tkinter and Pillow: How to rotate an image while it is on a canvas - python-3.x

I want to move a turtle around a canvas by clicking on the canvas, and the turtle should point in the direction it is moving. The moving part works, but the rotate function causes the image to become distorted and mangled.
What am I doing wrong and how do I fix this?
I have a class that adds an image of a turtle to a canvas:
class TurtleImage:
"""
A Turtle image that will be placed on the canvas that is given in the ctor.
The turtle can be moved around the canvas with the move() method.
"""
def __init__(self, canvas : Canvas):
self.__turtle_file : str = self.__find_file("turtle_example/turtle.png")
self.__canvas = canvas
self.__pilImage : PngImagePlugin.PngImageFile = PILImage.open(self.__turtle_file)
self.__pilTkImage : ImageTk.PhotoImage = ImageTk.PhotoImage(self.__pilImage)
self.__turtle_id : int = canvas.create_image(100,100, image=self.__pilTkImage)
self.__is_moving = False
This class also has a method to animate the turtle moving around the canvas. It moves the turtle by 1 pixel in the x direction, y direction, or both, and then scehdules itself to be called again after a time delay determined by the speed parameter. It also should rotate the turtle so it is pointing in the right direction:
def move(self, dest_x : int, dest_y :int, speed : float = 0.1):
self.__is_moving = True
delay_ms = math.floor(1/speed)
current_x, current_y = self.__canvas.coords(self.__turtle_id)
delta_x = 1 if current_x < dest_x else -1 if current_x > dest_x else 0
delta_y = 1 if current_y < dest_y else -1 if current_y > dest_y else 0
angle = math.atan2(delta_y,delta_x)
self.__rotate(angle)
if (delta_x, delta_y) != (0, 0):
self.__canvas.move(self.__turtle_id, delta_x, delta_y)
if (current_x, current_y) != (dest_x, dest_y):
self.__canvas.after(delay_ms, self.move, dest_x, dest_y, speed)
else:
self.__is_moving = False
Because the canvas does not have the ability to rotate its objects, I must replace the object with a rotated version of itself:
def __rotate(self, angle : float):
self.__pilImage = self.__pilImage.rotate(angle)
self.__pilTkImage = ImageTk.PhotoImage(self.__pilImage)
self.__replace_image(self.__pilTkImage)
def __replace_image(self, new_image : ImageTk.PhotoImage):
self.__canvas.itemconfig(self.__turtle_id, image = new_image)
The moving around works fine, but the rotate function causes the image to become distorted and mangled, and it gets worse every time it is called.
Can you tell me why this isn't working, and what I need to do to fix it?
Here's a screenshot of said turtle, before and after rotating:

I've found the sollution, I have to reopen the file:
def __rotate(self, angle : float):
self.__pilImage = PILImage.open(self.__turtle_file)
self.__pilImage = self.__pilImage.rotate(angle)
self.__pilTkImage = ImageTk.PhotoImage(self.__pilImage)
self.__replace_image(self.__pilTkImage)
I don't know why though.

Related

Interference of canvas items and problem in setting coordinates

I'm working on an animation of a moving object, while drawing it's path.
I want to draw the pixels in which the center of the object went through... but guess what? python decided to set the NW anchor of the image with the coordinates I send, instead of the center. I infer it has something to do with the pixels I draw simultaneously (creating a one pixel rectangle). so the image appear on the right of the path bellow... I want the center of it to be on the top of the pixels... adding the main of the code:
from tkinter import*
import time
dt = 0.01
clock_place = (500, 10)
def round_two(t, t0):
return round((t-t0)*100)/100
def round_three(t, t0):
return round((t-t0)*1000)/1000
# showing 'real time motion' for a known path (also cyclic), with
# parametric representation
def paint_known_path(x_pos, y_pos, t_0):
window = Tk()
canvas = Canvas(window, height=700, width=1000)
canvas.pack()
canvas.config(background='black')
tennis_ball = PhotoImage(file='tennis ball.png')
t = t_0
x = x_pos(t_0)
y = y_pos(t_0)
particle = canvas.create_image(x, y, image=tennis_ball)
clock = canvas.create_text(clock_place, text=round_two(t, t_0),
fill='white')
while True:
canvas.create_rectangle(x, y, x, y, outline='red')
canvas.itemconfig(clock, text=round_two(t, t_0))
t += dt
x = x_pos(t)
y = y_pos(t)
canvas.moveto(particle, x, y)
window.update()
if x == x_pos(t_0) and y == y_pos(t_0):
if t - t_0 > 100*dt:
break
time.sleep(dt)
canvas.create_text((500, 100), text='orbit duration: ' +
str(round_three(t, t_0)), fill='white')
window.mainloop()
It turns out to be quite a bit require, but here is the main completion components.
The first additional part that you need to add:
# print('the ten ball height', tennis_ball.height(), tennis_ball.width())
# tennis ball dimensions
tb_hght = tennis_ball.height()
tb_wdth = tennis_ball.width()
mid_point_x = x + tennis_ball.height() / 2
mid_point_y = y + tennis_ball.width() / 2
Secondly, also needed to add some functions to for x_pos and y_pos like this (these are just example functions to make the code work):
def x_pos(a):
# any function of t,
return 100
def y_pos(a):
# any function of t,
return 100
Furthermore, you need to call the function at the end like this:
paint_known_path(x_pos,y_pos,0)
Finally, need to add the mid_point_x and mid_point_y to the path that is drawn (as these will be the image centre points).

Extra pixels left and top of canvas python turtle

I'm not sure why but on the left and top of a canvas I'm getting weird behaviour while creating my snake game using turtle. There's a border of 10 pixels on the left and on the top.
Main Code
import functools
from turtle import Screen
import time
from Snake import Snake
# initialize screen
win = Screen()
WIDTH = HEIGHT = 800
win.setup(width=WIDTH, height=HEIGHT)
win.bgcolor("black")
canvas = win.getcanvas()
root = canvas.winfo_toplevel()
root.overrideredirect(1)
win.tracer(0)
s = Snake()
def main():
segments = s.init_segments()
win.update()
move_keys = ["Right", "Up", "Left", "Down"]
running = True
inc = 0
for k in move_keys:
win.onkeypress(functools.partial(s.move, segments, inc, k), key=k)
inc += 90
win.listen()
while running:
win.update()
time.sleep(0.1)
s.move(segments, segments[0].heading(), "none")
if s.crashed(win.window_width(), win.window_height(), segments):
running = False
break
win.exitonclick()
main()
Snake Code
from turtle import Turtle
class Snake:
def init_segments(self):
segs = []
x_pos = 0
for i in range(3):
t = Turtle(shape="square")
t.color("white")
t.penup()
t.speed(1)
segs.append(t)
if t != segs[0]:
x_pos -= 20
t.goto(x_pos, 0)
return segs
def __no_turn(self, h, d):
if h - d == -180 or h - d == 180:
return True
else:
return False
def __no_skip(self, k, dir):
if (k == "Right" and dir == 0 or k == "Up" and dir == 90
or k == "Left" and dir == 180 or k == "Down" and dir == 270):
return False
else:
return True
def move(self, t_list , dir, k):
for seg_num in range(len(t_list) - 1, -1, -1):
if self.__no_turn(t_list[0].heading(), dir):
continue
elif seg_num > 0:
if self.__no_skip(k, dir):
t_list[seg_num].goto(t_list[seg_num - 1].position())
else:
t_list[seg_num].setheading(dir)
if self.__no_skip(k, dir):
t_list[seg_num].forward(20)
def crashed(self, window_width, window_height, t_list):
head_position = t_list[0].position()
body_positions = []
hw = window_width/2
hh = window_height/2
for i in range(len(t_list)):
body_positions.append(t_list[i].position())
body_positions.pop(0)
for i in range(len(body_positions)):
if head_position == body_positions[i]:
return True
if head_position[0] >= hw:
print(f"Head Position: {head_position[0]} ... Wall Position: {hw}")
return True
elif head_position[0] <= -hw:
print(f"Head Position: {head_position[0]} ... Wall Position: {-hw}")
return True
elif head_position[1] >= hh:
print(f"Head Position: {head_position[1]} ... Wall Position: {hh}")
return True
elif head_position[1] <= -hh:
print(f"Head Position: {head_position[1]} ... Wall Position: {-hh}")
return True
The print lines in this file were added to try and figure out what was going on.
Please help me understand what's going on.
Thanks.
Edit:
Also I didn't notice it earlier, but there's a 1px border on the bottom and right hand sides.
Second Edit to illustrate the problem further.
I added the code for the pills a while ago. What I do is, I spawn every pill in all at once and hide them because you can't destroy a turtle without clearing the screen and it would be weird to get the turtle position every single time and redraw the turtle on the screen where it had been 0.1 seconds previously. Then I got this result.
As you can see the pills are spawning off screen.
The following is the code that I used in order to spawn in all pills and is similar to the code that I use for determining the edges of the screen.
hw = int(WIDTH/2)
hh = int(HEIGHT/2)
p_list = []
for i in range(-hw, hw, 20):
for j in range(-hh, hh, 20):
p = Pill((i, j))
p.show_pill(segments)
p_list.append(p)
What happens when I reorder the win.update()
As you can see here, the right side now crashes one whole segment in. If you look closely, you'll see in this picture that the white of the snake, covers the extra pixel of space that is not covered in the previous picture where it stops on the last pixel of the circle.
You'll also see in the second picture and this one that there is a row of circles on the bottom that are peeking over the edge of the window's border.
If you move win.update() after s.move(segments, segments[0].heading(), "none") instead of before it will work (i.e) crash in the same way in all four walls.
If you don't want the head to get burst into the wall when crashing then modify the crash boundary check accordingly.
I've finally figured out why this happens.
First problem: I was previously led to believe that all turtle shapes are created equal. No they're not. While the default size for a turtle might be 20px by 20px, a square and circle are both defaulted to 21px as they need a center point that is not an even number so that it can divide evenly between the 4 quadrants of turtle's co-ordinate system (which has a single pixel serving as the x and y axes).
How to solve:
Make the canvas a multiple of 21px.
Remember that the canvas, when split into 2 parts has half of a turtle on each edge, which counts as a full turtle but the turtle only completes over the canvas, so we're sitting with an extra column and row of turtles.
Remove the last column and row of turtles and move the entire grid over by one turtle.
If done correctly you'll end up with the result below. Unfortunately, 2px at the right and bottom will be cut off as it's used for Turtle's UI. The top and left face a similar problem in that a single row of pixels are used for Turtle's UI
The snake itself is done in a similar manner in that instead of shifting the starting segment 20px I shift it 11px first and then I appended the other parts of the snake. I calculate it's position by removing the 11px that I shifted it initially. This literally solved most of the problems with the snake apart from stuff that Turtle's ui takes away.

ipycanvas displaying final stroke_lines thoughout animation

So I was playing with animating some Bezier curves - just part of learning how to use ipycanvas (0,10,2) -- The animation I produced is really hurting my head. What I expected to see was a set of straight lines between 4 Bezier control points "bouncing" around the canvas with the Bezier curve moving along with them.
I did get the moving Bezier curve -- BUT the control points stayed static. Even stranger they were static in the final position and the curve came to meet them.
Now sometimes Python's structures and references can get a little tricky and so you can sometimes get confusing results if you are not really thinking it through -- and this totally could be what's going on - but I am at a loss.
So to make sure I was not confused I printed the control points (pts) at the beginning and then displayed them to the canvas. This confirmed my suspicion. Through quantum tunneling or some other magic time travel the line canvas.stroke_lines(pts) reaches into the future and grabs the pts array as it will exist in the future and keeps the control points in their final state.
Every other use of pts uses the current temporal state.
So what I need to know is A) The laws of physics are safe and I am just too dumb to understand my own code. B) There is some odd bug in ipycanvas that I should report. C) How to monetize this time-traveling code -- like, could we use it to somehow factor large numbers?
from ipycanvas import Canvas, hold_canvas
import numpy as np
def rgb_to_hex(rgb):
if len(rgb) == 3:
return '#%02x%02x%02x' % rgb
elif len(rgb) == 4:
return '#%02x%02x%02x%02x' % rgb
def Bezier4(t, pts):
p = t**np.arange(0, 4,1)
M=np.matrix([[0,0,0,1],[0,0,3,-3],[0,3,-6,3],[1,-3,3,-1]])
return np.asarray((p*M*pts))
canvas = Canvas(width=800, height=800)
display(canvas) # display the canvas in the output cell..
pts = np.random.randint(50, 750, size=[4, 2]) #choose random starting point
print(pts) #print so we can compare with ending state
d = np.random.uniform(-4,4,size=[4,2]) #some random velocity vectors
c = rgb_to_hex(tuple(np.random.randint(75, 255,size=3))) #some random color
canvas.font = '16px serif' #font for displaying the changing pts array
with hold_canvas(canvas):
for ani in range(300):
#logic to bounce the points about...
for n in range(0,len(pts)):
pts[n]=pts[n] + d[n]
if pts[n][0] >= 800 or pts[n][0] <= 0 :
d[n][0] = - d[n][0]
if pts[n][1] >= 800 or pts[n][1] <= 0 :
d[n][1] = - d[n][1]
#calculate the points needed to display a bezier curve
B = [(Bezier4(i, pts)).ravel() for i in np.linspace(0,1,15)]
#begin display output....
canvas.clear()
#first draw bezier curve...
canvas.stroke_style = c
canvas.stroke_lines(B)
#Now draw control points
canvas.stroke_style = rgb_to_hex((255,255,128, 50))
canvas.stroke_lines(pts)
#print the control points to the canvas so we can see them move
canvas.stroke_style = rgb_to_hex((255,255,128, 150))
canvas.stroke_text(str(pts), 10, 32)
canvas.sleep(20)
In all seriousness, I have tried to think through what can be happening and I am coming up blank. Since ipycanvas is talking to the browser/javascript maybe all of the data for the frames are rendered first and the array used to hold the pts data for the stroke_lines ends up with the final values... Whereas the B array is recreated in each loop... It's a guess.
There are two ways to get the code to behave as expected and avoid the unsightly time-traveling code. The first way is to switch the location of the line with hold_canvas(canvas): to inside the loop. This however renders the canvas.sleep(20) line rather useless.
canvas = Canvas(width=800, height=800)
display(canvas)
pts = np.random.randint(50, 750, size=[4, 2])
print(pts)
d = np.random.uniform(-8,8,size=[4,2])
c = rgb_to_hex(tuple(np.random.randint(75, 255,size=3)))
canvas.font = '16px serif'
#with hold_canvas(canvas):
for ani in range(300):
with hold_canvas(canvas):
for n in range(0,len(pts)):
if pts[n][0] > 800 or pts[n][0] < 0 :
d[n][0] = -d[n][0]
if pts[n][1] > 800 or pts[n][1] < 50 :
d[n][1] = -d[n][1]
pts[n]=pts[n] + d[n]
B = [(Bezier4(i, pts)).ravel() for i in np.linspace(0,1,25)]
canvas.clear()
canvas.stroke_style = c
canvas.stroke_lines(B)
canvas.stroke_style = rgb_to_hex((255,255,128, 50))
#pts2 = np.copy(pts)
canvas.stroke_lines(pts)
canvas.fill_style = rgb_to_hex((255,255,255, 150))
canvas.fill_circles(pts.T[0], pts.T[1],np.array([4]*4))
canvas.stroke_style = rgb_to_hex((255,255,128, 150))
canvas.fill_text(str(pts), 10, 32)
sleep(20/1000)
#canvas.sleep(20)
In this version, the control lines are updated as expected. This version is a little more "real time" and thus the sleep(20/1000) is needed to
The other way to do it would be just to ensure that a copy of pts is made and passed to canvas.stroke_lines:
canvas = Canvas(width=800, height=800)
display(canvas)
pts = np.random.randint(50, 750, size=[4, 2])
print(pts)
d = np.random.uniform(-8,8,size=[4,2])
c = rgb_to_hex(tuple(np.random.randint(75, 255,size=3)))
canvas.font = '16px serif'
with hold_canvas(canvas):
for ani in range(300):
#with hold_canvas(canvas):
for n in range(0,len(pts)):
if pts[n][0] > 800 or pts[n][0] < 0:
d[n][0] = -d[n][0]
if pts[n][1] > 800 or pts[n][1] < 50:
d[n][1] = -d[n][1]
pts[n]=pts[n] + d[n]
B = [(Bezier4(i, pts)).ravel() for i in np.linspace(0,1,35)]
canvas.clear()
canvas.stroke_style = c
canvas.stroke_lines(B)
canvas.stroke_style = rgb_to_hex((255,255,128, 50))
pts2 = np.copy(pts)
canvas.stroke_lines(pts2)
canvas.fill_style = rgb_to_hex((255,255,255, 150))
canvas.fill_circles(pts.T[0], pts.T[1],np.array([4]*4))
canvas.stroke_style = rgb_to_hex((255,255,128, 150))
canvas.fill_text(str(pts), 10, 32)
#sleep(20/1000)
canvas.sleep(20)
I could not actually find the data passed between the python and the browser but it seems pretty logical that what is happening is that python is finishing its work (and ani loop) before sending the widget instructions on what to draw, and the pts values sent are the final ones.
(yes I know there is a bug in the bouncing logic)

Mapping pixel value to temperature value on a thermal image

I have a thermal image (with a color bar) from an IR camera. My goal is to get the temperature of any point by clicking on it.
I have already written a script that retrieves the RBG values of any pixel by right-clicking on it.
I figure that using the max and min temperatures of the color bar, I can map pixel values to temperature values.
Is this possible or is there a better way to approach this?
Thank you very much.
from PIL import Image
import cv2
from win32api import GetSystemMetrics
counter = 0
max_value = input('Max Temp Value: ')
min_value = input('Min Temp Value: ')
def mouse_callback(event, x, y, flags, params): # Tracks the pixel the mouse it hovering on. When right click it prints the pixel location and its RBG values.
global counter
if event == 2:
counter += 1
r, g, b = rgb_img.getpixel((x, y))
print(f'{counter}: {[x, y]} value {r} {g} {b}')
else:
print([x, y], end='\t\r', flush=True)
path_image = 'colors.jpg'
img = cv2.imread(path_image)
im = Image.open(path_image)
rgb_img = im.convert('RGB')
width = GetSystemMetrics(0)
height = GetSystemMetrics(1)
scale_width = width / im.size[0]
scale_height = height / im.size[1]
scale = min(scale_width, scale_height)
window_width = int((im.size[0] * scale) * 0.5)
window_height = int((im.size[1] * scale) * 0.5)
cv2.namedWindow('image', cv2.WINDOW_NORMAL)
cv2.resizeWindow('image', window_width, window_height)
cv2.setMouseCallback('image', mouse_callback)
cv2.imshow('image', img)
cv2.waitKey(0)
cv2.destroyAllWindows()

How to get a rectangle to move along X axis using checkMouse()?

I'm trying to get a rectangle to move along the X axis but checkMouse() isn't working. What needs to be done to make it work?
from graphics import*
import time
from random import randrange
wd=GraphWin("Catch A Ball",500,500)
wd.setBackground("lightblue")
p1=220 #size of rectangle
p2=250
for i in range(1):
spt1=Point(p1,480)
spt2=Point(p2,500)
rct=Rectangle(spt1,spt2)
rct.setOutline("black")
rct.setFill("black")
rct.draw(wd)
p=wd.checkMouse()
c=rct.getCenter()
dx=p.getX() - c.getX()
dy=p.getY() - c.getY()
rct.move(dx,0)
You're missing a loop and I recommend working with getMouse() initially until you've a need to switch to checkMouse():
from graphics import *
window = GraphWin("Catch A Ball", 500, 500)
window.setBackground("lightblue")
point1 = Point(220, 480) # size of rectangle
point2 = Point(250, 500)
rectangle = Rectangle(point1, point2)
rectangle.setFill("black")
rectangle.draw(window)
while True:
point = window.getMouse()
if point is not None: # so we can switch to checkMouse() if desired
center = rectangle.getCenter()
dx = point.getX() - center.getX()
dy = point.getY() - center.getY()
rectangle.move(dx, 0)
You need to add some sort of action/event to break out of the while True: infinite loop.

Resources