天天看點

使用QuadTree算法在Python中實作Photo Stylizer

作者 | Richard Barrera

來源 | Medium

編輯 | 代碼醫生團隊

最近發現邁克爾·弗格曼(Michael Fogleman)完成了一個叫做四叉樹藝術的項目。它激發了嘗試編寫自己的項目版本。這就是将在本文中讨論的,如何實作自己的Quadtree藝術程式,就像在這裡所做的那樣:

github.com/ribab/quadart

使用QuadTree算法在Python中實作Photo Stylizer

上圖是用kstudio在freepik.com上找到的蘋果圖檔制作的圖像。原件看起來像這樣:

使用QuadTree算法在Python中實作Photo Stylizer

隻有當顔色的标準偏差太高時,算法才會基本上繼續将圖像劃分為象限。

為了說明算法工作,實作了QuadArt的最大遞歸功能,使用這個shell指令建立了10個不同遞歸深度的不同圖像:for i in {1..10}; do ./quadart.py apple.jpg -o r-out/apple-r$i.jpg -m $i --thresh 25; done 然後通過指令使用 ImageMagick 生成PNG convert -delay 40 -loop 0 *.jpg apple-r.gif 。GIF在下方,展示了四方魔法。

使用QuadTree算法在Python中實作Photo Stylizer

簡單來說,QuadArt算法

盡管程式QuadArt占用了181行代碼,但用于生成QuadArt的實際遞歸算法隻能在8行中描述

class QuadArt:
  ...
 def recursive_draw(self, x, y, w, h):
      '''Draw the QuadArt recursively
      '''
     if self.too_many_colors(int(x), int(y), int(w), int(h)):
         self.recursive_draw(x,         y,         w/2.0, h/2.0)
         self.recursive_draw(x + w/2.0, y,         w/2.0, h/2.0)
         self.recursive_draw(x,         y + h/2.0, w/2.0, h/2.0)
         self.recursive_draw(x + w/2.0, y + h/2.0, w/2.0, h/2.0)
     else:
         self.draw_avg(x, y, w, h)           

複制

以上算法直接從代碼中提取。class QuadArt是包含imageio圖像資料,wand繪制畫布和标準偏差門檻值的類。x,y,w,h,被傳遞到函數來指定x,則目前感分析後的子圖像的左上角的y位置,沿着與它的寬度和高度。

調試緩慢的QuadArt生成

最初使用Python Wand子產品實作了整個QuadArt程式,該子產品使用了ImageMagick。這個庫精美地渲染圓圈。在第一次實作基于四叉樹的照片過濾器的編碼後,遇到了一個代碼占用時間過長的問題。事實證明,讓Wand檢查每個像素的顔色對于計算标準偏差來說太長了,并且Wand沒有用于執行這種分析的内置功能。此外當沒有在螢幕上顯示任何内容時,很難判斷代碼是否卡住了。

為了判斷代碼是否有任何進展,需要某種加載條。但是使用疊代算法可以更加輕松地加載條形圖,可以準确地知道算法需要多少次疊代才能完成。使用基于四叉樹的遞歸算法,知道遞歸深度1最多可運作4次,深度2最多運作16次,依此類推。是以考慮到這個想法,實作了對算法的補充,以在程式執行時在終端中顯示加載條。此加載欄跟蹤遞歸算法在深度3處執行的次數。

使用QuadTree算法在Python中實作Photo Stylizer

對于跟蹤進度的加載欄功能 recursive_draw(),隻需要跟蹤其退出點,并跟蹤目前的遞歸深度。兩種退出點是 recursive_draw() 進一步遞歸或不進行遞歸。這是 recursive_draw() 修改為調用的函數 loading_bar():

def recursive_draw(self, x, y, w, h):
    '''Draw the QuadArt recursively
    '''
    if self.too_many_colors(int(x), int(y), int(w), int(h)):
        self.recurse_depth += 1
 
        self.recursive_draw(x,         y,         w/2.0, h/2.0)
        self.recursive_draw(x + w/2.0, y,         w/2.0, h/2.0)
        self.recursive_draw(x,         y + h/2.0, w/2.0, h/2.0)
        self.recursive_draw(x + w/2.0, y + h/2.0, w/2.0, h/2.0)
 
        self.recurse_depth -= 1
 
        if self.recurse_depth == 3:
            loading_bar(self.recurse_depth)
    else:
        self.draw_avg(x, y, w, h)
 
        loading_bar(self.recurse_depth)           

