Crop an image in the centre using PIL - python

How can I crop an image in the center? Because I know that the box is a 4-tuple defining the left, upper, right, and lower pixel coordinate but I don't know how to get these coordinates so it crops in the center.

Assuming you know the size you would like to crop to (new_width X new_height):
import Image
im = Image.open(<your image>)
width, height = im.size # Get dimensions
left = (width - new_width)/2
top = (height - new_height)/2
right = (width + new_width)/2
bottom = (height + new_height)/2
# Crop the center of the image
im = im.crop((left, top, right, bottom))
This will break if you attempt to crop a small image larger, but I'm going to assume you won't be trying that (Or that you can catch that case and not crop the image).

One potential problem with the proposed solution is in the case there is an odd difference between the desired size, and old size. You can't have a half pixel on each side. One has to choose a side to put an extra pixel on.
If there is an odd difference for the horizontal the code below will put the extra pixel to the right, and if there is and odd difference on the vertical the extra pixel goes to the bottom.
import numpy as np
def center_crop(img, new_width=None, new_height=None):
width = img.shape[1]
height = img.shape[0]
if new_width is None:
new_width = min(width, height)
if new_height is None:
new_height = min(width, height)
left = int(np.ceil((width - new_width) / 2))
right = width - int(np.floor((width - new_width) / 2))
top = int(np.ceil((height - new_height) / 2))
bottom = height - int(np.floor((height - new_height) / 2))
if len(img.shape) == 2:
center_cropped_img = img[top:bottom, left:right]
else:
center_cropped_img = img[top:bottom, left:right, ...]
return center_cropped_img

I feel like the simplest solution that is most suitable for most applications is still missing. The accepted answer has an issue with uneven pixels and especially for ML algorithms, the pixel count of the cropped image is paramount.
In the following example, I would like to crop an image to 224/100, from the center. I do not care if the pixels are shifted to the left or right by 0.5, as long as the output picture will always be of the defined dimensions. It avoids the reliance on math.*.
from PIL import Image
import matplotlib.pyplot as plt
im = Image.open("test.jpg")
left = int(im.size[0]/2-224/2)
upper = int(im.size[1]/2-100/2)
right = left +224
lower = upper + 100
im_cropped = im.crop((left, upper,right,lower))
print(im_cropped.size)
plt.imshow(np.asarray(im_cropped))
The output is before cropping (not shown in code):
after:
The touples show the dimensions.

This is the function I was looking for:
from PIL import Image
im = Image.open("test.jpg")
crop_rectangle = (50, 50, 200, 200)
cropped_im = im.crop(crop_rectangle)
cropped_im.show()
Taken from another answer

I originally used the accepted answer:
import Image
im = Image.open(<your image>)
width, height = im.size # Get dimensions
left = (width - new_width)/2
top = (height - new_height)/2
right = (width + new_width)/2
bottom = (height + new_height)/2
# Crop the center of the image
im = im.crop((left, top, right, bottom))
But I came into the problem mentioned by Dean Pospisil
One potential problem with the proposed solution is in the case there
is an odd difference between the desired size, and old size. You can't
have a half pixel on each side. One has to choose a side to put an
extra pixel on.
Dean Pospisil's solution works, I also came up with my own calculation to fix this:
import Image
im = Image.open(<your image>)
width, height = im.size # Get dimensions
left = round((width - new_width)/2)
top = round((height - new_height)/2)
x_right = round(width - new_width) - left
x_bottom = round(height - new_height) - top
right = width - x_right
bottom = height - x_bottom
# Crop the center of the image
im = im.crop((left, top, right, bottom))
With the accepted answer, an image of 180px x 180px to be cropped to 180px x 101px will result in a cropped image to 180px x 102px.
With my calculation, it will be correctly cropped to 180px x 101px

Crop center and around:
def im_crop_around(img, xc, yc, w, h):
img_width, img_height = img.size # Get dimensions
left, right = xc - w / 2, xc + w / 2
top, bottom = yc - h / 2, yc + h / 2
left, top = round(max(0, left)), round(max(0, top))
right, bottom = round(min(img_width - 0, right)), round(min(img_height - 0, bottom))
return img.crop((left, top, right, bottom))
def im_crop_center(img, w, h):
img_width, img_height = img.size
left, right = (img_width - w) / 2, (img_width + w) / 2
top, bottom = (img_height - h) / 2, (img_height + h) / 2
left, top = round(max(0, left)), round(max(0, top))
right, bottom = round(min(img_width - 0, right)), round(min(img_height - 0, bottom))
return img.crop((left, top, right, bottom))

