PIL Image in canvas not visible - python

I tried to create an Image using PIL. I wanted to show the image in a tkinter canvas with "create_image" method. But there is no image in the canvas. It looks empty:
self.image = Image.new("RGBA", (w, h), "white")
pixels = self.image.load()
for x in range(w):
for y in range(h):
pixels[x, y] = area.get_color(x, y)
photo = ImageTk.PhotoImage(self.image)
self.canvas.create_image(0, 0, image=photo)
self.canvas.pack(fill=tk.BOTH, expand=tk.TRUE)
The method "area.get_color(x, y)" returns a 4-tuple (r, g, b, alpha).

Well, few things:
First of all, python has this weird garbage collecting issue with tkinter images, in order to really pass the image to your canvas, you need to anchor down first otherwise it would just got wiped when it was passed to canvas.
This is what i did after learning from other people's example:
window.image = create_image(WIDTH, HEIGHT)
Once you anchor it down to your Tk(), it shouldn't get collected and erased anymore as long as your Tk() exists.
Second problem is your placement of image, you might want to place it to the center of your canvas instead of the corner of it:
canvas.create_image(WIDTH//2, HEIGHT//2, image=window.image)
In the end your program would look like this:
window = tk.Tk()
canvas = tk.Canvas(window, width=WIDTH, height=HEIGHT)
window.image = create_image(WIDTH, HEIGHT)
canvas.create_image(WIDTH//2, HEIGHT//2, image=window.image)
canvas.pack(fill=tk.BOTH, expand=tk.TRUE)
window.mainloop()
BTW a circle of a radius of 10 is just too small in a canvas of 640 x 480, you might want to increase this number to 100 or so.
if x**2 + y**2 < 10000:
Like that^

I posted just the relevant part of a pupils project code. They tried to create a tkinter application showing a fractal. But they should do it in
an absolute (crazy) objectoriented manner. After that it should be done in a very much faster implementation.
All the other code works fine (all tests ok), only the PIL part ...
Ok i create a small piece of code to show the pupils idea. But this code doesn't show a black piece of a circle as expected. The canvas is still empty:
import tkinter as tk
from PIL import Image, ImageTk
WHITE = (255, 255, 255, 255)
BLACK = (0, 0, 0, 255)
WIDTH = 640
HEIGHT = 480
def get_color(x, y):
if x**2 + y**2 < 100:
return BLACK
else:
return WHITE
def create_image(w, h):
image = Image.new("RGBA", (w, h), "white")
pixels = image.load()
for x in range(w):
for y in range(h):
pixels[x, y] = get_color(x, y)
return ImageTk.PhotoImage(image)
window = tk.Tk()
canvas = tk.Canvas(window, width=WIDTH, height=HEIGHT)
canvas.create_image(0, 0, image=create_image(WIDTH, HEIGHT))
canvas.pack(fill=tk.BOTH, expand=tk.TRUE)
window.mainloop()

Related

Convert Current Image to Black and White

The goal is to convert the current image in GUI window to black and white
Below is my code:
def BlackAndWhite(self):
from images import Image
LoadAFile = self.inputText.getText()
CurrentImage = open(LoadAFile)
image = self.image = PhotoImage(file = LoadAFile)
image.draw()
BlackAndWhite(image)
image.draw()
self.imageLabel["image"] = self.image
blackPixel = (0,0,0)
whitePixel = (255,255,255)
for y in range(image.getHeight()):
for x in range(image.getWidth()):
(r,g,b) = image.getPixel(x,y)
average = (r+b+g) /3
if average < 128:
image.setPixel(x,y,blackPixel)
else:
image.setPixel(x,y, whitePixel)
I am getting this error message:
image.draw()
AttributeError: 'PhotoImage' object has no attribute 'draw'
Here's working code, you should be able to tweak it to work with your work:
from tkinter import Tk, Canvas, NW
from PIL import ImageTk, Image
root = Tk()
canvas = Canvas(root, width=1000, height=1000)
canvas.pack()
img = Image.open("PATH_TO_AN_IMAGE")
blackPixel = (0, 0, 0)
whitePixel = (255, 255, 255)
for y in range(img.height):
for x in range(img.width):
pixelVal = img.getpixel((x, y))
# Unpacking in this way in case the pixel contains more than R, G, B (ex: a png)
r, g, b = pixelVal[0:3]
average = (r + b + g) / 3
if average < 128:
img.putpixel((x, y), blackPixel)
else:
img.putpixel((x, y), whitePixel)
photoimage = ImageTk.PhotoImage(img)
canvas.create_image((20, 20), anchor=NW, image=photoimage, state="normal")
root.mainloop()

Outline text on image in Python

I have been using PIL Image
I am trying to draw text on an image. I want this text to have a black outline like most memes. I've attempted to do this by drawing a shadow letter of a bigger font behind the letter in front. I've adjusted the x and y postions of the shadow accordingly. The shadow is slightly off though. The letter in front should be exactly in the middle of the shadow letter, but this isn't the case. The question mark certainly isn't centered horizontally, and all the letters are too low vertically. The outline also just doesn't look good.
Below is a minimum reproducible example to produce the image above.
Link to the font
Link to original image
from PIL import Image, ImageDraw, ImageFont
caption = "Why is the text slightly off?"
img = Image.open('./example-img.jpg')
d = ImageDraw.Draw(img)
x, y = 10, 400
font = ImageFont.truetype(font='./impact.ttf', size=50)
shadowFont = ImageFont.truetype(font='./impact.ttf', size=60)
for idx in range(0, len(caption)):
char = caption[idx]
w, h = font.getsize(char)
sw, sh = shadowFont.getsize(char) # shadow width, shadow height
sx = x - ((sw - w) / 2) # Shadow x
sy = y - ((sh - h) / 2) # Shadow y
# print(x,y,sx,sy,w,h,sw,sh)
d.text((sx, sy), char, fill="black", font=shadowFont) # Drawing the text
d.text((x, y), char, fill=(255,255,255), font=font) # Drawing the text
x += w + 5
img.save('example-output.jpg')
Another approach includes drawing the text 4 times in black behind the main text at positions slightly higher, slightly lower, slightly left, and slightly right, but these have also not been optimal as shown below
Code to produce the image above
from PIL import Image, ImageDraw, ImageFont
caption = "Why does the Y and i look weird?"
x, y = 10, 400
font = ImageFont.truetype(font='./impact.ttf', size=60)
img = Image.open('./example-img.jpg')
d = ImageDraw.Draw(img)
shadowColor = (0, 0, 0)
thickness = 4
d.text((x - thickness, y - thickness), caption, font=font, fill=shadowColor, thick=thickness)
d.text((x + thickness, y - thickness), caption, font=font, fill=shadowColor, thick=thickness)
d.text((x - thickness, y + thickness), caption, font=font, fill=shadowColor, thick=thickness)
d.text((x + thickness, y + thickness), caption, font=font, fill=shadowColor, thick=thickness)
d.text((x, y), caption, spacing=4, fill=(255, 255, 255), font=font) # Drawing the text
img.save('example-output.jpg')
I don't know since what version, but about a year ago Pillow added text stroking. You probably need to update it if you haven't do so lately. Example usage with stroke_width of 2:
from PIL import Image, ImageDraw, ImageFont
caption = 'I need to update my Pillow'
img = Image.open('./example-img.jpg')
d = ImageDraw.Draw(img)
font = ImageFont.truetype('impact.ttf', size=50)
d.text((10, 400), caption, fill='white', font=font,
stroke_width=2, stroke_fill='black')
img.save('example-output.jpg')
You can use mathlibplot text Stroke effect which uses PIL.
Example:
import matplotlib.pyplot as plt
import matplotlib.patheffects as path_effects
import matplotlib.image as mpimg
fig = plt.figure(figsize=(7, 5))
fig.figimage(mpimg.imread('seal.jpg'))
text = fig.text(0.5, 0.1, 'This text stands out because of\n'
'its black border.', color='white',
ha='center', va='center', size=30)
text.set_path_effects([path_effects.Stroke(linewidth=3, foreground='black'),
path_effects.Normal()])
plt.savefig('meme.png')
Result:
As #Abang pointed out, use stroke_width and stroke_fill.
Link for more details
Code:
from PIL import Image, ImageDraw, ImageFont
caption = 'Ans: stroke_width & stroke_fill'
img = Image.open('./example-img.jpg')
d = ImageDraw.Draw(img)
font = ImageFont.truetype('impact.ttf', size=50)
d.text((60, 400), caption, fill='white', font=font, spacing = 4, align = 'center',
stroke_width=4, stroke_fill='black')
img.save('example-output.jpg')

Transparent spritesheet has black background

I'm working on a game using Python and Pygame. I created a sprite sheet for one of the enemies and got my code for it working. The problem is that the image appears to have a black background even though it is a transparent image. The code for it is this:
enemySheet = pygame.image.load("resources/Alien.png").convert_alpha()
transColor = (255,255,255)
cells = []
for n in range(3):
width, height=(36,32)
rect = pygame.Rect(n * width, 0, width, height)
image = pygame.Surface(rect.size).convert_alpha()
image.blit(enemySheet, (0,0), rect)
cells.append(image)
enemyImg = cells[0]
enemyImg.set_colorkey(transColor)
enemy = enemyImg.get_rect()
enemy.center = (216,216)
I have already tried a few things but nothing has worked. Any ideas are welcome.
New surfaces are filled with black by default. If you want to make it transparent you can either add a fourth number to the transColor (the alpha value) and then fill the image,
transColor = (255,255,255,0)
# In the for loop.
image = pygame.Surface(rect.size).convert_alpha()
image.fill(transColor)
or just pass the pygame.SRCALPHA flag:
image = pygame.Surface(rect.size, pygame.SRCALPHA)
A nicer solution would be to use pygame.Surface.subsurface to cut the sheet:
for n in range(3):
width, height = (36, 32)
rect = pygame.Rect(n * width, 0, width, height)
image = enemySheet.subsurface(rect)
cells.append(image)

OpenCV & Python - Image too big to display

I have an image that is 6400 × 3200, while my screen is 1280 x 800. Therefore, the image needs to be resized for display only. I am using Python and OpenCV 2.4.9.
According to OpenCV Documentation,
If you need to show an image that is bigger than the screen resolution, you will need to call namedWindow("", WINDOW_NORMAL) before the imshow.
That is what I am doing, but the image is not fitted to the screen, only a portion is shown because it's too big. I've also tried with cv2.resizeWindow, but it doesn't make any difference.
import cv2
cv2.namedWindow("output", cv2.WINDOW_NORMAL) # Create window with freedom of dimensions
# cv2.resizeWindow("output", 400, 300) # Resize window to specified dimensions
im = cv2.imread("earth.jpg") # Read image
cv2.imshow("output", im) # Show image
cv2.waitKey(0) # Display the image infinitely until any keypress
Although I was expecting an automatic solution (fitting to the screen automatically), resizing solves the problem as well.
import cv2
cv2.namedWindow("output", cv2.WINDOW_NORMAL) # Create window with freedom of dimensions
im = cv2.imread("earth.jpg") # Read image
imS = cv2.resize(im, (960, 540)) # Resize image
cv2.imshow("output", imS) # Show image
cv2.waitKey(0) # Display the image infinitely until any keypress
The other answers perform a fixed (width, height) resize. If you wanted to resize to a specific size while maintaining aspect ratio, use this
def ResizeWithAspectRatio(image, width=None, height=None, inter=cv2.INTER_AREA):
dim = None
(h, w) = image.shape[:2]
if width is None and height is None:
return image
if width is None:
r = height / float(h)
dim = (int(w * r), height)
else:
r = width / float(w)
dim = (width, int(h * r))
return cv2.resize(image, dim, interpolation=inter)
Example
image = cv2.imread('img.png')
resize = ResizeWithAspectRatio(image, width=1280) # Resize by width OR
# resize = ResizeWithAspectRatio(image, height=1280) # Resize by height
cv2.imshow('resize', resize)
cv2.waitKey()
Use this for example:
cv2.namedWindow('finalImg', cv2.WINDOW_NORMAL)
cv2.imshow("finalImg",finalImg)
The only way resizeWindow worked for me was to have it after imshow. This is the order I'm using:
# Create a Named Window
cv2.namedWindow(win_name, cv2.WINDOW_NORMAL)
# Move it to (X,Y)
cv2.moveWindow(win_name, X, Y)
# Show the Image in the Window
cv2.imshow(win_name, image)
# Resize the Window
cv2.resizeWindow(win_name, width, height)
# Wait for <> miliseconds
cv2.waitKey(wait_time)
In OpenCV, cv2.namedWindow() just creates a window object, but doesn't resize the original image. You can use cv2.resize(img, resolution) to solve the problem.
Here's what it displays, a 740 * 411 resolution image.
image = cv2.imread("740*411.jpg")
cv2.imshow("image", image)
cv2.waitKey(0)
cv2.destroyAllWindows()
Here, it displays a 100 * 200 resolution image after resizing. Remember the resolution parameter use column first then is row.
image = cv2.imread("740*411.jpg")
image = cv2.resize(image, (200, 100))
cv2.imshow("image", image)
cv2.waitKey(0)
cv2.destroyAllWindows()
This code will resize the image so that it can retain it's aspect ratio and only ever take up a specified fraction of the screen area.
It will automatically adjust depending on your screen size and the size of the image.
Use the area variable to change the max screen area you want the image to be able to take up. The example shows it displayed at quarter the screen size.
import cv2
import tkinter as tk
from math import *
img = cv2.imread("test.jpg")
area = 0.25
h, w = img.shape[:2]
root = tk.Tk()
screen_h = root.winfo_screenheight()
screen_w = root.winfo_screenwidth()
vector = sqrt(area)
window_h = screen_h * vector
window_w = screen_w * vector
if h > window_h or w > window_w:
if h / window_h >= w / window_w:
multiplier = window_h / h
else:
multiplier = window_w / w
img = cv2.resize(img, (0, 0), fx=multiplier, fy=multiplier)
cv2.imshow("output", img)
cv2.waitKey(0)
I've also made a similar function where area is still a parameter but so is window height and window width.
If no area is input then it will use a defined height and width (window_h, window_w) of the window size you would like the image to fit inside.
If an input is given for all parameters then 'area' is prioritised.
import cv2
import tkinter as tk
from math import *
def resize_image(img, area=0.0, window_h=0, window_w=0):
h, w = img.shape[:2]
root = tk.Tk()
screen_h = root.winfo_screenheight()
screen_w = root.winfo_screenwidth()
if area != 0.0:
vector = math.sqrt(area)
window_h = screen_h * vector
window_w = screen_w * vector
if h > window_h or w > window_w:
if h / window_h >= w / window_w:
multiplier = window_h / h
else:
multiplier = window_w / w
img = cv2.resize(img, (0, 0), fx=multiplier, fy=multiplier)
return img
# using area
initial_image = cv2.imread("test.jpg")
resized_image = resize_image(initial_image, area=0.25))
cv2.imshow("output", resized_image)
cv2.waitKey(0)
# using window height and width
initial_image = cv2.imread("test.jpg")
resized_image = resize_image(initial_image, window_h = 480, window_w = 270))
cv2.imshow("output", resized_image)
cv2.waitKey(0)
Looks like opencv lib is pretty sensitive to parameters passed to the methods. The following code worked for me using opencv 4.3.0:
win_name = "visualization" # 1. use var to specify window name everywhere
cv2.namedWindow(win_name, cv2.WINDOW_NORMAL) # 2. use 'normal' flag
img = cv2.imread(filename)
h,w = img.shape[:2] # suits for image containing any amount of channels
h = int(h / resize_factor) # one must compute beforehand
w = int(w / resize_factor) # and convert to INT
cv2.resizeWindow(win_name, w, h) # use variables defined/computed BEFOREHAND
cv2.imshow(win_name, img)
Try this:
image = cv2.imread("img/Demo.jpg")
image = cv2.resize(image,(240,240))
The image is now resized. Displaying it will render in 240x240.
The cv2.WINDOW_NORMAL option works correctly but the first time it displays the window in an standard size.
If you resize the window like any other windows in your computer, by position the mouse over the edge of the window you want to resize and then drag the mouse to the position you want. If you do this to both width and height of the window to the size you want to obtain.
The following times you refresh the window, by executing the code, OpenCV will generate the window with the size of the last time it was shown or modified.
Try this code:
img = cv2.imread("Fab2_0.1 X 1.03MM GRID.jpg", cv2.IMREAD_GRAYSCALE)
image_scale_down = 3
x = (int)(img.shape[0]/image_scale_down)
y = (int)(img.shape[1]/image_scale_down)
image = cv2.resize(img, (x,y))
cv2.imshow("image_title", image)
cv2.waitKey(5000)
cv2.destroyAllWindows()
The most upvote answer is perfect !
I just add my code for those who want some "dynamic" resize handling depending of the ratio.
import cv2
from win32api import GetSystemMetrics
def get_resized_for_display_img(img):
screen_w, screen_h = GetSystemMetrics(0), GetSystemMetrics(1)
print("screen size",screen_w, screen_h)
h,w,channel_nbr = img.shape
# img get w of screen and adapt h
h = h * (screen_w / w)
w = screen_w
if h > screen_h: #if img h still too big
# img get h of screen and adapt w
w = w * (screen_h / h)
h = screen_h
w, h = w*0.9, h*0.9 # because you don't want it to be that big, right ?
w, h = int(w), int(h) # you need int for the cv2.resize
return cv2.resize(img, (w, h))
Try this code
img = cv2.resize(img,(1280,800))
Try with this code:
from PIL import Image
Image.fromarray(image).show()

