├── README.md ├── constant.py ├── main.py └── images2gif.py /README.md: -------------------------------------------------------------------------------- 1 | # Face Morpher 2 | Applying Delaunay triangulation and affline warping to face morph two pictures. 3 | 4 | ![Mathurkuriakose](http://imgur.com/NTxSicO.gif) 5 | 6 | ## Algorithm 7 | The algorithm uses Delaunay's triangulation. To do this, we indiviually select points that match each other on both images. After doing this, triangles are automatically created such that the size of both don't exceed 45 degrees. This allows each individual triangle to be affline warped to the set of points that are changed. After we create our triangulation, we affline warp a certain amount, depending on how far we want our image to look like 8 | 9 | ## Running Code 10 | ``` 11 | python main.py 12 | ``` 13 | Note you'll need a dependency on cv2 for the some of the helper functions. Both images also need to be of size 500 x 500 and the same size. 14 | -------------------------------------------------------------------------------- /constant.py: -------------------------------------------------------------------------------- 1 | # Kuriakose Sony Theakanath 2 | # Face Morphing 3 | # Constants 4 | 5 | RATIO = 0.047619047619047616 # total of 20 frames 6 | TOTAL_FEATURE = 66 # 66 total feature points 7 | TRIANGLES = [[20,21,23],[21,22,23],[0,1,36],[15,16,45],[0,17,36],[16,26,45],[17,18,37],[25,26,44],[17,36,37],[26,44,45],[18,19,38],[24,25,43],[18,37,38],[25,43,44],[19,20,38],[23,24,43],[20,21,39],[22,23,42],[20,38,39],[23,42,43],[21,22,27],[21,27,39],[22,27,42],[27,28,42],[27,28,39],[28,42,47],[28,39,40],[1,36,41],[15,45,46],[1,2,41],[14,15,46],[28,29,40],[28,29,47],[2,40,41],[14,46,47],[2,29,40],[14,29,47],[2,3,29],[13,14,29],[29,30,31],[29,30,35],[3,29,31],[13,29,35],[30,32,33],[30,33,34],[30,31,32],[30,34,35],[3,4,31],[12,13,35],[4,5,48],[11,12,54],[5,6,48],[10,11,54],[6,48,59],[10,54,55],[6,7,59],[9,10,55],[7,58,59],[9,55,56],[8,57,58],[8,56,57],[7,8,58],[8,9,56],[4,31,48],[12,35,54],[31,48,49],[35,53,54],[31,49,50],[35,52,53],[31,32,50],[34,35,52],[32,33,50],[33,34,52],[33,50,51],[33,51,52],[48,49,60],[49,60,50],[50,60,61],[50,51,61],[51,52,61],[61,62,52],[52,53,62],[53,54,62],[54,55,63],[55,56,63],[56,63,64],[56,57,64],[64,65,57],[57,58,65],[58,59,65],[48,59,65],[66,19,18],[66,18,17],[66,17,0],[67,66,0],[67,0,1],[67,1,2],[67,2,3],[67,3,68],[68,3,4],[68,4,5],[68,5,6],[68,6,7],[68,7,69],[69,7,8],[69,8,9],[69,9,70],[70,9,10],[70,10,11],[70,11,12],[70,12,13],[70,13,71],[71,13,14],[71,14,15],[71,15,16],[71,16,72],[72,16,26],[72,26,25],[72,25,24],[73,24,72],[73,23,24],[73,20,23],[73,19,20],[73,19,66], [60,65,61],[61,65,64],[61,64,62],[64,62,63],[36,37,41],[37,41,38],[41,38,40],[38,40,39],[42,43,47],[43,47,44],[44,47,46],[44,46,45],[48,60,65],[62,63,54]] 8 | CORNERS = [(0, 0), (0, 720), (0, 760), (720, 760), (720, 760), (760, 720), (760, 0), (720, 0)] # adjust according to picture size -------------------------------------------------------------------------------- /main.py: -------------------------------------------------------------------------------- 1 | # Kuriakose Sony Theakanath 2 | # Face Morphing 3 | 4 | import numpy as np 5 | import matplotlib.pyplot as plt 6 | import matplotlib.image as mpimg 7 | import matplotlib 8 | import constant 9 | from PIL import Image 10 | from scipy import misc 11 | import argparse 12 | from pylab import arange, plot, sin, ginput, show 13 | import sys 14 | sys.path.append('/usr/local/lib/python2.7/site-packages') 15 | import cv2 16 | from images2gif import writeGif 17 | 18 | # Part 1 - Allows user to select points on the supplied image. 19 | def selectPoints(im1_path, im2_path): 20 | im = Image.open(im1_path) 21 | plt.imshow(im) 22 | counter, f_points = constant.TOTAL_FEATURE, [] 23 | while counter != 0: 24 | print "Click on screen!" 25 | x = ginput(1) 26 | counter -= 1 27 | f_points.append([x[0][0], x[0][1]]) 28 | plt.scatter(x[0][0], x[0][1]) 29 | plt.draw() 30 | print("Clicked point at ", x, " | Clicks left: ", counter) 31 | plt.show() 32 | second_points = drag_control_points(mpimg.imread(im2_path), np.array(f_points)) 33 | 34 | intermediate_feature = interpolatePts(combinePoints(f_points, second_points)) 35 | frames = combineImages(intermediate_feature, constant.TRIANGLES, im1_path, im2_path) 36 | frames.extend(frames[::-1]) 37 | # otherone = [cv2.cvtColor(items, cv2.COLOR_RGB2BGR) for items in frames] 38 | # writeGif("lol.GIF", otherone, duration=0.07) 39 | while True: 40 | for i in range (0, len(frames)): 41 | f = frames[i] 42 | cv2.waitKey(20) 43 | cv2.imshow("Cameras",f) 44 | cv2.waitKey(20) 45 | 46 | # Step 2 - Creates a triangulation from the points given 47 | def interpolatePts(features): 48 | frame, middle = [(constant.RATIO * i) for i in xrange(0, 22)], [] 49 | for r in xrange(0, len(frame)): 50 | middle.append([(pair[0][0] * (1 - frame[r]) + pair[1][0] * frame[r], pair[0][1] * (1-frame[r]) + pair[1][1] * frame[r]) for pair in features] + constant.CORNERS) 51 | return middle 52 | 53 | # Step 3, takes the features and warps it according to the triangles. 54 | def warpImage(orig, features, diang, src): 55 | image = cv2.imread(src) 56 | masked_image = np.zeros(image.shape, dtype=np.uint8) 57 | for t in diang: 58 | mask = np.zeros(image.shape, dtype=np.uint8) 59 | cv2.fillPoly(mask, np.array([[features[t[0]], features[t[1]], features[t[2]]]], dtype=np.int32), (255, 255, 255)) 60 | masked_image = cv2.bitwise_or(masked_image, cv2.bitwise_and(cv2.warpAffine(image, cv2.getAffineTransform(np.float32([orig[t[0]], orig[t[1]], orig[t[2]]]), np.float32([features[t[0]], features[t[1]], features[t[2]]])), (image.shape[1],image.shape[0])), mask)) 61 | return masked_image 62 | 63 | # Step 4, takes the warped images, and warps it, creating a video frame for viewing. 64 | def combineImages(features, diag, path1, path2): 65 | frames = [] 66 | for i in xrange(0, 22): 67 | frames.append(cv2.addWeighted(warpImage(features[0], features[i], diag, path1), 1 - constant.RATIO * i, warpImage(features[21], features[i], diag, path2), constant.RATIO * i, 0)) 68 | return frames 69 | 70 | # Sub-process - calculates the average face with a provided folder 71 | def averageFace(path): 72 | TOTAL_NUM = 100 73 | w, h, arr = Image.open(path + "/1a.jpg").size, np.zeros((h,w,3),np.float) 74 | while TOTAL_NUM != 0: 75 | arr = arr + np.array(Image.open(path + "/" + str(TOTAL_NUM) + "a.jpg"), dtype=np.float) / 100 76 | TOTAL_NUM -= 1 77 | arr = np.array(np.round(arr), dtype=np.uint8) 78 | Image.fromarray(arr, mode="RGB").save("average_ " + path + ".png") 79 | 80 | # Sub-process - takes an image, allows a user to select points, and exports to .dat file 81 | def exportShape(path): 82 | plt.imshow(Image.open(path)) 83 | counter, f_points = constant.TOTAL_FEATURE, [] 84 | while counter != 0: 85 | print "Click on screen!" 86 | x = ginput(1) 87 | counter -= 1 88 | f_points.append([x[0][0], x[0][1]]) 89 | plt.scatter(x[0][0], x[0][1]) 90 | plt.draw() 91 | print("Clicked point at ", x, " | Clicks left: ", counter) 92 | plt.show() 93 | np.savetxt('shape.dat', f_points) 94 | return f_points 95 | 96 | # For selection of the second image - borrowed from Piazza with a few modifications 97 | def drag_control_points(img, cpts): 98 | cpts = cpts.copy() 99 | scale = (img.shape[0]**2 + img.shape[1]**2)**0.5/20 100 | fh = plt.figure('Close window to terminate') 101 | ah = fh.add_subplot(111) 102 | ah.imshow(img, cmap='gray') 103 | temp = ah.axis() 104 | ah.set_xlim(temp[0:2]) 105 | ah.set_ylim(temp[2:4]) 106 | lh = [None] 107 | lh[0] = ah.plot(cpts[:,0], cpts[:,1], 'g.')[0] 108 | 109 | idx = [None] 110 | figure_exist = [True] 111 | 112 | def on_press(event): 113 | diff = np.abs(np.array([[event.xdata, event.ydata]]) - cpts).sum(axis=(1,)) 114 | idx[0] = np.argmin(diff) 115 | if diff[idx[0]] > scale: 116 | idx[0] = None 117 | else: 118 | temp_cpts = np.delete(cpts, idx[0], axis=0) 119 | lh[0].remove() 120 | lh[0] = ah.plot(temp_cpts[:,0], temp_cpts[:,1], 'g.')[0] 121 | fh.canvas.draw() 122 | 123 | def on_release(event): 124 | if idx[0] != None: 125 | cpts[idx[0], 0] = event.xdata 126 | cpts[idx[0], 1] = event.ydata 127 | lh[0].remove() 128 | lh[0] = ah.plot(cpts[:,0], cpts[:,1], 'g.')[0] 129 | fh.canvas.draw() 130 | 131 | def handle_close(event): 132 | figure_exist[0] = False 133 | 134 | fh.canvas.mpl_connect('close_event', handle_close) 135 | fh.canvas.mpl_connect('button_press_event', on_press) 136 | fh.canvas.mpl_connect('button_release_event', on_release) 137 | fh.show() 138 | while figure_exist[0]: 139 | plt.waitforbuttonpress() 140 | return cpts 141 | 142 | # Helper function to combine points from image 1 and image 2 143 | def combinePoints(pt1, pt2): 144 | super_array = [] 145 | for coor1, coor2 in zip(pt1, pt2): 146 | super_array.append([tuple(coor1), tuple(coor2.tolist())]) 147 | return super_array 148 | 149 | # Main Function Calls 150 | # averageFace("frontimages") 151 | selectPoints(sys.argv[1], sys.argv[2]) 152 | -------------------------------------------------------------------------------- /images2gif.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Copyright (C) 2012, Almar Klein, Ant1, Marius van Voorden 3 | # 4 | # This code is subject to the (new) BSD license: 5 | # 6 | # Redistribution and use in source and binary forms, with or without 7 | # modification, are permitted provided that the following conditions are met: 8 | # * Redistributions of source code must retain the above copyright 9 | # notice, this list of conditions and the following disclaimer. 10 | # * Redistributions in binary form must reproduce the above copyright 11 | # notice, this list of conditions and the following disclaimer in the 12 | # documentation and/or other materials provided with the distribution. 13 | # * Neither the name of the nor the 14 | # names of its contributors may be used to endorse or promote products 15 | # derived from this software without specific prior written permission. 16 | # 17 | # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 18 | # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 19 | # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 20 | # ARE DISCLAIMED. IN NO EVENT SHALL BE LIABLE FOR ANY 21 | # DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 22 | # (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 23 | # LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND 24 | # ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 25 | # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 26 | # SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 27 | 28 | """ Module images2gif 29 | 30 | Provides functionality for reading and writing animated GIF images. 31 | Use writeGif to write a series of numpy arrays or PIL images as an 32 | animated GIF. Use readGif to read an animated gif as a series of numpy 33 | arrays. 34 | 35 | Note that since July 2004, all patents on the LZW compression patent have 36 | expired. Therefore the GIF format may now be used freely. 37 | 38 | Acknowledgements 39 | ---------------- 40 | 41 | Many thanks to Ant1 for: 42 | * noting the use of "palette=PIL.Image.ADAPTIVE", which significantly 43 | improves the results. 44 | * the modifications to save each image with its own palette, or optionally 45 | the global palette (if its the same). 46 | 47 | Many thanks to Marius van Voorden for porting the NeuQuant quantization 48 | algorithm of Anthony Dekker to Python (See the NeuQuant class for its 49 | license). 50 | 51 | Many thanks to Alex Robinson for implementing the concept of subrectangles, 52 | which (depening on image content) can give a very significant reduction in 53 | file size. 54 | 55 | This code is based on gifmaker (in the scripts folder of the source 56 | distribution of PIL) 57 | 58 | 59 | Usefull links 60 | ------------- 61 | * http://tronche.com/computer-graphics/gif/ 62 | * http://en.wikipedia.org/wiki/Graphics_Interchange_Format 63 | * http://www.w3.org/Graphics/GIF/spec-gif89a.txt 64 | 65 | """ 66 | # todo: This module should be part of imageio (or at least based on) 67 | 68 | import os, time 69 | 70 | def encode(x): 71 | if False: 72 | return x.encode('utf-8') 73 | return x 74 | 75 | try: 76 | import PIL 77 | from PIL import Image 78 | from PIL.GifImagePlugin import getheader, getdata 79 | except ImportError: 80 | PIL = None 81 | 82 | try: 83 | import numpy as np 84 | except ImportError: 85 | np = None 86 | 87 | def get_cKDTree(): 88 | try: 89 | from scipy.spatial import cKDTree 90 | except ImportError: 91 | cKDTree = None 92 | return cKDTree 93 | 94 | 95 | # getheader gives a 87a header and a color palette (two elements in a list). 96 | # getdata()[0] gives the Image Descriptor up to (including) "LZW min code size". 97 | # getdatas()[1:] is the image data itself in chuncks of 256 bytes (well 98 | # technically the first byte says how many bytes follow, after which that 99 | # amount (max 255) follows). 100 | 101 | def checkImages(images): 102 | """ checkImages(images) 103 | Check numpy images and correct intensity range etc. 104 | The same for all movie formats. 105 | """ 106 | # Init results 107 | images2 = [] 108 | 109 | for im in images: 110 | if PIL and isinstance(im, PIL.Image.Image): 111 | # We assume PIL images are allright 112 | images2.append(im) 113 | 114 | elif np and isinstance(im, np.ndarray): 115 | # Check and convert dtype 116 | if im.dtype == np.uint8: 117 | images2.append(im) # Ok 118 | elif im.dtype in [np.float32, np.float64]: 119 | im = im.copy() 120 | im[im<0] = 0 121 | im[im>1] = 1 122 | im *= 255 123 | images2.append( im.astype(np.uint8) ) 124 | else: 125 | im = im.astype(np.uint8) 126 | images2.append(im) 127 | # Check size 128 | if im.ndim == 2: 129 | pass # ok 130 | elif im.ndim == 3: 131 | if im.shape[2] not in [3,4]: 132 | raise ValueError('This array can not represent an image.') 133 | else: 134 | raise ValueError('This array can not represent an image.') 135 | else: 136 | raise ValueError('Invalid image type: ' + str(type(im))) 137 | 138 | # Done 139 | return images2 140 | 141 | 142 | def intToBin(i): 143 | """ Integer to two bytes """ 144 | # devide in two parts (bytes) 145 | i1 = i % 256 146 | i2 = int( i/256) 147 | # make string (little endian) 148 | return chr(i1) + chr(i2) 149 | 150 | 151 | class GifWriter: 152 | """ GifWriter() 153 | 154 | Class that contains methods for helping write the animated GIF file. 155 | 156 | """ 157 | 158 | def getheaderAnim(self, im): 159 | """ getheaderAnim(im) 160 | 161 | Get animation header. To replace PILs getheader()[0] 162 | 163 | """ 164 | bb = "GIF89a" 165 | bb += intToBin(im.size[0]) 166 | bb += intToBin(im.size[1]) 167 | bb += "\x87\x00\x00" 168 | return bb 169 | 170 | 171 | def getImageDescriptor(self, im, xy=None): 172 | """ getImageDescriptor(im, xy=None) 173 | 174 | Used for the local color table properties per image. 175 | Otherwise global color table applies to all frames irrespective of 176 | whether additional colors comes in play that require a redefined 177 | palette. Still a maximum of 256 color per frame, obviously. 178 | 179 | Written by Ant1 on 2010-08-22 180 | Modified by Alex Robinson in Janurari 2011 to implement subrectangles. 181 | 182 | """ 183 | 184 | # Defaule use full image and place at upper left 185 | if xy is None: 186 | xy = (0,0) 187 | 188 | # Image separator, 189 | bb = '\x2C' 190 | 191 | # Image position and size 192 | bb += intToBin( xy[0] ) # Left position 193 | bb += intToBin( xy[1] ) # Top position 194 | bb += intToBin( im.size[0] ) # image width 195 | bb += intToBin( im.size[1] ) # image height 196 | 197 | # packed field: local color table flag1, interlace0, sorted table0, 198 | # reserved00, lct size111=7=2^(7+1)=256. 199 | bb += '\x87' 200 | 201 | # LZW minimum size code now comes later, begining of [image data] blocks 202 | return bb 203 | 204 | 205 | def getAppExt(self, loops=float('inf')): 206 | """ getAppExt(loops=float('inf')) 207 | 208 | Application extention. This part specifies the amount of loops. 209 | If loops is 0 or inf, it goes on infinitely. 210 | 211 | """ 212 | 213 | if loops==0 or loops==float('inf'): 214 | loops = 2**16-1 215 | #bb = "" # application extension should not be used 216 | # (the extension interprets zero loops 217 | # to mean an infinite number of loops) 218 | # Mmm, does not seem to work 219 | if True: 220 | bb = "\x21\xFF\x0B" # application extension 221 | bb += "NETSCAPE2.0" 222 | bb += "\x03\x01" 223 | bb += intToBin(loops) 224 | bb += '\x00' # end 225 | return bb 226 | 227 | 228 | def getGraphicsControlExt(self, duration=0.1, dispose=2): 229 | """ getGraphicsControlExt(duration=0.1, dispose=2) 230 | 231 | Graphics Control Extension. A sort of header at the start of 232 | each image. Specifies duration and transparancy. 233 | 234 | Dispose 235 | ------- 236 | * 0 - No disposal specified. 237 | * 1 - Do not dispose. The graphic is to be left in place. 238 | * 2 - Restore to background color. The area used by the graphic 239 | must be restored to the background color. 240 | * 3 - Restore to previous. The decoder is required to restore the 241 | area overwritten by the graphic with what was there prior to 242 | rendering the graphic. 243 | * 4-7 -To be defined. 244 | 245 | """ 246 | 247 | bb = '\x21\xF9\x04' 248 | bb += chr((dispose & 3) << 2) # low bit 1 == transparency, 249 | # 2nd bit 1 == user input , next 3 bits, the low two of which are used, 250 | # are dispose. 251 | bb += intToBin( int(duration*100) ) # in 100th of seconds 252 | bb += '\x00' # no transparant color 253 | bb += '\x00' # end 254 | return bb 255 | 256 | 257 | def handleSubRectangles(self, images, subRectangles): 258 | """ handleSubRectangles(images) 259 | 260 | Handle the sub-rectangle stuff. If the rectangles are given by the 261 | user, the values are checked. Otherwise the subrectangles are 262 | calculated automatically. 263 | 264 | """ 265 | 266 | if isinstance(subRectangles, (tuple,list)): 267 | # xy given directly 268 | 269 | # Check xy 270 | xy = subRectangles 271 | if xy is None: 272 | xy = (0,0) 273 | if hasattr(xy, '__len__'): 274 | if len(xy) == len(images): 275 | xy = [xxyy for xxyy in xy] 276 | else: 277 | raise ValueError("len(xy) doesn't match amount of images.") 278 | else: 279 | xy = [xy for im in images] 280 | xy[0] = (0,0) 281 | 282 | else: 283 | # Calculate xy using some basic image processing 284 | 285 | # Check Numpy 286 | if np is None: 287 | raise RuntimeError("Need Numpy to use auto-subRectangles.") 288 | 289 | # First make numpy arrays if required 290 | for i in range(len(images)): 291 | im = images[i] 292 | if isinstance(im, Image.Image): 293 | tmp = im.convert() # Make without palette 294 | a = np.asarray(tmp) 295 | if len(a.shape)==0: 296 | raise MemoryError("Too little memory to convert PIL image to array") 297 | images[i] = a 298 | 299 | # Determine the sub rectangles 300 | images, xy = self.getSubRectangles(images) 301 | 302 | # Done 303 | return images, xy 304 | 305 | 306 | def getSubRectangles(self, ims): 307 | """ getSubRectangles(ims) 308 | 309 | Calculate the minimal rectangles that need updating each frame. 310 | Returns a two-element tuple containing the cropped images and a 311 | list of x-y positions. 312 | 313 | Calculating the subrectangles takes extra time, obviously. However, 314 | if the image sizes were reduced, the actual writing of the GIF 315 | goes faster. In some cases applying this method produces a GIF faster. 316 | 317 | """ 318 | 319 | # Check image count 320 | if len(ims) < 2: 321 | return ims, [(0,0) for i in ims] 322 | 323 | # We need numpy 324 | if np is None: 325 | raise RuntimeError("Need Numpy to calculate sub-rectangles. ") 326 | 327 | # Prepare 328 | ims2 = [ims[0]] 329 | xy = [(0,0)] 330 | t0 = time.time() 331 | 332 | # Iterate over images 333 | prev = ims[0] 334 | for im in ims[1:]: 335 | 336 | # Get difference, sum over colors 337 | diff = np.abs(im-prev) 338 | if diff.ndim==3: 339 | diff = diff.sum(2) 340 | # Get begin and end for both dimensions 341 | X = np.argwhere(diff.sum(0)) 342 | Y = np.argwhere(diff.sum(1)) 343 | # Get rect coordinates 344 | if X.size and Y.size: 345 | x0, x1 = X[0], X[-1]+1 346 | y0, y1 = Y[0], Y[-1]+1 347 | else: # No change ... make it minimal 348 | x0, x1 = 0, 2 349 | y0, y1 = 0, 2 350 | 351 | # Cut out and store 352 | im2 = im[y0:y1,x0:x1] 353 | prev = im 354 | ims2.append(im2) 355 | xy.append((x0,y0)) 356 | 357 | # Done 358 | #print('%1.2f seconds to determine subrectangles of %i images' % 359 | # (time.time()-t0, len(ims2)) ) 360 | return ims2, xy 361 | 362 | 363 | def convertImagesToPIL(self, images, dither, nq=0): 364 | """ convertImagesToPIL(images, nq=0) 365 | 366 | Convert images to Paletted PIL images, which can then be 367 | written to a single animaged GIF. 368 | 369 | """ 370 | 371 | # Convert to PIL images 372 | images2 = [] 373 | for im in images: 374 | if isinstance(im, Image.Image): 375 | images2.append(im) 376 | elif np and isinstance(im, np.ndarray): 377 | if im.ndim==3 and im.shape[2]==3: 378 | im = Image.fromarray(im,'RGB') 379 | elif im.ndim==3 and im.shape[2]==4: 380 | im = Image.fromarray(im[:,:,:3],'RGB') 381 | elif im.ndim==2: 382 | im = Image.fromarray(im,'L') 383 | images2.append(im) 384 | 385 | # Convert to paletted PIL images 386 | images, images2 = images2, [] 387 | if nq >= 1: 388 | # NeuQuant algorithm 389 | for im in images: 390 | im = im.convert("RGBA") # NQ assumes RGBA 391 | nqInstance = NeuQuant(im, int(nq)) # Learn colors from image 392 | if dither: 393 | im = im.convert("RGB").quantize(palette=nqInstance.paletteImage()) 394 | else: 395 | im = nqInstance.quantize(im) # Use to quantize the image itself 396 | images2.append(im) 397 | else: 398 | # Adaptive PIL algorithm 399 | AD = Image.ADAPTIVE 400 | for im in images: 401 | im = im.convert('P', palette=AD, dither=dither) 402 | images2.append(im) 403 | 404 | # Done 405 | return images2 406 | 407 | 408 | def writeGifToFile(self, fp, images, durations, loops, xys, disposes): 409 | """ writeGifToFile(fp, images, durations, loops, xys, disposes) 410 | 411 | Given a set of images writes the bytes to the specified stream. 412 | 413 | """ 414 | 415 | # Obtain palette for all images and count each occurance 416 | palettes, occur = [], [] 417 | for im in images: 418 | #palette = getheader(im)[1] 419 | palette = getheader(im)[0][-1] 420 | if not palette: 421 | #palette = PIL.ImagePalette.ImageColor 422 | palette = im.palette.tobytes() 423 | palettes.append(palette) 424 | for palette in palettes: 425 | occur.append( palettes.count( palette ) ) 426 | 427 | # Select most-used palette as the global one (or first in case no max) 428 | globalPalette = palettes[ occur.index(max(occur)) ] 429 | 430 | # Init 431 | frames = 0 432 | firstFrame = True 433 | 434 | 435 | for im, palette in zip(images, palettes): 436 | 437 | if firstFrame: 438 | # Write header 439 | 440 | # Gather info 441 | header = self.getheaderAnim(im) 442 | appext = self.getAppExt(loops) 443 | 444 | # Write 445 | fp.write(encode(header)) 446 | fp.write(globalPalette) 447 | fp.write(encode(appext)) 448 | 449 | # Next frame is not the first 450 | firstFrame = False 451 | 452 | if True: 453 | # Write palette and image data 454 | 455 | # Gather info 456 | data = getdata(im) 457 | imdes, data = data[0], data[1:] 458 | graphext = self.getGraphicsControlExt(durations[frames], 459 | disposes[frames]) 460 | # Make image descriptor suitable for using 256 local color palette 461 | lid = self.getImageDescriptor(im, xys[frames]) 462 | 463 | # Write local header 464 | if (palette != globalPalette) or (disposes[frames] != 2): 465 | # Use local color palette 466 | fp.write(encode(graphext)) 467 | fp.write(encode(lid)) # write suitable image descriptor 468 | fp.write(palette) # write local color table 469 | fp.write(encode('\x08')) # LZW minimum size code 470 | else: 471 | # Use global color palette 472 | fp.write(encode(graphext)) 473 | fp.write(imdes) # write suitable image descriptor 474 | 475 | # Write image data 476 | for d in data: 477 | fp.write(d) 478 | 479 | # Prepare for next round 480 | frames = frames + 1 481 | 482 | fp.write(encode(";")) # end gif 483 | return frames 484 | 485 | 486 | 487 | 488 | ## Exposed functions 489 | 490 | def writeGif(filename, images, duration=0.1, repeat=True, dither=False, 491 | nq=0, subRectangles=True, dispose=None): 492 | """ writeGif(filename, images, duration=0.1, repeat=True, dither=False, 493 | nq=0, subRectangles=True, dispose=None) 494 | 495 | Write an animated gif from the specified images. 496 | 497 | Parameters 498 | ---------- 499 | filename : string 500 | The name of the file to write the image to. 501 | images : list 502 | Should be a list consisting of PIL images or numpy arrays. 503 | The latter should be between 0 and 255 for integer types, and 504 | between 0 and 1 for float types. 505 | duration : scalar or list of scalars 506 | The duration for all frames, or (if a list) for each frame. 507 | repeat : bool or integer 508 | The amount of loops. If True, loops infinitetely. 509 | dither : bool 510 | Whether to apply dithering 511 | nq : integer 512 | If nonzero, applies the NeuQuant quantization algorithm to create 513 | the color palette. This algorithm is superior, but slower than 514 | the standard PIL algorithm. The value of nq is the quality 515 | parameter. 1 represents the best quality. 10 is in general a 516 | good tradeoff between quality and speed. When using this option, 517 | better results are usually obtained when subRectangles is False. 518 | subRectangles : False, True, or a list of 2-element tuples 519 | Whether to use sub-rectangles. If True, the minimal rectangle that 520 | is required to update each frame is automatically detected. This 521 | can give significant reductions in file size, particularly if only 522 | a part of the image changes. One can also give a list of x-y 523 | coordinates if you want to do the cropping yourself. The default 524 | is True. 525 | dispose : int 526 | How to dispose each frame. 1 means that each frame is to be left 527 | in place. 2 means the background color should be restored after 528 | each frame. 3 means the decoder should restore the previous frame. 529 | If subRectangles==False, the default is 2, otherwise it is 1. 530 | 531 | """ 532 | 533 | # Check PIL 534 | if PIL is None: 535 | raise RuntimeError("Need PIL to write animated gif files.") 536 | 537 | # Check images 538 | images = checkImages(images) 539 | 540 | # Instantiate writer object 541 | gifWriter = GifWriter() 542 | 543 | # Check loops 544 | if repeat is False: 545 | loops = 1 546 | elif repeat is True: 547 | loops = 0 # zero means infinite 548 | else: 549 | loops = int(repeat) 550 | 551 | # Check duration 552 | if hasattr(duration, '__len__'): 553 | if len(duration) == len(images): 554 | duration = [d for d in duration] 555 | else: 556 | raise ValueError("len(duration) doesn't match amount of images.") 557 | else: 558 | duration = [duration for im in images] 559 | 560 | # Check subrectangles 561 | if subRectangles: 562 | images, xy = gifWriter.handleSubRectangles(images, subRectangles) 563 | defaultDispose = 1 # Leave image in place 564 | else: 565 | # Normal mode 566 | xy = [(0,0) for im in images] 567 | defaultDispose = 2 # Restore to background color. 568 | 569 | # Check dispose 570 | if dispose is None: 571 | dispose = defaultDispose 572 | if hasattr(dispose, '__len__'): 573 | if len(dispose) != len(images): 574 | raise ValueError("len(xy) doesn't match amount of images.") 575 | else: 576 | dispose = [dispose for im in images] 577 | 578 | 579 | # Make images in a format that we can write easy 580 | images = gifWriter.convertImagesToPIL(images, dither, nq) 581 | 582 | # Write 583 | fp = open(filename, 'wb') 584 | try: 585 | gifWriter.writeGifToFile(fp, images, duration, loops, xy, dispose) 586 | finally: 587 | fp.close() 588 | 589 | 590 | 591 | def readGif(filename, asNumpy=True): 592 | """ readGif(filename, asNumpy=True) 593 | 594 | Read images from an animated GIF file. Returns a list of numpy 595 | arrays, or, if asNumpy is false, a list if PIL images. 596 | 597 | """ 598 | 599 | # Check PIL 600 | if PIL is None: 601 | raise RuntimeError("Need PIL to read animated gif files.") 602 | 603 | # Check Numpy 604 | if np is None: 605 | raise RuntimeError("Need Numpy to read animated gif files.") 606 | 607 | # Check whether it exists 608 | if not os.path.isfile(filename): 609 | raise IOError('File not found: '+str(filename)) 610 | 611 | # Load file using PIL 612 | pilIm = PIL.Image.open(filename) 613 | pilIm.seek(0) 614 | 615 | # Read all images inside 616 | images = [] 617 | try: 618 | while True: 619 | # Get image as numpy array 620 | tmp = pilIm.convert() # Make without palette 621 | a = np.asarray(tmp) 622 | if len(a.shape)==0: 623 | raise MemoryError("Too little memory to convert PIL image to array") 624 | # Store, and next 625 | images.append(a) 626 | pilIm.seek(pilIm.tell()+1) 627 | except EOFError: 628 | pass 629 | 630 | # Convert to normal PIL images if needed 631 | if not asNumpy: 632 | images2 = images 633 | images = [] 634 | for im in images2: 635 | images.append( PIL.Image.fromarray(im) ) 636 | 637 | # Done 638 | return images 639 | 640 | 641 | class NeuQuant: 642 | """ NeuQuant(image, samplefac=10, colors=256) 643 | 644 | samplefac should be an integer number of 1 or higher, 1 645 | being the highest quality, but the slowest performance. 646 | With avalue of 10, one tenth of all pixels are used during 647 | training. This value seems a nice tradeof between speed 648 | and quality. 649 | 650 | colors is the amount of colors to reduce the image to. This 651 | should best be a power of two. 652 | 653 | See also: 654 | http://members.ozemail.com.au/~dekker/NEUQUANT.HTML 655 | 656 | License of the NeuQuant Neural-Net Quantization Algorithm 657 | --------------------------------------------------------- 658 | 659 | Copyright (c) 1994 Anthony Dekker 660 | Ported to python by Marius van Voorden in 2010 661 | 662 | NEUQUANT Neural-Net quantization algorithm by Anthony Dekker, 1994. 663 | See "Kohonen neural networks for optimal colour quantization" 664 | in "network: Computation in Neural Systems" Vol. 5 (1994) pp 351-367. 665 | for a discussion of the algorithm. 666 | See also http://members.ozemail.com.au/~dekker/NEUQUANT.HTML 667 | 668 | Any party obtaining a copy of these files from the author, directly or 669 | indirectly, is granted, free of charge, a full and unrestricted irrevocable, 670 | world-wide, paid up, royalty-free, nonexclusive right and license to deal 671 | in this software and documentation files (the "Software"), including without 672 | limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, 673 | and/or sell copies of the Software, and to permit persons who receive 674 | copies from any such party to do so, with the only requirement being 675 | that this copyright notice remain intact. 676 | 677 | """ 678 | 679 | NCYCLES = None # Number of learning cycles 680 | NETSIZE = None # Number of colours used 681 | SPECIALS = None # Number of reserved colours used 682 | BGCOLOR = None # Reserved background colour 683 | CUTNETSIZE = None 684 | MAXNETPOS = None 685 | 686 | INITRAD = None # For 256 colours, radius starts at 32 687 | RADIUSBIASSHIFT = None 688 | RADIUSBIAS = None 689 | INITBIASRADIUS = None 690 | RADIUSDEC = None # Factor of 1/30 each cycle 691 | 692 | ALPHABIASSHIFT = None 693 | INITALPHA = None # biased by 10 bits 694 | 695 | GAMMA = None 696 | BETA = None 697 | BETAGAMMA = None 698 | 699 | network = None # The network itself 700 | colormap = None # The network itself 701 | 702 | netindex = None # For network lookup - really 256 703 | 704 | bias = None # Bias and freq arrays for learning 705 | freq = None 706 | 707 | pimage = None 708 | 709 | # Four primes near 500 - assume no image has a length so large 710 | # that it is divisible by all four primes 711 | PRIME1 = 499 712 | PRIME2 = 491 713 | PRIME3 = 487 714 | PRIME4 = 503 715 | MAXPRIME = PRIME4 716 | 717 | pixels = None 718 | samplefac = None 719 | 720 | a_s = None 721 | 722 | 723 | def setconstants(self, samplefac, colors): 724 | self.NCYCLES = 100 # Number of learning cycles 725 | self.NETSIZE = colors # Number of colours used 726 | self.SPECIALS = 3 # Number of reserved colours used 727 | self.BGCOLOR = self.SPECIALS-1 # Reserved background colour 728 | self.CUTNETSIZE = self.NETSIZE - self.SPECIALS 729 | self.MAXNETPOS = self.NETSIZE - 1 730 | 731 | self.INITRAD = self.NETSIZE/8 # For 256 colours, radius starts at 32 732 | self.RADIUSBIASSHIFT = 6 733 | self.RADIUSBIAS = 1 << self.RADIUSBIASSHIFT 734 | self.INITBIASRADIUS = self.INITRAD * self.RADIUSBIAS 735 | self.RADIUSDEC = 30 # Factor of 1/30 each cycle 736 | 737 | self.ALPHABIASSHIFT = 10 # Alpha starts at 1 738 | self.INITALPHA = 1 << self.ALPHABIASSHIFT # biased by 10 bits 739 | 740 | self.GAMMA = 1024.0 741 | self.BETA = 1.0/1024.0 742 | self.BETAGAMMA = self.BETA * self.GAMMA 743 | 744 | self.network = np.empty((self.NETSIZE, 3), dtype='float64') # The network itself 745 | self.colormap = np.empty((self.NETSIZE, 4), dtype='int32') # The network itself 746 | 747 | self.netindex = np.empty(256, dtype='int32') # For network lookup - really 256 748 | 749 | self.bias = np.empty(self.NETSIZE, dtype='float64') # Bias and freq arrays for learning 750 | self.freq = np.empty(self.NETSIZE, dtype='float64') 751 | 752 | self.pixels = None 753 | self.samplefac = samplefac 754 | 755 | self.a_s = {} 756 | 757 | def __init__(self, image, samplefac=10, colors=256): 758 | 759 | # Check Numpy 760 | if np is None: 761 | raise RuntimeError("Need Numpy for the NeuQuant algorithm.") 762 | 763 | # Check image 764 | if image.size[0] * image.size[1] < NeuQuant.MAXPRIME: 765 | raise IOError("Image is too small") 766 | if image.mode != "RGBA": 767 | raise IOError("Image mode should be RGBA.") 768 | 769 | # Initialize 770 | self.setconstants(samplefac, colors) 771 | self.pixels = np.fromstring(image.tostring(), np.uint32) 772 | self.setUpArrays() 773 | 774 | self.learn() 775 | self.fix() 776 | self.inxbuild() 777 | 778 | def writeColourMap(self, rgb, outstream): 779 | for i in range(self.NETSIZE): 780 | bb = self.colormap[i,0]; 781 | gg = self.colormap[i,1]; 782 | rr = self.colormap[i,2]; 783 | outstream.write(rr if rgb else bb) 784 | outstream.write(gg) 785 | outstream.write(bb if rgb else rr) 786 | return self.NETSIZE 787 | 788 | def setUpArrays(self): 789 | self.network[0,0] = 0.0 # Black 790 | self.network[0,1] = 0.0 791 | self.network[0,2] = 0.0 792 | 793 | self.network[1,0] = 255.0 # White 794 | self.network[1,1] = 255.0 795 | self.network[1,2] = 255.0 796 | 797 | # RESERVED self.BGCOLOR # Background 798 | 799 | for i in range(self.SPECIALS): 800 | self.freq[i] = 1.0 / self.NETSIZE 801 | self.bias[i] = 0.0 802 | 803 | for i in range(self.SPECIALS, self.NETSIZE): 804 | p = self.network[i] 805 | p[:] = (255.0 * (i-self.SPECIALS)) / self.CUTNETSIZE 806 | 807 | self.freq[i] = 1.0 / self.NETSIZE 808 | self.bias[i] = 0.0 809 | 810 | # Omitted: setPixels 811 | 812 | def altersingle(self, alpha, i, b, g, r): 813 | """Move neuron i towards biased (b,g,r) by factor alpha""" 814 | n = self.network[i] # Alter hit neuron 815 | n[0] -= (alpha*(n[0] - b)) 816 | n[1] -= (alpha*(n[1] - g)) 817 | n[2] -= (alpha*(n[2] - r)) 818 | 819 | def geta(self, alpha, rad): 820 | try: 821 | return self.a_s[(alpha, rad)] 822 | except KeyError: 823 | length = rad*2-1 824 | mid = int(length//2) 825 | q = np.array(list(range(mid-1,-1,-1))+list(range(-1,mid))) 826 | a = alpha*(rad*rad - q*q)/(rad*rad) 827 | a[mid] = 0 828 | self.a_s[(alpha, rad)] = a 829 | return a 830 | 831 | def alterneigh(self, alpha, rad, i, b, g, r): 832 | if i-rad >= self.SPECIALS-1: 833 | lo = i-rad 834 | start = 0 835 | else: 836 | lo = self.SPECIALS-1 837 | start = (self.SPECIALS-1 - (i-rad)) 838 | 839 | if i+rad <= self.NETSIZE: 840 | hi = i+rad 841 | end = rad*2-1 842 | else: 843 | hi = self.NETSIZE 844 | end = (self.NETSIZE - (i+rad)) 845 | 846 | a = self.geta(alpha, rad)[start:end] 847 | 848 | p = self.network[lo+1:hi] 849 | p -= np.transpose(np.transpose(p - np.array([b, g, r])) * a) 850 | 851 | #def contest(self, b, g, r): 852 | # """ Search for biased BGR values 853 | # Finds closest neuron (min dist) and updates self.freq 854 | # finds best neuron (min dist-self.bias) and returns position 855 | # for frequently chosen neurons, self.freq[i] is high and self.bias[i] is negative 856 | # self.bias[i] = self.GAMMA*((1/self.NETSIZE)-self.freq[i])""" 857 | # 858 | # i, j = self.SPECIALS, self.NETSIZE 859 | # dists = abs(self.network[i:j] - np.array([b,g,r])).sum(1) 860 | # bestpos = i + np.argmin(dists) 861 | # biasdists = dists - self.bias[i:j] 862 | # bestbiaspos = i + np.argmin(biasdists) 863 | # self.freq[i:j] -= self.BETA * self.freq[i:j] 864 | # self.bias[i:j] += self.BETAGAMMA * self.freq[i:j] 865 | # self.freq[bestpos] += self.BETA 866 | # self.bias[bestpos] -= self.BETAGAMMA 867 | # return bestbiaspos 868 | def contest(self, b, g, r): 869 | """ Search for biased BGR values 870 | Finds closest neuron (min dist) and updates self.freq 871 | finds best neuron (min dist-self.bias) and returns position 872 | for frequently chosen neurons, self.freq[i] is high and self.bias[i] is negative 873 | self.bias[i] = self.GAMMA*((1/self.NETSIZE)-self.freq[i])""" 874 | i, j = self.SPECIALS, self.NETSIZE 875 | dists = abs(self.network[i:j] - np.array([b,g,r])).sum(1) 876 | bestpos = i + np.argmin(dists) 877 | biasdists = dists - self.bias[i:j] 878 | bestbiaspos = i + np.argmin(biasdists) 879 | self.freq[i:j] *= (1-self.BETA) 880 | self.bias[i:j] += self.BETAGAMMA * self.freq[i:j] 881 | self.freq[bestpos] += self.BETA 882 | self.bias[bestpos] -= self.BETAGAMMA 883 | return bestbiaspos 884 | 885 | 886 | 887 | 888 | def specialFind(self, b, g, r): 889 | for i in range(self.SPECIALS): 890 | n = self.network[i] 891 | if n[0] == b and n[1] == g and n[2] == r: 892 | return i 893 | return -1 894 | 895 | def learn(self): 896 | biasRadius = self.INITBIASRADIUS 897 | alphadec = 30 + ((self.samplefac-1)/3) 898 | lengthcount = self.pixels.size 899 | samplepixels = lengthcount / self.samplefac 900 | delta = samplepixels / self.NCYCLES 901 | alpha = self.INITALPHA 902 | 903 | i = 0; 904 | rad = biasRadius * 2**self.RADIUSBIASSHIFT 905 | if rad <= 1: 906 | rad = 0 907 | 908 | print("Beginning 1D learning: samplepixels = %1.2f rad = %i" % 909 | (samplepixels, rad) ) 910 | step = 0 911 | pos = 0 912 | if lengthcount%NeuQuant.PRIME1 != 0: 913 | step = NeuQuant.PRIME1 914 | elif lengthcount%NeuQuant.PRIME2 != 0: 915 | step = NeuQuant.PRIME2 916 | elif lengthcount%NeuQuant.PRIME3 != 0: 917 | step = NeuQuant.PRIME3 918 | else: 919 | step = NeuQuant.PRIME4 920 | 921 | i = 0 922 | printed_string = '' 923 | while i < samplepixels: 924 | if i%100 == 99: 925 | tmp = '\b'*len(printed_string) 926 | printed_string = str((i+1)*100/samplepixels)+"%\n" 927 | print(tmp + printed_string) 928 | p = self.pixels[pos] 929 | r = (p >> 16) & 0xff 930 | g = (p >> 8) & 0xff 931 | b = (p ) & 0xff 932 | 933 | if i == 0: # Remember background colour 934 | self.network[self.BGCOLOR] = [b, g, r] 935 | 936 | j = self.specialFind(b, g, r) 937 | if j < 0: 938 | j = self.contest(b, g, r) 939 | 940 | if j >= self.SPECIALS: # Don't learn for specials 941 | a = (1.0 * alpha) / self.INITALPHA 942 | self.altersingle(a, j, b, g, r) 943 | if rad > 0: 944 | self.alterneigh(a, rad, j, b, g, r) 945 | 946 | pos = (pos+step)%lengthcount 947 | 948 | i += 1 949 | if i%delta == 0: 950 | alpha -= alpha / alphadec 951 | biasRadius -= biasRadius / self.RADIUSDEC 952 | rad = biasRadius * 2**self.RADIUSBIASSHIFT 953 | if rad <= 1: 954 | rad = 0 955 | 956 | finalAlpha = (1.0*alpha)/self.INITALPHA 957 | print("Finished 1D learning: final alpha = %1.2f!" % finalAlpha) 958 | 959 | def fix(self): 960 | for i in range(self.NETSIZE): 961 | for j in range(3): 962 | x = int(0.5 + self.network[i,j]) 963 | x = max(0, x) 964 | x = min(255, x) 965 | self.colormap[i,j] = x 966 | self.colormap[i,3] = i 967 | 968 | def inxbuild(self): 969 | previouscol = 0 970 | startpos = 0 971 | for i in range(self.NETSIZE): 972 | p = self.colormap[i] 973 | q = None 974 | smallpos = i 975 | smallval = p[1] # Index on g 976 | # Find smallest in i..self.NETSIZE-1 977 | for j in range(i+1, self.NETSIZE): 978 | q = self.colormap[j] 979 | if q[1] < smallval: # Index on g 980 | smallpos = j 981 | smallval = q[1] # Index on g 982 | 983 | q = self.colormap[smallpos] 984 | # Swap p (i) and q (smallpos) entries 985 | if i != smallpos: 986 | p[:],q[:] = q, p.copy() 987 | 988 | # smallval entry is now in position i 989 | if smallval != previouscol: 990 | self.netindex[previouscol] = (startpos+i) >> 1 991 | for j in range(previouscol+1, smallval): 992 | self.netindex[j] = i 993 | previouscol = smallval 994 | startpos = i 995 | self.netindex[previouscol] = (startpos+self.MAXNETPOS) >> 1 996 | for j in range(previouscol+1, 256): # Really 256 997 | self.netindex[j] = self.MAXNETPOS 998 | 999 | 1000 | def paletteImage(self): 1001 | """ PIL weird interface for making a paletted image: create an image which 1002 | already has the palette, and use that in Image.quantize. This function 1003 | returns this palette image. """ 1004 | if self.pimage is None: 1005 | palette = [] 1006 | for i in range(self.NETSIZE): 1007 | palette.extend(self.colormap[i][:3]) 1008 | 1009 | palette.extend([0]*(256-self.NETSIZE)*3) 1010 | 1011 | # a palette image to use for quant 1012 | self.pimage = Image.new("P", (1, 1), 0) 1013 | self.pimage.putpalette(palette) 1014 | return self.pimage 1015 | 1016 | 1017 | def quantize(self, image): 1018 | """ Use a kdtree to quickly find the closest palette colors for the pixels """ 1019 | if get_cKDTree(): 1020 | return self.quantize_with_scipy(image) 1021 | else: 1022 | print('Scipy not available, falling back to slower version.') 1023 | return self.quantize_without_scipy(image) 1024 | 1025 | 1026 | def quantize_with_scipy(self, image): 1027 | w,h = image.size 1028 | px = np.asarray(image).copy() 1029 | px2 = px[:,:,:3].reshape((w*h,3)) 1030 | 1031 | cKDTree = get_cKDTree() 1032 | kdtree = cKDTree(self.colormap[:,:3],leafsize=10) 1033 | result = kdtree.query(px2) 1034 | colorindex = result[1] 1035 | print("Distance: %1.2f" % (result[0].sum()/(w*h)) ) 1036 | px2[:] = self.colormap[colorindex,:3] 1037 | 1038 | return Image.fromarray(px).convert("RGB").quantize(palette=self.paletteImage()) 1039 | 1040 | 1041 | def quantize_without_scipy(self, image): 1042 | """" This function can be used if no scipy is availabe. 1043 | It's 7 times slower though. 1044 | """ 1045 | w,h = image.size 1046 | px = np.asarray(image).copy() 1047 | memo = {} 1048 | for j in range(w): 1049 | for i in range(h): 1050 | key = (px[i,j,0],px[i,j,1],px[i,j,2]) 1051 | try: 1052 | val = memo[key] 1053 | except KeyError: 1054 | val = self.convert(*key) 1055 | memo[key] = val 1056 | px[i,j,0],px[i,j,1],px[i,j,2] = val 1057 | return Image.fromarray(px).convert("RGB").quantize(palette=self.paletteImage()) 1058 | 1059 | def convert(self, *color): 1060 | i = self.inxsearch(*color) 1061 | return self.colormap[i,:3] 1062 | 1063 | def inxsearch(self, r, g, b): 1064 | """Search for BGR values 0..255 and return colour index""" 1065 | dists = (self.colormap[:,:3] - np.array([r,g,b])) 1066 | a= np.argmin((dists*dists).sum(1)) 1067 | return a 1068 | 1069 | 1070 | 1071 | if __name__ == '__main__': 1072 | im = np.zeros((200,200), dtype=np.uint8) 1073 | im[10:30,:] = 100 1074 | im[:,80:120] = 255 1075 | im[-50:-40,:] = 50 1076 | 1077 | images = [im*1.0, im*0.8, im*0.6, im*0.4, im*0] 1078 | writeGif('lala3.gif',images, duration=0.5, dither=0) --------------------------------------------------------------------------------