[英]How Can I Add an Outline/Stroke/Border to a PNG Image with Pillow Library in Python?
我正在嘗試使用 Pillow (python-imaging-library) Python 庫在 my.png 圖像周圍創建輪廓/描邊/邊框(選擇任何顏色和寬度)。 你可以在這里看到原始圖像和我想要的結果(通過手機應用程序創建): https://i.stack.imgur.com/4x4qh.png
您可以在此處下載原始圖像的 png 文件: https://pixabay.com/illustrations/brain-character-organ-smart-eyes-1773885/
我用中號 (1280x1138) 完成了它,但也許最好用最小尺寸 (640x569) 來完成。
我試圖用兩種方法解決問題。
方法一
第一種方法是創建 brain.png 圖像的全黑圖像,將其放大,然后將原始彩色大腦圖像粘貼在其上。 這是我的代碼:
brain_black = Image.open("brain.png") #load brain image
width = brain_black.width #in order not to type a lot
height = brain_black.height #in order not to type a lot
rectangle = Image.new("RGBA", (width, height), "black") #creating a black rectangle in the size of the brain image
brain_black.paste(rectangle, mask=brain_black) #pasting on the brain image the black rectangle, and masking it with the brain picture
#now brain_black is the brain.png image, but all its pixels are black. Let's continue:
brain_black = brain_black.resize((width+180, height+180)) #resizing the brain_black by some factor
brain_regular = Image.open("brain.png") #load the brain image in order to paste later on
brain_black.paste(brain_regular,(90,90), mask=brain_regular) #paste the regular (colored) brain on top of the enlarged black brain (in x=90, y=90, the middle of the black brain)
brain_black.save("brain_method_resize.png") #saving the image
正如您在上面的圖片鏈接中看到的那樣,此方法不起作用。 它可能適用於簡單的幾何形狀,但不適用於像這樣的復雜形狀。
方法二
第二種方法是將大腦圖像像素數據加載到二維數組中,並循環遍歷所有像素。 檢查每個像素的顏色,並在每個不透明的像素(意味着 A(或 Alpha)在 rgbA 形式中不為 0)在上、下、右、左、主對角線向下的像素中繪制黑色像素,主對角線向上,副對角線 (/) 向下,副對角線 (/) 向上。 然后在上面的第二個像素、下面的第二個像素等中繪制一個像素。這是通過“for 循環”完成的,其中重復次數是所需的筆划寬度(在本例中為 30)。 這是我的代碼:
brain=Image.open("brain.png") #load brain image
background=Image.new("RGBA", (brain.size[0]+400, brain.size[1]+400), (0, 0, 0, 0)) #crate a background transparent image to create the stroke in it
background.paste(brain, (200,200), brain) #paste the brain image in the middle of the background
pixelsBrain = brain.load() #load the pixels array of brain
pixelsBack=background.load() #load the pixels array of background
for i in range(brain.size[0]):
for j in range(brain.size[1]):
r, c = i+200, j+200 #height and width offset
if(pixelsBrain[i,j][3]!=0): #checking if the opacity is not 0, if the alpha is not 0.
for k in range(30): #the loop
pixelsBack[r, c + k] = (0, 0, 0, 255)
pixelsBack[r, c - k] = (0, 0, 0, 255)
pixelsBack[r + k, c] = (0, 0, 0, 255)
pixelsBack[r - k, c] = (0, 0, 0, 255)
pixelsBack[r + k, c + k] = (0, 0, 0, 255)
pixelsBack[r - k, c - k] = (0, 0, 0, 255)
pixelsBack[r + k, c - k] =(0, 0, 0, 255)
pixelsBack[r - k, c + k] = (0, 0, 0, 255)
background.paste(brain, (200,200), brain) #pasting the colored brain onto the background, because the loop "destroyed" the picture.
background.save("brain_method_loop.png")
這種方法確實有效,但是非常耗時(僅一張圖片和30個像素筆划大約需要30秒)。 我想對很多圖片都這樣做,所以這種方法對我不利。
使用 Python Pillow 庫是否有更簡單更好的方法來達到我想要的結果。 我該怎么做? 而且,我怎樣才能固定我的循環代碼(我了解一些關於 Numpy 和 OpenCV 的信息,哪個更適合這個目的?)
我知道如果手機應用程序可以在幾毫秒內完成,那么 python 也可以,但我沒有找到任何方法。
謝謝你。
我使用 OpenCV 嘗試了一些類似於 Photoshop 筆划效果的解決方案(它並不完美,我仍然找到更好的解決方案)
該算法基於歐幾里得距離變換。 我還嘗試了橢圓kernel結構的膨脹算法,它與photoshop有點不同,並且有一些信息表明距離變換是photoshop使用的方式。
def stroke(origin_image, threshold, stroke_size, colors):
img = np.array(origin_image)
h, w, _ = img.shape
padding = stroke_size + 50
alpha = img[:,:,3]
rgb_img = img[:,:,0:3]
bigger_img = cv2.copyMakeBorder(rgb_img, padding, padding, padding, padding,
cv2.BORDER_CONSTANT, value=(0, 0, 0, 0))
alpha = cv2.copyMakeBorder(alpha, padding, padding, padding, padding, cv2.BORDER_CONSTANT, value=0)
bigger_img = cv2.merge((bigger_img, alpha))
h, w, _ = bigger_img.shape
_, alpha_without_shadow = cv2.threshold(alpha, threshold, 255, cv2.THRESH_BINARY) # threshold=0 in photoshop
alpha_without_shadow = 255 - alpha_without_shadow
dist = cv2.distanceTransform(alpha_without_shadow, cv2.DIST_L2, cv2.DIST_MASK_3) # dist l1 : L1 , dist l2 : l2
stroked = change_matrix(dist, stroke_size)
stroke_alpha = (stroked * 255).astype(np.uint8)
stroke_b = np.full((h, w), colors[0][2], np.uint8)
stroke_g = np.full((h, w), colors[0][1], np.uint8)
stroke_r = np.full((h, w), colors[0][0], np.uint8)
stroke = cv2.merge((stroke_b, stroke_g, stroke_r, stroke_alpha))
stroke = cv2pil(stroke)
bigger_img = cv2pil(bigger_img)
result = Image.alpha_composite(stroke, bigger_img)
return result
def change_matrix(input_mat, stroke_size):
stroke_size = stroke_size - 1
mat = np.ones(input_mat.shape)
check_size = stroke_size + 1.0
mat[input_mat > check_size] = 0
border = (input_mat > stroke_size) & (input_mat <= check_size)
mat[border] = 1.0 - (input_mat[border] - stroke_size)
return mat
def cv2pil(cv_img):
cv_img = cv2.cvtColor(cv_img, cv2.COLOR_BGRA2RGBA)
pil_img = Image.fromarray(cv_img.astype("uint8"))
return pil_img
output = stroke(test_image, threshold=0, stroke_size=10, colors=((0,0,0),))
我目前無法為您提供經過全面測試的 Python 解決方案,因為我還有其他承諾,但我當然可以在幾毫秒內向您展示如何完成並給您一些指導。
我只是在命令行中使用了ImageMagick 。 它在 Linux 和 macOS(使用brew install imagemagick
)和 Windows 上運行。 因此,我提取了 alpha/透明度通道並丟棄了所有顏色信息。 然后使用形態學“邊緣輸出”操作在 Alpha 通道中圍繞形狀邊緣生成一條粗線。 然后我反轉白色邊緣,使它們變為黑色並使所有白色像素透明。 然后覆蓋在原始圖像的頂部。
這是完整的命令:
magick baby.png \( +clone -alpha extract -morphology edgeout octagon:9 -threshold 10% -negate -transparent white \) -flatten result.png
所以這基本上打開了圖像,在括號內弄亂了 alpha 層的克隆副本,然后將生成的黑色輪廓展平回原始圖像並保存它。 讓我們一次一個地執行步驟:
將 alpha 層提取為alpha.png
:
magick baby.png -alpha extract alpha.png
現在使邊緣變胖,反轉並使所有不是黑色的東西都變得透明並保存為overlay.png
:
magick alpha.png -morphology edgeout octagon:9 -threshold 10% -negate -transparent white overlay.png
這是最終結果,將octagon:9
更改為octagon:19
以獲得更粗的線條:
因此,使用 PIL... 您需要打開圖像並轉換為RGBA
,然后拆分通道。 您無需觸摸 RGB 通道,只需觸摸 A 通道。
im = Image.open('baby.png').convert('RGBA')
R, G, B, A = im.split()
這里需要一些形態 - 見這里。
將原始 RGB 通道與新的 A 通道合並並保存:
result = Image.merge((R,G,B,modifiedA))
result.save('result.png')
請注意,有 Python 綁定到 ImageMagick 稱為wand
,您可能會發現使用該... wand更容易翻譯我的命令行內容。 此外, scikit-image
也有一個易於使用的 形態學套件。
我寫了這個 function,它基於形態膨脹,讓你設置筆畫大小和顏色。 但它非常慢,而且它似乎不適用於小元素。
如果有人可以幫助我加快速度,那將非常有幫助。
def addStroke(image,strokeSize=1,color=(0,0,0)):
#Create a disc kernel
kernel=[]
kernelSize=math.ceil(strokeSize)*2+1 #Should always be odd
kernelRadius=strokeSize+0.5
kernelCenter=kernelSize/2-1
pixelRadius=1/math.sqrt(math.pi)
for x in range(kernelSize):
kernel.append([])
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[x].append(value)
kernelExtent=int(len(kernel)/2)
imageWidth,imageHeight=image.size
outline=image.copy()
outline.paste((0,0,0,0),[0,0,imageWidth,imageHeight])
imagePixels=image.load()
outlinePixels=outline.load()
#Morphological grayscale dilation
for x in range(imageWidth):
for y in range(imageHeight):
highestValue=0
for kx in range(-kernelExtent,kernelExtent+1):
for ky in range(-kernelExtent,kernelExtent+1):
kernelValue=kernel[kx+kernelExtent][ky+kernelExtent]
if(x+kx>=0 and y+ky>=0 and x+kx<imageWidth and y+ky<imageHeight and kernelValue>0):
highestValue=max(highestValue,min(255,int(round(imagePixels[x+kx,y+ky][3]*kernelValue))))
outlinePixels[x,y]=(color[0],color[1],color[2],highestValue)
outline.paste(image,(0,0),image)
return outline
非常簡單和原始的解決方案:使用 PIL.ImageFilter.FIND_EDGES 找到繪圖的邊緣,大約 1px 厚,並在邊緣的每個點上畫一個圓。 它非常快並且需要很少的庫,但是沒有平滑的缺點。
from PIL import Image, ImageFilter, ImageDraw
from pathlib import Path
def mystroke(filename: Path, size: int, color: str = 'black'):
outf = filename.parent/'mystroke'
if not outf.exists():
outf.mkdir()
img = Image.open(filename)
X, Y = img.size
edge = img.filter(ImageFilter.FIND_EDGES).load()
stroke = Image.new(img.mode, img.size, (0,0,0,0))
draw = ImageDraw.Draw(stroke)
for x in range(X):
for y in range(Y):
if edge[x,y][3] > 0:
draw.ellipse((x-size,y-size,x+size,y+size),fill=color)
stroke.paste(img, (0, 0), img )
# stroke.show()
stroke.save(outf/filename.name)
if __name__ == '__main__':
folder = Path.cwd()/'images'
for img in folder.iterdir():
if img.is_file(): mystroke(img, 10)
我面臨着同樣的需求:概述 PNG 圖像。
我看到已經找到了一些解決方案,但如果你們中的一些人想要另一種選擇,這是我的:
基本上,我的解決方案工作流程如下:
給你 go。你有一個帶輪廓的 PNG 圖像,其寬度和顏色由你選擇。
這是實現工作流的代碼:
from PIL import Image
# Set the border and color
borderSize = 20
color = (255, 0, 0)
imgPath = "<YOUR_IMAGE_PATH>"
# Open original image and extract the alpha channel
im = Image.open(imgPath)
alpha = im.getchannel('A')
# Create red image the same size and copy alpha channel across
background = Image.new('RGBA', im.size, color=color)
background.putalpha(alpha)
# Make the background bigger
background=background.resize((background.size[0]+borderSize, background.size[1]+borderSize))
# Merge the targeted image (foreground) with the background
foreground = Image.open(imgPath)
background.paste(foreground, (int(borderSize/2), int(borderSize/2)), foreground.convert("RGBA"))
imageWithBorder = background
imageWithBorder.show()
希望能幫助到你!
我找到了一種使用 ImageFilter 模塊執行此操作的方法,它比我在這里看到的任何自定義實現都快得多,並且不依賴於不適用於凸包的調整大小
from PIL import Image, ImageFilter
stroke_radius = 5
img = Image.open("img.png") # RGBA image
stroke_image = Image.new("RGBA", img.size, (255, 255, 255, 255))
img_alpha = img.getchannel(3).point(lambda x: 255 if x>0 else 0)
stroke_alpha = img_alpha.filter(ImageFilter.MaxFilter(stroke_radius))
# optionally, smooth the result
stroke_alpha = stroke_alpha.filter(ImageFilter.SMOOTH)
stroke_image.putalpha(stroke_alpha)
output = Image.alpha_composite(stroke_image, img)
output.save("output.png")
聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.