How to vectorize tasks in python? - python

I (will) have a list of coordinates; using python's pillow module, I want to save a series of (cropped) smaller images to disk. Currently, I am using a for loop to act to determine one coordinate at a time then crop/save the image before proceeding to the next coordinate.
Is there a way to divide this job up such that multiple images can be cropped/saved simultaneously? I understand that this would take up more RAM but would be decrease performance time.
I'm sure this is possible but I'm not sure if this is simple. I've heard terms like 'vectorization' and 'multi-threading' that sound vaguely appropriate to this situation. But these topics extend beyond my experience.
I've attached the code for reference. However, I'm simply trying to solicit recommended strategies. (i.e. what techniques should I learn about to better tailor my approach, take multiple crops at once, etc?)
def parse_image(source, square_size, count, captures, offset=0, offset_type=0, print_coords=False):
"""
Starts at top left corner of image. Iterates through image by square_size (width = height)
across x values and after exhausting the row, begins next row lower by function of
square_size. Offset parameter is available such that, with multiple function calls,
overlapping images could be generated.
"""
src = Image.open(source)
dimensions = src.size
max_down = int(src.height/square_size) * square_size + square_size
max_right = int(src.width/square_size) * square_size + square_size
if offset_type == 1:
tl_x = 0 + offset
tl_y = 0
br_x = square_size + offset
br_y = square_size
for y in range(square_size,max_down,square_size):
for x in range(square_size + offset,max_right - offset,square_size):
if (tl_x,tl_y) not in captures:
sample = src.crop((tl_x,tl_y,br_x,br_y))
sample.save(f"{source[:-4]}_sample_{count}_x{tl_x}_y{tl_y}.jpg")
captures.append((tl_x,tl_y))
if print_coords == True:
print(f"image {count}: top-left (x,y): {(tl_x,tl_y)}, bottom-right (x,y): {(br_x,br_y)}")
tl_x = x
br_x = x + square_size
count +=1
else:
continue
tl_x = 0 + offset
br_x = square_size + offset
tl_y = y
br_y = y + square_size
else:
tl_x = 0
tl_y = 0 + offset
br_x = square_size
br_y = square_size + offset
for y in range(square_size + offset,max_down - offset,square_size):
for x in range(square_size,max_right,square_size):
if (tl_x,tl_y) not in captures:
sample = src.crop((tl_x,tl_y,br_x,br_y))
sample.save(f"{source[:-4]}_sample_{count}_x{tl_x}_y{tl_y}.jpg")
captures.append((tl_x,tl_y))
if print_coords == True:
print(f"image {count}: top-left (x,y): {(tl_x,tl_y)}, bottom-right (x,y): {(br_x,br_y)}")
tl_x = x
br_x = x + square_size
count +=1
else:
continue
tl_x = 0
br_x = square_size
tl_y = y + offset
br_y = y + square_size + offset
return count

What you want to achieve here is to have a higher degree of parallelism, the first thing to do is to understand what is the minimum task that you need to do here, and from that, think in a way to better distribute it.
First thing to notice here is that there is two behaviour, first if you have offset_type 0, and another if you have offset_type 1, split that off into two different functions.
Second thing is: given an image, you're taking crops of a given size, at a given offset(x,y) for the whole image. You could for instance, simplify this function to take one crop of the image, given the image offset(x,y). Then, you could call this function for all the x and y of the image in parallel. That's pretty much what most image processing frameworks tries to achieve, even more the one's that run code inside the GPU, small blocks of code, that operates locally in the image.
So lets say your image has width=100, height=100, and you're trying to make crops of w=10,h=10. Given the simplistic function that I described, I will call it crop(img, x, y, crop_size_x, crop_size_y) All you have to do is create the image:
img = Image.open(source)
crop_size_x = 10
crop_size_y = 10
crops = [crop(img, x, y, crop_size_x, crop_size_y) for x, y in zip(range(img.width), range(img.height))]
later on, you can then replace the list comprehension for a multi_processing library that can actually spawn many processes, do real parallelism, or even write such code inside a GPU kernel/shader, and use the GPU parallelism to achieve high performance.

Related

