Article Outline
Python pil example 'cImage'
Functions in program:
def formatPixel(data):
python cImage
Python pil example: cImage
"""
image.py
This module provides a simple interface to create a window, load an image and experiment
with image based algorithms. Many of which require pixel-by-pixel manipulation. This
is a educational module, its not intended to replace the excellent Python Image Library, in fact
it uses PIL.
The module and its interface and some of the code were inspired/copied by/from John Zelle's graphics.py
which serves a similar purpose in the graphics primitive world.
"""
# Release Notes:
# Version 1.0 Fall 2005
#
# Brad Miller, Luther College
#
# Version 1.1 December 7, 2005
# Changes:
# Modify class name for base image to be AbstractImage This way we don't have a lower case
# class name running around. We still don't expect people to create an AbstractImage but
# rather create an image through FileImage, ListImage, or EmptyImage.
# Add ability to convert an image to a list
# Add save function to write an image back to disk.
#
# Version 1.2 November 2007
# Changes:
# Modify the setPosition function to position the image by the top left corner rather than
# the center.
# Add the exitOnClick method to ImageWin. Use this as the last call in the program to
# avoid early exits when running from the command line, and nasty hangs when running
# from within IDLE
#
# Version 1.3 May 2008
# Changes:
# Modify all code to be Python 3.0 ready. -- still runs under 2.x
# Modify all code so that if PIL is not available then image.py will still
# function using Tkimages. N.B. Tk restricts image types to gif or ppm
#
try:
import tkinter
except:
import Tkinter as tkinter
pilAvailable = True
try:
import Image
import ImageTk
except:
pilAvailable = False
#import exceptions
# Borrow some ideas from Zelle
# create an invisible global main root for all windows
tk = tkinter
_imroot = tk.Tk()
_imroot.withdraw()
def formatPixel(data):
if type(data) == tuple:
return '{#%02x%02x%02x}'%data
elif isinstance(data,Pixel):
return '{#%02x%02x%02x}'%data.getColorTuple()
class ImageWin(tk.Canvas):
"""
ImageWin: Make a frame to display one or more images.
"""
def __init__(self,title,width,height):
"""
Create a window with a title, width and height.
"""
master = tk.Toplevel(_imroot)
master.protocol("WM_DELETE_WINDOW", self._close)
#super(ImageWin, self).__init__(master, width=width, height=height)
tk.Canvas.__init__(self, master, width=width, height=height)
self.master.title(title)
self.pack()
master.resizable(0,0)
self.foreground = "black"
self.items = []
self.mouseX = None
self.mouseY = None
self.bind("<Button-1>", self._onClick)
self.height = height
self.width = width
self._mouseCallback = None
self.trans = None
_imroot.update()
def _close(self):
"""Close the window"""
self.master.destroy()
self.quit()
_imroot.update()
def getMouse(self):
"""Wait for mouse click and return a tuple with x,y position in screen coordinates after
the click"""
self.mouseX = None
self.mouseY = None
while self.mouseX == None or self.mouseY == None:
self.update()
return ((self.mouseX,self.mouseY))
def setMouseHandler(self, func):
self._mouseCallback = func
def _onClick(self, e):
self.mouseX = e.x
self.mouseY = e.y
if self._mouseCallback:
self._mouseCallback(Point(e.x, e.y))
def exitOnClick(self):
"""When the Mouse is clicked close the window and exit"""
self.getMouse()
self._close()
class Pixel(object):
"""This simple class abstracts the RGB pixel values."""
def __init__(self, red, green, blue):
super(Pixel, self).__init__()
self.__red = red
self.__green = green
self.__blue = blue
self.max = 255
def getRed(self):
"""Return the red component of the pixel"""
return self.__red
def getGreen(self):
"""Return the green component of the pixel"""
return self.__green
def getBlue(self):
"""Return the blue component of the pixel"""
return self.__blue
def getColorTuple(self):
"""Return all color information as a tuple"""
return (self.__red, self.__green, self.__blue)
def setRed(self,red):
"""Modify the red component"""
if self.max >= red >= 0:
self.__red = red
else:
raise ValueError("Error: pixel value %d is out of range" % red)
def setGreen(self,green):
"""Modify the green component"""
if self.max >= green >= 0:
self.__green = green
else:
raise ValueError("Error: pixel value %d is out of range" % green)
def setBlue(self,blue):
"""Modify the blue component"""
if self.max >= blue >= 0:
self.__blue = blue
else:
raise ValueError("Error: pixel value %d is out of range" % blue)
def __getitem__(self,key):
"""Allow new style pixel class to act like a color tuple:
0 --> red
1 --> green
2 --> blue
"""
if key == 0:
return self.__red
elif key == 1:
return self.__green
elif key == 2:
return self.__blue
else:
raise ValueError("Error %d Index out of range" % key)
def setRange(self,pmax):
"""docstring for setRange"""
if pmax == 1.0:
self.max = 1.0
elif pmax == 255:
self.max = 255
else:
raise ValueError("Error range must be 1.0 or 256")
def __str__(self):
return str(self.getColorTuple())
def __repr__(self):
"""docstring for __repr__"""
return str(self.getColorTuple())
red = property(getRed, setRed, None, "I'm the red property.")
green = property(getGreen, setGreen, None, "I'm the green property.")
blue = property(getBlue, setBlue, None, "I'm the blue property.")
class AbstractImage(object):
"""
Create an image. The image may be created in one of four ways:
1. From an image file such as gif, jpg, png, ppm for example: i = image('fname.jpb)
2. From a list of lists
3. From another image object
4. By specifying the height and width to create a blank image.
"""
imageCache = {} # tk photoimages go here to avoid GC while drawn
imageId = 1
def __init__(self,fname=None,data=[],imobj=None,height=0,width=0):
"""
An image can be created using any of the following keyword parameters. When image creation is
complete the image will be an rgb image.
fname: A filename containing an image. Can be jpg, gif, and others
data: a list of lists representing the image. This might be something you construct by
reading an asii format ppm file, or an ascii art file and translate into rgb yourself.
imobj: Make a copy of another image.
height:
width: Create a blank image of a particular height and width.
"""
super(AbstractImage, self).__init__()
# if PIL is available then use the PIL functions otherwise fall back to Tk
if pilAvailable:
self.loadImage = self.loadPILImage
self.createBlankImage = self.createBlankPILImage
self.setPixel = self.setPILPixel
self.getPixel = self.getPILPixel
self.save = self.savePIL
else:
self.loadImage = self.loadTkImage
self.createBlankImage = self.createBlankTkImage
self.setPixel = self.setTkPixel
self.getPixel = self.getTkPixel
self.save = self.saveTk
if fname:
self.loadImage(fname)
self.imFileName = fname
elif data:
height = len(data)
width = len(data[0])
self.createBlankImage(height,width)
for row in range(height):
for col in range(width):
self.setPixel(x,y,Pixel(data[row][col]))
elif height > 0 and width > 0:
self.createBlankImage(height,width)
elif imobj:
self.im = imobj.copy()
if pilAvailable:
self.width,self.height = self.im.size
else:
self.width = self.im.width()
self.height = self.im.height()
self.centerX = self.width/2+3 # +3 accounts for the ~3 pixel border in Tk windows
self.centerY = self.height/2+3
self.id = None
def loadPILImage(self,fname):
self.im = Image.open(fname)
ni = self.im.convert("RGB")
self.im = ni
def loadTkImage(self,fname):
sufstart = fname.rfind('.')
if sufstart < 0:
suffix = ""
else:
suffix = fname[sufstart:]
if suffix not in ['.gif', '.ppm']:
raise ValueError("Bad Image Type: %s : Without PIL, only .gif or .ppm files are allowed" % suffix)
self.im = tkinter.PhotoImage(file=fname)
def createBlankPILImage(self,height,width):
self.im = Image.new("RGB",(width,height))
ni = self.im.convert("RGB")
self.im = ni
def createBlankTkImage(self,height,width):
self.im = tkinter.PhotoImage(height=height,width=width)
def copy(self):
"""Return a copy of this image"""
newI = AbstractImage(imobj=self.im)
return newI
def clone(self):
"""Return a copy of this image"""
newI = AbstractImage(imobj=self.im)
return newI
def getHeight(self):
"""Return the height of the image"""
return self.height
def getWidth(self):
"""Return the width of the iamge"""
return self.width
def getTkPixel(self,x,y):
"""Get a pixel at the given x,y coordinate. The pixel is returned as an rgb color tuple
for eaxamplle foo.getPixel(10,10) --> (10,200,156) """
p = [int(j) for j in self.im.get(x,y)]
return Pixel(p[0],p[1],p[2])
def setTkPixel(self,x,y,pixel):
"""Set the color of a pixel at position x,y. The color must be specified as an rgb tuple (r,g,b) where
the rgb values are between 0 and 255."""
self.im.put(formatPixel(pixel.getColorTuple()),(x,y))
def getPILPixel(self,x,y):
"""docstring for getPILPIxel"""
p = self.im.getpixel((x,y))
return Pixel(p[0],p[1],p[2])
def setPILPixel(self,x,y,pixel):
"""docstring for setPILPixel"""
self.im.putpixel((x,y),pixel.getColorTuple())
def setPosition(self,x,y):
"""Set the position in the window where the top left corner of the window should be."""
self.top = y
self.left = x
self.centerX = x + (self.width/2)+3
self.centerY = y + (self.height/2)+3
def getImage(self):
if pilAvailable:
return ImageTk.PhotoImage(self.im)
else:
return self.im
def draw(self,win):
"""Draw this image in the ImageWin window."""
ig = self.getImage()
self.imageCache[self.imageId] = ig # save a reference else Tk loses it...
AbstractImage.imageId = AbstractImage.imageId + 1
self.canvas=win
self.id = self.canvas.create_image(self.centerX,self.centerY,image=ig)
_imroot.update()
def saveTk(self,fname=None,ftype='gif'):
if fname == None:
fname = self.imFileName
sufstart = fname.rfind('.')
if sufstart < 0:
suffix = ""
else:
suffix = fname[sufstart:]
if suffix == "":
suffix = "."+ftype
fname = fname+suffix
if suffix not in ['.gif', '.ppm']:
raise ValueError("Without PIL, only .gif or .ppm files are allowed")
try:
self.im.write(fname,format=ftype)
except Exception as inst:
print(type(inst)) # the exception instance
print(inst.args) # arguments stored in .args
print(inst) # __str__ allows args to be printed directly,
# but may be overridden in exception subclasses
print("Error saving, Could Not open ", fname, " to write.", sys.exc_info()[0])
def savePIL(self,fname=None,ftype='jpg'):
if fname == None:
fname = self.imFileName
sufstart = fname.rfind('.')
if sufstart < 0:
suffix = ""
else:
suffix = fname[sufstart:]
if suffix == "":
suffix = "."+ftype
fname = fname+suffix
try:
self.im.save(fname)
except:
print("Error saving, Could Not open ", fname, " to write.")
def toList(self):
"""
Convert the image to a List of Lists representation
"""
res = []
for i in range(self.height):
res.append([])
for j in range(self.width):
res[i].append(self.getPixel(j,i))
return res
class FileImage(AbstractImage):
def __init__(self,thefile):
super(FileImage, self).__init__(fname = thefile)
class EmptyImage(AbstractImage):
def __init__(self,cols,rows):
super(EmptyImage, self).__init__(height = rows, width = cols)
class ListImage(AbstractImage):
def __init__(self,thelist):
super(ListImage, self).__init__(data=thelist)
# Example program Read in an image and calulate the negative.
"""
if __name__ == '__main__':
win = ImageWin("My Window",480,640)
oImage = FileImage('lcastle.jpg')
print(oImage.getWidth(), oImage.getHeight())
oImage.draw(win)
myImage = oImage.copy()
for row in range(myImage.getHeight()):
for col in range(myImage.getWidth()):
v = myImage.getPixel(col,row)
v.red = 255 - v.red
v.green = 255 - v.green
v.blue = 255 - v.blue
# x = map(lambda x: 255-x, v)
myImage.setPixel(col,row,v)
myImage.setPosition(myImage.getWidth()+1,0)
myImage.draw(win)
print(win.getMouse())
myImage.save('/Users/bmiller/tmp/testfoo.jpg')
print(myImage.toList())
win.exitOnClick()
"""
Python links
- Learn Python: https://pythonbasics.org/
- Python Tutorial: https://pythonprogramminglanguage.com