Photoshop中磁力套索的一種簡陋實現(基於Python)


經常用Photoshop的人應該熟悉磁力套索(Magnetic Lasso)這個功能,就是人為引導下的摳圖輔助工具。在研發領域一般不這么叫,通常管這種邊緣提取的辦法叫Intelligent Scissors或者Livewire。

本來是給一個圖像分割項目算法評估時的Python框架,覺得有點意思,就稍稍拓展了一下,用PyQt加了個殼,在非常簡陋的程度上模擬了一下的磁力套索功能。為什么簡陋:1) 只實現了最基本的邊緣查找。路徑冷卻,動態訓練,鼠標位置修正都沒有,更別提曲線閉合,摳圖,Alpha Matting等等;2) 沒考慮性能規范,只為了書寫方便;3) 我對Qt了解很淺,至今不會寫Signal-Slot,不知道GUI寫得是否合理;4) 沒調試。

基本算法

相關算法我並沒有做很深入的調研,不過相信這類應用中影響力最大的算法是來源於[1],也是本文的主要參考,基本思想是把圖片看成是一個無向圖,相鄰像素之間就可以計算出一個局部cost,於是就轉化成了最短路徑問題了,接下來就是基於Dijkstra算法產生路徑,就是需要提取的邊緣。主要涉及的算法有兩部分:1) 相鄰像素的cost計算;2) 最短路徑算法。

邊緣檢測

計算相鄰像素cost的最終目的還是為了尋找邊緣,所以本質還是邊緣檢測。基本思想是,通過各種不同手段檢測邊緣,並且根據檢測到的強度來求加權值,作為cost。從最短路徑的角度來說,就是邊緣越明顯的地方,cost的值越小。[1]中的建議是用三種指標求加權:1) 邊緣檢測算子;2) 梯度強度(Gradient Magnitude);3) 梯度方向(Gradient Direction)。本文的方法和[1]有那么一些不一樣,因為懶,用了OpenCV中的Canny算子檢測邊緣而不是Laplacian Zero-Crossing Operator。表達式如下:

\[l\left( p,q \right)={{w}_{E}}{{f}_{E}}\left( q \right)+{{w}_{G}}{{f}_{G}}\left( q \right)+{{w}_{D}}{{f}_{D}}\left( p,q \right)\]

Canny算子

基本思想是根據梯度信息,先檢測出許多連通的像素,然后對於每一坨連通的像素只取其中最大值且連通的部分,將周圍置零,得到初始的邊緣(Edges),這個過程叫做Non-Maximum Suppression。然后用二閾值的辦法將這些檢測到的初始邊緣分為Strong, Weak, and None三個等級,顧名思義,Strong就是很確定一定是邊緣了,None就被舍棄,然后從Weak中挑選和Strong連通的作為保留的邊緣,得到最后的結果,這個過程叫做Hysteresis Thresholding。這個算法太經典,更多細節一Google出來一大堆,我就不贅述了。公式如下:

\[{{f}_{E}}\left( q \right)=\left\{ \begin{matrix}
   0;\text{ if }q\text{ is on a edge}  \\
   1;\text{ if }q\text{ is not on a edge}  \\
\end{matrix} \right.\]

其實從權值的計算上和最大梯度有些重復,因為如果沿着最大梯度方向找出來的路徑基本上就是邊緣,這一項的作用我的理解主要應該是1) 避免梯度都很大的區域出現離明顯邊緣的偏離;2) 保證提取邊緣的連續性,一定程度上來講也是保證平滑。

梯度強度

就是梯度求模而已,x和y兩個方向的梯度值平方相加在開方,公式如下:

\[{{I}_{G}}\left( q \right)=\sqrt{{{I}_{x}}\left( q \right)+{{I}_{y}}\left( q \right)}\]

因為要求cost,所以反向並歸一化:

\[{{f}_{G}}\left( q \right)=1-\frac{{{I}_{G}}\left( q \right)}{\max \left( {{I}_{G}} \right)}\]

梯度方向

這一項其實是個平滑項,會給變化劇烈的邊緣賦一個比較高的cost,讓提取的邊緣避免噪聲的影響。具體公式如下:

\[{{f}_{D}}\left( p,q \right)=\frac{2}{3\pi }\left( \arccos \left( {{d}_{p}}\left( p,q \right) \right)+\arccos \left( {{d}_{q}}\left( p,q \right) \right) \right)\]

其中,

\[{{d}_{p}}\left( p,q \right)=\left\langle {{d}_{\bot }}\left( p \right),{{l}_{D}}\left( p,q \right) \right\rangle \]

\[{{d}_{q}}\left( p,q \right)=\left\langle {{l}_{D}}\left( p,q \right),{{d}_{\bot }}\left( q \right) \right\rangle \]

\[{{l}_{D}}\left( p,q \right)=\left\{ \begin{matrix}
   q-p;\text{ if }\left\langle {{d}_{\bot }}\left( p \right),q-p \right\rangle \ge 0  \\
   p-q;\text{ if }\left\langle {{d}_{\bot }}\left( p \right),q-p \right\rangle <0  \\
\end{matrix} \right.\]

\({{d}_{\bot }}\left( p \right)\)是取p的垂直方向,另外注意上式中符號的判斷會將\({{d}_{\bot }}\left( p \right)\)和\({{l}_{D}}\left( p,q \right)\)的取值限制在π/2以內。

\[{{d}_{\bot }}\left( p \right)=\left( {{p}_{y}},-{{p}_{x}} \right)\]

斜對角方向的cost修正

在二維圖像中,相鄰的像素通常按照間隔歐式距離分為兩種:1) 上下左右相鄰,間隔為像素邊長;2) 斜對角相鄰,間隔為像素邊長的\(\sqrt{2}\)倍。在計算局部cost的時候通常要把這種距離差異的影響考慮進去,比如下面這幅圖:

2 3 4
5 6 6
7 8 9

如果不考慮像素位置的影響,那么查找最小cost的時候會認為左上角的cost=2最小。然而如果考慮到像素間距的影響,我們來看左上角方向,和中心的差異是6-2,做個線性插值的話,則左上角距中心單位距離上的值應該為\(6-\left( 6-2 \right)\times 1/\sqrt{2}\ =3.17>3\),所以正上方的才是最小cost的正確方向。

最短路徑查找

在磁力套索中,一般的用法是先單擊一個點,然后移動鼠標,在鼠標和一開始單擊的點之間就會出現自動貼近邊緣的線,這里我們定義一開始單擊的像素點為種子點(seed),而磁力套索其實在考慮上部分提到的邊緣相關cost的情況下查找種子點到當前鼠標的最短路徑。如下圖,紅色的就是種子點,而移動鼠標時,最貼近邊緣的種子點和鼠標坐標的連線就會實時顯示,這也是為什么磁力套索也叫Livewire。

實現最短路徑的辦法很多,一般而言就是動態規划了,這里介紹的是基於Dijkstra算法的一種實現,基本思想是,給定種子點后,執行Dijkstra算法將圖像的所有像素遍歷,得到每個像素到種子點的最短路徑。以下面這幅圖為例,在一個cost矩陣中,利用Dijkstra算法遍歷每一個元素后,每個元素都會指向一個相鄰的元素,這樣任意一個像素都能找到一條到seed的路徑,比如右上角的42和39對應的像素,沿着箭頭到了0。

算法如下:

輸入:
  s              // 種子點
  l(q,r)         // 計算局部cost

數據結構:
  L             // 當前待處理的像素
  N(q)          // 當前像素相鄰的像素
  e(q)          // 標記一個像素是否已經做過相鄰像素展開的Bool函數
  g(q)          // 從s到q的總cost

