You are not taking advantage of any of Numpy's vectorization techniques which can decrease processing time significantly. I'm assuming this is why you want to multiprocess operations on windows/chunks of the image(s) - I don't know what Docker is so I don't know whether that is a factor in your multiprocess approach.
Here is a vectorized solution with the caveat that it possibly excludes bottom and right edge pixels from the operations. If that is not acceptable no need to read any further.
The size of the right and bottom edge windows in your example are, more likely than not, different than the other windows. Looks like you arbitrarily chose a factor of ten to chunk up your image - if ten was an arbitrary choice, you can optimize the bottom and right edge deltas easily - I'll post that function at the end of the answer.
The image needs to be reshaped into patches to vectorize the operations . I've used an sklearn function sklearn.feature_extraction.image._extract_patches because it is convenient and allows creation of non-overlapping patches (which appears to be what you want). Notice the underscore prefix - this used to be an exposed function, image.extract_patches, but that has been deprecated. The function uses numpy.lib.stride_tricks.as_strided - it might be possible to just reshape the array but I haven't tried that.
Setup
import numpy as np
from sklearn.feature_extraction import image
img = np.arange(4864*3546*3).reshape(4864,3546,3)
# all shape dimensions in the following example derived from img's shape
Define the patch size (see opt_size below) and reshape the image.
hsize, h_remainder, h_windows = opt_size(img.shape[0])
wsize, w_remainder, w_windows = opt_size(img.shape[1])
# rgb - not designed for rgba
if img.ndim == 3:
patch_shape = (hsize,wsize,img.shape[-1])
else:
patch_shape = (hsize,wsize)
patches = image._extract_patches(img,patch_shape=patch_shape,
extraction_step=patch_shape)
patches = patches.squeeze()
patches is a view of the original array changes to it will be seen in the original. Its shape is (8, 9, 608, 394, 3) There are 8x9, (608,394,3) windows/patches.
Find the upper and lower bounds of each patch; compare each pixel to the bounds for its patch; extract the indices for each pixel that is between its bounds and needs to be changed.
lower = patches.min((2,3)) * .6
lower = lower[...,None,None,:]
upper = patches.max((2,3)) * .6
upper = upper[...,None,None,:]
indices = np.logical_and(patches > lower, patches < upper).nonzero()
Find the mean of each patch then change the required pixel values,
avg = patches.mean((2,3)) # shape (8,9,3)
patches[indices] = avg[indices[0],indices[1],indices[-1]]
Function that puts it all together
def g(img, opt_shape=False):
original_shape = img.shape
# determine patch shape
if opt_shape:
hsize, h_remainder, h_windows = opt_size(img.shape[0])
wsize, w_remainder, w_windows = opt_size(img.shape[1])
else:
patch_size = img.shape[0] // 10
hsize, wsize = patch_size,patch_size
# constraint checking here(?) for
# number of windows,
# orphaned pixels
if img.ndim == 3:
patch_shape = (hsize,wsize,img.shape[-1])
else:
patch_shape = (hsize,wsize)
patches = image._extract_patches(img,patch_shape=patch_shape,
extraction_step=patch_shape)
#squeeze??
patches = patches.squeeze()
#assume color (h,w,3)
lower = patches.min((2,3)) * .6
lower = lower[...,None,None,:]
upper = patches.max((2,3)) * .6
upper = upper[...,None,None,:]
indices = np.logical_and(patches > lower, patches < upper).nonzero()
avg = patches.mean((2,3))
## del lower, upper, mask
patches[indices] = avg[indices[0],indices[1],indices[-1]]
def opt_size(size):
'''Maximize number of windows, minimize loss at the edge
size -> int
Number of "windows" constrained to 4-10
Returns (int,int,int)
size in pixels,
loss in pixels,
number of windows
'''
size = [(divmod(size,n),n) for n in range(4,11)]
n_windows = 0
remainder = 99
patch_size = 0
for ((p,r),n) in size:
if r <= remainder and n > n_windows:
remainder = r
n_windows = n
patch_size = p
return patch_size, remainder, n_windows
Tested against your naïve process - I hope I executed it correctly. About a 35x improvement on the 4864x3546 color image. There are probably further optimizations maybe some wizards will comment.
Test using your chunk factor of ten:
#yours
def f(img):
window_size = int(img.shape[0] / 10)
window_shape = (window_size, window_size)
for y in range(0, img.shape[0], window_size):
for x in range(0, img.shape[1], window_size):
window = img[y:y + window_shape[1], x:x + window_shape[0]]
upper_bound = window.max((0,1)) * .6
lower_bound = window.min((0,1)) * .6
avg = window.mean((0,1))
for y_2 in range(0, window.shape[0]):
for x_2 in range(0, window.shape[1]):
tmp = img[y + y_2, x + x_2]
indices = np.logical_and(tmp < upper_bound,tmp > lower_bound)
tmp[indices] = avg[indices]
img0 = np.arange(4864*3546*3).reshape(4864,3546,3)
#get everything the same shape
size = img0.shape[0] // 10
h,w = size*10, size * (img0.shape[1]//size)
img1 = img0[:h,:w].copy()
img2 = img1.copy()
assert np.all(np.logical_and(img1==img2,img2==img0[:h,:w]))
f(img1) # ~44 seconds
g(img2) # ~1.2 seconds
assert(np.all(img1==img2))
if not np.all(img2==img0[:h,:w]):
pass
else:
raise Exception('did not change')
indices is an index array. It is a tuple of arrays, one for each dimension. indices[0][0],indices[1][0],indices[2][0] would be the index for one element in a 3d array. The complete tuple can be used to index multiple elements of an array.
>>> indices
(array([1, 0, 2]), array([1, 0, 0]), array([1, 1, 1]))
>>> list(zip(*indices))
[(1, 1, 1), (0, 0, 1), (2, 0, 1)]
>>> arr = np.arange(27).reshape(3,3,3)
>>> arr[1,1,1], arr[0,0,1],arr[2,0,2]
(13, 1, 20)
>>> arr[indices]
array([13, 1, 19])
# arr[indices] <-> np.array([arr[1,1,1],arr[0,0,1],arr[2,0,1]])
np.logical_and(patches > lower, patches < upper) returns a boolean array and nonzero() returns the indices of all the elements with a value of True.
getThresholdBounds?getThresholdBoundsmay differ in my experiments. But basically I take the0.6 * maxValueOfWindowfor the upper bound and vice verser on the lower bound.