Filling a Tkinter Canvas Element with an Image - python

Is there any way to fill a Tkinter element (more specifically an oval) with an image. If not, is there any way to resize an image to fit into an oval? I also would prefer not to use PIL.
canvas.create_oval(val1, val2, val1+sz, val2+sz, fill=clr, outline=outln)
How would you get an image to fit inside a circle like so?
I would also certainly cull the edges around the image if you were wondering.

In plain Tkinter your options are limited. One thing you could do is create a
'mask' image that has a transparent circle in the middle. Then lower your
own image in behind it. (not very efficient though)
from tkinter import *
root = Tk()
canvas = Canvas(root, width=200, height=200, bd=0,
highlightthickness=0)
canvas.pack()
mask = PhotoImage(width=200, height=200)
cx,cy = 100,100 # image & circle center point
r = 100 # circle radius
r_squared = r*r
for y in range(200):
for x in range(200):
# using the formula for a circle:
# (x - h)^2 + (y - k)^2 = r^2
# any pixel outside our circle gets filled
if (x - cx)**2 + (y - cy)**2 > r_squared:
mask.put('blue', (x,y))
canvas.create_image(100,100, image=mask, anchor='c')
myimage = PhotoImage(file='bigpython.gif')
item = canvas.create_image(100,100, image=myimage, anchor='c')
canvas.lower(item)
root.mainloop()

No, there is no way to put an image inside an oval. That is to say, you can't make one fill up an oval. Images can only be rectangular.
Your only choice for resizing images without using PIL is to resize it by factors of two.

I've tried all ways to try and get a photo to fit inside a circle but the only kind-of effective way is to resize the image using the Paint 2D app in Windows and then using the resized image as the image used to fill the circle.

Related

Image won't clear when saved from Tkinter canvas

I'm making a handwritten digit interpreter with Tkinter using a model I made. I save the canvas image, then read it back through my model to get the prediction. I clear the canvas with cv.delete("all") then I draw another digit to predict and I get a wonky prediction.
Initial Outcome: (3 is 3 :)!!!)
Then I clear it and write another number:
And I go to my folder where the image saves and the picture looks like this:
Here's my code to define my canvas and the image I'm going to draw on.
# define the canvas and image to save
lastx, lasty = None, None
cv = Canvas(root, width=420, height=420, bg='black')
image1 = PIL.Image.new("L",(420,420),"black")
draw = ImageDraw.Draw(image1)
cv.bind('<1>', activate_paint)
cv.pack(expand=YES, fill=BOTH)
Here's the code I used to paint:
def activate_paint(e):
global lastx, lasty
cv.bind('<B1-Motion>', paint)
lastx, lasty = e.x, e.y
def paint(e):
global lastx, lasty
x, y = e.x, e.y
cv.create_line((lastx, lasty, x, y), fill = 'white',width=30)
# --- PIL
draw.line((lastx, lasty, x, y), fill='white', width=30)
lastx, lasty = x, y
And here's the code I use to save it when I click the predict button:
filename = f'img_to_predict.png'
image1.save(filename)
I just need the clear button to make the image truly blank so it doesn't save over the previous image. Can anyone push me in the right direction?
Thank you!
Jackson
You need to clear the image as well by calling
draw.rectangle((0, 0, 420, 420), fill="black")
However, I would propose to remove drawing on the image. Just drawing on the canvas, and then take a snapshot on the canvas using ImageGrab.grab() (from Pillow module as well) and save the snapshot to file when you need to do the prediction:
# get the canvas bounding box on screen
x, y = cv.winfo_rootx(), cv.winfo_rooty()
w, h = cv.winfo_width(), cv.winfo_height()
# take a snapshot on the canvas and save the image to file
ImageGrab.grab((x, y, x+w, y+h)).save('img_to_predict.png')
UPDATE: This doesn't seem to matter at all, so please ignore it
Assuming you copy pasted the line cv.delete("all"), the issue might be using "all" as a string. You might need cv.delete(ALL) to clear it away?

Jupyter notebook: let a user inputs a drawing