複制

loading_bar() 有邏輯隻在深度<= 3的情況下計算進度,但是仍然需要 self.recurse_depth 在第一個出口點檢查電流是否等于3,recursive_draw() 否則會 loading_bar() 因遞歸而産生備援調用。

這就是 loading_bar() 看起來像

def loading_bar(recurse_depth):
    global load_progress
    global start_time
    load_depth=3
    recursion_spread=4
    try:
        load_progress
        start_time
    except:
        load_progress = 0
        start_time = time.time()
        print('[' + ' '*(recursion_spread**load_depth) + ']\r', end='')
    if recurse_depth <= load_depth:
        load_progress += recursion_spread**(load_depth - recurse_depth)
        cur_time = time.time()
        time_left = recursion_spread**load_depth*(cur_time - start_time)/load_progress \
                  - cur_time + start_time
        print('[' + '='*load_progress \
                  + ' '*(recursion_spread**load_depth - load_progress) \
                  + '] ' \
                  + 'time left: {} secs'.format(int(time_left)).ljust(19) \
                  + '\r', end='')           

複制

為了監視自己的遞歸函數,可以很容易地将它放在python代碼的頂部,修改 recursion_spread 為每次遞歸時函數調用自身的次數,然後 loading_bar() 從所有遞歸函數的端點調用,確定它是每個遞歸分支隻調用一次。

使用imageio和numpy進行圖像分析

對于 recursive_draw() 是否分割成更多象限的門檻值,該函數 too_many_colors() 計算紅色,綠色和藍色True的标準偏差,并在标準偏差超過門檻值時傳回。對于QuadArt生成,發現一個漂亮的門檻值大約是25 STD,否則圖像變得太像素化或太細粒度。python圖像分析庫imageio非常适合這種分析,因為它可以直接插入numpy以進行快速統計計算。

用于經由圖像分析初始設定imageio和numpy如下:

import imageio
import numpy as np           

複制

使用imageio讀取圖像(檔案名是正在分析的圖像的名稱)

img = imageio.imread(filename)           

複制

選擇正在分析的圖像部分。有效地裁剪img。“left”,“right”,“up”和“down”指定img的裁剪位置。

self.img = self.img[up:down,left:right]           

複制

找到圖像的寬度和高度

input_width = img.shape[1]
input_height = img.shape[0]           

複制

通過減去較短邊的較長邊的差異,確定img為正方形

if input_width < input_height:
    difference = input_height - input_width
    subtract_top = int(difference/2)
    subtract_bot = difference - subtract_top
    img = img[subtract_top:-subtract_bot,:]
elif input_height < input_width:
    difference = input_width - input_height
    subtract_left = int(difference/2)
    subtract_right = difference - subtract_left
img = img[:,subtract_left:-subtract_right]           

複制

現在imageio對象“img”可用于計算标準偏差,如下所示:

# Selecting colors
red = img[:,:,0]
green = img[:,:,1]
blue = img[:,:,2]
# Calculating averages from colors
red_avg = np.average(red)
green_avg = np.average(green)
blue_avg = np.average(blue)
# Calculating standard deviations from colors
red_std = np.std(red)
green_std = np.std(green)
blue_std = np.std(blue)           

複制

這就是程式QuadArt計算該recursive_draw()函數是否由于高顔色偏差而進一步遞歸的方式。看一眼too_many_colors()

class QuadArt:
    ...
    def too_many_colors(self, x, y, w, h):
        if w * self.output_scale <= 2 or w <= 2:
            return False
        img = self.img[y:y+h,x:x+w]
        red = img[:,:,0]
        green = img[:,:,1]
        blue = img[:,:,2]
 
        red_avg = np.average(red)
        green_avg = np.average(green)
        blue_avg = np.average(blue)
 
        if red_avg >= 254 and green_avg >= 254 and blue_avg >= 254:
            return False
 
        if 255 - red_avg < self.std_thresh and 255 - green_avg < self.std_thresh \
                                           and 255 - blue_avg < self.std_thresh:
            return True
 
        red_std = np.std(red)
        if red_std > self.std_thresh:
            return True
 
        green_std = np.std(green)
        if green_std > self.std_thresh:
            return True
 
        blue_std = np.std(blue)
        if blue_std > self.std_thresh:
            return True
 
        return False           