輸出:
  p             // 記錄所有路徑的map

算法:
  g(s)←0; L←s;                 // 將種子點作為第一點初始化
  while L≠Ø:                   // 遍歷尚未結束
    q←min(L);                  // 取出最小cost的像素並從待處理像素中移除
    e(q)←TRUE;                 // 將當前像素記錄為已經做過相鄰像素展開
    for each r∈N(q) and not e(r):
      gtemp←g(q)+l(q,r);        // 計算相鄰像素的總cost
      if r∈L and gtemp<g(r):    // 找到了更好的路徑
        r←L; { from list.}     // 舍棄較大cost的路徑
      else if r∉L:
        g(r)←gtemp;             // 記錄當前找到的最小路徑
        p(r)←q;
        L←r;                    // 加入待處理以試圖尋找更短的路徑

遍歷的過程會優先經過cost最低的區域,如下圖:

所有像素對應的到種子像素的最短路徑都找到后,移動鼠標時就直接畫出到seed的最短路徑就可以了。

Python實現

算法部分直接調用了OpenCV的Canny函數和Sobel函數(求梯度),對於RGB的處理也很簡陋,直接用梯度最大的值來近似。另外因為懶,cost map和path map都直接用了字典(dict),而記錄展開過的像素則直接采用了集合(set)。GUI部分因為不會用QThread所以用了Python的threading,只有圖像顯示交互區域和狀態欄提示,左鍵點擊設置種子點,右鍵結束,已經提取的邊緣為綠色線,正在提取的為藍色線。

代碼

算法部分

  1 from __future__ import division
  2 import cv2
  3 import numpy as np
  4 
  5 SQRT_0_5 = 0.70710678118654757
  6 
  7 class Livewire():
  8     """
  9     A simple livewire implementation for verification using 
 10         1. Canny edge detector + gradient magnitude + gradient direction
 11         2. Dijkstra algorithm
 12     """
 13     
 14     def __init__(self, image):
 15         self.image = image
 16         self.x_lim = image.shape[0]
 17         self.y_lim = image.shape[1]
 18         # The values in cost matrix ranges from 0~1
 19         self.cost_edges = 1 - cv2.Canny(image, 85, 170)/255.0
 20         self.grad_x, self.grad_y, self.grad_mag = self._get_grad(image)
 21         self.cost_grad_mag = 1 - self.grad_mag/np.max(self.grad_mag)
 22         # Weight for (Canny edges, gradient magnitude, gradient direction)
 23         self.weight = (0.425, 0.425, 0.15)
 24         
 25         self.n_pixs = self.x_lim * self.y_lim
 26         self.n_processed = 0
 27     
 28     @classmethod
 29     def _get_grad(cls, image):
 30         """
 31         Return the gradient magnitude of the image using Sobel operator
 32         """
 33         rgb = True if len(image.shape) > 2 else False
 34         grad_x = cv2.Sobel(image, cv2.CV_64F, 1, 0, ksize=3)
 35         grad_y = cv2.Sobel(image, cv2.CV_64F, 0, 1, ksize=3)
 36         if rgb:
 37             # A very rough approximation for quick verification...
 38             grad_x = np.max(grad_x, axis=2)
 39             grad_y = np.max(grad_y, axis=2)
 40             
 41         grad_mag = np.sqrt(grad_x**2+grad_y**2)
 42         grad_x /= grad_mag
 43         grad_y /= grad_mag
 44         
 45         return grad_x, grad_y, grad_mag
 46     
 47     def _get_neighbors(self, p):
 48         """
 49         Return 8 neighbors around the pixel p
 50         """
 51         x, y = p
 52         x0 = 0 if x == 0 else x - 1
 53         x1 = self.x_lim if x == self.x_lim - 1 else x + 2
 54         y0 = 0 if y == 0 else y - 1
 55         y1 = self.y_lim if y == self.y_lim - 1 else y + 2
 56         
 57         return [(x, y) for x in xrange(x0, x1) for y in xrange(y0, y1) if (x, y) != p]
 58     
 59     def _get_grad_direction_cost(self, p, q):
 60         """
 61         Calculate the gradient changes refer to the link direction
 62         """
 63         dp = (self.grad_y[p[0]][p[1]], -self.grad_x[p[0]][p[1]])
 64         dq = (self.grad_y[q[0]][q[1]], -self.grad_x[q[0]][q[1]])
 65         
 66         l = np.array([q[0]-p[0], q[1]-p[1]], np.float)
 67         if 0 not in l:
 68             l *= SQRT_0_5
 69         
 70         dp_l = np.dot(dp, l)
 71         l_dq = np.dot(l, dq)
 72         if dp_l < 0:
 73             dp_l = -dp_l
 74             l_dq = -l_dq
 75         
 76         # 2/3pi * ...
 77         return 0.212206590789 * (np.arccos(dp_l)+np.arccos(l_dq))
 78     
 79     def _local_cost(self, p, q):
 80         """
 81         1. Calculate the Canny edges & gradient magnitude cost taking into account Euclidean distance
 82         2. Combine with gradient direction
 83         Assumption: p & q are neighbors
 84         """
 85         diagnol = q[0] == p[0] or q[1] == p[1]
 86         
 87         # c0, c1 and c2 are costs from Canny operator, gradient magnitude and gradient direction respectively
 88         if diagnol:
 89             c0 = self.cost_edges[p[0]][p[1]]-SQRT_0_5*(self.cost_edges[p[0]][p[1]]-self.cost_edges[q[0]][q[1]])
 90             c1 = self.cost_grad_mag[p[0]][p[1]]-SQRT_0_5*(self.cost_grad_mag[p[0]][p[1]]-self.cost_grad_mag[q[0]][q[1]])
 91             c2 = SQRT_0_5 * self._get_grad_direction_cost(p, q)
 92         else:
 93             c0 = self.cost_edges[q[0]][q[1]]
 94             c1 = self.cost_grad_mag[q[0]][q[1]]
 95             c2 = self._get_grad_direction_cost(p, q)
 96         
 97         if np.isnan(c2):
 98             c2 = 0.0
 99         
