├── README.md ├── cat.bmp ├── gf.py └── tulips.bmp /README.md: -------------------------------------------------------------------------------- 1 | # python-guided-filter 2 | Numpy/Scipy implementation of the (fast) Guided Filter. Adapted from Kaiming's Matlab code. 3 | 4 | gf.py:guided_filter runs the filter on one- or three-channel guide images (I) and filtering inputs (p) with any number of channels (the filter is applied per-channel of p). 5 | 6 | Citations: 7 | * [Guided Image Filtering](http://kaiminghe.com/eccv10/), by Kaiming He, Jian Sun, and Xiaoou Tang, in TPAMI 2013 8 | * [Fast Guided Filter](https://arxiv.org/abs/1505.00996), by Kaiming He and Jian Sun, arXiv 2015 9 | -------------------------------------------------------------------------------- /cat.bmp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/swehrwein/python-guided-filter/e1d6e8ace0f6f108a0d2f836e1df7d2340a347da/cat.bmp -------------------------------------------------------------------------------- /gf.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | import scipy as sp 3 | import scipy.ndimage 4 | 5 | 6 | def box(img, r): 7 | """ O(1) box filter 8 | img - >= 2d image 9 | r - radius of box filter 10 | """ 11 | (rows, cols) = img.shape[:2] 12 | imDst = np.zeros_like(img) 13 | 14 | 15 | tile = [1] * img.ndim 16 | tile[0] = r 17 | imCum = np.cumsum(img, 0) 18 | imDst[0:r+1, :, ...] = imCum[r:2*r+1, :, ...] 19 | imDst[r+1:rows-r, :, ...] = imCum[2*r+1:rows, :, ...] - imCum[0:rows-2*r-1, :, ...] 20 | imDst[rows-r:rows, :, ...] = np.tile(imCum[rows-1:rows, :, ...], tile) - imCum[rows-2*r-1:rows-r-1, :, ...] 21 | 22 | tile = [1] * img.ndim 23 | tile[1] = r 24 | imCum = np.cumsum(imDst, 1) 25 | imDst[:, 0:r+1, ...] = imCum[:, r:2*r+1, ...] 26 | imDst[:, r+1:cols-r, ...] = imCum[:, 2*r+1 : cols, ...] - imCum[:, 0 : cols-2*r-1, ...] 27 | imDst[:, cols-r: cols, ...] = np.tile(imCum[:, cols-1:cols, ...], tile) - imCum[:, cols-2*r-1 : cols-r-1, ...] 28 | 29 | return imDst 30 | 31 | def _gf_color(I, p, r, eps, s=None): 32 | """ Color guided filter 33 | I - guide image (rgb) 34 | p - filtering input (single channel) 35 | r - window radius 36 | eps - regularization (roughly, variance of non-edge noise) 37 | s - subsampling factor for fast guided filter 38 | """ 39 | fullI = I 40 | fullP = p 41 | if s is not None: 42 | I = sp.ndimage.zoom(fullI, [1/s, 1/s, 1], order=1) 43 | p = sp.ndimage.zoom(fullP, [1/s, 1/s], order=1) 44 | r = round(r / s) 45 | 46 | h, w = p.shape[:2] 47 | N = box(np.ones((h, w)), r) 48 | 49 | mI_r = box(I[:,:,0], r) / N 50 | mI_g = box(I[:,:,1], r) / N 51 | mI_b = box(I[:,:,2], r) / N 52 | 53 | mP = box(p, r) / N 54 | 55 | # mean of I * p 56 | mIp_r = box(I[:,:,0]*p, r) / N 57 | mIp_g = box(I[:,:,1]*p, r) / N 58 | mIp_b = box(I[:,:,2]*p, r) / N 59 | 60 | # per-patch covariance of (I, p) 61 | covIp_r = mIp_r - mI_r * mP 62 | covIp_g = mIp_g - mI_g * mP 63 | covIp_b = mIp_b - mI_b * mP 64 | 65 | # symmetric covariance matrix of I in each patch: 66 | # rr rg rb 67 | # rg gg gb 68 | # rb gb bb 69 | var_I_rr = box(I[:,:,0] * I[:,:,0], r) / N - mI_r * mI_r; 70 | var_I_rg = box(I[:,:,0] * I[:,:,1], r) / N - mI_r * mI_g; 71 | var_I_rb = box(I[:,:,0] * I[:,:,2], r) / N - mI_r * mI_b; 72 | 73 | var_I_gg = box(I[:,:,1] * I[:,:,1], r) / N - mI_g * mI_g; 74 | var_I_gb = box(I[:,:,1] * I[:,:,2], r) / N - mI_g * mI_b; 75 | 76 | var_I_bb = box(I[:,:,2] * I[:,:,2], r) / N - mI_b * mI_b; 77 | 78 | a = np.zeros((h, w, 3)) 79 | for i in range(h): 80 | for j in range(w): 81 | sig = np.array([ 82 | [var_I_rr[i,j], var_I_rg[i,j], var_I_rb[i,j]], 83 | [var_I_rg[i,j], var_I_gg[i,j], var_I_gb[i,j]], 84 | [var_I_rb[i,j], var_I_gb[i,j], var_I_bb[i,j]] 85 | ]) 86 | covIp = np.array([covIp_r[i,j], covIp_g[i,j], covIp_b[i,j]]) 87 | a[i,j,:] = np.linalg.solve(sig + eps * np.eye(3), covIp) 88 | 89 | b = mP - a[:,:,0] * mI_r - a[:,:,1] * mI_g - a[:,:,2] * mI_b 90 | 91 | meanA = box(a, r) / N[...,np.newaxis] 92 | meanB = box(b, r) / N 93 | 94 | if s is not None: 95 | meanA = sp.ndimage.zoom(meanA, [s, s, 1], order=1) 96 | meanB = sp.ndimage.zoom(meanB, [s, s], order=1) 97 | 98 | q = np.sum(meanA * fullI, axis=2) + meanB 99 | 100 | return q 101 | 102 | 103 | def _gf_gray(I, p, r, eps, s=None): 104 | """ grayscale (fast) guided filter 105 | I - guide image (1 channel) 106 | p - filter input (1 channel) 107 | r - window raidus 108 | eps - regularization (roughly, allowable variance of non-edge noise) 109 | s - subsampling factor for fast guided filter 110 | """ 111 | if s is not None: 112 | Isub = sp.ndimage.zoom(I, 1/s, order=1) 113 | Psub = sp.ndimage.zoom(p, 1/s, order=1) 114 | r = round(r / s) 115 | else: 116 | Isub = I 117 | Psub = p 118 | 119 | 120 | (rows, cols) = Isub.shape 121 | 122 | N = box(np.ones([rows, cols]), r) 123 | 124 | meanI = box(Isub, r) / N 125 | meanP = box(Psub, r) / N 126 | corrI = box(Isub * Isub, r) / N 127 | corrIp = box(Isub * Psub, r) / N 128 | varI = corrI - meanI * meanI 129 | covIp = corrIp - meanI * meanP 130 | 131 | 132 | a = covIp / (varI + eps) 133 | b = meanP - a * meanI 134 | 135 | meanA = box(a, r) / N 136 | meanB = box(b, r) / N 137 | 138 | if s is not None: 139 | meanA = sp.ndimage.zoom(meanA, s, order=1) 140 | meanB = sp.ndimage.zoom(meanB, s, order=1) 141 | 142 | q = meanA * I + meanB 143 | return q 144 | 145 | 146 | def _gf_colorgray(I, p, r, eps, s=None): 147 | """ automatically choose color or gray guided filter based on I's shape """ 148 | if I.ndim == 2 or I.shape[2] == 1: 149 | return _gf_gray(I, p, r, eps, s) 150 | elif I.ndim == 3 and I.shape[2] == 3: 151 | return _gf_color(I, p, r, eps, s) 152 | else: 153 | print("Invalid guide dimensions:", I.shape) 154 | 155 | 156 | def guided_filter(I, p, r, eps, s=None): 157 | """ run a guided filter per-channel on filtering input p 158 | I - guide image (1 or 3 channel) 159 | p - filter input (n channel) 160 | r - window raidus 161 | eps - regularization (roughly, allowable variance of non-edge noise) 162 | s - subsampling factor for fast guided filter 163 | """ 164 | if p.ndim == 2: 165 | p3 = p[:,:,np.newaxis] 166 | 167 | out = np.zeros_like(p3) 168 | for ch in range(p3.shape[2]): 169 | out[:,:,ch] = _gf_colorgray(I, p3[:,:,ch], r, eps, s) 170 | return np.squeeze(out) if p.ndim == 2 else out 171 | 172 | 173 | def test_gf(): 174 | import imageio 175 | cat = imageio.imread('cat.bmp').astype(np.float32) / 255 176 | tulips = imageio.imread('tulips.bmp').astype(np.float32) / 255 177 | 178 | r = 8 179 | eps = 0.05 180 | 181 | cat_smoothed = guided_filter(cat, cat, r, eps) 182 | cat_smoothed_s4 = guided_filter(cat, cat, r, eps, s=4) 183 | 184 | imageio.imwrite('cat_smoothed.png', cat_smoothed) 185 | imageio.imwrite('cat_smoothed_s4.png', cat_smoothed_s4) 186 | 187 | tulips_smoothed4s = np.zeros_like(tulips) 188 | for i in range(3): 189 | tulips_smoothed4s[:,:,i] = guided_filter(tulips, tulips[:,:,i], r, eps, s=4) 190 | imageio.imwrite('tulips_smoothed4s.png', tulips_smoothed4s) 191 | 192 | tulips_smoothed = np.zeros_like(tulips) 193 | for i in range(3): 194 | tulips_smoothed[:,:,i] = guided_filter(tulips, tulips[:,:,i], r, eps) 195 | imageio.imwrite('tulips_smoothed.png', tulips_smoothed) 196 | -------------------------------------------------------------------------------- /tulips.bmp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/swehrwein/python-guided-filter/e1d6e8ace0f6f108a0d2f836e1df7d2340a347da/tulips.bmp --------------------------------------------------------------------------------