Package topo :: Package plotting :: Module bitmap
[hide private]
[frames] | no frames]

Source Code for Module topo.plotting.bitmap

  1  """ 
  2  Topographica Bitmap Class. 
  3   
  4  Encapsulates the PIL Image class so that an input matrix can be displayed 
  5  as a bitmap image without needing to know about PIL proper. 
  6   
  7  There are three different base image Classes which inherit Bitmap: 
  8   
  9  PaletteBitmap  - 1 2D Matrix, 1 1D Color Map 
 10  HSVBitmap    - 3 2D Matrices, Color (H), Confidence (S), Strength (V) 
 11  RGBBitmap    - 3 2D Matrices, Red, Green, Blue Channels. 
 12   
 13  All maps are assumed to be on a nominal range of 0.0 to 1.0.  Matrices 
 14  are passed in as part of the constructor and the image is generaed. 
 15  For more information, see the documentation for each of the Bitmap 
 16  classes. 
 17   
 18  The encapsulated PIL Image is accessible through the .bitmap attribute. 
 19   
 20  $Id: bitmap.py 11314 2010-07-27 17:18:52Z ceball $ 
 21  """ 
 22  __version__='$Revision: 11314 $' 
 23   
 24  import os 
 25  import Image 
 26  import ImageDraw 
 27  import ImageFont 
 28  from colorsys import hsv_to_rgb 
 29  import numpy.oldnumeric as Numeric 
 30  import numpy 
 31   
 32  import param 
 33  from param import resolve_path 
 34   
 35   
 36  # CEBALERT: can we just use load_default()? Do we even need TITLE_FONT 
 37  # at all? 
 38  try: 
 39     import matplotlib 
 40     vera_path = resolve_path(os.path.join(matplotlib.__file__,'matplotlib/mpl-data/fonts/ttf/Vera.ttf')) 
 41     TITLE_FONT = ImageFont.truetype(vera_path,20) 
 42  except: 
 43     TITLE_FONT = ImageFont.load_default() 
 44   
 45   
 46  ### JCALERT: To do: 
 47  ###        - Update the test file. 
 48  ###        - Write PaletteBitmap when the Palette class is fixed 
 49  ###        - Get rid of accessing function (copy, show...) (should we really?) 
 50   
 51   