複制

上面的功能是這樣的:

  1. 選擇顔色
  2. 從顔色計算平均值
  3. False如果平均值非常接近白色,則立即傳回
  4. 計算顔色的标準偏差
  5. True如果标準偏差大于任何顔色的門檻值,則傳回(進一步遞歸)
  6. 否則傳回 False

最後顯示圓圈

現在到了簡單的部分:在中顯示圓圈wand。

執行圖像過濾器的政策是從空白畫布建構結果圖像。

這是如何使用Wand繪制内容的模闆

# Import Wand
from wand.image import Image
from wand.display import Display
from wand.color import Color
from wand.drawing import Drawing
 
# Set up canvas to draw on
canvas = Image(width = output_size,
               height = output_size,
               background = Color('white'))
canvas.format = 'png'
draw = Drawing()
 
# Draw circles and rectangles and anything else here
draw.fill_color = Color('rgb(%s,%s,%s)' % (red, green, blue))
draw.circle((x_center, y_center), (x_edge, y_edge))
draw.rectangle(x, y, x + w, y + h)
 
# Write drawing to the canvas
draw(canvas)
 
# If you want to display image to the screen
display(canvas)
 
# If you want to save image to a file
canvas.save(filename='output.png')           

複制

生成的畫布的寬高比QuadArt總是為正方形,是以QuadArt的遞歸算法可以将圖像均勻地分割為象限。預設情況下,使用output_size=512512是2的幂,并且可以連續分成兩半而不會失去分辨率。

但是輸入圖像的大小可能會有所不同。為了解釋這一點,将所需的outptu大小除以裁剪的輸入圖像的寬度,如下所示:

output_scale = float(output_size) / input_width           

複制

上面使用的功能 recursive_draw() 是 draw_avg() 。這是一個簡單的函數,可以計算邊界内輸入圖像的平均顔色,然後在一個框内繪制一個圓(如果使用者喜歡,則繪制一個正方形)。

class QuadArt:
    ...
    def draw_avg(self, x, y, w, h):
        avg_color = self.get_color(int(x), int(y), int(w), int(h))
        self.draw_in_box(avg_color, x, y, w, h)           

複制

該函數 get_color() 首先抓取輸入圖像的裁剪部分(imageio格式),然後計算該裁剪部分中的紅色,綠色和藍色的平均值,然後 wand.color.Color 根據計算的平均顔色建立一個對象。

class QuadArt:
    ...
    def get_color(self, x, y, w, h):
        img = self.img[y : y + h,
                       x : x + w]
        red = np.average(img[:,:,0])
        green = np.average(img[:,:,1])
        blue = np.average(img[:,:,2])
        color = Color('rgb(%s,%s,%s)' % (red, green, blue))
        return color           

複制

該函數 draw_in_box() 在定義的框内繪制圓形或正方形,這是先前由 too_many_colors() 具有足夠低偏差計算的象限。在繪制到畫布之前,坐标以及寬度和高度乘以 output_scale。并且填充顔色wand.drawing設定為先前計算的平均顔色。然後将圓形或方形繪制到畫布上。

class QuadArt:
    ...
    def draw_in_box(self, color, x, y, w, h):
        x *= self.output_scale
        y *= self.output_scale
        w *= self.output_scale
        h *= self.output_scale
 
        self.draw.fill_color = color
 
        if self.draw_type == 'circle':
            self.draw.circle((int(x + w/2.0), int(y + h/2.0)),
                             (int(x + w/2.0), int(y)))
        else:
            self.draw.rectangle(x, y, x + w, y + h)           

複制

這就是實作Quadtree Photo Stylizer的方法,以及如何實作它,或者啟發并建立自己的算法來設定照片風格。

整個代碼:

github.com/ribab/quadart/blob/master/quadart.py