Package topo :: Package pattern :: Module image
[hide private]
[frames] | no frames]

Source Code for Module topo.pattern.image

  1  """ 
  2  PatternGenerators based on bitmap images stored in files. 
  3   
  4  $Id: image.py 11293 2010-07-27 14:19:32Z ceball $ 
  5  """ 
  6   
  7  import Image 
  8  import ImageOps 
  9  import numpy 
 10   
 11  from numpy.oldnumeric import array, Float, sum, ravel, ones 
 12   
 13  import param 
 14  from param.parameterized import overridable_property 
 15   
 16  from topo.base.boundingregion import BoundingBox 
 17  from topo.base.patterngenerator import PatternGenerator 
 18  from topo.base.sheetcoords import SheetCoordinateSystem 
 19  from topo.transferfn.basic import DivisiveNormalizeLinf,TransferFn 
 20   
 21   
22 -class ImageSampler(param.Parameterized):
23 """ 24 A class of objects that, when called, sample an image. 25 """ 26 __abstract=True 27
28 - def _get_image(self):
29 # CB: In general, might need to consider caching to avoid 30 # loading of image/creation of scs and application of wpofs 31 # every time/whatever the sampler does to set up the image 32 # before sampling 33 return self._image
34
35 - def _set_image(self,image):
36 self._image = image
37
38 - def _del_image(self):
39 del self._image
40 41 # As noted by JP in FastImageSampler, this isn't easy to figure out.
42 - def __call__(self,image,x,y,sheet_xdensity,sheet_ydensity,width=1.0,height=1.0):
43 raise NotImplementedError
44 45 image = overridable_property(_get_image,_set_image,_del_image)
46 47 48 49 # CEBALERT: ArraySampler?
50 -class PatternSampler(ImageSampler):
51 """ 52 When called, resamples - according to the size_normalization 53 parameter - an image at the supplied (x,y) sheet coordinates. 54 55 (x,y) coordinates outside the image are returned as the background 56 value. 57 """ 58 whole_pattern_output_fns = param.HookList(class_=TransferFn,default=[],doc=""" 59 Functions to apply to the whole image before any sampling is done.""") 60 61 background_value_fn = param.Callable(default=None,doc=""" 62 Function to compute an appropriate background value. Must accept 63 an array and return a scalar.""") 64 65 size_normalization = param.ObjectSelector(default='original', 66 objects=['original','stretch_to_fit','fit_shortest','fit_longest'], 67 doc=""" 68 Determines how the pattern is scaled initially, relative to the 69 default retinal dimension of 1.0 in sheet coordinates: 70 71 'stretch_to_fit': scale both dimensions of the pattern so they 72 would fill a Sheet with bounds=BoundingBox(radius=0.5) (disregards 73 the original's aspect ratio). 74 75 'fit_shortest': scale the pattern so that its shortest dimension 76 is made to fill the corresponding dimension on a Sheet with 77 bounds=BoundingBox(radius=0.5) (maintains the original's aspect 78 ratio, filling the entire bounding box). 79 80 'fit_longest': scale the pattern so that its longest dimension is 81 made to fill the corresponding dimension on a Sheet with 82 bounds=BoundingBox(radius=0.5) (maintains the original's 83 aspect ratio, fitting the image into the bounding box but not 84 necessarily filling it). 85 86 'original': no scaling is applied; each pixel of the pattern 87 corresponds to one matrix unit of the Sheet on which the 88 pattern being displayed.""") 89
90 - def _get_image(self):
91 return self.scs.activity
92
93 - def _set_image(self,image):
94 # Stores a SheetCoordinateSystem with an activity matrix 95 # representing the image 96 if not isinstance(image,numpy.ndarray): 97 image = array(image,Float) 98 99 rows,cols = image.shape 100 self.scs = SheetCoordinateSystem(xdensity=1.0,ydensity=1.0, 101 bounds=BoundingBox(points=((-cols/2.0,-rows/2.0), 102 ( cols/2.0, rows/2.0)))) 103 self.scs.activity=image
104
105 - def _del_image(self):
106 self.scs = None
107 108
109 - def __call__(self, image, x, y, sheet_xdensity, sheet_ydensity, width=1.0, height=1.0):
110 """ 111 Return pixels from the supplied image at the given Sheet (x,y) 112 coordinates. 113 114 The image is assumed to be a NumPy array or other object that 115 exports the NumPy buffer interface (i.e. can be converted to a 116 NumPy array by passing it to numpy.array(), e.g. Image.Image). 117 The whole_pattern_output_fns are applied to the image before 118 any sampling is done. 119 120 To calculate the sample, the image is scaled according to the 121 size_normalization parameter, and any supplied width and 122 height. sheet_xdensity and sheet_ydensity are the xdensity and 123 ydensity of the sheet on which the pattern is to be drawn. 124 """ 125 # CEB: could allow image=None in args and have 'if image is 126 # not None: self.image=image' here to avoid re-initializing the 127 # image. 128 self.image=image 129 130 for wpof in self.whole_pattern_output_fns: 131 wpof(self.image) 132 if not self.background_value_fn: 133 self.background_value = 0.0 134 else: 135 self.background_value = self.background_value_fn(self.image) 136 137 pattern_rows,pattern_cols = self.image.shape 138 139 if width==0 or height==0 or pattern_cols==0 or pattern_rows==0: 140 return ones(x.shape, Float)*self.background_value 141 142 # scale the supplied coordinates to match the pattern being at density=1 143 x=x*sheet_xdensity # deliberately don't operate in place (so as not to change supplied x & y) 144 y=y*sheet_ydensity 145 146 # scale according to initial pattern size_normalization selected (size_normalization) 147 self.__apply_size_normalization(x,y,sheet_xdensity,sheet_ydensity,self.size_normalization) 148 149 # scale according to user-specified width and height 150 x/=width 151 y/=height 152 153 # now sample pattern at the (r,c) corresponding to the supplied (x,y) 154 r,c = self.scs.sheet2matrixidx(x,y) 155 # (where(cond,x,y) evaluates x whether cond is True or False) 156 r.clip(0,pattern_rows-1,out=r) 157 c.clip(0,pattern_cols-1,out=c) 158 left,bottom,right,top = self.scs.bounds.lbrt() 159 return numpy.where((x>=left) & (x<right) & (y>bottom) & (y<=top), 160 self.image[r,c], 161 self.background_value)
162 163
164 - def __apply_size_normalization(self,x,y,sheet_xdensity,sheet_ydensity,size_normalization):
165 pattern_rows,pattern_cols = self.image.shape 166 167 # Instead of an if-test, could have a class of this type of 168 # function (c.f. OutputFunctions, etc)... 169 if size_normalization=='original': 170 return 171 172 elif size_normalization=='stretch_to_fit': 173 x_sf,y_sf = pattern_cols/sheet_xdensity, pattern_rows/sheet_ydensity 174 x*=x_sf; y*=y_sf 175 176 elif size_normalization=='fit_shortest': 177 if pattern_rows<pattern_cols: 178 sf = pattern_rows/sheet_ydensity 179 else: 180 sf = pattern_cols/sheet_xdensity 181 x*=sf;y*=sf 182 183 elif size_normalization=='fit_longest': 184 if pattern_rows<pattern_cols: 185 sf = pattern_cols/sheet_xdensity 186 else: 187 sf = pattern_rows/sheet_ydensity 188 x*=sf;y*=sf
189 190 191 192
193 -def edge_average(a):
194 "Return the mean value around the edge of an array." 195 196 if len(ravel(a)) < 2: 197 return float(a[0]) 198 else: 199 top_edge = a[0] 200 bottom_edge = a[-1] 201 left_edge = a[1:-1,0] 202 right_edge = a[1:-1,-1] 203 204 edge_sum = sum(top_edge) + sum(bottom_edge) + sum(left_edge) + sum(right_edge) 205 num_values = len(top_edge)+len(bottom_edge)+len(left_edge)+len(right_edge) 206 207 return float(edge_sum)/num_values
208 209 210
211 -class FastImageSampler(ImageSampler):
212 """ 213 A fast-n-dirty image sampler using Python Imaging Library 214 routines. Currently this sampler doesn't support user-specified 215 size_normalization or cropping but rather simply scales and crops 216 the image to fit the given matrix size without distorting the 217 aspect ratio of the original picture. 218 """ 219 220 sampling_method = param.Integer(default=Image.NEAREST,doc=""" 221 Python Imaging Library sampling method for resampling an image. 222 Defaults to Image.NEAREST.""") 223
224 - def _set_image(self,image):
225 if not isinstance(image,Image.Image): 226 self._image = Image.new('L',image.shape) 227 self._image.putdata(image.ravel()) 228 else: 229 self._image = image
230
231 - def __call__(self, image, x, y, sheet_xdensity, sheet_ydensity, width=1.0, height=1.0):
232 self.image=image 233 234 # JPALERT: Right now this ignores all options and just fits the image into given array. 235 # It needs to be fleshed out to properly size and crop the 236 # image given the options. (maybe this class needs to be 237 # redesigned? The interface to this function is pretty inscrutable.) 238 im = ImageOps.fit(self.image,x.shape,self.sampling_method) 239 return array(im,dtype=Float)
240 241 242 243 # Would be best called Image, but that causes confusion with Image's Image
244 -class GenericImage(PatternGenerator):
245 """ 246 Generic 2D image generator. 247 248 Generates a pattern from a Python Imaging Library image object. 249 Subclasses should override the _get_image method to produce the 250 image object. 251 252 The background value is calculated as an edge average: see 253 edge_average(). Black-bordered images therefore have a black 254 background, and white-bordered images have a white 255 background. Images with no border have a background that is less 256 of a contrast than a white or black one. 257 258 At present, rotation, size_normalization, etc. just resample; it 259 would be nice to support some interpolation options as well. 260 """ 261 262 __abstract = True 263 264 aspect_ratio = param.Number(default=1.0,bounds=(0.0,None), 265 softbounds=(0.0,2.0),precedence=0.31,doc=""" 266 Ratio of width to height; size*aspect_ratio gives the width.""") 267 268 size = param.Number(default=1.0,bounds=(0.0,None),softbounds=(0.0,2.0), 269 precedence=0.30,doc=""" 270 Height of the image.""") 271 272 pattern_sampler = param.ClassSelector(class_=ImageSampler, 273 default=PatternSampler(background_value_fn=edge_average, 274 size_normalization='fit_shortest', 275 whole_pattern_output_fns=[DivisiveNormalizeLinf()]),doc=""" 276 The PatternSampler to use to resample/resize the image.""") 277 278 cache_image = param.Boolean(default=True,doc=""" 279 If False, discards the image and pattern_sampler after drawing the pattern each time, 280 to make it possible to use very large databases of images without 281 running out of memory.""") 282 283
284 - def _get_image(self,p):
285 raise NotImplementedError
286 287 # CEB: not currently possible, because _get_image needs access to p 288 #image = property(_get_image,_set_image,_del_image,doc=" ") 289
290 - def function(self,p):
291 height = p.size 292 width = p.aspect_ratio*height 293 294 result = p.pattern_sampler(self._get_image(p),p.pattern_x,p.pattern_y,float(p.xdensity),float(p.ydensity), 295 float(width),float(height)) 296 297 if p.cache_image is False: 298 self._image = None 299 del self.pattern_sampler.image 300 301 return result
302 303 ### support pickling of Image.Image 304 305 # CEBALERT: almost identical code to that in topo.plotting.bitmap.Bitmap. 306 # Can we instead patch PIL? (Note that we can't use copy_reg as we do for 307 # e.g. numpy ufuncs because Image's Image is not a new-style class. So patching 308 # PIL is probably the only option to handle this problem in one place.) 309 310 # CEB: by converting to string and back, we probably incur some speed 311 # penalty on copy()ing GenericImages (since __getstate__ and __setstate__ are 312 # used for copying, unless __copy__ and __deepcopy__ are defined instead).
313 - def __getstate__(self):
314 """ 315 Return the object's state (as in the superclass), but replace 316 the '_image' attribute's Image with a string representation. 317 """ 318 state = super(GenericImage,self).__getstate__() 319 320 if '_image' in state and state['_image'] is not None: 321 import StringIO 322 f = StringIO.StringIO() 323 image = state['_image'] 324 image.save(f,format=image.format or 'TIFF') # format could be None (we should probably just not save in that case) 325 state['_image'] = f.getvalue() 326 f.close() 327 328 return state
329
330 - def __setstate__(self,state):
331 """ 332 Load the object's state (as in the superclass), but replace 333 the '_image' string with an actual Image object. 334 """ 335 # CEBALERT: Need to figure out how state['_image'] could ever 336 # actually be None; apparently it is sometimes (see SF 337 # #2276819). 338 if '_image' in state and state['_image'] is not None: 339 import StringIO 340 state['_image'] = Image.open(StringIO.StringIO(state['_image'])) 341 super(GenericImage,self).__setstate__(state)
342 343 344
345 -class FileImage(GenericImage):
346 """ 347 2D Image generator that reads the image from a file. 348 349 The image at the supplied filename is converted to grayscale if it 350 is not already a grayscale image. See Image's Image class for 351 details of supported image file formats. 352 """ 353 354 filename = param.Filename(default='images/ellen_arthur.pgm',precedence=0.9,doc=""" 355 File path (can be relative to Topographica's base path) to a bitmap image. 356 The image can be in any format accepted by PIL, e.g. PNG, JPG, TIFF, or PGM. 357 """) 358 359
360 - def __init__(self, **params):
361 """ 362 Create the last_filename attribute, used to hold the last 363 filename. This allows reloading an existing image to be 364 avoided. 365 """ 366 super(FileImage,self).__init__(**params) 367 self.last_filename = None
368 369
370 - def _get_image(self,p):
371 """ 372 If necessary as indicated by the parameters, get a new image, 373 assign it to self._image and return True. If no new image is 374 needed, return False. 375 """ 376 if p.filename!=self.last_filename or self._image is None: 377 self.last_filename=p.filename 378 self._image = ImageOps.grayscale(Image.open(p.filename)) 379 return self._image
380