How to autocrop randomly using PIL? - python

I'm trying to write a script in PIL that basically creates a bunch of images out of a larger image.
I want to take a larger image, let's say it's 1000X1000 pixels, and crop squares of 250x250 out of it at "random" locations.
I know that .crop(box) exists and works one image and one crop at a time. Is there any way I can use this to do the task I want?

from PIL import Image
from random import randrange
img = Image.open(r"image_path")
x, y = img.size
matrix = 250
sample = 10
sample_list = []
for i in range(sample):
x1 = randrange(0, x - matrix)
y1 = randrange(0, y - matrix)
sample_list.append(img.crop((x1, y1, x1 + matrix, y1 + matrix)))
matrix => Size of the matrix that would be used for cropping (has to be symmetric & should be smaller then the image size by a considerable amount)
sample => Number of samples that must be taken (or the number of cropped images)
sample_list => The list the would be used to store the cropped images
After the execution of the code, sample_list will contain several image objects which could be individually referenced by their index numbers (ex. sample_list[0])

a small adaptation of #Vasu answer, added margin crop if needed (if margins are not important) and set matrix based on image height / width. Can be adapted to your needs.
from random import randrange
from PIL import Image
def autocrop(pil_img, pct_focus=0.3, matrix_HW_pct=0.3, sample=1):
"""
random crop from an input image
Args:
- pil_img
- pct_focus(float): PCT of margins to remove based on image H/W
- matrix_HW_pct(float): crop size in PCT based on image Height
- sample(int): number of random crops to return
returns:
- crop_list(list): list of PIL cropped images
"""
x, y = pil_img.size
img_focus = pil_img.crop((x*pct_focus, y*pct_focus, x*(1-pct_focus), y*(1-pct_focus)))
x_focus, y_focus = img_focus.size
matrix = round(matrix_HW_pct*y_focus)
crop_list = []
for i in range(sample):
x1 = randrange(0, x_focus - matrix)
y1 = randrange(0, y_focus - matrix)
cropped_img = img_focus.crop((x1, y1, x1 + matrix, y1 + matrix))
#display(cropped_img)
crop_list.append(cropped_img)
return crop_list

Related

How to split an image into multiple images based on white borders between them

I need to split an image into multiple images, based on the white borders between them.
for example:
output:
using Python, I don't know how to start this mission.
Here is a solution for the "easy" case where we know the grid configuration. I provide this solution even though I doubt this is what you were asked to do.
In your example image of the cat, if we are given the grid configuration, 2x2, we can do:
from PIL import Image
def subdivide(file, nx, ny):
im = Image.open(file)
wid, hgt = im.size # Size of input image
w = int(wid/nx) # Width of each subimage
h = int(hgt/ny) # Height of each subimage
for i in range(nx):
x1 = i*w # Horicontal extent...
x2 = x1+w # of subimate
for j in range(ny):
y1 = j*h # Certical extent...
y2 = y1+h # of subimate
subim = im.crop((x1, y1, x2, y2))
subim.save(f'{i}x{j}.png')
subdivide("cat.png", 2, 2)
The above will create these images:
My previous answer depended on knowing the grid configuration of the input image. This solution does not.
The main challenge is to detect where the borders are and, thus, where the rectangles that contain the images are located.
To detect the borders, we'll look for (vertical and horizontal) image lines where all pixels are "white". Since the borders in the image are not really pure white, we'll use a value less than 255 as the whiteness threshold (WHITE_THRESH in the code.)
The gist of the algorithm is in the following lines of code:
whitespace = [np.all(gray[:,i] > WHITE_THRESH) for i in range(gray.shape[1])]
Here "whitespace" is a list of Booleans that looks like
TTTTTFFFFF...FFFFFFFFTTTTTTTFFFFFFFF...FFFFTTTTT
where "T" indicates the corresponding horizontal location is part of the border (white).
We are interested in the x-locations where there are transitions between T and F. The call to the function slices(whitespace) returns a list of tuples of indices
[(x1, x2), (x1, x2), ...]
where each (x1, x2) pair indicates the xmin and xmax location of images in the x-axis direction.
The slices function finds the "edges" where there are transitions between True and False using the exclusive-or operator and then returns the locations of the transitions as a list of tuples (pairs of indices).
Similar code is used to detect the vertical location of borders and images.
The complete runnable code below takes as input the OP's image "cat.png" and:
Extracts the sub-images into 4 PNG files "fragment-0-0.png", "fragment-0-1.png", "fragment-1-0.png" and "fragment-1-1.png".
Creates a (borderless) version of the original image by pasting together the above fragments.
The runnable code and resulting images follow. The program runs in about 0.25 seconds.
from PIL import Image
import numpy as np
def slices(lst):
""" Finds the indices where lst changes value and returns them in pairs
lst is a list of booleans
"""
edges = [lst[i-1] ^ lst[i] for i in range(len(lst))]
indices = [i for i,v in enumerate(edges) if v]
pairs = [(indices[i], indices[i+1]) for i in range(0, len(indices), 2)]
return pairs
def extract(xx_locs, yy_locs, image, prefix="image"):
""" Locate and save the subimages """
data = np.asarray(image)
for i in range(len(xx_locs)):
x1,x2 = xx_locs[i]
for j in range(len(yy_locs)):
y1,y2 = yy_locs[j]
arr = data[y1:y2, x1:x2, :]
Image.fromarray(arr).save(f'{prefix}-{i}-{j}.png')
def assemble(xx_locs, yy_locs, prefix="image", result='composite'):
""" Paste the subimages into a single image and save """
wid = sum([p[1]-p[0] for p in xx_locs])
hgt = sum([p[1]-p[0] for p in yy_locs])
dst = Image.new('RGB', (wid, hgt))
x = y = 0
for i in range(len(xx_locs)):
for j in range(len(yy_locs)):
img = Image.open(f'{prefix}-{i}-{j}.png')
dst.paste(img, (x,y))
y += img.height
x += img.width
y = 0
dst.save(f'{result}.png')
WHITE_THRESH = 110 # The original image borders are not actually white
image_file = 'cat.png'
image = Image.open(image_file)
# To detect the (almost) white borders, we make a grayscale version of the image
gray = np.asarray(image.convert('L'))
# Detect location of images along the x axis
whitespace = [np.all(gray[:,i] > WHITE_THRESH) for i in range(gray.shape[1])]
xx_locs = slices(whitespace)
# Detect location of images along the y axis
whitespace = [np.all(gray[i,:] > WHITE_THRESH) for i in range(gray.shape[0])]
yy_locs = slices(whitespace)
extract(xx_locs, yy_locs, image, prefix='fragment')
assemble(xx_locs, yy_locs, prefix='fragment', result='composite')
Individual fragments:
The composite image:

Rotating a Color Image in Python Without Using Packages and Using Transformation Matrix

I am trying to create a method that rotates a color image in Python by 0, 90, 180, or 270 degrees. This is my code so far. As it stands, the output is a blank image. I also am trying to get the code to be as efficient as possible. Not sure what I am doing wrong. Image is a 3D array while rotate_angle is a int. The idea I was trying was to take all the (x,y) coordinates of pixels from the original image and multiply them with the transformation matrix (x_transformed, y_transformed). Then, the colors at (x_transformed, y_transformed) on the new image would just be the colors at (x,y) on the old image.
def rotate_image(image, rotate_angle):
output_image = image
# Convert degrees to radian
angle = math.radians(rotate_angle)
# For all values of height
for i in range(image.shape[0]):
# And all values of width
for j in range(image.shape[1]):
# Take the x and y coordinates of the existing points
y = image.shape[0]-1
x = image.shape[1]-1
# Keep in mind rotation matrix is [(cos, sin), (-sin, cos)] [(x,y)]
y_n = int(-x * math.sin(angle) + y * math.cos(angle))
x_n = int(x * math.cos(angle) + y * math.sin(angle))
# Rotating to where we are and then copying data from where we were
output_image[y_n, x_n, :] = image[i, j, :]
return output_image
Of course, if there is a more efficient way, I am also interested in knowing what it is. I know there are packages out there that can do what I am doing, but I wish to make the method myself and with the tools I have.

How to make a shape larger or smaller without changing the resolution of the image using OpenCV or PIL in Python

I would like to be able to make a certain shape in either a PIL image or an OpenCV image 3 times larger and smaller without changing the resolution of the image or changing the shape of the shape I want to make larger. I have tried using OpenCV's dilation method but that is not it's intended use, plus it changed the shape of the image. For an example:
Thanks.
Here's a way of doing it:
find the interesting shape, i.e. non-white ROI area
extract it
scale it up by a factor
clear the original image to white
paste the scaled ROI back into image with same centre
#!/usr/bin/env python3
import cv2
import numpy as np
if __name__ == "__main__":
# Open image
orig = cv2.imread('image.png',cv2.IMREAD_COLOR)
# Get extent of interesting part, i.e. non-white part
y, x, _ = np.nonzero(~orig)
y0, y1 = np.min(y), np.max(y) # top and bottom rows
x0, x1 = np.min(x), np.max(x) # left and right cols
h, w = y1-y0, x1-x0 # height and width
ROI = orig[y0:y1, x0:x1] # extract ROI
cv2.imwrite('ROI.png', ROI) # DEBUG only
# Upscale ROI
factor = 3
scaledROI = cv2.resize(ROI, (w*factor,h*factor), interpolation=cv2.INTER_NEAREST)
newH, newW = scaledROI.shape[:2]
# Clear original image to white
orig[:] = [255,255,255]
# Get centre of original shape, and position of top-left of ROI in output image
cx, cy = (x0 + x1) //2, (y0 + y1)//2
top = cy - newH//2
left = cx - newW//2
# Paste in rescaled ROI
orig[top:top+newH, left:left+newW] = scaledROI
cv2.imwrite('result.png', orig)
That transforms this:
to this:
Puts me in mind of a pantograph:

Extracting windows/subarrays along a binary circular line/path with view_as_windows from skimage