translate an image after rotation without using library

I try to rotate an image clockwise 45 degree and translate the image -50,-50.
Rotation process works fine:(I refer to this page:How do I rotate an image manually without using cv2.getRotationMatrix2D)
import numpy as np
import math
from scipy import ndimage
from PIL import Image
# inputs
img = ndimage.imread("A.png")
rotation_amount_degree = 45
# convert rotation amount to radian
rotation_amount_rad = rotation_amount_degree * np.pi / 180.0
# get dimension info
height, width, num_channels = img.shape
# create output image, for worst case size (45 degree)
max_len = int(math.sqrt(height*height + width*width))
rotated_image = np.zeros((max_len, max_len, num_channels))
#rotated_image = np.zeros((img.shape))
rotated_height, rotated_width, _ = rotated_image.shape
mid_row = int( (rotated_height+1)/2 )
mid_col = int( (rotated_width+1)/2 )
# for each pixel in output image, find which pixel
#it corresponds to in the input image
for r in range(rotated_height):
for c in range(rotated_width):
# apply rotation matrix, the other way
y = (r-mid_col)*math.cos(rotation_amount_rad) + (c-mid_row)*math.sin(rotation_amount_rad)
x = -(r-mid_col)*math.sin(rotation_amount_rad) + (c-mid_row)*math.cos(rotation_amount_rad)
# add offset
y += mid_col
x += mid_row
# get nearest index
#a better way is linear interpolation
x = round(x)
y = round(y)
#print(r, " ", c, " corresponds to-> " , y, " ", x)
# check if x/y corresponds to a valid pixel in input image
if (x >= 0 and y >= 0 and x < width and y < height):
rotated_image[r][c][:] = img[y][x][:]
# save output image
output_image = Image.fromarray(rotated_image.astype("uint8"))
output_image.save("rotated_image.png")
However, when I try to translate the image. I edited the above code to this:
if (x >= 0 and y >= 0 and x < width and y < height):
rotated_image[r-50][c-50][:] = img[y][x][:]
But I got something like this:
It seems the right and the bottom did not show the right pixel. How could I solve it?
Any suggestions would be highly appreciated.
The translation needs to be handled as a wholly separate step. Trying to translate the value from the source image doesn't account for newly created 0,0,0 (if RGB) valued pixels by the rotation.
Further, simply subtracting 50 from the rotated array index values, without validating them at that stage for positivity, is allowing for a negative valued index, which is fully supported by Python. That is why you are getting a "wrap" effect instead of a translation
You said your script rotated the image as intended, so while perhaps not the most efficient, the most intuitive is to simply shift the values of the image assembled after you rotate. You could test that the values for the new image remain positive after subtracting 50 and only saving the ones >= 0 or being cognizant of the fact that you are shifting the values downward by 50, any number less than 50 will be discarded and you get:
<what you in the block you said was functional then:>
translated_image = np.zeros((max_len, max_len, num_channels))
for i in range(0, rotated_height-50): # range(start, stop[, step])
for j in range(0, rotated_width-50):
translated_image[i+50][j+50][:] = rotated[i][j][:]
# save output image
output_image = Image.fromarray(translated_image.astype("uint8"))
output_image.save("rotated_translated_image.png")

Randomizing coordinates