Simple question but I'm not able to find something out there...
Is there a simple and user friendly tool that can be used within a jupyter-notebook to let the user draw something in black on a white space (let say of size (x,y) pixels) after running a cell?
The drawing has to be returned (or even temporarily saved) as an array/image which can then be used by numpy for example.
you can do that using PIL and tkinter libraries, like:
from PIL import ImageTk, Image, ImageDraw
import PIL
from tkinter import *
width = 200 # canvas width
height = 200 # canvas height
center = height//2
white = (255, 255, 255) # canvas back
def save():
# save image to hard drive
filename = "user_input.jpg"
output_image.save(filename)
def paint(event):
x1, y1 = (event.x - 1), (event.y - 1)
x2, y2 = (event.x + 1), (event.y + 1)
canvas.create_oval(x1, y1, x2, y2, fill="black",width=5)
draw.line([x1, y1, x2, y2],fill="black",width=5)
master = Tk()
# create a tkinter canvas to draw on
canvas = Canvas(master, width=width, height=height, bg='white')
canvas.pack()
# create an empty PIL image and draw object to draw on
output_image = PIL.Image.new("RGB", (width, height), white)
draw = ImageDraw.Draw(output_image)
canvas.pack(expand=YES, fill=BOTH)
canvas.bind("<B1-Motion>", paint)
# add a button to save the image
button=Button(text="save",command=save)
button.pack()
master.mainloop()
You can modify the save function to read the image using PIL and numpy to have it as an numpy array.
hope this helps!
Try ipycanvas with interactive drawing mode.
It actually has a demo notebook with interactive drawing that could be easily modified to do exactly what you want. It also has numpy support.
There is a discussion on jupyterlab's github page on this issue: https://github.com/jupyterlab/jupyterlab/issues/9194. Apparently, it is planned to add Excalidraw at some point and until then, https://github.com/nicknytko/notebook-drawing was recommended.

How do I display an extremly long image in Tkinter? (how to get around canvas max limit)

I've tried multiple ways of displaying large images with tkinterreally long image No matter what I've tried, there doesn't seem to be any code that works. The main issue is that Canvas has a maximum height limit of around 30,000 pixels.
Is there a way to display this whole image? increase, or get around the canvas limit? See the example image below.
There is no way around the size limit of the canvas, short of modifying and recompiling the underlying tk code. This would likely not be a trivial task.
Assuming you are trying to display the image on a typical computer screen, there are still ways to view the image. Basically it boils down to only loading the part of the image that the user can see at any one time.
For example, an image of the world is considerably larger than 64k by 64k, yet google maps is able to let you scroll around all you want. It does this by displaying the map as a series of tiles. As you move around the image, off-screen tiles are thrown away and new tiles are loaded.
This same technique can be used in tkinter, and can even be used with scrollbars instead of a dragging motion. You just need to hook the scrollbars up to a function rather than directly to the canvas. Then, when the function is called, it can compute which part of the image that the user is looking at, and load it into memory.
This is a rather unattractive answer, but an answer non-the-less. This divides up extremely long images into "tiles" of 1000 pixel lengths. It does not divide the width. I've spliced together code from several sources until I got it all to work. If someone could make this with a scroll-bar functionality, that would be cool.
from tkinter import *
from PIL import ImageTk as itk
from PIL import Image
import math
import numpy as np
Image.MAX_IMAGE_PIXELS = None #prevents the "photo bomb" warning from popping up. Have to have this for really large images.
#----------------------------------------------------------------------
# makes a simple window with a button right in the middle that let's you go "down" an image.
class MainWindow():
#----------------
def __init__(self, main):
# canvas for image
_, th, tw, rows, cols = self.getrowsandcols()
self.canvas = Canvas(main, width=tw, height=th)
#self.canvas.grid(row=0, column=0)
self.canvas.pack()
# images
self.my_images = self.cropimages() # crop the really large image down into several smaller images and append to this list
self.my_image_number = 0 #
# set first image on canvas
self.image_on_canvas = self.canvas.create_image(0, 0, anchor = NW, image = self.my_images[self.my_image_number])
# button to change image
self.upbutton = Button(main, text="UP", command=self.onUpButton)
self.downbutton = Button(main, text="DOWN", command=self.onDownButton)
self.upbutton.pack()
self.downbutton.pack()
#self.downbutton.grid(row=1, column=0)
#self.upbutton.grid(row=1, column=0)
#----------------
def getimage(self):
im = Image.open("Test_3.png") # import the image
im = im.convert("RGBA") # convert the image to color including the alpha channel (which is the transparency best I understand)
width, height = im.size # get the width and height
return width, height, im # return relevent variables/objects
def getrowsandcols(self):
width, height, im = self.getimage()
im = np.asarray(im) # Convert image to Numpy Array
tw = width # Tile width will equal the width of the image
th = int(math.ceil(height / 100)) # Tile height
rows = int(math.ceil(height / th)) # Number of tiles/row
cols = int(math.ceil(width / tw)) # Number of tiles/column
return im, th, tw, rows, cols #return selected variables
def cropimages(self):
self.my_images = [] # initialize list to hold Tkinter "PhotoImage objects"
im, th, tw, rows, cols = self.getrowsandcols() # pull in needed variables to crop the really long image
for r in range(rows): # loop row by row to crop all of the image
crop_im =im[r * th:((r * th) + th), 0:tw] # crop the image for the current row (r). (th) stands for tile height.
crop_im = Image.fromarray(crop_im) # convert the image from an Numpy Array to a PIL image.
crop_im = itk.PhotoImage(crop_im) # convert the PIL image to a Tkinter Photo Object (whatever that is)
self.my_images.append(crop_im) # Append the photo object to the list
crop_im = None
return self.my_images
def onUpButton(self):
# next image
if self.my_image_number == 0:
self.my_image_number = len(self.my_images)-1
else:
self.my_image_number -= 1 # every button pressed will
# change image
self.canvas.itemconfig(self.image_on_canvas, image=self.my_images[self.my_image_number]) # attaches the image from the image list to the canvas
def onDownButton(self):
# next image
self.my_image_number += 1 #every button pressed will
# return to first image
if self.my_image_number == len(self.my_images):
self.my_image_number = 0
# change image
self.canvas.itemconfig(self.image_on_canvas, image = self.my_images[self.my_image_number]) #attaches the image from the image list to the canvas
#----------------------------------------------------------------------
root = Tk()
MainWindow(root)
root.mainloop()