100         w0, w1, w2 = self.weight
101         cost_pq = w0*c0 + w1*c1 + w2*c2
102         
103         return cost_pq * cost_pq
104 
105     def get_path_matrix(self, seed):
106         """
107         Get the back tracking matrix of the whole image from the cost matrix
108         """
109         neighbors = []          # 8 neighbors of the pixel being processed
110         processed = set()       # Processed point
111         cost = {seed: 0.0}      # Accumulated cost, initialized with seed to itself
112         paths = {}
113 
114         self.n_processed = 0
115         
116         while cost:
117             # Expand the minimum cost point
118             p = min(cost, key=cost.get)
119             neighbors = self._get_neighbors(p)
120             processed.add(p)
121 
122             # Record accumulated costs and back tracking point for newly expanded points
123             for q in [x for x in neighbors if x not in processed]:
124                 temp_cost = cost[p] + self._local_cost(p, q)
125                 if q in cost:
126                     if temp_cost < cost[q]:
127                         cost.pop(q)
128                 else:
129                     cost[q] = temp_cost
130                     processed.add(q)
131                     paths[q] = p
132             
133             # Pop traversed points
134             cost.pop(p)
135             
136             self.n_processed += 1
137         
138         return paths
livewire.py

GUI部分

  1 from __future__ import division
  2 import time
  3 import cv2
  4 from PyQt4 import QtGui, QtCore
  5 from threading import Thread
  6 from livewire import Livewire
  7 
  8 class ImageWin(QtGui.QWidget):
  9     def __init__(self):
 10         super(ImageWin, self).__init__()
 11         self.setupUi()
 12         self.active = False
 13         self.seed_enabled = True
 14         self.seed = None
 15         self.path_map = {}
 16         self.path = []
 17         
 18     def setupUi(self):
 19         self.hbox = QtGui.QVBoxLayout(self)
 20         
 21         # Load and initialize image
 22         self.image_path = ''
 23         while self.image_path == '':
 24             self.image_path = QtGui.QFileDialog.getOpenFileName(self, '', '', '(*.bmp *.jpg *.png)')
 25         self.image = QtGui.QPixmap(self.image_path)
 26         self.cv2_image = cv2.imread(str(self.image_path))
 27         self.lw = Livewire(self.cv2_image)
 28         self.w, self.h = self.image.width(), self.image.height()
 29         
 30         self.canvas = QtGui.QLabel(self)
 31         self.canvas.setMouseTracking(True)
 32         self.canvas.setPixmap(self.image)
 33         
 34         self.status_bar = QtGui.QStatusBar(self)
 35         self.status_bar.showMessage('Left click to set a seed')
 36         
 37         self.hbox.addWidget(self.canvas)
 38         self.hbox.addWidget(self.status_bar)
 39         self.setLayout(self.hbox)
 40     
 41     def mousePressEvent(self, event):            
 42         if self.seed_enabled:
 43             pos = event.pos()
 44             x, y = pos.x()-self.canvas.x(), pos.y()-self.canvas.y()
 45             
 46             if x < 0:
 47                 x = 0
 48             if x >= self.w:
 49                 x = self.w - 1
 50             if y < 0:
 51                 y = 0
 52             if y >= self.h:
 53                 y = self.h - 1
 54 
 55             # Get the mouse cursor position
 56             p = y, x
 57             seed = self.seed
 58             
 59             # Export bitmap
 60             if event.buttons() == QtCore.Qt.MidButton:
 61                 filepath = QtGui.QFileDialog.getSaveFileName(self, 'Save image audio to', '', '*.bmp\n*.jpg\n*.png')
 62                 image = self.image.copy()
 63                 
 64                 draw = QtGui.QPainter()
 65                 draw.begin(image)
 66                 draw.setPen(QtCore.Qt.blue)
 67                 if self.path_map:
 68                     while p != seed:
 69                         draw.drawPoint(p[1], p[0])
 70                         for q in self.lw._get_neighbors(p):
 71                             draw.drawPoint(q[1], q[0])
 72                         p = self.path_map[p]
 73                 if self.path:
 74                     draw.setPen(QtCore.Qt.green)
 75                     for p in self.path:
 76                         draw.drawPoint(p[1], p[0])
 77                         for q in self.lw._get_neighbors(p):
 78                             draw.drawPoint(q[1], q[0])
 79                 draw.end()
 80                 
 81                 image.save(filepath, quality=100)
 82             
 83             else:
 84                 self.seed = p
 85                 
 86                 if self.path_map:
 87                     while p != seed:
 88                         p = self.path_map[p]
 89                         self.path.append(p)
 90                 
 91                 # Calculate path map
 92                 if event.buttons() == QtCore.Qt.LeftButton:
 93                     Thread(target=self._cal_path_matrix).start()
 94                     Thread(target=self._update_path_map_progress).start()
 95                 
 96                 # Finish current task and reset
 97                 elif event.buttons() == QtCore.Qt.RightButton:
 98                     self.path_map = {}
 99                     self.status_bar.showMessage('Left click to set a seed')