I am quite new to Python, but I find it is really fun to code
dictionary_of_locations = {
'location_1': (2214, 1026), # x, y coordinates
'location_2': (2379, 1016),
'location_3': (2045, 1092),
'location_4': (2163, 1080),
'location_5': (2214, 1080),
'location_6': (2262, 1078),
}
I want to run a code that selects the location coordinates and randomizes x and y values by +-15
what exactly I am trying to do is:
for i in dictionary_of_locations.values():
pyautogui.click(i), print('clicking on location ' + str(i + 1) + ' !'), time.sleep(.75)
Use random.randint:
import random
loc = ... # Do whatever to get your desired coordinates
loc = (loc[0] + random.randint(-15, 15), loc[1] + random.randint(-15, 15))
Or if you don't want just integers but also floats:
import random
loc = ... # Do whatever to get your desired coordinates
loc = (loc[0] + random.random() * 30 - 15, loc[1] + random.random() * 30 - 15)
You can use randint(lb, ub) to get a random integer in between the lower / upper bound
from random import randint
for loc in dictOfLoc:
newLoc = (dictOfLoc[loc][0] + randint(-15, 15),
dictOfLoc[loc][1] + randint(-15, 15))
dictOfLoc[loc] = newLoc
You may use numpy.random.randomint()
Example:
import numpy as np
import time
dictionary_of_locations = {
'location_1': (2214, 1026), # x, y coordinates
'location_2': (2379, 1016),
'location_3': (2045, 1092),
'location_4': (2163, 1080),
'location_5': (2214, 1080),
'location_6': (2262, 1078),
}
def get_random(x, y):
"""
Get (x, y) and return (random_x, random_y) according to rand_range
+1 is added to high bound because randint is high exclusive [low, high)
:param x: position x
:param y: position y
:return: random values for x and y with range 15
"""
rand_range = 15 # for -/+ 15
x_low = x - rand_range
x_high = x + rand_range + 1
y_low = y - rand_range
y_high = y + rand_range + 1
rand_x = np.random.randint(low=x_low, high=x_high)
rand_y = np.random.randint(low=y_low, high=y_high)
return rand_x, rand_y
for location, positions in dictionary_of_locations.items():
current_x, current_y = positions
new_x, new_y = get_random(current_x, current_y)
print(f'clicking on {location} (x, y): ({new_x},{new_y}) !')
time.sleep(.75)
# output
# clicking on location_1 (x, y): (2209,1040) !
# clicking on location_2 (x, y): (2364,1005) !
# clicking on location_3 (x, y): (2052,1086) !
# clicking on location_4 (x, y): (2160,1092) !
# clicking on location_5 (x, y): (2222,1079) !
# clicking on location_6 (x, y): (2275,1082) !
One thing I like about python is how it gently steers people into thinking functionally. So when I see a problem like this, I think - "What single, reusable function would really help in this situation?"
And in answer to that question, I guess some kind of blurrer that, given an input and some parameters, spits out a blurred version. Maybe we want to blurr by a fixed amount (+-15 as per your question) or maybe later, we might want to blur by some amount defined as a ratio.
Here's a starting point that covers the first idea (fixed range of blur), we'll call the input value value and the amount of blur, blur (other naming ideas are available).
Python has a lot of useful randomisation code in its random module, so we'll import that for some of that functionality too.
import random
def blur(value, blur):
blur_sign = random.choice([-1,1]) # is the blur going to be positive or negative?
blur_rand = random.randint(0,blur) * blur_sign # pick a random number up to "blur" and multiply by sign
return value + blur_rand
N.B. use of random.choice in the above is probably a bit on the
clunky side, as others have demonstrated here, calling
random.randint with a lower-bound, upper-bound as parameters is a
cleaner way of getting the range you want and will cross the
positive/negative without having to expressly setting it as I've done
here.
Try it out, feed the 'blur' function with any value, and some blurring parameter, and it should spit out the kinds of results you want. Now you've got a function that does the job, just use some python to glue it into whatever workflow you're interested in.
An example of how that might look given your existing code:
for x,y in dictionary_of_locations.values():
i = (blur(x,15), blur(y,15))
pyautogui.click(i), print('clicking on location ' + str(i + 1) + ' !'), time.sleep(.75)

Fastest way to create list of (X,Y) incrementing tuples with step value?