python pil draw text offset

I tried to draw a character on an image with Python PIL. With the function ImageDraw.Draw.text(), the xy parameter points to the left-top corner of text. However I set xy to (0,0), the character haven't been draw the the left-top of images.
from PIL import ImageFont, ImageDraw, Image
imageSize=(40,40)
mage = Image.new("RGB", imageSize, (0,0,0))
draw = ImageDraw.Draw(image)
txt = "J"
font = ImageFont.truetype("ANTQUAB.ttf",35)
draw.text((0,0), txt, font=font)
why?
The xy parameter of draw.text() is the top left corner of the text (http://pillow.readthedocs.io/en/3.1.x/reference/ImageDraw.html), however the font might have some padding around the text, especially vertically. What I did was set the y part of the tuple to a negative number (maybe somewhere around -5?) and it worked for me.
This code:
from PIL import ImageFont, ImageDraw, Image
imageSize=(100,100)
image = Image.new("RGB", imageSize, (0,0,0))
draw = ImageDraw.Draw(image)
txt = "J"
font = ImageFont.truetype("ARIAL.ttf",35)
draw.text((0,0), txt, font=font)
image.show()
generates this:
Is this not what you were expecting?
Looks like there are font-specific offsets; you can get the vertical offset from the "top" value returned from FreeTypeFont.getbbox(). Subtracting this offset from your y-coordinate on the draw.text call will align top of text with top of image.
from PIL import ImageFont, ImageDraw, Image
text = 'J'
font = "arial.ttf"
fontsize = 12
img_w = 100
img_h = 100
canvas = Image.new('RGBA', size=(img_w,img_h), color='white')
draw = ImageDraw.Draw(canvas)
y_text = 0
font_obj = ImageFont.truetype(font, fontsize)
(left, top, right, bottom) = font_obj.getbbox(text)
x_pos = 0
y_pos = 0
# offset the y coordinate in the draw call by the "top" parameter fromgetbbox; this is the font-specific text padding
draw.text(xy=(x_pos, y_pos-top),
text=text,
font=font_obj,
align='left',
fill='black')
canvas.show()

Categories

Resources