100                     self.active = False
101     
102     def mouseMoveEvent(self, event):
103         if self.active and event.buttons() == QtCore.Qt.NoButton:
104             pos = event.pos()
105             x, y = pos.x()-self.canvas.x(), pos.y()-self.canvas.y()
106 
107             if x < 0 or x >= self.w or y < 0 or y >= self.h:
108                 pass
109             else:
110                 # Draw livewire
111                 p = y, x
112                 path = []
113                 while p != self.seed:
114                     p = self.path_map[p]
115                     path.append(p)
116                 
117                 image = self.image.copy()
118                 draw = QtGui.QPainter()
119                 draw.begin(image)
120                 draw.setPen(QtCore.Qt.blue)
121                 for p in path:
122                     draw.drawPoint(p[1], p[0])
123                 if self.path:
124                     draw.setPen(QtCore.Qt.green)
125                     for p in self.path:
126                         draw.drawPoint(p[1], p[0])
127                 draw.end()
128                 self.canvas.setPixmap(image)
129     
130     def _cal_path_matrix(self):
131         self.seed_enabled = False
132         self.active = False
133         self.status_bar.showMessage('Calculating path map...')
134         path_matrix = self.lw.get_path_matrix(self.seed)
135         self.status_bar.showMessage(r'Left: new seed / Right: finish')
136         self.seed_enabled = True
137         self.active = True
138         
139         self.path_map = path_matrix
140     
141     def _update_path_map_progress(self):
142         while not self.seed_enabled:
143             time.sleep(0.1)
144             message = 'Calculating path map... {:.1f}%'.format(self.lw.n_processed/self.lw.n_pixs*100.0)
145             self.status_bar.showMessage(message)
146         self.status_bar.showMessage(r'Left: new seed / Right: finish')
gui.py

