Pillow
Pillow copied to clipboard
Better morphological operations
As far as I understand, right now we're limited to only use 3x3 kernels and the set of operations is pretty limited. I'd really want a more powerful ImageMorph class with ability to set any kernel and ability to work with non-binary images.
I think you can use an arbitrary 3x3 or 5x5 kernel with ImageFilter.Kernel
: https://pillow.readthedocs.io/en/stable/reference/ImageFilter.html#PIL.ImageFilter.Kernel
Thanks, I didn't see that. 5x5 is good. Still I want to be able to apply 7x7, 9x9 and so on. Right now I've written a function to do that but it's extremely slow. It takes 2.5 seconds to apply it to 250x250 pixel image. The function adds an outline of a configurable size to a PNG with alpha channel. I'm trying to speed it up.
def addStroke(image,strokeSize=1,color=(0,0,0)):
#Create a disc kernel
kernelSize=math.ceil(strokeSize)*2+1 #Should always be odd
kernelExtent=int(kernelSize/2)
kernelRadius=strokeSize+0.5
kernelCenter=kernelSize/2-1
pixelRadius=1/math.sqrt(math.pi)
kernel=[]
for x in range(kernelSize):
for y in range(kernelSize):
distanceToCenter=math.sqrt((kernelCenter-x+0.5)**2+(kernelCenter-y+0.5)**2)
if(distanceToCenter<=kernelRadius-pixelRadius):
value=1 #This pixel is fully inside the circle
elif(distanceToCenter<=kernelRadius):
value=min(1,(kernelRadius-distanceToCenter+pixelRadius)/(pixelRadius*2)) #Mostly inside
elif(distanceToCenter<=kernelRadius+pixelRadius):
value=min(1,(pixelRadius-(distanceToCenter-kernelRadius))/(pixelRadius*2)) #Mostly outside
else:
value=0 #This pixel is fully outside the circle
kernel.append((x-kernelExtent,y-kernelExtent,value))
kernel=tuple(kernel)
#Debug save kernel to image
# image_out=Image.new("L",(kernelSize,kernelSize))
# image_out.putdata([int(round((1-k[2])*255)) for k in kernel])
# image_out.save('kernel.png')
alphaPixels=image.getchannel("A").load()
outlineValues=[]
imageWidth,imageHeight=image.size
start_time=time.time()
#Morphological grayscale dilation
for y in range(imageWidth):
for x in range(imageHeight):
values=[]
##############
#This part slows everything down A LOT
##############
for kx,ky,kval in kernel:
mx,my=x+kx,y+ky
if(0<=mx<imageWidth and 0<=my<imageHeight):
values.append(alphaPixels[mx,my]*kval)
outlineValues.append(int(max(values)))
print("--- Calculated outline in "+str((time.time()-start_time))+" seconds ---")
outline=Image.new(mode='RGB',size=image.size,color=color)
outlineAlpha=Image.new(mode='L',size=image.size,color=0)
outlineAlpha.putdata(outlineValues)
outline.putalpha(outlineAlpha)
outline.paste(image,(0,0),image)
return outline
PIL (and Python loops) is not designed for these convolution-type operations, which are more suited for Numpy. You can utilise the np.array(image)
and Image.fromarray(array)
conversions and speed up your code as follows:
Add this function to convert the kernel into a Numpy array:
def convertKernelNP(k, ksize):
k = np.array(k)
k[:,0:2] -= np.min(k[:,0:2])
out = np.zeros((ksize,ksize))
out[k[:,0].astype('int'),k[:,1].astype('int')] = k[:,2]
return out
Replace the big loop after #Morphological grayscale dilation
with:
kernelNP = convertKernelNP(kernel, kernelSize)
imageNP = np.array(image)
alphaNP = np.pad(imageNP[:,:,3], strokeSize)
outlineAlphaNP = np.zeros_like(imageNP[:,:,0], dtype='float')
for y in range(kernelSize):
for x in range(kernelSize):
np.maximum(outlineAlphaNP,
kernelNP[y,x] * alphaNP[y:y+imageHeight,x:x+imageWidth],
out=outlineAlphaNP)
Replace these two lines
outlineAlpha=Image.new(mode='L',size=image.size,color=0)
outlineAlpha.putdata(outlineValues)
with:
outlineAlpha=Image.fromarray(outlineAlphaNP.astype('uint8'))
Result: on my computer calling with strokeSize = 8
Before: 3.5253 seconds
After: 0.0156 seconds