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
23 """
24 A class of objects that, when called, sample an image.
25 """
26 __abstract=True
27
29
30
31
32
33 return self._image
34
37
40
41
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
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
91 return self.scs.activity
92
104
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
126
127
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
143 x=x*sheet_xdensity
144 y=y*sheet_ydensity
145
146
147 self.__apply_size_normalization(x,y,sheet_xdensity,sheet_ydensity,self.size_normalization)
148
149
150 x/=width
151 y/=height
152
153
154 r,c = self.scs.sheet2matrixidx(x,y)
155
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
165 pattern_rows,pattern_cols = self.image.shape
166
167
168
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
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
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
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):
240
241
242
243
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
285 raise NotImplementedError
286
287
288
289
302
303
304
305
306
307
308
309
310
311
312
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')
325 state['_image'] = f.getvalue()
326 f.close()
327
328 return state
329
331 """
332 Load the object's state (as in the superclass), but replace
333 the '_image' string with an actual Image object.
334 """
335
336
337
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
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
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
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