I need a fast way to create a list of tuples representing image pixel coordinates (X, Y).
Where X is from 0 to size and Y is from 0 to size.
A step value of 1 results in X and Y values of (0, 1, 2, 3...) which is too many tuples. Using a step value greater than 1 will reduce processing time. For example, if the step value is 2 the values would be (0, 2, 4, 6...). If the step value is 4 the values would be (0, 4, 8, 12...).
In pure python range command might be used. However, NumPy is installed by default in my Linux distribution. In NumPy the arrange command might be used but I'm having a hard time wrapping my mind around NumPy array syntax.
PS: After a list of tuples is created it will be randomly shuffled and then read in the loop.
Edit 1
Using this answer below:
Instead of the image fading in it's doing some kind of weird wipe left to right. Using the code from the answer with a slight modification:
step = 4
size = self.play_rotated_art.size[0] - step
self.xy_list = [
(x, y)
for x in range(0, size - step, step)
for y in range(0, size - step, step)
]
Bug Update
There was an error in my code, it's working fine now:
The updated code is:
self.step = 4
size = self.play_rotated_art.size[0] - self.step
self.xy_list = [
(x, y)
for x in range(0, size - self.step, self.step)
for y in range(0, size - self.step, self.step)
]
shuffle(self.xy_list)
# Convert numpy array into python list & calculate chunk size
self.current_chunk = 0
self.chunk_size = int(len(self.xy_list) / 100)
# Where we stop copying pixels for current 1% chunck
end = self.current_chunk + self.chunk_size
if end > len(self.xy_list) - 1:
end = len(self.xy_list) - 1
while self.current_chunk < end:
x0, y0 = self.xy_list[self.current_chunk]
x1 = x0 + self.step
y1 = y0 + self.step
box = (x0, y0, x1, y1)
region = self.play_rotated_art.crop(box)
self.fade.paste(region, box)
self.current_chunk += 1
self.play_artfade_count += 1
return self.fade
TL;DR
I already have code with step value 1 but this code is overly complex and inefficient to request a modification. The above generic question would help others more and, still help me, if it were answered.
Existing code with step value 1:
def play_artfade2(self):
''' PILLOW VERSION:
Fade in artwork in 100 chunks leaving loop after chunk and
reentering after Tkinter updates screen and pauses.
'''
if self.play_artfade_count == 100:
# We'have completed a full cycle. Force graphical effects exit
self.play_artfade_count = 0 # Reset art fade count
self.play_rotated_value = -361 # Force Spin Art
return None
# Initialize numpy arrays first time through
if self.play_artfade_count == 0:
# Create black image to fade into
self.fade = Image.new('RGBA', self.play_rotated_art.size, \
color='black')
# Generate a randomly shuffled array of the coordinates
im = np.array(self.play_rotated_art)
X,Y = np.where(im[...,0]>=0)
coords = np.column_stack((X,Y))
np.random.shuffle(coords)
# Convert numpy array into python list & calculate chunk size
self.xy_list = list(coords)
self.current_chunk = 0
self.chunk_size = int(len(self.xy_list) / 100)
# Where we stop copying pixels for current 1% chunck
end = self.current_chunk + self.chunk_size
if end > len(self.xy_list) - 1:
end = len(self.xy_list) - 1
while self.current_chunk < end:
x0, y0 = self.xy_list[self.current_chunk]
x1 = x0 + 1
y1 = y0 + 1
box = (x0, y0, x1, y1)
region = self.play_rotated_art.crop(box)
self.fade.paste(region, box)
self.current_chunk += 1
self.play_artfade_count += 1
return self.fade
Using Pillow's Image.crop() and Image.paste() is overkill for a single pixel but the initial working design was future focused to utilize "super pixels" with box size of 2x2, 3x3, 5x5, etc as image is resized from 200x200 to 333x333 to 512x512, etc.
I need fast way to create a list of tuples representing image pixel coordinates (X, Y).
Where X is from 0 to size and Y is from 0 to size
A list comprehension with range will work:
xsize = 10
ysize = 10
coords = [(x, y) for x in range(xsize) for y in range(ysize)]
# this verifies the shape is correct
assert len(coords) == xsize * ysize
If you wanted a step other than 1, this is setting the step argument:
coords = [(x, y) for x in range(0, xsize, 2) for y in range(0, ysize, 2)]
You can use a generator expression:
size = 16
step = 4
coords = (
(x, y)
for x in range(0, size, step)
for y in range(0, size, step)
)
Then you can iterate on that like you would do with a list
for coord in coords:
print(coord)
Using a generator instead of a list or tuple has the advantage of being more memory efficient.

python loops very slow