52 -class Bitmap(param.Parameterized):
53 """ 54 Wrapper class for the PIL Image class. 55 56 The main purpose for this base class is to provide a consistent 57 interface for defining bitmaps constructed in various different 58 ways. The resulting bitmap is a PIL Image object that can be 59 accessed using the normal PIL interface. 60 61 If subclasses use the _arrayToImage() function provided, any 62 pixels larger than the maximum that can be displayed will 63 be counted before they are clipped; these are stored in the 64 clipped_pixels attribute. 65 """ 66 clipped_pixels = 0 67
68 - def __init__(self,image):
69 self.image = image
70 71
72 - def __copy__(self):
73 # avoid calling __getstate__ for copy (not required) 74 image = self.image.copy() 75 return Bitmap(image)
76 77 # CB: could define a __deepcopy__ too, but we don't need 78 # deepcopy to be fast. 79
80 - def __getstate__(self):
81 """ 82 Return the object's state (as in the superclass), but replace 83 the 'image' attribute's Image with a string representation. 84 """ 85 state = super(Bitmap,self).__getstate__() 86 import StringIO 87 f = StringIO.StringIO() 88 image = state['image'] 89 image.save(f,format=image.format or 'TIFF') # format could be None (we should probably just not save in that case) 90 state['image'] = f.getvalue() 91 f.close() 92 93 return state
94
95 - def __setstate__(self,state):
96 """ 97 Load the object's state (as in the superclass), but replace 98 the 'image' string with an actual Image object. 99 """ 100 import StringIO 101 state['image'] = Image.open(StringIO.StringIO(state['image'])) 102 super(Bitmap,self).__setstate__(state)
103 104
105 - def show(self):
106 """ 107 Renaming of Image.show() for the Bitmap.bitmap attribute. 108 """ 109 self.image.show()
110
111 - def width(self): return self.image.size[0]
112 - def height(self): return self.image.size[1]
113
114 - def zoom(self, factor):
115 """ 116 Return a resized Image object, given the input 'factor' 117 parameter. 1.0 is the same size, 2.0 is doubling the height 118 and width, 0.5 is 1/2 the original size. The original Image 119 is not changed. 120 """ 121 if factor%1==0: 122 # CEBALERT: work around PIL bug (see SF #2820821) so that 123 # integer scaling works in the typical case (where an 124 # image is being enlarged). 125 a = numpy.array(self.image).repeat(int(factor),axis=0).repeat(int(factor),axis=1) 126 zoomed = Image.fromarray(a,mode=self.image.mode) 127 else: 128 x,y = self.image.size 129 zx, zy = int(x*factor), int(y*factor) 130 zoomed = self.image.resize((zx,zy)) 131 132 return zoomed
133 134
135 - def _arrayToImage(self, inArray):
136 """ 137 Generate a 1-channel PIL Image from an array of values from 0 to 1.0. 138 139 Values larger than 1.0 are clipped, after adding them to the total 140 clipped_pixels. Returns a one-channel (monochrome) Image. 141 """ 142 143 # PIL 'L' Images use a range of 0 to 255, so we scale the 144 # input array to match. The pixels are scaled by 255, not 145 # 256, so that 1.0 maps to fully white. 146 max_pixel_value=255 147 inArray = (Numeric.floor(inArray * max_pixel_value)).astype(Numeric.Int) 148 149 # Clip any values that are still larger than max_pixel_value 150 to_clip = (Numeric.greater(inArray.ravel(),max_pixel_value)).sum() 151 if (to_clip>0): 152 self.clipped_pixels = self.clipped_pixels + to_clip 153 inArray.clip(0,max_pixel_value,out=inArray) 154 self.verbose("Bitmap: clipped",to_clip,"image pixels that were out of range") 155 156 r,c = inArray.shape 157 # The size is (width,height), so we swap r and c: 158 newImage = Image.new('L',(c,r),None) 159 newImage.putdata(inArray.ravel()) 160 return newImage
161 162
163 -class PaletteBitmap(Bitmap):
164 """ 165 Bitmap constructed using a single 2D array. 166 167 The image is monochrome by default, but more colorful images can 168 be constructed by specifying a Palette. 169 """ 170
171 - def __init__(self,inArray,palette=None):
172 """ 173 inArray should have values in the range from 0.0 to 1.0. 174 175 Palette can be any color scale depending on the type of ColorMap 176 desired. Examples: 177 178 [0,0,0 ... 255,255,255] = grayscale 179 [0,0,0 ... 255,0,0] = grayscale but through a Red filter. 180 181 The default palette is grayscale, with 0.0 mapping to black 182 and 1.0 mapping to white. 183 """ 184 ### JABALERT: Should accept a Palette class, not a data 185 ### structure, unless for some reason we want to get rid of 186 ### the Palette classes and always use data structures 187 ### instead. 188 ### JC: not yet properly implemented anyway. 189 max_pixel_value=255 190 191 newImage = self._arrayToImage(inArray) 192 if palette == None: 193 palette = [i for i in range(max_pixel_value+1) for j in range(3)] 194 newImage.putpalette(palette) 195 newImage = newImage.convert('P') 196 super(PaletteBitmap,self).__init__(newImage)
197 198 199
200 -class HSVBitmap(Bitmap):
201 """ 202 Bitmap constructed from 3 2D arrays, for hue, saturation, and value. 203 204 The hue matrix determines the pixel colors. The saturation matrix 205 determines how strongly the pixels are saturated for each hue, 206 i.e. how colorful the pixels appear. The value matrix determines 207 how bright each pixel is. 208 209 An RGB image is constructed from the HSV matrices using 210 hsv_to_rgb; the resulting image is of the same type that is 211 constructed by RGBBitmap, and can be used in the same way. 212 """ 213
214 - def __init__(self,hue,sat,val):
215 """Each matrix must be the same size, with values in the range 0.0 to 1.0.""" 216 shape = hue.shape # Assumed same as sat.shape and val.shape 217 rmat = Numeric.zeros(shape,Numeric.Float) 218 gmat = Numeric.zeros(shape,Numeric.Float) 219 bmat = Numeric.zeros(shape,Numeric.Float) 220 221 # Note: should someday file a feature request for PIL for them 222 # to accept an image of type 'HSV', so that they will do this 223 # conversion themselves, without us needing an explicit loop 224 # here. That should speed this up. 225 ch = hue.clip(0.0,1.0) 226 cs = sat.clip(0.0,1.0) 227 cv = val.clip(0.0,1.0) 228 229 for i in range(shape[0]): 230 for j in range(shape[1]): 231 r,g,b = hsv_to_rgb(ch[i,j],cs[i,j],cv[i,j]) 232 rmat[i,j] = r 233 gmat[i,j] = g 234 bmat[i,j] = b 235 236 rImage = self._arrayToImage(rmat) 237 gImage = self._arrayToImage(gmat) 238 bImage = self._arrayToImage(bmat) 239 240 super(HSVBitmap,self).__init__(Image.merge('RGB',(rImage,gImage,bImage)))
241 242 243
244 -class RGBBitmap(Bitmap):
245 """ 246 Bitmap constructed from three 2D arrays, for red, green, and blue. 247 248 Each matrix is used as the corresponding channel of an RGB image. 249 """ 250
251 - def __init__(self,rMapArray,gMapArray,bMapArray):
252 """Each matrix must be the same size, with values in the range 0.0 to 1.0.""" 253 rImage = self._arrayToImage(rMapArray) 254 gImage = self._arrayToImage(gMapArray) 255 bImage = self._arrayToImage(bMapArray) 256 257 super(RGBBitmap,self).__init__(Image.merge('RGB',(rImage,gImage,bImage)))
258 259 260
261 -class MontageBitmap(Bitmap):
262 """ 263 A bitmap composed of tiles containing other bitmaps. 264 265 Bitmaps are scaled to fit in the given tile size, and tiled 266 right-to-left, top-to-bottom into the given number of rows and columns. 267 """ 268 bitmaps = param.List(class_=Bitmap,doc=""" 269 The list of bitmaps to compose.""") 270 271 rows = param.Integer(default=2, doc=""" 272 The number of rows in the montage.""") 273 cols = param.Integer(default=2, doc=""" 274 The number of columns in the montage.""") 275 shape = param.Composite(attribs=['rows','cols'], doc=""" 276 The shape of the montage. Same as (self.rows,self.cols).""") 277 278 margin = param.Integer(default=5,doc=""" 279 The size in pixels of the margin to put around each 280 tile in the montage.""") 281 282 tile_size = param.NumericTuple(default=(100,100), doc=""" 283 The size in pixels of a tile in the montage.""") 284 285 titles = param.List(class_=str, default=[], doc=""" 286 A list of titles to overlay on the tiles.""") 287 288 title_pos = param.NumericTuple(default=(10,10), doc=""" 289 The position of the upper left corner of the title in each tile.""") 290 291 title_options = param.Dict(default={}, doc=""" 292 Dictionary of options for drawing the titles. Dict should 293 contain keyword options for the PIL draw.text method. Possible 294 options include 'fill' (fill color), 'outline' (outline color), 295 and 'font' (an ImageFont font instance). The PIL defaults will 296 be used for any omitted options.""", 297 instantiate=False) 298 299 hooks = param.List(default=[], doc=""" 300 A list of functions, one per tile, that take a PIL image as 301 input and return a PIL image as output. The hooks are applied 302 to the tile images before resizing. The value None can be 303 inserted as a placeholder where no hook function is needed.""") 304 305 resize_filter = param.Integer(default=Image.NEAREST,doc=""" 306 The filter used for resizing the images. Defaults 307 to NEAREST. See PIL Image module documentation for other 308 options and their meanings.""") 309 310 bg_color = param.NumericTuple(default=(0,0,0), doc=""" 311 The background color for the montage, as (r,g,b).""") 312
313 - def __init__(self,**params):
314 ## JPALERT: The Bitmap class is a Parameterized object,but its 315 ## __init__ doesn't take **params and doesn't call super.__init__, 316 ## so we have to skip it. 317 ## JAB: Good point; Bitmap should be modified to be more like 318 ## other PO classes. 319 param.Parameterized.__init__(self,**params) 320 321 rows,cols = self.shape 322 tilew,tileh = self.tile_size 323 bgr,bgg,bgb = self.bg_color 324 325 width = tilew*cols + self.margin*(cols*2) 326 height = tileh*rows + self.margin*(rows*2) 327 self.image = Image.new('RGB',(width,height), 328 (bgr*255,bgg*255,bgb*255)) 329 330 self.title_options.setdefault('font',TITLE_FONT) 331 332 for r in xrange(rows): 333 for c in xrange(cols): 334 i = r*self.cols+c 335 if i < len(self.bitmaps): 336 bm = self.bitmaps[i] 337 bmw,bmh = bm.image.size 338 if bmw > bmh: 339 bmh = int( float(tilew)/bmw * bmh ) 340 bmw = tilew 341 else: 342 bmw = int( float(tileh)/bmh * bmw ) 343 bmh = tileh 344 345 if self.hooks and self.hooks[i]: 346 f = self.hooks[i] 347 else: 348 f = lambda x:x 349 new_bm = Bitmap(f(bm.image).resize((bmw,bmh))) 350 if self.titles: 351 draw = ImageDraw.Draw(new_bm.image) 352 draw.text(self.title_pos,self.titles[i],**self.title_options) 353 self.image.paste( new_bm.image, 354 (c * width/cols + tilew/2 - bmw/2 + self.margin, 355 r * height/rows + tileh/2 - bmh/2 + self.margin) ) 356 357 else: 358 break
359