├── image_processor.py ├── lip.jpg ├── main_snakes.py ├── snakes.py └── visualize_snake.py /image_processor.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | import cv2 3 | 4 | def normalizeRange(Img, minVal=0, maxVal=255): 5 | srcMinVal = np.min(Img) 6 | srcMaxVal = np.max(Img) 7 | 8 | # rescale 9 | ds = srcMaxVal -srcMinVal 10 | dt = maxVal - minVal 11 | 12 | # Normalize 13 | Out = (Img - srcMinVal) * 1.0 14 | Out = dt * Out / ds 15 | 16 | return Out 17 | 18 | def drawSnakes(Img, S, c=(255, 0, 0)): 19 | R = Img.copy() 20 | 21 | for s in S: 22 | [v1, v2] = s 23 | v1 = int(v1) 24 | v2 = int(v2) 25 | 26 | cv2.circle(R, (v1, v2), 3, c) 27 | 28 | return R 29 | -------------------------------------------------------------------------------- /lip.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ghif/ActiveContourModel/cd2f41ff29fee187afcccd4858bbed972d398eec/lip.jpg -------------------------------------------------------------------------------- /main_snakes.py: -------------------------------------------------------------------------------- 1 | import image_processor as iproc 2 | import numpy as np 3 | # import active_contour as ac 4 | import snakes 5 | 6 | import skimage.io as sio 7 | from skimage.color import rgb2gray 8 | from skimage.draw import circle_perimeter 9 | from skimage.filters import gaussian 10 | from skimage.exposure import equalize_hist 11 | 12 | import cv2 13 | 14 | # Read image 15 | # Im = cv2.imread('babylip.jpg') 16 | # Im = cv2.imread('koreanlip.jpg') 17 | Im = cv2.imread('lip.jpg') 18 | Img = rgb2gray(Im) 19 | # Img = equalize_hist(Img) 20 | 21 | # Initialize contour 22 | ## circle 23 | # s = np.linspace(0, 2*np.pi, 400) 24 | # x = 260 + 210 * np.cos(s) 25 | # y = 310 + 210 * np.sin(s) 26 | 27 | ## ellipse 28 | s = np.linspace(0, 2*np.pi, 400) 29 | x = 210 + 210 * np.cos(s) 30 | y = 90 + 75 * np.sin(s) 31 | 32 | V = np.array([x, y]).T 33 | # V = np.delete(V, -1, axis=0) 34 | 35 | R = iproc.drawSnakes(Im, V, c=(255, 0, 0)) 36 | # cv2.imshow('footage', R) 37 | 38 | 39 | # Run Snakes 40 | sn = snakes.Snakes() 41 | S, Edge, Map, Gim = sn.fit_kaas( 42 | Img, V, 43 | alpha=0.1, beta=3, tau=100, 44 | w_line=0.0, w_edge=1.0, convergence=0.1 45 | ) 46 | 47 | R = iproc.drawSnakes(R, S, c=(0, 0, 255)) 48 | # cv2.imshow('footage', R) 49 | # cv2.imshow('Edge', Edge) 50 | # cv2.imshow('Map', Map) 51 | 52 | cv2.imwrite('results/snake.jpg', R) 53 | cv2.imwrite('results/map.jpg', iproc.normalizeRange(Map, minVal=0, maxVal=255)) 54 | cv2.imwrite('results/gim.jpg', iproc.normalizeRange(Gim, minVal=0, maxVal=255)) 55 | 56 | # Store snake history 57 | np.save('results/sn_hist.npy', sn.hist) 58 | -------------------------------------------------------------------------------- /snakes.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | 3 | import scipy 4 | from skimage.util import img_as_float 5 | from skimage.filters import sobel, gaussian 6 | from scipy.interpolate import RectBivariateSpline 7 | 8 | import image_processor as iproc 9 | 10 | class Snakes(object): 11 | def __init__(self, name='snakes'): 12 | self.name = name 13 | self.hist = {} 14 | 15 | def fit_kaas(self, Im, init_snake, alpha=0.01, beta=0.1, 16 | w_line=0, w_edge=1, tau=100, 17 | bc='periodic', max_px_move=1.0, 18 | max_iter=2500, convergence=0.1, convergence_order=10): 19 | """ 20 | Snake fitting based on the original method 21 | Kaas, M., Witkin, A., Terzopoulos, D. "Snakes: Active Contour Models". [IJCV 1998] 22 | 23 | Adopted from skimage active_contour 24 | https://github.com/scikit-image/scikit-image/blob/master/skimage/segmentation/active_contour_model.py 25 | 26 | Args: 27 | Im (ndarray) : (H x W) grayscaled image 28 | init_snake (ndarray) : (n, 2) initial snake contour 29 | For periodic snakes, it should not include duplicate endpoints 30 | alpha (float) : Snake length shape parameter 31 | beta (float) : Snake smoothness shape parameter 32 | w_line (float) : Controls attraction to brightness. Use negative values to attract to dark regions 33 | w_edge (float) : Controls attraction to edges. Use negative values to repel snake from edges 34 | tau (float) : time step 35 | bc {'periodic', 'free', 'fixed'} : Boundary conditions for snake. 'periodic' attaches the two ends of the snake 36 | max_px_move : maximum pixel distance to move per iteration 37 | max_iter : maximum iterations to optimize snake shape 38 | convergence : convergence criteria 39 | 40 | Returns: 41 | snake (ndarray) : (n, 2) final snake contour 42 | Edge (ndarray) : (H x W) sobel edge image 43 | Map (ndarray) : (H x W) external energy 44 | 45 | """ 46 | # Gaussian smoothing 47 | Img = img_as_float(Im) 48 | Img = gaussian(Img, 2) 49 | 50 | # Compute edge 51 | Edge = sobel(Img) 52 | 53 | # Superimpose intensity and edge images 54 | Img = iproc.normalizeRange(Img, minVal=0, maxVal=1) # normalize to 0-1 values, IMPORTANT! 55 | Edge = iproc.normalizeRange(Edge, minVal=0, maxVal=1) 56 | Map = w_line * Img + w_edge * Edge 57 | 58 | # Interpolate for smoothness 59 | intp_fn = RectBivariateSpline( 60 | np.arange(Map.shape[1]), 61 | np.arange(Map.shape[0]), 62 | Map.T, kx=2, ky=2, s=0 63 | ) 64 | 65 | # Get snake contour axes 66 | x0, y0 = init_snake[:, 0].astype(np.float), init_snake[:, 1].astype(np.float) 67 | 68 | # store snake progress 69 | sn = np.array([x0, y0]).T 70 | self.hist['snakes'] = [] 71 | self.hist['snakes'].append(sn) 72 | 73 | # for convergence computation 74 | xsave = np.empty((convergence_order, len(x0))) 75 | ysave = np.empty((convergence_order, len(y0))) 76 | 77 | # Build finite difference matrices 78 | n = len(x0) 79 | A = np.roll(np.eye(n), -1, axis=0) + \ 80 | np.roll(np.eye(n), -1, axis=1) - \ 81 | 2*np.eye(n) # second order derivative, central difference 82 | B = np.roll(np.eye(n), -2, axis=0) + \ 83 | np.roll(np.eye(n), -2, axis=1) - \ 84 | 4*np.roll(np.eye(n), -1, axis=0) - \ 85 | 4*np.roll(np.eye(n), -1, axis=1) + \ 86 | 6*np.eye(n) # fourth order derivative, central difference 87 | Z = -alpha*A + beta*B 88 | 89 | 90 | # Impose boundary conditions different from periodic: 91 | sfixed = False 92 | if bc.startswith('fixed'): 93 | Z[0, :] = 0 94 | Z[1, :] = 0 95 | Z[1, :3] = [1, -2, 1] 96 | sfixed = True 97 | 98 | efixed = False 99 | if bc.endswith('fixed'): 100 | Z[-1, :] = 0 101 | Z[-2, :] = 0 102 | Z[-2, -3:] = [1, -2, 1] 103 | efixed = True 104 | 105 | sfree = False 106 | if bc.startswith('free'): 107 | Z[0, :] = 0 108 | Z[0, :3] = [1, -2, 1] 109 | Z[1, :] = 0 110 | Z[1, :4] = [-1, 3, -3, 1] 111 | sfree = True 112 | 113 | efree = False 114 | if bc.endswith('free'): 115 | Z[-1, :] = 0 116 | Z[-1, -3:] = [1, -2, 1] 117 | Z[-2, :] = 0 118 | Z[-2, -4:] = [-1, 3, -3, 1] 119 | efree = True 120 | 121 | # Calculate inverse 122 | # Zinv = scipy.linalg.inv(Z + gamma * np.eye(n)) 123 | Zinv = scipy.linalg.inv(np.eye(n) + tau * Z) 124 | 125 | # Snake energy minimization 126 | x = np.copy(x0) 127 | y = np.copy(y0) 128 | 129 | 130 | 131 | for i in range(max_iter): 132 | fx = intp_fn(x, y, dx=1, grid=False) 133 | fy = intp_fn(x, y, dy=1, grid=False) 134 | 135 | # # Normalize external forces 136 | # fx = fx / (np.linalg.norm(fx) + 1e-6) 137 | # fy = fy / (np.linalg.norm(fy) + 1e-6) 138 | 139 | if sfixed: 140 | fx[0] = 0 141 | fx[0] = 0 142 | if efixed: 143 | fx[-1] = 0 144 | fy[-1] = 0 145 | if sfree: 146 | fx[0] *= 2 147 | fy[0] *= 2 148 | if efree: 149 | fx[-1] *= 2 150 | fy[-1] *= 2 151 | 152 | # xn = np.dot(Zinv, gamma *x + fx) 153 | # yn = np.dot(Zinv, gamma *y + fy) 154 | xn = np.dot(Zinv, x + tau * fx) 155 | yn = np.dot(Zinv, y + tau * fy) 156 | 157 | # Movements are capped to max_px_move per iter 158 | dx = max_px_move * np.tanh(xn-x) 159 | dy = max_px_move * np.tanh(yn-y) 160 | 161 | if sfixed: 162 | dx[0] = 0 163 | dy[0] = 0 164 | 165 | if efixed: 166 | dx[-1] = 0 167 | dy[-1] = 0 168 | 169 | x += dx 170 | y += dy 171 | 172 | # Convergence criteria 173 | # - x(t) - x(t-i} < convergence 174 | # - i: the last k (convergence_order) of previous snakes 175 | 176 | j = i % (convergence_order+1) 177 | dist = 10000000 178 | if j < convergence_order: 179 | xsave[j, :] = x 180 | ysave[j, :] = y 181 | else: 182 | dist = np.min(np.max(np.abs(xsave-x[None, :]) + 183 | np.abs(ysave-y[None, :]), 1)) 184 | if dist < convergence: 185 | break 186 | print('Iter-%d/%d: convergence_order: %d, j: %d, dist: %f' % (i, max_iter, convergence_order, j, dist)) 187 | # store snake progress 188 | sn = np.array([x, y]).T 189 | self.hist['snakes'].append(sn) 190 | # end for 191 | 192 | snake = np.array([x, y]).T 193 | 194 | return snake, Edge, Map, Img 195 | -------------------------------------------------------------------------------- /visualize_snake.py: -------------------------------------------------------------------------------- 1 | import image_processor as iproc 2 | import numpy as np 3 | 4 | import cv2 5 | 6 | # Load snakes history 7 | hist = np.load('results/sn_hist.npy').item() 8 | snake_list = hist['snakes'] 9 | 10 | # Read input image 11 | Im = cv2.imread('lip.jpg') 12 | 13 | for i, S in enumerate(snake_list): 14 | R = iproc.drawSnakes(Im, S, c=(0, 0, 255)) 15 | R = iproc.normalizeRange(R, minVal=0, maxVal=255) 16 | impath = "results/snakes.%d.jpg" % i 17 | cv2.imwrite(impath, R) 18 | 19 | --------------------------------------------------------------------------------