主函數

 1 import sys
 2 from PyQt4 import QtGui
 3 from gui import ImageWin
 4 
 5 def main():
 6     app = QtGui.QApplication(sys.argv)
 7     window = ImageWin()
 8     window.setMouseTracking(True)
 9     window.setWindowTitle('Livewire Demo')
10     window.show()
11     window.setFixedSize(window.size())
12     sys.exit(app.exec_())
13 
14 if __name__ == '__main__':
15     main()    
main.py

蛋疼地上傳到了Github(傳送門),歡迎fork。

效率的改進

因為這個代碼的原型只是為了用C++開發之前的Python評估和驗證,所以完全沒考慮效率,執行速度是完全不行的,基本上400x400的圖片就不能忍了……至於基於Python版本的效率提升我沒有仔細想過,只是大概看來有這么幾個比較明顯的地方:

1) 取出當前最小cost像素操作

 p = min(cost, key=cost.get) 

這個雖然寫起來很爽但顯然是不行的,至少得用個min heap什么的。因為我是用dict同時表示待處理像素和cost了,也懶得想一下怎么和Python的heapq結合起來,所以直接用了粗暴省事的min()。

2) 梯度方向的計算

三角函數的計算應該是盡量避免的,另外原文可能是為了將值域擴展到>π所以把q-p也用上了,其實這一項本來權重就小,那怕直接用兩個像素各自的梯度方向向量做點積然后歸一化一下結果也是還行的。即使要用arccos,也可以考慮寫個look-up table近似。當然我最后想說的是個人覺得其實這項真沒那么必要,直接自適應spilne或者那怕三點均值平滑去噪效果就不錯了。

3) 計算相鄰像素的位置

如果兩個像素相鄰,則他們各自周圍的8個相鄰像素也會重合。的我的辦法比較原始,可以考率不用模塊化直接計算。

4) 替換部分數據結構

比如path map其實本質是給出每個像素在最短路徑上的上一個像素,是個矩陣。其實可以考慮用線性的數據結構代替,不過如果真這樣做一般來說都是在C/C++代碼里了。

5) numpy

我印象中對numpy的調用順序也會影響到效率,連續調用numpy的內置方法似乎會帶來效率的整體提升,不過話還是說回來,實際應用中如果到了這一步,應該也屬於C/C++代碼范疇了。

6) 算法層面的改進