May be i am late to this party but at least i am here
I want to center crop the image convert 9:16 image to 16:9 portrait to landscape
This is the algo i used :
divide image in 4 equal parts
discard part 1 and part four
Set left to 0, right to width of image
code :
from PIL import Image
im = Image.open('main.jpg')
width, height = im.size
if height > width:
h2 = height/2
h4 = h2/2
border = (0, h4, width, h4*3)
cropped_img = im.crop(border)
cropped_img.save("test.jpg")
before :
after:
I hope this helps

You could use Torchvision's CenterCrop transformation for this. Here's an example
from PIL import Image
from torchvision.transforms import functional as F
crop_size = 256 # can be either an integer or a tuple of ints for (height, width) separately
img = Image.open(<path_to_your_image>)
cropped_img = F.center_crop(img, crop_size)
F.center_crop works with torch.Tensors or PIL.Images and retains the data type i.e. when input is a PIL.Image then output is also a (cropped) PIL.Image. An added bonus is that the above transformation would automatically apply padding in case the input image size is smaller than the requested crop size.

Related

Resize image to use as a blurred background but with a zoom aspect in relation to the original image

The original image is this:
Using this website tool with these settings:
The final result that I am trying to reproduce (the only difference is that I'm going to add a readjustment to the image that is on top, have 1080 width too) here is this:
It is clear that there is a zoom effect in the blurred image, so when I use resize to keep the aspect ratio and the highest quality possible, I use im = im.resize((1080,1080), resample=Image.Resampling.LANCZOS), but as I don't want that, I removed resample=Image.Resampling.LANCZOS imagining that the image would be generated without proportion generating a zoom:
from PIL import Image, ImageFilter, ImageChops
import numpy
import cv2
def remove_border(file_img):
im = Image.open(file_img)
bg = Image.new("RGB", im.size, im.getpixel((0,0)))
diff = ImageChops.difference(im.convert("RGB"), bg)
diff = ImageChops.add(diff, diff, 2.0, -30)
bbox = diff.getbbox()
if bbox:
return im.crop(bbox)
def resize_blur(img_blur,sizers):
img_blur = img_blur.resize(sizers, resample=Image.Resampling.LANCZOS)
img_blur = img_blur.filter(ImageFilter.GaussianBlur(10))
return img_blur
def resize_width_main(img_border,size_width):
img_width = img_border
basewidth = size_width
wpercent = (basewidth/float(img_width.size[0]))
hsize = int((float(img_width.size[1])*float(wpercent)))
img_width = img_width.resize((basewidth,hsize), Image.Resampling.LANCZOS)
return img_width
def center_overlay(name_file,overlay,background):
img = numpy.asarray(overlay)
img = cv2.cvtColor(img, cv2.COLOR_RGB2BGR)
h, w = img.shape[:2]
back = numpy.asarray(background)
back = cv2.cvtColor(back, cv2.COLOR_RGB2BGR)
hh, ww = back.shape[:2]
yoff = round((hh-h)/2)
xoff = round((ww-w)/2)
result = back.copy()
result[yoff:yoff+h, xoff:xoff+w] = img
cv2.imwrite(name_file, result)
def main():
img_border = remove_border('resized_download.png')
img_blur = resize_blur(img_border, (1080,1080))
img_width = resize_width_main(img_border, 1080)
center_overlay('resized.png', img_width, img_blur)
if __name__ == '__main__':
main()
But the current result is this:
A few observations.
Your remove_border function isn't doing a lot; when I tried it with your test image, all it did was remove 9 pixels from the left side. Maybe it has a bug. At the risk of ignoring the lesson of Chesterton's Fence I'd say you don't need it and could eliminate it entirely.
Blurring before you resize makes more sense than doing it the other way around.
When you resize a blurred image, you don't need to worry much about the quality of the resampling filter. I wouldn't use NEAREST, but BILINEAR should be perfectly adequate and reasonably fast.
When you resize an image, the old and new size should have the same aspect ratio or you will get distortion. The resample method makes no difference in this. You are resizing a rectangle into a square, resulting in bad distortion. You can crop your original to the desired aspect ratio before you resize, or crop it after you resize. It will be more efficient to crop first.
def resize_to_target(im, target_size, resample=Image.BILINEAR):
''' Resize an image to a target size. If the aspect ratio
of the original image is different from the target, the
image will be cropped to the destination aspect ratio
before resizing.
'''
if (im.size[0] / im.size[1]) < (target_size[0] / target_size[1]):
# if (im.size[0] * target_size[1]) != (target_size[0] * im.size[1]):
# Existing image is narrower, crop top and bottom
crop_height = round(im.size[0] * target_size[1] / target_size[0])
if crop_height < im.size[1]:
top = (im.size[1] - crop_height) // 2
bottom = top + crop_height
im = im.crop((0, top, im.size[0], bottom))
else:
# existing image is wider, crop left and right
crop_width = round(im.size[1] * target_size[0] / target_size[1])
if crop_width < im.size[0]:
left = (im.size[0] - crop_width) // 2
right = left + crop_width
im = im.crop((left, 0, right, im.size[1]))
return im.resize(target_size, resample=resample)

Circle is not perfect PIL

I'm kinda new with the PIL was wondering why my circle is not perfect. Is there a fix for this? Thanks.
here's my code:
avatar_image = avatar_image.resize((128, 128))
avatar_size = (avatar_image.size[0] * 3, avatar_image.size[1] * 3)
circle_image = Image.new('L', avatar_size, 0)
circle_draw = ImageDraw.Draw(circle_image)
circle_draw.ellipse((0, 0) + avatar_size, fill=255)
mask = circle_image.resize(avatar_image.size, Image.ANTIALIAS)
avatar_image.putalpha(mask)
final = ImageOps.fit(avatar_image, mask.size, centering=(0.5, 0.5))
final.putalpha(mask)
final.show()
Draw Circle: right side of the circle looks off
Circle with Picture:
You have an off-by-one error, commonly caused by a confusion between size and position which is the case here too.
image.new takes a width and height in number of pixels.
circle_draw.ellipse takes a start and end position, which is based on a 0-indexed grid.
To get a full circle you need to make the circle one pixel smaller than it is now to fit inside circle_image

How to keep image alignment when cropping in Python?

I'm trying to crop and resize images in Python, and I want them to be in a fixed format
afterwards (47x62 Pixels). However, if the original image is in landscape, my algorithm doesn't work, there are blank areas.
import Image, sys
MAXSIZEX = 47
MAXSIZEY = 62
im = Image.open(sys.argv[1])
(width, height) = im.size
ratio = 1. * MAXSIZEX / MAXSIZEY
im = im.crop((0, 0, int(width*ratio), int(height*ratio)))
im = im.resize((MAXSIZEX, MAXSIZEY), Image.ANTIALIAS)
im.save(sys.argv[2])
I want the resized image to be fully 47x62 - there should be no empty area visible.
You should first check if MAXSIZEX is greater then the width or the MAXSIZEY is greater than the height. If they are first rescale the image and then do the cropping:
MAXSIZEX = 64
MAXSIZEY = 42
width, height = im.size
xrat = width / float(MAXSIZEX)
yrat = height / float(MAXSIZEY)
if xrat < 1 or yrat < 1:
rat = min(xrat, yrat)
im = im.resize((int(width / rat), int(height / rat)))
res = im.crop((0, 0, MAXSIZEX, MAXSIZEY))
res.show()
Chosing x/y as the scaling is an implicit assumption that your source's y dimension will always be smaller relative to your target resolution than your source's x dimension. First, figure out which dimension to scale on, then crop:
width_count = float(width) / MAXSIZEX
height_count = float(height) / MAXSIZEY
if width_count == height_count:
pass
elif width_count < height_count:
im = im.crop(0, 0, width, int(width_count * height / height_count))
else:
im = im.crop(0, 0, int(height_count * width / width_count), height)
Now you know you have the largest subimage from the original that matches your target aspect ratio, so you can resize without warping the image.

Adding borders to an image using python

I have a large number of images of a fixed size (say 500*500). I want to write a python script which will resize them to a fixed size (say 800*800) but will keep the original image at the center and fill the excess area with a fixed color (say black).
I am using PIL. I can resize the image using the resize function now, but that changes the aspect ratio. Is there any way to do this?
You can create a new image with the desired new size, and paste the old image in the center, then saving it. If you want, you can overwrite the original image (are you sure? ;o)
import Image
old_im = Image.open('someimage.jpg')
old_size = old_im.size
new_size = (800, 800)
new_im = Image.new("RGB", new_size) ## luckily, this is already black!
box = tuple((n - o) // 2 for n, o in zip(new_size, old_size))
new_im.paste(old_im, box)
new_im.show()
# new_im.save('someimage.jpg')
You can also set the color of the new border with a third argument of Image.new() (for example: Image.new("RGB", new_size, "White"))
Yes, there is.
Make something like this:
from PIL import Image, ImageOps
ImageOps.expand(Image.open('original-image.png'),border=300,fill='black').save('imaged-with-border.png')
You can write the same at several lines:
from PIL import Image, ImageOps
img = Image.open('original-image.png')
img_with_border = ImageOps.expand(img,border=300,fill='black')
img_with_border.save('imaged-with-border.png')
And you say that you have a list of images. Then you must use a cycle to process all of them:
from PIL import Image, ImageOps
for i in list-of-images:
img = Image.open(i)
img_with_border = ImageOps.expand(img,border=300,fill='black')
img_with_border.save('bordered-%s' % i)
Alternatively, if you are using OpenCV, they have a function called copyMakeBorder that allows you to add padding to any of the sides of an image. Beyond solid colors, they've also got some cool options for fancy borders like reflecting or extending the image.
import cv2
img = cv2.imread('image.jpg')
color = [101, 52, 152] # 'cause purple!
# border widths; I set them all to 150
top, bottom, left, right = [150]*4
img_with_border = cv2.copyMakeBorder(img, top, bottom, left, right, cv2.BORDER_CONSTANT, value=color)
Sources: OpenCV border tutorial and
OpenCV 3.1.0 Docs for copyMakeBorder
PIL's crop method can actually handle this for you by using numbers that are outside the bounding box of the original image, though it's not explicitly stated in the documentation. Negative numbers for left and top will add black pixels to those edges, while numbers greater than the original width and height for right and bottom will add black pixels to those edges.
This code accounts for odd pixel sizes:
from PIL import Image
with Image.open('/path/to/image.gif') as im:
old_size = im.size
new_size = (800, 800)
if new_size > old_size:
# Set number of pixels to expand to the left, top, right,
# and bottom, making sure to account for even or odd numbers
if old_size[0] % 2 == 0:
add_left = add_right = (new_size[0] - old_size[0]) // 2
else:
add_left = (new_size[0] - old_size[0]) // 2
add_right = ((new_size[0] - old_size[0]) // 2) + 1
if old_size[1] % 2 == 0:
add_top = add_bottom = (new_size[1] - old_size[1]) // 2
else:
add_top = (new_size[1] - old_size[1]) // 2
add_bottom = ((new_size[1] - old_size[1]) // 2) + 1
left = 0 - add_left
top = 0 - add_top
right = old_size[0] + add_right
bottom = old_size[1] + add_bottom
# By default, the added pixels are black
im = im.crop((left, top, right, bottom))
Instead of the 4-tuple, you could instead use a 2-tuple to add the same number of pixels on the left/right and top/bottom, or a 1-tuple to add the same number of pixels to all sides.
It is important to consider old dimension, new dimension and their difference here. If the difference is odd (not even), you will need to specify slightly different values for left, top, right and bottom borders.
Assume the old dimension is ow,oh and new one is nw,nh.
So, this would be the answer:
import Image, ImageOps
img = Image.open('original-image.png')
deltaw=nw-ow
deltah=nh-oh
ltrb_border=(deltaw/2,deltah/2,deltaw-(deltaw/2),deltah-(deltah/2))
img_with_border = ImageOps.expand(img,border=ltrb_border,fill='black')
img_with_border.save('imaged-with-border.png')
You can load the image with scipy.misc.imread as a numpy array. Then create an array with the desired background with numpy.zeros((height, width, channels)) and paste the image at the desired location:
import numpy as np
import scipy.misc
im = scipy.misc.imread('foo.jpg', mode='RGB')
height, width, channels = im.shape
# make canvas
im_bg = np.zeros((height, width, channels))
im_bg = (im_bg + 1) * 255 # e.g., make it white
# Your work: Compute where it should be
pad_left = ...
pad_top = ...
im_bg[pad_top:pad_top + height,
pad_left:pad_left + width,
:] = im
# im_bg is now the image with the background.
ximg = Image.open(qpath)
xwid,xhgt = func_ResizeImage(ximg)
qpanel_3 = tk.Frame(Body,width=xwid+10,height=xhgt+10,bg='white',bd=5)
ximg = ximg.resize((xwid,xhgt),Image.ANTIALIAS)
ximg = ImageTk.PhotoImage(ximg)
panel = tk.Label(qpanel_3,image=ximg)
panel.image = ximg
panel.grid(row = 2)
from PIL import Image
from PIL import ImageOps
img = Image.open("dem.jpg").convert("RGB")
This part will add black borders at the sides (10% of width)
img_side = ImageOps.expand(img, border=(int(0.1*img.size[0]),0,int(0.1*img.size[0]),0), fill=(0,0,0))
img_side.save("sunset-sides.jpg")
This part will add black borders at the bottom & top (10% of height)
img_updown = ImageOps.expand(img, border=(0,int(0.1*img.size[1]),0,int(0.1*img.size[1])), fill=(0,0,0))
img_updown.save("sunset-top_bottom.jpg")
This part will add black borders at the bottom,top & sides (10% of height-width)
img_updown_side = ImageOps.expand(img, border=(int(0.1*img.size[0]),int(0.1*img.size[1]),int(0.1*img.size[0]),int(0.1*img.size[1])), fill=(0,0,0))
img_updown_side.save("sunset-all_sides.jpg")
img.close()
img_side.close()
img_updown.close()
img_updown_side.close()

Resize image maintaining aspect ratio AND making portrait and landscape images exact same size?

Currently I am using:
os.chdir(album.path)
images = glob.glob('*.*')
# thumbs size
size = 80,80
for image in images:
#create thumb
file, ext = os.path.splitext(image)
im = Image.open(os.path.join(album.path,image))
im.thumbnail(size, Image.ANTIALIAS)
thumb_path = os.path.join(album.path, 'thumbs', file + ".thumb" + ".jpeg")
im.save(thumb_path)
Although this works, I end up with different sizes images (some are portrait and some are landscape), but I want all of the images to have an exact size. Maybe a sensible cropping?
UPDATE:
I don't mind cropping a small portion of the image. When I said sensible cropping I mean something like this algorythm:
if image is portrait:
make width 80px
crop the height (will be more than 80px)
else if image is landscape:
make height 80px
crop the width to 80px (will be more than 80px)
Here is my take on doing a padded fit for an image:
#!/usr/bin/env python
from PIL import Image, ImageChops
F_IN = "/path/to/image_in.jpg"
F_OUT = "/path/to/image_out.jpg"
size = (80,80)
image = Image.open(F_IN)
image.thumbnail(size, Image.ANTIALIAS)
image_size = image.size
thumb = image.crop( (0, 0, size[0], size[1]) )
offset_x = max( (size[0] - image_size[0]) / 2, 0 )
offset_y = max( (size[1] - image_size[1]) / 2, 0 )
thumb = ImageChops.offset(thumb, offset_x, offset_y)
thumb.save(F_OUT)
It first uses the thumbnail operation to bring the image down to within your original bounds and preserving the aspect. Then it crops it back out to actually fill the size of your bounds (since unless the original image was square, it will be smaller now), and we find the proper offset to center the image. The image is offset to the center, so you end up with black padding but no image cropping.
Unless you can make a really sensible guess at a proper center crop without losing possible important image data on the edges, a padded fit approach will work better.
Update
Here is a version that can do either center crop or pad fit.
#!/usr/bin/env python
from PIL import Image, ImageChops, ImageOps
def makeThumb(f_in, f_out, size=(80,80), pad=False):
image = Image.open(f_in)
image.thumbnail(size, Image.ANTIALIAS)
image_size = image.size
if pad:
thumb = image.crop( (0, 0, size[0], size[1]) )
offset_x = max( (size[0] - image_size[0]) / 2, 0 )
offset_y = max( (size[1] - image_size[1]) / 2, 0 )
thumb = ImageChops.offset(thumb, offset_x, offset_y)
else:
thumb = ImageOps.fit(image, size, Image.ANTIALIAS, (0.5, 0.5))
thumb.save(f_out)
source = "/path/to/source/image.JPG"
makeThumb(source, "/path/to/source/image_padded.JPG", pad=True)
makeThumb(source, "/path/to/source/image_centerCropped.JPG", pad=False)
Obviously, you would need to crop or pad the images. You could do something like below to get a maximal centered crop according to the aspect ratio of the thumbnails (untested):
aspect = lambda size: float(size[0]) / float(size[1])
sa = aspect(size)
if aspect(im.size) > sa:
width = int(sa * im.size[1])
left = (im.size[0] - width) / 2
im = im.crop((left, 0, left + width, im.size[1]))
else:
height = int(im.size[0] / sa)
top = (im.size[1] - height) / 2
im = im.crop((0, top, im.size[0], top + height))
im.thumbnail(size, Image.ANTIALIAS)
If you use easy-thumbnails you'll need to set crop to True and upscale to True to always fill-up the space (have the exact same dimensions).
Ex: makes image_2 fits in image_1 dimensions:
thumbnailer = get_thumbnailer(image_2)
thumbnail = thumbnailer.generate_thumbnail(thumbnail_options={
'crop': True,
'upscale': True,
'size': image_1.size
})
image_2 = thumbnail.image

Categories

Resources