簡體   English   中英

如何在 Python 中使用 Pillow 庫向 PNG 圖像添加輪廓/描邊/邊框?

[英]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,它基於形態膨脹,讓你設置筆畫大小和顏色。 但它非常慢,而且它似乎不適用於小元素。

如果有人可以幫助我加快速度,那將非常有幫助。

透明png無描邊輪廓

帶有描邊輪廓的透明 png

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)

使用 PIL 的解決方案

我面臨着同樣的需求:概述 PNG 圖像。

這是輸入圖像:

輸入圖像

我看到已經找到了一些解決方案,但如果你們中的一些人想要另一種選擇,這是我的:

基本上,我的解決方案工作流程如下:

  1. 讀取並用邊框顏色填充 PNG 圖像的非 alpha 通道
  2. 調整單色圖像的大小使其變大
  3. 將原始圖像合並為更大的單色圖像

給你 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()

這是輸出圖像:

Output 圖片

希望能幫助到你!

我找到了一種使用 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.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM