Pillow icon indicating copy to clipboard operation
Pillow copied to clipboard

Better morphological operations

Open asudyn opened this issue 4 years ago • 3 comments

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.

asudyn avatar Jul 06 '20 22:07 asudyn

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

nulano avatar Jul 09 '20 15:07 nulano

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.

nostroke stroke

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

asudyn avatar Jul 09 '20 20:07 asudyn

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

AX-I avatar Aug 20 '24 23:08 AX-I