I have an 8bit binary image that shows me the outline of a circle. The outline is only 1 pixel wide. Using the function view_as_windows lets me generate smaller arrays or windows of an input array like this picture, with adjacent overlapping windows. The size of this image is 250×250.
from skimage.io import imread
from skimage.util import view_as_windows
fname = "C:\\Users\\Username\\Desktop\\Circle.tif"
array = imread(fname)
window_shape = (50, 50)
step = 20
new_array = view_as_windows(array, window_shape, step=step)
This gives me 11×11 overplapping windows. However, I want to extract only windows along the line of the circle so that I can reassemble this object at a later time. The line of a each window should be positioned centrally or in a way so that I have access to the information right under the circle.
This is what I have tried so far:
First I replaced the values (0) and (255) with (1) and (0), respectively. This way, math is a bit easier.
array[array==0] = 1
array[array==255] = 0
Then I iterated over the windows in new_array. In this case over the first two dimensions. new_array.shape is (11, 11, 50, 50)
for j in range(new_array.shape[0]):
for i in range(new_array.shape[1]):
Window = new_array[j, i]
SliceOfWindow = Slice[20:30, 20:30]
sumAxis0 = np.sum(Slice, axis=0)
sumSlice = np.sum(sumAxis0)
if sumSlice >= SliceOfWindow.shape[0]
imsave(...)
I created a smaller slice of the shape = (10, 10) within each window, placed in the center. If the sum of each slice >= the length of a slice I have saved that array as an image.
Can this be done in a more precise way? Is there a way to yield better results (better windows!)?
For a convex curve, you could use polar coordinates and sort the edge pixels by their angle through numpy.argsort and numpy.arctan2.
Demo
from skimage import io
import matplotlib.pyplot as plt
import numpy as np
img = io.imread('https://i.stack.imgur.com/r3D6I.png')
# Arbitrary point inside the curve
row_cen, col_cen = 125, 125
# Coordinates of the edge pixels
row, col = np.nonzero(img == 0)
# Put the origin on the lower left corner
x = col - col_cen
y = -(row - row_cen)
# Indices of the centers of the windows
step = 60
idx = np.argsort(np.arctan2(y, x))[::step]
windows = np.zeros_like(img)
size = 15
for _, n in enumerate(idx):
windows[row[n] - size:row[n] + size, col[n] - size:col[n] + size] = 255
plt.imshow(windows, cmap='gray')
for i, n in enumerate(idx):
plt.text(col[n], row[n], i, fontsize='14',
horizontalalignment='center',
verticalalignment='center')
plt.show()

How I calculate the covariance between 2 images?

I am working on image processing with python. Specifically, I am trying to implement an algorithm called Structural similarity index measure (SSIM) between two images (x and y), which I extracted from this article this article.
In that formula I need the covariance between the two images. I know how to calculate the covariance between two vectors, but I don't know how to calculate the covariance of two matrices (I assume each image is a matrix of pixels), anyone can help me? I tried using the numpy function numpy.cov(x,y) [doc] but I have a large 3-D matrix, and I actually need a scalar value
Using python we can calculate covariance between two images with following way
import numpy as np
def Covariance(x, y):
xbar, ybar = x.mean(), y.mean()
return np.sum((x - xbar)*(y - ybar))/(len(x) - 1)
now take two images img1,img2 and call the function and print it as given way
print( Covariance(img1,img2))
Check this library: pyssim. Might be what you're looking for.
import cv2
import numpy as np
from PIL import Image, ImageOps #PIL = pillow
from numpy import asarray
'''read image via PIL -- in opencv it equals to img1 = cv2.imread("c1.jpg") '''
img1 = Image.open('c1.jpg')
img2 = Image.open('d1.jpg')
gimg1 = ImageOps.grayscale(img1) #convert to grayscale PIL
gimg2 = ImageOps.grayscale(img2)
#asarray() class is used to convert PIL images into NumPy arrays
numpydata1 = asarray(gimg1)
numpydata2 = asarray(gimg2)
print("Array of image 1: ", numpydata1.shape)
print("Array of image 2: ", numpydata2.shape)
#grayscale images are saved as 2D ndarray of rows(height) x columns(width)
height = int(numpydata2.shape[0] *
(numpydata1.shape[0]/numpydata2.shape[0] ) )
width = int(numpydata2.shape[1] * (numpydata1.shape[1]/
numpydata2.shape[1] ) )
#print(width)
#print(height)
#when using resize(), format should be width x height therefore, create a new image called new and set it to w x h
new = (width, height)
#resize image so dimensions of both images are same
resized = cv2.resize(numpydata2, new, interpolation = cv2.INTER_AREA)
print("Array of resized image 2: ", resized.shape)
def Covariance(x, y):
xbar, ybar = x.mean(), y.mean()
return np.sum((x - xbar)*(y - ybar))/(len(x) - 1)
print( Covariance(numpydata1, resized))
'''#Alternative Method - convert grayscale image to array using np.array
np_img1 = np.array(gimg1)
np_img2 = np.array(gimg2)
'''

Categories