Reconciling pixel coordinates on tkinter canvas and PIL image (Python)

I have a form (in .png format) with blank text boxes (usually filled out by hand). I would like to populate the boxes with text.
To do this, I am using tkinter to display the form on the screen, I then use the mouse (with finer positioning using arrow keys) to get the pixel coordinates of a box, then use PIL to write text to that box. A working example is below.
My primary issue is I am struggling to align pixel coordinates in the tkinter canvas and pixel coordinates in the PIL image.
Some additional background. The image is in high resolution and is circa 4961 by 7016 pixels. My screen resolution is 1920 x 1080. I had problems writing text where I needed it to write like this and found greater success if I scaled the image to fit entirely within my screen. I can only assume this is because I was / am confusing screen pixels with picture pixels (and resolved this when I fit the image to the screen to align these two - but understanding the differences here and how to do this task without scaling would be most helpful also).
But I am also having trouble reconciling the pixel coordinates on the tkinter canvas with the PIL picture. For example, the code below is designed to write (x, y) pixel coordinates and then its page relativity {x% across the page, y% up the page} to the box (the reason for this is because it is an input into another process). An example is: (346, 481) >> {49.856, 51.018}
But if (using a scaling factor of 0.14) I click very low to the bottom of the image, I get (209, 986) >> {30.115, -0.407}. The relativities should be bounded between 0 and 100% so should not be negative and I cannot see this on my PIL produced .png file.
If I use a scaling factor of 0.125, I can write text to the tkinter canvas box fine, but the text appears quite a bit lower (ie outside the box) on the PIL .png file which is saved to the drives. So something is clearly not working between these two systems.
How can I reconcile PIL and tkinter pixel coordinates?
As an aside, I have a four separate functions to handle finer key adjustments. Ideally these would be one function, but I could not get the arrow buttons ( etc) to work inside an if elif block (eg, I tried this and some more derivatives of left, right etc)
def mouseMovement(event):
moveSpeed = 1
try:
int(event.char)
moveSpeed = max(1, int(event.char)*5)
return True
except ValueError:
return False
x, y = pyautogui.position()
if event.char == '<Left>':
pyautogui.moveTo(x-moveSpeed, y)
elif event.char == '<Right>':
pyautogui.moveTo(x+moveSpeed, y)
root.bind('<Key>' , mouseMovement)
Any help greatly appreciated!
Almost working example below:
from tkinter import *
from PIL import Image, ImageDraw, ImageFont, ImageTk
import pyautogui
# Form
formName = '2013+ MCS4'
# PIL image'
formImage = Image.open(formName+'.png')
wForm, hForm = formImage.size
scale = 0.14
formImage = formImage.resize((int(scale*wForm), int(scale*hForm)), Image.ANTIALIAS)
draw = ImageDraw.Draw(formImage)
font = ImageFont.truetype('arial.ttf', 10)
textColor = (255, 40, 40)
# tkinter canvas
def colorConversion(RGB):
def hexadecimalScale(RGB):
hexadecimalSystem = (0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 'A', 'B', 'C', 'D', 'E', 'F')
return str(hexadecimalSystem[RGB//16]) + str(hexadecimalSystem[RGB%16])
return '#' + hexadecimalScale(RGB[0]) + hexadecimalScale(RGB[1]) + hexadecimalScale(RGB[2])
fontCanvas = 'arial 7'
textColorCanvas = colorConversion(textColor)
# generate canvas
if __name__ == '__main__':
root = Tk()
# set up tkinter canvas with scrollbars
frame = Frame(root, bd=2, relief=SUNKEN)
frame.grid_rowconfigure(0, weight=1)
frame.grid_columnconfigure(0, weight=1)
xscroll = Scrollbar(frame, orient=HORIZONTAL)
xscroll.grid(row=1, column=0, sticky=E+W)
yscroll = Scrollbar(frame)
yscroll.grid(row=0, column=1, sticky=N+S)
canvas = Canvas(frame, width=int(scale*wForm), height=int(scale*hForm), bd=0, xscrollcommand=xscroll.set, yscrollcommand=yscroll.set)
canvas.grid(row=0, column=0, sticky=N+S+E+W)
xscroll.config(command=canvas.xview)
yscroll.config(command=canvas.yview)
frame.pack(fill=BOTH,expand=1)
# add image
#img = PhotoImage(file=formName+'.png')
img = ImageTk.PhotoImage(formImage)
canvas.create_image(0,0,image=img,anchor="nw")
canvas.config(scrollregion=canvas.bbox(ALL))
wForm = img.width()
hForm = img.height()
# finer mouse movements
moveSpeed = 1
def setMoveSpeed(event):
global moveSpeed
try:
int(event.char)
moveSpeed = max(1, int(event.char)*5)
return moveSpeed
except ValueError:
return False
def moveMouseLeft(event):
x, y = pyautogui.position()
pyautogui.moveTo(x-moveSpeed, y)
def moveMouseRight(event):
x, y = pyautogui.position()
pyautogui.moveTo(x+moveSpeed, y)
def moveMouseUp(event):
x, y = pyautogui.position()
pyautogui.moveTo(x, y-moveSpeed)
def moveMouseDown(event):
x, y = pyautogui.position()
pyautogui.moveTo(x, y+moveSpeed)
root.bind('<Key>' , setMoveSpeed)
root.bind('<Left>' , moveMouseLeft)
root.bind('<Right>', moveMouseRight)
root.bind('<Up>' , moveMouseUp)
root.bind('<Down>' , moveMouseDown)
# print coordinates
def printCoordinates(event):
x = event.x # minor adjustments to correct for differences in tkinter vs PIL methods (investigate further)
y = event.y # minor adjustments to correct for differences in tkinter vs PIL methods (investigate further)
canvas.create_text(x, y-5, fill= textColorCanvas, font= fontCanvas, anchor= 'sw',
text= '{'+str(round(x/wForm*100,3))+', '+str(round((1-y/hForm)*100,3))+'}' )
draw.text( (x, y-5), '{'+str(round(x/wForm*100,3))+', '+str(round((1-y/hForm)*100,3))+'}' , fill=textColor, font=font)
print('('+str(x)+', '+str(y)+') >> {'+str(round(x/wForm*100,3))+', '+str(round((1-y/hForm)*100,3))+'}')
root.bind('<Return>', printCoordinates)
root.mainloop()
formImage.save('coordinates - '+formName+'.png')
I cannot run your code, so this is just an educated guess.
Since the canvas doesn't typically have focus, and the binding is on the root window, the values for event.x and event.y are possibly relative to the window as a whole rather than the canvas.
This should be easy to determine. You can print out the coordinates in the binding, and then click in the upper left corner of the canvas, as near to 0,0 as possible. The printed coordinates should also be very near to 0,0 if the coordinates are relative to the canvas. If they are off, they might be off by the distance of the top-left corner of the canvas to the top-left corner of the window.
It's due to Pillow setting the text coordinates on the left-hand corner(i.e. the coordinates selected become the top left-hand corner of whatever textbox pillow adds). But in newer versions of Pillow(I think 8.1), the text has an anchor option. Simply in the parameters for creating text add anchor="mm". The Pillow docs have more info on this(ps. you'll also have to slightly increase the font in some cases due to Pillow font sizes being slightly smaller. I found adding 4 gets pretty close)
EDIT: make sure the cords you're using are also Canvas cords

limit the size of a button with an image

Problem:
I want to create my own widget that uses an image in a button, but the image causes the button to be way too big. How can I resize the button to the normal button size (the size of normal Text).
Code:
add = Button(master=controlfrm , image=myimagepath)
add.pack()
Result:
Goal:
I want the image to be resized to a height equal to the Entry widget.
Tkinter doesn't shrink or expand images. The best you can hope for is to use the zoom and subsample methods on a PhotoImage, which will allow you to change the size by a factor of 2.
If you want to use an image on a button, and you want it to be smaller, the best solution is to start with an image that is the right size.
You can use ImageTk.PhotoImage to resize the image and call it in a Button. Use the zoom to shrink or expand your button image.
button_image_file = "images/square-button-1.png"
button_image = Image.open(button_image_file)
zoom = .40 # multiplier for image size by zooming -/+
pixels_x, pixels_y = tuple([int(zoom * x) for x in button_image.size])
button_image = ImageTk.PhotoImage(button_image.resize((pixels_x, pixels_y)))
button = tk.Button(root_main, text="Button", command=lambda: do_something(), font="Arial",
bg="#20bebe", fg="white", image=button_image)
button.place(rely=0.01, relx=.01)
Try to set the width and height properties of the image, so that the image can fit to size of the button that you require.

Categories