Resize rectangular image to square, keeping ratio and fill background with black
Question:
I’m trying to resize a batch of grayscale images that are 256 x N pixels (N varies, but is always ≤256).
My intention is to downscale the images.
The resize would have to output a square (1:1) image, with:
- resized image centered vertically
- aspect ratio maintained
- remaining pixels rendered black
Visually this would be the desired result:
I have tried creating a numpy zeroes matrix with the target size (e.g. 200 x 200) but have not been able to paste the resized image into its vertical center.
Any suggestions using cv2, PIL or numpy are welcome.
Answers:
PIL has the thumbnail method which will scale keeping the aspect ratio. From there you just need to paste it centered onto your black background rectangle.
from PIL import Image
def black_background_thumbnail(path_to_image, thumbnail_size=(200,200)):
background = Image.new('RGBA', thumbnail_size, "black")
source_image = Image.open(path_to_image).convert("RGBA")
source_image.thumbnail(thumbnail_size)
(w, h) = source_image.size
background.paste(source_image, ((thumbnail_size[0] - w) / 2, (thumbnail_size[1] - h) / 2 ))
return background
if __name__ == '__main__':
img = black_background_thumbnail('hLARP.png')
img.save('tmp.jpg')
img.show()
You can use Pillow
to accomplish that:
Code:
from PIL import Image
def make_square(im, min_size=256, fill_color=(0, 0, 0, 0)):
x, y = im.size
size = max(min_size, x, y)
new_im = Image.new('RGBA', (size, size), fill_color)
new_im.paste(im, (int((size - x) / 2), int((size - y) / 2)))
return new_im
Test Code:
test_image = Image.open('hLarp.png')
new_image = make_square(test_image)
new_image.show()
For a white background you can do:
new_image = make_square(test_image, fill_color=(255, 255, 255, 0))
Result:
from PIL import Image
def reshape(image):
'''
Reshapes the non-square image by pasting
it to the centre of a black canvas of size
n*n where n is the biggest dimension of
the non-square image.
'''
old_size = image.size
max_dimension, min_dimension = max(old_size), min(old_size)
desired_size = (max_dimension, max_dimension)
position = int(max_dimension/2) - int(min_dimension/2)
blank_image = Image.new("RGB", desired_size, color='black')
if image.height<image.width:
blank_image.paste(image, (0, position))
else:
blank_image.paste(image, (position, 0))
return blank_image
Here is a code that solve your question with OPENCV module (using NUMPY module too)
#Importing modules opencv + numpy
import cv2
import numpy as np
#Reading an image (you can use PNG or JPG)
img = cv2.imread("image.png")
#Getting the bigger side of the image
s = max(img.shape[0:2])
#Creating a dark square with NUMPY
f = np.zeros((s,s,3),np.uint8)
#Getting the centering position
ax,ay = (s - img.shape[1])//2,(s - img.shape[0])//2
#Pasting the 'image' in a centering position
f[ay:img.shape[0]+ay,ax:ax+img.shape[1]] = img
#Showing results (just in case)
cv2.imshow("IMG",f)
#A pause, waiting for any press in keyboard
cv2.waitKey(0)
#Saving the image
cv2.imwrite("img2square.png",f)
cv2.destroyAllWindows()
Behold! A greatly-overengineered version of @Stepeh Rauch’s answer that contains an interactive element and accounts for odd-pixel padding.
Usage
# Note: PySide2 can also be replaced by PyQt5, PyQt6, PySide6
# Also note! Any of the above are >100MB
pip install utilitys pyside2 pillow
$ python <file.py> --help
usage: <file>.py [-h] [--folder FOLDER] [--ext EXT]
optional arguments:
-h, --help show this help message and exit
--folder FOLDER Folder of images allowed for viewing. Must have at least one image (default: .)
--ext EXT Image extension to look for (default: png)
$ python <file>.py --folder "./path/to/folder/of/your/image(s).png" --ext "jpg"
file.py
contents
import argparse
from pathlib import Path
from typing import Tuple, Union, Any
import numpy as np
import pyqtgraph as pg
from PIL import Image
from utilitys import fns, widgets, RunOpts
def pad_to_size(
image: Image.Image,
size_wh: Union[int, Tuple[int, int]] = None,
fill_color: Any = 0,
**resize_kwargs,
) -> Image.Image:
"""
Keeps an image's aspect ratio by resizing until the largest side is constrained
by the specified output size. Then, the deficient dimension is padded until
the image is the specified size.
"""
if size_wh is None:
size_wh = max(image.size)
if isinstance(size_wh, int):
size_wh = (size_wh, size_wh)
im_size_wh = np.array(image.size)
ratios = im_size_wh / size_wh
# Resize until the largest side is constrained by the specified output size
im_size_wh = np.ceil(im_size_wh / ratios.max()).astype(int)
# Prefer 1-pixel difference in aspect ratio vs. odd padding
pad_amt = np.array(size_wh) - im_size_wh
use_ratio_idx = np.argmax(ratios)
unused_ratio_idx = 1 - use_ratio_idx
# Sanity check for floating point accuracy: At least one side must match
# user-requested dimension
if np.all(pad_amt != 0):
# Adjust dimension that is supposed to match
im_size_wh[use_ratio_idx] += pad_amt[use_ratio_idx]
# Prefer skewing aspect ratio by 1 pixel instead of odd padding
# If odd, 1 will be added. Otherwise, the dimension remains unchanged
im_size_wh[unused_ratio_idx] += pad_amt[unused_ratio_idx] % 2
image = image.resize(tuple(im_size_wh), **resize_kwargs)
new_im = Image.new("RGB", size_wh, fill_color)
width, height = image.size
new_im.paste(image, (int((size_wh[0] - width) / 2), int((size_wh[1] - height) / 2)))
return new_im
def main(folder=".", ext="png"):
"""
Parameters
----------
folder: str, Path
Folder of images allowed for viewing. Must have at least one image
ext: str, Path
Image extension to look for
"""
folder = Path(folder)
files = fns.naturalSorted(folder.glob(f"*.{ext}"))
err_msg = f"{folder} must have at least one image file with extension `{ext}`"
assert len(files), err_msg
pg.mkQApp()
viewer = widgets.ImageViewer()
def readim(file_index=0, try_pad=False, output_w=512, output_h=512):
if 0 > file_index > len(files):
return
image = Image.open(files[file_index])
if try_pad:
image = pad_to_size(image, (output_w, output_h), fill_color=(255, 255, 255))
viewer.setImage(np.array(image))
viewer.toolsEditor.registerFunc(readim, runOpts=RunOpts.ON_CHANGED)
wc = viewer.widgetContainer()
readim()
wc.show()
pg.exec()
if __name__ == "__main__":
# Print defaults in help signature
fmt = dict(formatter_class=argparse.ArgumentDefaultsHelpFormatter)
cli = fns.makeCli(main, parserKwargs=fmt)
args = cli.parse_args()
main(**vars(args))
from PIL import Image, ImageOps
with Image.open('hLARP.png') as im:
im = ImageOps.pad(im, (200, 200), color='black')
im.save('result.png')
I’m trying to resize a batch of grayscale images that are 256 x N pixels (N varies, but is always ≤256).
My intention is to downscale the images.
The resize would have to output a square (1:1) image, with:
- resized image centered vertically
- aspect ratio maintained
- remaining pixels rendered black
Visually this would be the desired result:
I have tried creating a numpy zeroes matrix with the target size (e.g. 200 x 200) but have not been able to paste the resized image into its vertical center.
Any suggestions using cv2, PIL or numpy are welcome.
PIL has the thumbnail method which will scale keeping the aspect ratio. From there you just need to paste it centered onto your black background rectangle.
from PIL import Image
def black_background_thumbnail(path_to_image, thumbnail_size=(200,200)):
background = Image.new('RGBA', thumbnail_size, "black")
source_image = Image.open(path_to_image).convert("RGBA")
source_image.thumbnail(thumbnail_size)
(w, h) = source_image.size
background.paste(source_image, ((thumbnail_size[0] - w) / 2, (thumbnail_size[1] - h) / 2 ))
return background
if __name__ == '__main__':
img = black_background_thumbnail('hLARP.png')
img.save('tmp.jpg')
img.show()
You can use Pillow
to accomplish that:
Code:
from PIL import Image
def make_square(im, min_size=256, fill_color=(0, 0, 0, 0)):
x, y = im.size
size = max(min_size, x, y)
new_im = Image.new('RGBA', (size, size), fill_color)
new_im.paste(im, (int((size - x) / 2), int((size - y) / 2)))
return new_im
Test Code:
test_image = Image.open('hLarp.png')
new_image = make_square(test_image)
new_image.show()
For a white background you can do:
new_image = make_square(test_image, fill_color=(255, 255, 255, 0))
Result:
from PIL import Image
def reshape(image):
'''
Reshapes the non-square image by pasting
it to the centre of a black canvas of size
n*n where n is the biggest dimension of
the non-square image.
'''
old_size = image.size
max_dimension, min_dimension = max(old_size), min(old_size)
desired_size = (max_dimension, max_dimension)
position = int(max_dimension/2) - int(min_dimension/2)
blank_image = Image.new("RGB", desired_size, color='black')
if image.height<image.width:
blank_image.paste(image, (0, position))
else:
blank_image.paste(image, (position, 0))
return blank_image
Here is a code that solve your question with OPENCV module (using NUMPY module too)
#Importing modules opencv + numpy
import cv2
import numpy as np
#Reading an image (you can use PNG or JPG)
img = cv2.imread("image.png")
#Getting the bigger side of the image
s = max(img.shape[0:2])
#Creating a dark square with NUMPY
f = np.zeros((s,s,3),np.uint8)
#Getting the centering position
ax,ay = (s - img.shape[1])//2,(s - img.shape[0])//2
#Pasting the 'image' in a centering position
f[ay:img.shape[0]+ay,ax:ax+img.shape[1]] = img
#Showing results (just in case)
cv2.imshow("IMG",f)
#A pause, waiting for any press in keyboard
cv2.waitKey(0)
#Saving the image
cv2.imwrite("img2square.png",f)
cv2.destroyAllWindows()
Behold! A greatly-overengineered version of @Stepeh Rauch’s answer that contains an interactive element and accounts for odd-pixel padding.
Usage
# Note: PySide2 can also be replaced by PyQt5, PyQt6, PySide6
# Also note! Any of the above are >100MB
pip install utilitys pyside2 pillow
$ python <file.py> --help
usage: <file>.py [-h] [--folder FOLDER] [--ext EXT]
optional arguments:
-h, --help show this help message and exit
--folder FOLDER Folder of images allowed for viewing. Must have at least one image (default: .)
--ext EXT Image extension to look for (default: png)
$ python <file>.py --folder "./path/to/folder/of/your/image(s).png" --ext "jpg"
file.py
contents
import argparse
from pathlib import Path
from typing import Tuple, Union, Any
import numpy as np
import pyqtgraph as pg
from PIL import Image
from utilitys import fns, widgets, RunOpts
def pad_to_size(
image: Image.Image,
size_wh: Union[int, Tuple[int, int]] = None,
fill_color: Any = 0,
**resize_kwargs,
) -> Image.Image:
"""
Keeps an image's aspect ratio by resizing until the largest side is constrained
by the specified output size. Then, the deficient dimension is padded until
the image is the specified size.
"""
if size_wh is None:
size_wh = max(image.size)
if isinstance(size_wh, int):
size_wh = (size_wh, size_wh)
im_size_wh = np.array(image.size)
ratios = im_size_wh / size_wh
# Resize until the largest side is constrained by the specified output size
im_size_wh = np.ceil(im_size_wh / ratios.max()).astype(int)
# Prefer 1-pixel difference in aspect ratio vs. odd padding
pad_amt = np.array(size_wh) - im_size_wh
use_ratio_idx = np.argmax(ratios)
unused_ratio_idx = 1 - use_ratio_idx
# Sanity check for floating point accuracy: At least one side must match
# user-requested dimension
if np.all(pad_amt != 0):
# Adjust dimension that is supposed to match
im_size_wh[use_ratio_idx] += pad_amt[use_ratio_idx]
# Prefer skewing aspect ratio by 1 pixel instead of odd padding
# If odd, 1 will be added. Otherwise, the dimension remains unchanged
im_size_wh[unused_ratio_idx] += pad_amt[unused_ratio_idx] % 2
image = image.resize(tuple(im_size_wh), **resize_kwargs)
new_im = Image.new("RGB", size_wh, fill_color)
width, height = image.size
new_im.paste(image, (int((size_wh[0] - width) / 2), int((size_wh[1] - height) / 2)))
return new_im
def main(folder=".", ext="png"):
"""
Parameters
----------
folder: str, Path
Folder of images allowed for viewing. Must have at least one image
ext: str, Path
Image extension to look for
"""
folder = Path(folder)
files = fns.naturalSorted(folder.glob(f"*.{ext}"))
err_msg = f"{folder} must have at least one image file with extension `{ext}`"
assert len(files), err_msg
pg.mkQApp()
viewer = widgets.ImageViewer()
def readim(file_index=0, try_pad=False, output_w=512, output_h=512):
if 0 > file_index > len(files):
return
image = Image.open(files[file_index])
if try_pad:
image = pad_to_size(image, (output_w, output_h), fill_color=(255, 255, 255))
viewer.setImage(np.array(image))
viewer.toolsEditor.registerFunc(readim, runOpts=RunOpts.ON_CHANGED)
wc = viewer.widgetContainer()
readim()
wc.show()
pg.exec()
if __name__ == "__main__":
# Print defaults in help signature
fmt = dict(formatter_class=argparse.ArgumentDefaultsHelpFormatter)
cli = fns.makeCli(main, parserKwargs=fmt)
args = cli.parse_args()
main(**vars(args))
from PIL import Image, ImageOps
with Image.open('hLARP.png') as im:
im = ImageOps.pad(im, (200, 200), color='black')
im.save('result.png')