I have a question concerning the speed of loops in Python.
I created the following loops to fill values in my array, but it is very slow.
Is there a way to make it process faster ?
winW = 1
winH = 200
runlength = np.zeros(shape=(img.shape[0], img.shape[1]))
for y in range(0, img.shape[0] - winH, 1):
for x in range(0, img.shape[1] - winW, 1):
runlength[y, x] += np.sum(img[y:y + winH, x:x + winW]) / (winH * winW)
runlength[y + winH, x] += np.sum(img[y:y + winH, x:x + winW]) / (winH * winW)
Thanks for your help
Edit : I precise that I can only use numpy but not scipy
Let me describe how to speed up the first operation in the for loop, given by
runlength[y, x] += np.sum(img[y:y + winH, x:x + winW]) / (winH * winW)
Basically, you are moving a rectangle of width winW and height winH over the image. You start with the upper-left corner of the rectangle at point (0,0) of the image, then you sum all values in the image which lie below this rectangle and divide them by the total number of points. The output at position (0,0) is that number. Then you shift the rectangle one to the right and repeat the procedure until you are at the right end of the image. You move one row down and repeat.
In image processing terms: you apply a spatial filter mask to the image. The filter is an average filter of width winW and height winH.
To implement this efficiently, you can use the scipy.ndimage.correlate function. The input is your image, the weights contains the weight by which element below the rectangle is multiplied. In this case that is an array with dimensions (winH, winW) where every element contains the number 1 / (winH * winW). Thus every point of the image which lies below the rectangle is multiplied by 1 / (winH * winW), and then everything is summed.
To match your algorithm exactly, we need to set the origin to (-np.floor(winH/2), -np.floor(winW/2)) to specify that the mean of the rectangle is placed at the location upper right corner of the rectangle in the output.
Finally, to match your algorithm exactly, we have to set all points below (img.shape[0] - winH) or right of (img.shape[1] - winW) to zero. The for-loop can thus be replaced with
runlength_corr = correlate(input=img,
weights=np.ones((winH, winW)) / (winW * winH),
origin=(-np.floor(winH/2), -np.floor(winW/2)))
runlength_corr[(img.shape[0] - winH):, :] = 0
runlength_corr[:, (img.shape[1] - winW):] = 0
I compared the run time of the nested for-loops and the correlate method on a test image of size 512-by-512:
For-loops: Elapsed time: 0.665 sec
Correlate: Elapsed time: 0.085 sec
So this gives a nice speed-up of factor 8. The sum of absolute differences over the entire output is as low as 7.04e-09, so the outputs of both methods are essentially the same.
For starters, you seem to be calculating the same quantity twice, inside you loop. That alone could half your running time.
Second, if winW is always 1, then np.sum(img[y:y + winH, x:x + winW]) is just
np.sum(img[y:y + winH, x]). That should speed it up a bit.
What remains is how you can speed up np.sum(img[y:y + winH, x]). You can start with calculating
sum0 = np.sum(img[0: 0 + winH, x])
Now, note that the quantity
sum1 = np.sum(img[1: 1 + winH, x])
differs from the previous one by two pixels only, so, it is equal to sum0 - img[0, x] + img[1 + winH, x]. For the next y
sum2 = sum1 - img[1, x] + img[2 + winH, x]`
and so on

Creating and rotating ellipses in python, whilst checking for collisions

I've managed to create a bit of code that creates various discs and checks for collisions, but how would I go about doing it for ellipses instead?
height = 100
width= 100
circles = 15
radius = 4
xL = []
yL = []
count = 0
print("Placing ", circles, "circles on a" , width,\
"x", height, "grid...")
for j in range(circles):
x = randint(0,width-1)
y = randint(0,height-1)
if len(xList)>0:
for i in range(len(xL)):
distance = sqrt((xL[i] - x)**2 + (yL[i] - y)**2)
if (distance < radius*2):
print("Circle ", j, "collides with circle ", i)
count += 1
xL.append(x)
yL.append(y)
print("Complete.", count, "collisions.")
Would I need to go about re-writing the code completely?
Also as a follow up I've been able to implement an algorithm that moves the discs ever so slightly and then accept or reject the move if they've come closer together. With ellipses its a bit different, as I'm going to need to rotate them. How would I do that?

Categories

Resources