這塊沒有深入研究,第一感覺是實際應用中沒必要一上來就計算整幅圖像,可以根據seed位置做一些區塊划分,鼠標本身也會留下軌跡,也或許可以考慮只在鼠標軌跡方向進行啟發式搜索。另外計算路徑的時候也許可以考慮借鑒有點類似於Image Pyramid的思想,沒必要一上來就對全分辨率下的路徑進行查找。由於后來做的項目沒有采用這個算法,所以我也沒有繼續研究,雖然挺好奇的,其實有好多現成的代碼,比如GIMP,不過沒有精力去看了。

更多的改進

雖然都沒做,大概介紹一下,都是考慮了實用性的改進。

路徑冷卻(Path Cooling)

用過Photoshop和GIMP磁力套索的人都知道,即使鼠標不點擊圖片,在移動過程中也會自動生成一些將摳圖軌跡固定住的點,這些點其實就是新的種子點,而這種使用過程中自動生成新的種子點的方法叫Path cooling。這個方法的基本思路如下:隨着鼠標移動過程中如果一定時間內一段路徑都保持固定不變,那么就把這段路徑中離種子最遠的點設置為新的種子,其實背后隱藏的還是動態規划的思想,貝爾曼最優。這個名字也是比較形象的,路徑冷卻。

動態訓練(Interactive Dynamic Training)

單純的最短路徑查找在使用的時候常常出現找到的邊緣不是想要的邊緣的問題,比如上圖,綠色的線是上一段提取的邊緣,藍色的是當前正在提取的邊緣。左圖中,鏡子外面Lena的帽子邊緣是我們想要提取的,然而由於鏡子里的Lena的帽子邊緣的cost更低,所以實際提取出的藍色線段如右圖中貼到右邊了。所以Interactive Dynamic Training的思想是,認為綠色的線段是正確提取的邊緣,然后利用綠色線段作為訓練數據來給當前提取邊緣的cost函數附加一個修正值。

[1]中采用的方法是統計前一段邊緣上點的梯度強度的直方圖,然后按照梯度出現頻率給當前圖中的像素加權。舉例來說如果綠色線段中的所有像素對應的梯度強度都是在50到100之間的話,那么可以將50到100以10為單位分為5個bin,統計每個bin里的出現頻率,也就是直方圖,然后對當前檢測到的梯度強度,做個線性加權。比方說50~60區間內對應的像素最多有10個,那么把10作為最大值,並且對當前檢測到的梯度強度處於50~60之間的像素均乘上系數1.0;如果訓練數據中70~80之間有5個,那么cost加權系數為5/10=0.5,則對所有當前檢測到的梯度強度處於70~80之間的像素均乘上系數0.5;如果訓練數據中100以上沒有,所以cost附加為0/10=0,則加權系數為0,這樣即使檢測到更強的邊緣也不會偏離前一段邊緣了。這是基本思想,當然實際的實現沒有這么簡單,除了邊緣上的像素還要考慮垂直邊緣上左邊和右邊的兩個像素點,這樣保證了邊緣的pattern。另外隨着鼠標越來越遠離訓練邊緣,檢測到的邊緣的pattern可能會出現不一樣,所以Training可能會起反作用,所以這種Training的作用范圍也需要考慮到鼠標離種子點的距離,最后還要有一些平滑去噪的處理,具體都在[1]里有講到,挺繁瑣的(那會好像還沒有SIFT),不詳述了。

種子點位置的修正(Cursor Snap)

雖然這個算法可以自動找出種子點和鼠標之間最貼近邊緣的路徑,不過,人的手,常常抖,所以種子點未必能很好地設置到邊緣上。所以可以在用戶設置完種子點位置之后,自動在其坐標周圍小范圍內,比如7x7的區域內搜索cost最低的像素,作為真正的種子點位置,這個過程叫做Cursor snap。

參考文獻:
[1] Mortensen, Eric N., and William A. Barrett. "Intelligent scissors for image composition." Proceedings of the 22nd annual conference on Computer graphics and interactive techniques. ACM, 1995.


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM