Attachment 'BitmapManip.py'
Download 1 """
2 Advanced image and image mask manipulation.
3
4 Note: The terms "plane", "band", "layer" and "channel" are used interchangibly.
5
6 Tested on Win7 64-bit (6.1.7600) and Win XP SP3 (5.1.2600) using Python 32-bit.
7
8 Platform Windows 6.1.7600
9 Python 2.5.4 (r254:67916, Dec 23 2008, 15:10:54) [MSC v.1310 32 bit (Intel) (x86)]
10 Python wx 2.8.10.1
11 Pil 1.1.7
12
13 Ray Pasco
14 pascor(at)verizon(dot)net
15
16 Last modification: 2011-04-02
17
18 This code may be modified and distributed for any purpose whatsoever.
19
20 """
21
22 import os
23 import wx
24 import Image # Pil
25 import ImgConv # wxImage <==> PilImage
26
27 #------------------------------------------------------------------------------
28
29 def CreateMaskBitmapFromPilImage( pilImage, useTransparency=True, threshold=128 ) :
30 """
31 Return a binary mask wxBitmap derived from an image file.
32
33 If the image file has either binary or variable transparency
34 (aka "multivalued" or "alpha") then use it and ignore the image itself
35 unless useTransparency=False. The pilImage may have any "L" mode.
36
37 Transparency in the mask bmap will be indicated by the values less than "threshold"
38 and opaqueness by alpha values >= "threshold". Alpha values will be quantized into [0, 255].
39
40 If the pilImage has no alpha transparency layer then the image, itself,
41 will be used to create the mask 0/255 mask values. The pilImage may have any non-"L" mode.
42 An RGB pilImage used as a mask will be converted to grey level by Pil :
43 L = (R * 299/1000) + (G * 587/1000) + (B * 114/1000)
44 """
45 sizeX, sizeY = pilImage.size
46
47 pilMode = pilImage.mode
48 hasTransparency = pilMode[ -1 ] == 'A' # This looks like a hack, but it always works.
49
50 if hasTransparency and useTransparency : # Extract the alpha plane for use as a mask.
51
52 # convert to only 2 planes
53 if not (pilMode == 'LA' ) :
54 pilImage = pilImage.convert( 'LA' )
55
56 # Extract the tranparency plane
57 mask_pilImage = pilImage.split()[1] # Keep only the alpha; the image data is discarded.
58
59 else : # no transparency present or useTransparency=False was given.
60
61 # Convert image data to greyLevel for use as the mask
62 if not (pilMode == 'L' ) :
63 mask_pilImage = pilImage.convert( 'L' )
64
65 #end if
66
67 # Quantize any alpha (non-binary) values to [0, 255]
68 mask_pilImage = mask_pilImage.point(lambda i: (i / threshold) * 255)
69
70 return ImgConv.WxBitmapFromPilImage( mask_pilImage )
71
72 #end def CreateMaskBitmapFromPilImage
73
74 #------------------------------------------------------------------------------
75
76 def CreateMaskBitmapFromFile( imageFilename, useTransparency=True, threshold=128 ) :
77 """
78 Return a mask wxBitmap derived from an image file. Image files read by PIL
79 will have had any mask transparency automatically converted to alpha transparency.
80
81 If the image file has either mask or alpha transparency then use that transparency
82 information by default and ignore the image itself unless given [ useTransparency=False ].
83
84 Transparency in the mask bmap will be indicated by the alpha values less than 127
85 and opaqueness by values >= 128. Alpha values will be quantized using the 50% level
86 into black and white unless another [ threshold ] parameter is given.
87
88 If no transparency information is within the image, then the image, itself,
89 will be used to create the mask values. It may be RGB or L format.
90
91 All RGB images will be converted to grey level:
92 L = (R * 299/1000) + (G * 587/1000) + (B * 114/1000)
93 and quantized to binary 0 and 255 at the "threshold" level.
94 """
95 imageFile_pilImage = Image.open( imageFilename )
96
97 mask_wxBitmap = CreateMaskBitmapFromPilImage( imageFile_pilImage,
98 useTransparency=useTransparency,
99 threshold=128 )
100
101 return mask_wxBitmap
102
103 #end def CreateMaskBitmapFromFile
104
105 #------------------------------------------------------------------------------
106
107 def GetCombinedImageSize( image1_size, image2_size, offset2, extend=True ) :
108
109 # Default settings if ofset image2 is entirely within image1.
110 # I.e., if no target image extent increases are needed or are not wanted.
111 image1Origin = [ 0, 0 ] # Where to paste image1 into the future target image
112 image2Origin = [ offset2[0], offset2[1] ] # Where to paste image2
113 combinedSize = [ image1_size[0], image1_size[1] ] # Start by setting target image's size to image1's size
114
115 # Check and adjust the default settings if any portion of image2
116 # extends beyond any of image1's 4 borders.
117 if not extend :
118 pass # Image2 will get clipped if any portion extends past image1.
119
120 else :
121 if offset2[0] < image1Origin[0] : # is image2 X offset negative ?
122 image1Origin[0] = 0 - offset2[0] # Offset image1 towards the right
123 image2Origin[0] = 0 # Make a new left border.
124 combinedSize[0] = image1_size[0] - offset2[0] # Enlarge target to the left of image1
125 #end if
126 #
127 if offset2[0] + image2_size[0] > image1_size[0] : # ofsetted image2's right border is past image1's
128 combinedSize[0] += (offset2[0] + image2_size[0]) - image1_size[0]
129 #end if
130
131
132 if offset2[1] < image1Origin[1] : # is image2's Y offset negative ?
133 image1Origin[1] = 0 - offset2[1] # Offset image1 towards the bottom
134 image2Origin[1] = 0 # Make a new top border.
135 combinedSize[1] = image1_size[1] - offset2[1] # Enlarge target to hold both image1 and image2
136 #end if
137 #
138 if offset2[1] + image2_size[1] > image1_size[1] : # ofsetted image2's bottom border is past image1's
139 combinedSize[1] += (offset2[1] + image2_size[1]) - image1_size[1]
140 #end if
141
142 #end if
143
144 # Convert the lists into tuples.
145 image1Origin = ( image1Origin[0], image1Origin[1] ) # Where to paste image1
146 image2Origin = ( image2Origin[0], image2Origin[1] ) # Where to paste image1
147 combinedSize = ( combinedSize[0], combinedSize[1] ) # New combined image size.
148
149 return (combinedSize, image1Origin, image2Origin)
150
151 #end def GetCombinedImageSize
152
153 #------------------------------------------------------------------------------
154
155 def ConvertToPilImageAndGetImageType( inputImage ) :
156 """
157 The input object may be a pilImage, a wx.Bitmap, a wx.Image or an image filename.
158 The image type returned will be according to inputImage's object type:
159
160 Mask1 Type returned Image Type
161 ---------- -------------------
162 Pil Image Pil Image
163 wx Image wx Image
164 wx Bitmap wx Bitmap
165 Filename (string) wx Bitmap
166 """
167
168 # Determine the image's object type.
169 if inputImage.__class__ == Image.Image : # pilImage
170 returnType = 'pilImage'
171 pilImage = image # Already PilImage.
172
173 elif inputImage.__class__ == str : # a file image
174 returnType = 'wxBitmap'
175 pilImage = Image.open( inputImage )
176
177 elif inputImage.__class__ == wx._gdi.Bitmap : # wxBitmap
178 returnType = 'wxBitmap'
179 pilImage = ImgConv.PilImageFromWxBitmap( inputImage )
180
181 elif inputImage.__class__ == wx._core.Image : # wxImage
182 returnType = 'wxImage'
183 pilImage = ImgConv.PilImageFromWxImage( inputImage )
184 #end if
185
186 return (pilImage, returnType)
187
188 #end def ConvertToPilImageAndGetImageType
189
190 #------------------------------------------------------------------------------
191
192 def CombineMasks( mask1_image, mask2_image, offset2=(0, 0), extend=True, threshold=128 ) :
193 """
194 Combine one transparency mask image with another.
195 Fully transparent pixels are indicated by their values being < "threshold".
196 Fully opaque pixels are indicated by values > "threshold".
197
198 All RGB images will be converted to grey level:
199 L = (R * 299/1000) + (G * 587/1000) + (B * 114/1000)
200 and quantized to binary 0 and 255 at the "threshold" value.
201
202 All wx mask bitmaps are RGB, but the given mask images may be either greyscale
203 or RGB. TRANSPARENCY LAYERS ARE PERMITTED, BUT ARE IGNORED.
204
205 The resultant image size will be the union of file1's area and the offset
206 file2's area. That is, if extend=True and mask2_image extends past mask1_image's borders
207 then the returned area will be extended to include all of offset mask2_image's area.
208 Setting extend=False would crop mask2_image at mask1_image's borders.
209 """
210
211 mask1_pilImage, returnType = ConvertToPilImageAndGetImageType( mask1_image )
212 mask2_pilImage, returnTypeDummy = ConvertToPilImageAndGetImageType( mask2_image )
213
214 # Create easy-to-process 1-layer (grey-level) Pil images.
215 # The given images should have already been processed into binary values.
216 if not (mask1_pilImage.mode == 'L') :
217 mask1_pilImage = mask1_pilImage.convert( 'L' )
218 if not (mask2_pilImage.mode == 'L') :
219 mask2_pilImage = mask2_pilImage.convert( 'L' )
220
221 mask1_size = mask1_pilImage.size
222 mask2_size = mask2_pilImage.size
223
224 #--------------
225
226 # The resulting size is automatically enlarged if there are any areas of non-overlap.
227 combinedSize, image1Origin, image2Origin = \
228 GetCombinedImageSize( mask1_size, mask2_size, offset2, extend=extend )
229
230 combinedMask_pilImage = Image.new( 'L', combinedSize, color=(0) ) # completely transparent to start
231
232 # Use Pil to quickly paste image1 and image2 into combinedMask_pilImage
233 # using their own greylevel data as masks.
234 combinedMask_pilImage.paste( mask1_pilImage, image1Origin, mask1_pilImage )
235 combinedMask_pilImage.paste( mask2_pilImage, image2Origin, mask2_pilImage )
236
237 #--------------
238
239 # Convert the finished combined bitmask to Image1's format, whatever that happens to be.
240 if returnType == 'pilImage' :
241 combinedMask = combinedMask_pilImage
242
243 elif returnType == 'wxBitmap' :
244 combinedMask = ImgConv.WxBitmapFromPilImage( combinedMask_pilImage )
245
246 elif returnType == 'wxImage' :
247 combinedMask = ImgConv.WxImageFromPilImage( combinedMask_pilImage )
248 #end if
249
250 return combinedMask
251
252 #end def CombineMasks
253
254 #------------------------------------------------------------------------------
255
256 def CombinePilImagesUsingMasks( image1_pilImage, mask1_pilImage,
257 image2_pilImage, mask2_pilImage,
258 offset2=(0, 0) ) :
259
260 image1_size = image1_pilImage.size
261 image2_size = image2_pilImage.size
262
263 mask1_size = mask1_pilImage.size
264 mask2_size = mask2_pilImage.size
265
266 # Size the combined output as the union of the 2 given.
267 targetSizeX = image1_size[0] # Start with image1's size
268 if (image2_size[0] + offset2[0]) > targetSizeX : # Extend right border
269 targetSizeX = image2_size[0] + offset2[0]
270
271 targetSizeY = image1_size[1] # Start with image1's size
272 if (image2_size[1] + offset2[1]) > targetSizeY :
273 targetSizeY = image2_size[1] + offset2[1] # Extend bottom border
274
275 # Create a brand new Target PilImage and Mask PilImage
276 targetSize = (targetSizeX, targetSizeY)
277 targetRGB_pilImage = Image.new( 'RGB', targetSize, color=(0, 0, 0) )
278 targetMask_pilImage = Image.new( 'L', targetSize, color=(0) )
279
280 #----------------------------------
281
282 if mask1_size != image1_size : # !! Fatal error !!
283 print '\n#### BitmapManip: CombineFileImagesUsingFileMasks(): Unequal Mask1 and Image1 Sizes'
284 print ' mask1_size, image1_size', mask1_size, image1_size
285 os._exit(1)
286 #end if
287 size1 = image1_size
288
289 if mask2_size != image2_size :
290 print '\n#### BitmapManip: CombineFileImagesUsingFileMasks(): Unequal Mask2 and Image2 Sizes'
291 print ' mask2_size, image2_size', mask2_size, image2_size
292 os._exit(1)
293 #end if
294 size2 = image2_size
295
296 #----------------------------------
297
298 # Copy image1 and mask1 images into the target image and target mask, respectively.
299 image1Origin = (0, 0)
300 targetRGB_pilImage.paste( image1_pilImage, image1Origin, mask1_pilImage )
301 targetMask_pilImage.paste( mask1_pilImage, image1Origin, mask1_pilImage )
302
303 # Copy image2 and mask2 into the target image and target mask.
304 image2Origin = offset2
305 targetRGB_pilImage.paste( image2_pilImage, image2Origin, mask2_pilImage )
306 targetMask_pilImage.paste( mask2_pilImage, image2Origin, mask2_pilImage )
307
308 # Compose the complete target RGBA PilImage.
309 targetRGBA_pilImage = targetRGB_pilImage.convert( 'RGBA' )
310 targetRGBA_pilImage.putalpha( targetMask_pilImage ) # IN-PLACE METHOD
311
312 return targetRGBA_pilImage
313
314 #end def CombinePilImagesUsingMasks
315
316 #------------------------------------------------------------------------------
317
318 def CombineFileImagesUsingFileMasks( image1_filename, mask1_filename,
319 image2_filename, mask2_filename,
320 offset2=(0, 0) ) :
321 """
322 Combine file images into a single PilImage.
323 Use file-based masks to determine the valid pxls in each image file.
324 File2's opaque pixels are copied over File1's.
325
326 The resultant image size will be the union of file1's area
327 and the offset file2's area.
328 """
329 # File1 Image & Mask
330 file1_pilImage = Image.open( image1_filename )
331 if file1_pilImage.mode != 'RGB' :
332 file1_pilImage = file1_pilImage.convert( 'RGB' )
333
334 if ( mask1_filename ) :
335 mask1_pilImage = Image.open( mask1_filename )
336 if mask1_pilImage.mode != 'L' :
337 mask1_pilImage = mask1_pilImage.convert( 'L' )
338 else :
339 mask1_bmap = CreateMaskBitmapFromFile( image1_filename, useTransparency=True )
340 mask1_pilImage = ImgConv.PilImageFromWxBitmap( mask1_bmap ) # Always RGB
341 mask1_pilImage = mask1_pilImage.convert( 'L' )
342 #end if
343
344 #------
345
346 # File2 Image & Mask
347 file2_pilImage = Image.open( image2_filename )
348 if file2_pilImage.mode != 'RGB' :
349 file2_pilImage = file2_pilImage.convert( 'RGB' )
350
351 if ( mask2_filename ) :
352
353 mask2_pilImage = Image.open( mask2_filename )
354 if mask2_pilImage.mode != 'L' :
355 mask2_pilImage = mask2_pilImage.convert( 'L' )
356 else :
357
358 mask2_bmap = CreateMaskBitmapFromFile( image2_filename, useTransparency=True )
359 mask2_pilImage = ImgConv.PilImageFromWxBitmap( mask2_bmap ) # Always RGB
360 mask2_pilImage = mask2_pilImage.convert( 'L' )
361 #end if
362
363 targetRGBA_pilImage = CombinePilImagesUsingMasks( file1_pilImage, mask1_pilImage,
364 file2_pilImage, mask2_pilImage,
365 offset2 )
366 return targetRGBA_pilImage
367
368 #end def CombineFileImagesUsingFileMasks
369
370 #------------------------------------------------------------------------------
371
372 def GetTextExtent( text, fontSize=12, family=wx.DEFAULT, style=wx.NORMAL, weight=wx.NORMAL,
373 underline=False, face='', encoding=wx.FONTENCODING_DEFAULT ) :
374
375
376 textBmap = wx.EmptyBitmap( 0, 0 ) # Give a dummy size
377
378 dc = wx.MemoryDC() # Pen and Brush are irrelevant.
379 dc.SelectObject( textBmap )
380
381 dc.SetBackgroundMode( wx.TRANSPARENT ) # wx.SOLID (BG will be painted) or wx.TRANSPARENT
382 dc.SetTextBackground( wx.BLUE ) # Color is irrelevent - invisible because wx.TRANSPARENT
383
384 # The pen and brush colors are irrelevant.
385 dc.SetFont( wx.Font( fontSize, family, style, weight, underline, face, encoding ) )
386 dc.SetTextForeground( (255, 255, 255) )
387 textOffset = (0, 0)
388 dc.DrawText( text, *textOffset )
389 textExtent = dc.GetTextExtent( text )
390
391 # The reported text extent is wrong !
392 trueExtentX, trueExtentY = textExtent
393 trueExtentX += 0
394 trueExtentY += 0
395 trueExtent = (trueExtentX, trueExtentY)
396
397 # The apparent offset is wrong !
398 # The offset where actual text writing must go into trueExtent.
399 trueOffset = (0, 0)
400
401 return (trueExtent, trueOffset)
402
403 #end def GetTextExtent
404
405 #------------------------------------------------------------------------------
406
407 """
408 AddSequences.py
409
410 Ray Pasco
411 pascor(at)verizon(dot)net
412
413 2011-03-25 Rev. 1.0
414
415 """
416
417 def AddSequences( seq1, seq2 ) :
418 """
419 Sum the numerical values in 2 sequences. Either may be a tuple or a list.
420 The sequences may have different lengths.
421
422 All values to be summed must be numerical, else the value None will be returned
423 instead of a resulting sequence.
424
425 If one list is longer its unassociated trailing elements will simply be
426 copied to the end of the returned sequence. Thus, the returned sequence size
427 will always be the length of the longer sequence.
428
429 The type of seq1 determines the returned sequence type (list or tuple).
430
431 It's a shame that this isn't a Python builtin function !
432 """
433 #------------------------
434
435 def IsNumeric( var ) :
436 """
437 It's a real shame that this isn't a Python builtin !
438 """
439 try:
440 float( var )
441 return True
442 except ValueError:
443 return False
444 #end try
445 #end def
446
447 #------------------------
448
449 # Temporarily convert any tuples into lists so they can be manipulated.
450 list1 = seq1
451 outputType = 'list' # First assume that it is a list.
452 if type( seq1 ) == 'tuple' : # Is it a tuple, instead ?
453 list1 = list( seq1 )
454 outputType = 'tuple' # Set sequenceSum to this type on exit.
455 #end if
456
457 list2 = seq2
458 if type( seq2 ) == 'tuple' :
459 list2 = list( seq2 )
460
461 # Make sure both sequences contain all numerical values.
462 seqIndex = 0
463 for seq in (seq1, seq2) : # iterate thru both sequences
464 for i in xrange( len( seq ) ) :
465 if not IsNumeric( seq[ i ] ) :
466 print '\n#### AddSequences(): Non-Numerical Value given in the',
467 if seqIndex == 0 : print 'first',
468 else : print 'second',
469 print 'sequence, index', i, ':'
470 print seq
471 return None
472 #end if
473 #end for
474 seqIndex += 1 # move on to seq2
475 #end for
476
477 longerSeq = seq1 # first assume seq1 is the longer
478 shorterSeq = seq2
479 if len( shorterSeq ) > len( longerSeq ) :
480 longerSeq = seq2 # swap
481 shorterSeq = seq1
482 #end if
483
484 seqSum = []
485 for i in xrange( len( longerSeq ) ) : # Iterate thru the longer sequence
486 try :
487 seqSum.append( longerSeq[ i ] + shorterSeq[ i ] ) # except if 2nd seq has no element at [i]
488 except IndexError :
489 seqSum.append( longerSeq[ i ] ) # just use the longer list's original value
490 #end try
491 #end for
492
493 # seqSum is a list. Convert to a tuple according to outputType.
494 if outputType == 'tuple' : seqSum = tuple( seqSum )
495
496 return seqSum
497
498 #end def AddSequences
499
500 #------------------------------------------------------------------------------
501
502 def CreateDropshadowBitmap( text, textWxFont, textColor=(255, 255, 255), bgColor=(0, 0, 0) ) :
503 """
504 Create a bitmap of a text dropshadow. It is expected that the same text string
505 is to be drawn on top using dc.DrawText()
506
507 This backdrop needs to be offset from the actual text drawn
508 by the relative coord (-2, -2) to account for the increased dropshadow margins.
509 """
510
511 # Create a DC and an RGB bitmap larger than the expected maximum extent.
512 fontSize = textWxFont.GetPointSize()
513 textLen = len(text)
514 sizeX = (fontSize * textLen * 4) /3 # empirical heuristic
515 trialBmap_size = (sizeX, 500)
516 textTrial_bmap = wx.EmptyBitmap( trialBmap_size[0], trialBmap_size[1] )
517
518 dc = wx.MemoryDC( textTrial_bmap )
519 dc.SetBrush( wx.Brush( bgColor, wx.SOLID) )
520 dc.SetPen( wx.Pen( bgColor, 1) )
521 dc.DrawRectangle( 0, 0, *trialBmap_size )
522 dc.SetFont( textWxFont )
523 dc.SetBackgroundMode( wx.TRANSPARENT ) # wx.SOLID or wx.TRANSPARENT
524 dc.SetTextForeground( textColor )
525
526 # Get the size of the needed bitmap.
527 trialTextPosn = (25, 25)
528 dc.DrawText( text, *trialTextPosn )
529 trialTextExtent = dc.GetTextExtent( text )
530
531 # The trialExtent X length is always short by 1 pixel.
532 # Add more border to the bottom and the right sides.
533 delta = max( 2, fontSize/10 ) # empirical heuristic
534 trialTextExtent = (trialTextExtent[0]+3*delta, trialTextExtent[1]+3*delta)
535
536 dc.SelectObject( wx.NullBitmap ) # Done with this dc.
537
538 #--------------
539
540 # Create another enlarged RGB bitmap and a dc to draw the dropshadow onto.
541 dropshadowSize = trialTextExtent
542 dropshadow_bmap = wx.EmptyBitmap( dropshadowSize[0], dropshadowSize[1] )
543
544 dc.SelectObject( dropshadow_bmap )
545 dc.SetBrush (wx.Brush( bgColor, wx.SOLID))
546 dc.SetPen( wx.Pen( bgColor, 1) )
547 dc.DrawRectangle( 0, 0, *trialBmap_size )
548 textOffsetIntoBmap = (delta, delta) # Text center position
549 textOffsetIntoBmapX, textOffsetIntoBmapY = textOffsetIntoBmap
550
551 # The dropshadow offset positions coordinate list.
552 # This is why there is a +/- delta pxl border.
553 skip = max( 1, fontSize/10 )
554 offsetPosnList = []
555 for i in range( 0-delta, delta+1, skip) : # How much +/- to vary the text position
556 for j in range( 0-delta, delta+1, skip) :
557 offsetPosnList.append( (i, j) )
558 #end for
559 #end for
560
561 dc.SetBackgroundMode( wx.TRANSPARENT ) # wx.SOLID or wx.TRANSPARENT
562 dc.SetTextBackground( wx.BLUE ) # Color is irrelevent; ivisible because wx.TRANSPARENT
563 dc.SetTextForeground( textColor )
564
565 # Write the text string on it at all the text offset positions.
566 for positionIndex in xrange( len( offsetPosnList ) ) :
567
568 anOffsetX, anOffsetY = offsetPosnList[ positionIndex ]
569 textPosn = (textOffsetIntoBmapX+anOffsetX, textOffsetIntoBmapY+anOffsetY)
570 dc.DrawText( text, *textPosn )
571
572 #end for
573 dc.SelectObject( wx.NullBitmap ) # Close the DC to drawing.
574
575 return (dropshadow_bmap, dropshadowSize, delta)
576
577 #end def CreateDropshadowBitmap
578
579 #------------------------------------------------------------------------------
Attached Files
To refer to attachments on a page, use attachment:filename, as shown below in the list of files. Do NOT use the URL of the [get] link, since this is subject to change and can break easily.You are not allowed to attach a file to this page.