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
37
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
47
48
49
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
70
71
76
77
78
79
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')
90 state['image'] = f.getvalue()
91 f.close()
92
93 return state
94
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
106 """
107 Renaming of Image.show() for the Bitmap.bitmap attribute.
108 """
109 self.image.show()
110
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
123
124
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
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
144
145
146 max_pixel_value=255
147 inArray = (Numeric.floor(inArray * max_pixel_value)).astype(Numeric.Int)
148
149
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
158 newImage = Image.new('L',(c,r),None)
159 newImage.putdata(inArray.ravel())
160 return newImage
161
162
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
185
186
187
188
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
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
215 """Each matrix must be the same size, with values in the range 0.0 to 1.0."""
216 shape = hue.shape
217 rmat = Numeric.zeros(shape,Numeric.Float)
218 gmat = Numeric.zeros(shape,Numeric.Float)
219 bmat = Numeric.zeros(shape,Numeric.Float)
220
221
222
223
224
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
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
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
314
315
316
317
318
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