├── .gitattributes ├── .gitignore ├── README.md ├── lena.jpg ├── pencil0.jpg └── pencil_draw.py /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | 4 | # Custom for Visual Studio 5 | *.cs diff=csharp 6 | 7 | # Standard to msysgit 8 | *.doc diff=astextplain 9 | *.DOC diff=astextplain 10 | *.docx diff=astextplain 11 | *.DOCX diff=astextplain 12 | *.dot diff=astextplain 13 | *.DOT diff=astextplain 14 | *.pdf diff=astextplain 15 | *.PDF diff=astextplain 16 | *.rtf diff=astextplain 17 | *.RTF diff=astextplain 18 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Windows image file caches 2 | Thumbs.db 3 | ehthumbs.db 4 | 5 | # Folder config file 6 | Desktop.ini 7 | 8 | # Recycle Bin used on file shares 9 | $RECYCLE.BIN/ 10 | 11 | # Windows Installer files 12 | *.cab 13 | *.msi 14 | *.msm 15 | *.msp 16 | 17 | # Windows shortcuts 18 | *.lnk 19 | 20 | # ========================= 21 | # Operating System Files 22 | # ========================= 23 | 24 | # OSX 25 | # ========================= 26 | 27 | .DS_Store 28 | .AppleDouble 29 | .LSOverride 30 | 31 | # Thumbnails 32 | ._* 33 | 34 | # Files that might appear in the root of a volume 35 | .DocumentRevisions-V100 36 | .fseventsd 37 | .Spotlight-V100 38 | .TemporaryItems 39 | .Trashes 40 | .VolumeIcon.icns 41 | 42 | # Directories potentially created on remote AFP share 43 | .AppleDB 44 | .AppleDesktop 45 | Network Trash Folder 46 | Temporary Items 47 | .apdisk 48 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## Introduction 2 | This project is an implentation of the paper [<>](http://www.cse.cuhk.edu.hk/leojia/projects/pencilsketch/pencil_drawing.htm) using python. 3 | 4 | Now it could only produce grayscale result, the color result will be done in future. 5 | 6 | The code reference to the an matlab implentation on github, see (https://github.com/candycat1992/PencilDrawing) 7 | 8 | 9 | ## Dependcy 10 | * numpy 11 | * scipy 12 | * opencv 13 | 14 | 15 | ## Usage 16 | `python pencil_draw.py [image] [pencil texture img]` 17 | 18 | eg: `python pencil_draw.py lena.jpg pencil0.jpg` 19 | 20 | the result image will be showed 21 | 22 | ## Reference 23 | [1]Lu C, Xu L, Jia J. Combining sketch and tone for pencil drawing production[C]//Proceedings of the Symposium on Non-Photorealistic Animation and Rendering. Eurographics Association, 2012: 65-73. 24 | -------------------------------------------------------------------------------- /lena.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/moonfighting/PencilDrawing--python-version/7b7b062bd326f540f02b172f26c1668c7302a065/lena.jpg -------------------------------------------------------------------------------- /pencil0.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/moonfighting/PencilDrawing--python-version/7b7b062bd326f540f02b172f26c1668c7302a065/pencil0.jpg -------------------------------------------------------------------------------- /pencil_draw.py: -------------------------------------------------------------------------------- 1 | import cv2 2 | import numpy as np 3 | from matplotlib import pyplot as plt 4 | from scipy.sparse import spdiags 5 | from scipy.sparse import dia_matrix 6 | from scipy.sparse.linalg import cg 7 | 8 | import math 9 | import sys 10 | import os 11 | 12 | 13 | def rotate_img(img, angle): 14 | row, col = img.shape 15 | M = cv2.getRotationMatrix2D((row / 2, col / 2), angle, 1) 16 | res = cv2.warpAffine(img, M, (row, col)) 17 | return res 18 | 19 | def get_eight_directions(l_len): 20 | L = np.zeros((8, l_len, l_len)) 21 | half_len = (l_len + 1) / 2 22 | for i in range(8): 23 | if i == 0 or i == 1 or i == 2 or i == 7: 24 | for x in range(l_len): 25 | y = half_len - int(round((x + 1 - half_len) * math.tan(math.pi * i / 8))) 26 | if y >0 and y <= l_len: 27 | L[i, x, y - 1 ] = 1 28 | if i != 7: 29 | L[i + 4] = np.rot90(L[i]) 30 | L[3] = np.rot90(L[7], 3) 31 | return L 32 | 33 | 34 | 35 | # compute and get the stroke of the raw img 36 | def get_stroke(img, ks, dirNum): 37 | height , width = img.shape[0], img.shape[1] 38 | img = np.float32(img) / 255.0 39 | img = cv2.medianBlur(img, 3) 40 | #cv2.imshow('blur', img) 41 | imX = np.append(np.absolute(img[:, 0 : width - 1] - img[:, 1 : width]), np.zeros((height, 1)), axis = 1) 42 | imY = np.append(np.absolute(img[0 : width - 1, :] - img[1 : width, :]), np.zeros((1, width)), axis = 0) 43 | #img_gredient = np.sqrt((imX ** 2 + imY ** 2)) 44 | img_gredient = imX + imY 45 | 46 | kernel_Ref = np.zeros((ks * 2 + 1, ks * 2 + 1)) 47 | kernel_Ref [ks, :] = 1 48 | 49 | response = np.zeros((dirNum, height, width)) 50 | L = get_eight_directions(2 * ks + 1) 51 | for n in range(dirNum): 52 | ker = rotate_img(kernel_Ref, n * 180 / dirNum) 53 | response[n, :, :] = cv2.filter2D(img, -1, ker) 54 | 55 | Cs = np.zeros((dirNum, height, width)) 56 | for x in range(width): 57 | for y in range(height): 58 | i = np.argmax(response[:,y,x]) 59 | Cs[i, y, x] = img_gredient[y,x] 60 | 61 | spn = np.zeros((8, img.shape[0], img.shape[1])) 62 | 63 | kernel_Ref = np.zeros((2 * ks + 1, 2 * ks + 1)) 64 | kernel_Ref [ks, :] = 1 65 | for n in range(width): 66 | if (ks - n) >= 0: 67 | kernel_Ref[ks - n, :] = 1 68 | if (ks + n) < ks * 2: 69 | kernel_Ref[ks + n, :] = 1 70 | 71 | kernel_Ref = np.zeros((2*ks + 1, 2 * ks + 1)) 72 | kernel_Ref [ks, :] = 1 73 | 74 | for i in range(8): 75 | ker = rotate_img(kernel_Ref, i * 180 / dirNum) 76 | spn[i]= cv2.filter2D(Cs[i], -1, ker) 77 | 78 | sp = np.sum(spn, axis = 0) 79 | sp = (sp - np.min(sp)) / (np.max(sp) - np.min(sp)) 80 | S = 1 - sp 81 | 82 | return S 83 | 84 | 85 | #for histogram matching 86 | def natural_histogram_matching(img): 87 | ho = np.zeros( 256) 88 | po = np.zeros( 256) 89 | for i in range(256): 90 | po[i] = np.sum(img == i) 91 | po = po / np.sum(po) 92 | ho[0] = po[0] 93 | for i in range(1,256): 94 | ho[i] = ho[i - 1] + po[i] 95 | 96 | p1 = lambda x : (1 / 9.0) * np.exp(-(255 - x) / 9.0) 97 | p2 = lambda x : (1.0 / (225 - 105)) * (x >= 105 and x <= 225) 98 | p3 = lambda x : (1.0 / np.sqrt(2 * math.pi *11) ) * np.exp(-((x - 90) ** 2) / float((2 * (11 **2)))) 99 | p = lambda x : (76 * p1(x) +22 * p2(x) + 2 * p3(x)) * 0.01 100 | prob = np.zeros(256) 101 | histo = np.zeros(256) 102 | total = 0 103 | for i in range(256): 104 | prob[i] = p(i) 105 | total = total + prob[i] 106 | prob = prob / np.sum(prob) 107 | 108 | histo[0] = prob[0] 109 | for i in range(1, 256): 110 | histo[i] = histo[i - 1] + prob[i] 111 | 112 | Iadjusted = np.zeros((img.shape[0], img.shape[1])) 113 | for x in range(img.shape[0]): 114 | for y in range(img.shape[1]): 115 | histogram_value = ho[img[x,y]] 116 | i = np.argmin(np.absolute(histo - histogram_value)) 117 | Iadjusted[x, y] = i 118 | 119 | Iadjusted = np.float64(Iadjusted) / 255.0 120 | return Iadjusted 121 | 122 | 123 | #compute the tone map 124 | def get_toneMap(img, P): 125 | P = np.float64(P) / 255.0 126 | J = natural_histogram_matching(img) 127 | J = cv2.blur(J, (10, 10)) 128 | theta = 0.2 129 | 130 | height, width = img.shape 131 | 132 | P = cv2.resize(P, (height, width)) 133 | P = P.reshape((1, height * width)) 134 | logP = np.log(P) 135 | logP = spdiags(logP, 0, width * height, width * height) 136 | 137 | 138 | J = cv2.resize(J, (height, width)) 139 | J = J.reshape( height * width) 140 | logJ = np.log(J) 141 | 142 | e = np.ones(width * height) 143 | 144 | Dx = spdiags([-e, e], np.array([0, height]), width *height, width * height) 145 | Dy = spdiags([-e, e], np.array([0, 1]), width * height, width * height) 146 | 147 | 148 | A = theta * (Dx.dot(Dx.transpose()) + Dy.dot(Dy.transpose())) + logP.dot(logP.transpose()) 149 | 150 | b= logP.transpose().dot(logJ) 151 | beta, info = cg(A, b , tol = 1e-6, maxiter = 60) 152 | 153 | beta = beta.reshape((height, width)) 154 | P = P.reshape((height, width)) 155 | T = np.power(P, beta) 156 | 157 | return T 158 | 159 | def pencil_drawing(img_path, pencil_texture): 160 | P = cv2.imread(pencil_texture, cv2.IMREAD_GRAYSCALE) 161 | img = cv2.imread(img_path, cv2.IMREAD_GRAYSCALE) 162 | 163 | S = get_stroke(img, 3, 8) 164 | T = get_toneMap(img, P) 165 | 166 | res = S * T 167 | 168 | return res 169 | 170 | 171 | 172 | if __name__ == '__main__': 173 | 174 | if len(sys.argv) < 3: 175 | print 'usage: python %s [img] [pencil texture]' % os.path.basename(sys.argv[0]) 176 | img_path = sys.argv[1] 177 | pencil_texture = sys.argv[2] 178 | 179 | #img_path = 'lena.jpg' 180 | #pencil_texture = 'pencil0.jpg' 181 | res = pencil_drawing(img_path, pencil_texture) 182 | cv2.imshow('res', res) 183 | cv2.waitKey(0) --------------------------------------------------------------------------------