2.1 Modelamiento de los procesos de estudio
2.1.2 Modelo matemático de un tanque de mezclado
In this example, we shall create a utility class for handling photos that can be used by other applications (as a module) to access Photo metadata and display preview images easily.
Getting ready
The following script makes use of Python Image Library (PIL); a compatible version for Python 3 is Pillow.
Pillow has not been included in the Raspbian repository (used by apt-get); therefore, we will need to install Pillow using a Python Package Manager called PIP.
To install packages for Python 3, we will use the Python 3 version of PIP (this requires 50 MB of available space).
The following commands can be used to install PIP:
sudo apt-get update
sudo apt-get install python3-pip
Before you use PIP, ensure that you have installed libjpeg-dev to allow Pillow to handle JPEG files. You can do this using the following command:
sudo apt-get install libjpeg-dev
Now you can install Pillow using the following PIP command:
sudo pip-3.2 install pillow
PIP also makes it easy to uninstall packages using uninstall.
Finally, you can confirm that it has installed successfully by running python3:
>>>import PIL >>>help(PIL)
You should not get any errors and see lots of information about PIL and its uses:
>>PIL.PILLOW_VERSION
PIP can also be used with Python 2 by installing pip-2.x using the following command:
sudo apt-get install python-pip
Any packages installed using sudo pip install will be installed just for Python 2.
How to do it…
To display photo information in an application, create the following photohandler.py script: ##!/usr/bin/python3
#photohandler.py from PIL import Image from PIL import ExifTags import datetime
import os
#set module values previewsize=240,240
defaultimagepreview="./preview.ppm" filedate_to_use="Exif DateTime" #Define expected inputs
ARG_IMAGEFILE=1 ARG_LENGTH=2 class Photo: def __init__(self,filename): """Class constructor""" self.filename=filename self.filevalid=False self.exifvalid=False img=self.initImage() if self.filevalid==True: self.initExif(img) self.initDates() def initImage(self):
"""opens the image and confirms if valid, returns Image""" try:
except IOError:
print ("Target image not found/valid %s" % (self.filename)) img=None self.filevalid=False return img def initExif(self,image):
"""gets any Exif data from the photo""" try:
self.exif_info={ ExifTags.TAGS[x]:y
for x,y in image._getexif().items() if x in ExifTags.TAGS
}
self.exifvalid=True except AttributeError:
print ("Image has no Exif Tags") self.exifvalid=False
def initDates(self):
"""determines the date the photo was taken"""
#Gather all the times available into YYYY-MM-DD format self.filedates={}
if self.exifvalid:
#Get the date info from Exif info
exif_ids=["DateTime","DateTimeOriginal", "DateTimeDigitized"] for id in exif_ids: dateraw=self.exif_info[id] self.filedates["Exif "+id]= dateraw[:10].replace(":","-") modtimeraw = os.path.getmtime(self.filename) self.filedates["File ModTime"]="%s" % datetime.datetime.fromtimestamp(modtimeraw).date() createtimeraw = os.path.getctime(self.filename) self.filedates["File CreateTime"]="%s" % datetime.datetime.fromtimestamp(createtimeraw).date() def getDate(self):
date = self.filedates[filedate_to_use] except KeyError:
print ("Exif Date not found")
date = self.filedates["File ModTime"] return date
def previewPhoto(self):
"""creates a thumbnail image suitable for tk to display""" imageview=self.initImage()
imageview=imageview.convert('RGB')
imageview.thumbnail(previewsize,Image.ANTIALIAS) imageview.save(defaultimagepreview,format='ppm') return defaultimagepreview
The previous code defines our Photo class; it is of no use to us until we run it in the There's more… section and in the next example.
How it works…
We define a general class called Photo; it contains details about itself and provides functions to access Exchangeable Image File Format (EXIF) information and generate a preview image.
In the __init__() function, we set values for our class variables and call self.
initImage(), which will open the image using the Image() function from the PIL. We then call self.initExif() and self.initDates() and set a flag to indicate whether the file was valid or not. If not valid, the Image() function would raise an IOError exception. The initExif() function uses PIL to read the EXIF data from the img object as shown in the following code snippet:
self.exif_info={
ExifTags.TAGS[id]:y
for id,y in image._getexif().items() if id in ExifTags.TAGS
}
The previous code is a series of compound statements that result in self.exif_info being populated with a dictionary of tag names and their related values.
ExifTag.TAGS is a dictionary that contains a list of possible tag names linked with their IDs as shown in the following code snippet:
514: 'JpegIFByteCount', 40963: 'ExifImageHeight', …etc…}
The image._getexif() function returns a dictionary that contains all the values set by the camera of the image, each linked to their relevant IDs, as shown in the following code snippet:
Image._getexif()={ 256: 3264, 257: 2448, 37378: (281, 100), 36867: '2013:02:04 09:12:16', …etc…}
The for loop will go through each item in the image's EXIF value dictionary and check for its occurrence in the ExifTags.TAGS dictionary; the result will get stored in self.exif_info. The code for the same is shown as follows:
self.exif_info={ 'YResolution': (72, 1), 'ResolutionUnit': 2, 'ExposureMode': 0, 'Flash': 24, …etc…}
Again, if there are no exceptions, we set a flag to indicate that the EXIF data is valid, or if there is no EXIF data, we raise an AttributeError exception.
The initDates() function allows us to gather all the possible file dates and dates from the EXIF data so that we can select one of them as the date we wish to use for the file. For example, it allows us to rename all the images to a filename in the standard date format. We create a dictionary self.filedates that we populate with three dates extracted from the EXIF information. We then add the filesystem dates (created and modified) just in case no EXIF data is available. The os module allows us to use os.path.getctime() and os.path. getmtime() to obtain an epoch value of the file creation—it can also be the date and time when the file was moved—and file modification—when it was last written to (for example, it often refers to the date when the picture was taken). The epoch value is the number of seconds since January 1, 1970, but we can use datetime.datetime.fromtimestamp() to convert it into years, months, days, hours, and seconds. Adding date() simply limits it to years, months, and days.
Now if the Photo class was to be used by another module, and we wished to know the date of the image that was taken, we could look at the self.dates dictionary and pick out a suitable date. However, this would require the programmer to know how the self.dates values are arranged, and if we later changed how they are stored, it would break their program. For this reason, it is recommended that we access data in a class through access functions so the implementation is independent of the interfaces (this process is known as encapsulation). We provide a function that returns a date when called; the programmer does not need to know that it could be one of the five available dates or even that they are stored as epoch values. Using a function, we can ensure that the interface will remain the same no matter how the data is stored or collected.
Finally, the last function we want the Photo class to provide is previewPhoto(). This function provides a method to generate a small thumbnail image and save it as a Portable
Pixmap Format (PPM) file. As we will discover in a moment, Tkinter allows us to place images
on its Canvas widget, but unfortunately, it does not support JPGs (and only supports GIF or PPM) directly. Therefore, we simply save a small copy of the image we want to display in the PPM format—with the added caveat that the image pallet must be converted to RGB too—and then get Tkinter to load it onto the Canvas when required.
To summarize, the Photo class we have created is as follows:
Operations Description
__init__(self,filename) This is the object initialization function
initImage(self) This returns img, a PIL-type image object
initExif(self,image) This extracts all the EXIF information, if any is present
initDates(self) This creates a dictionary of all the dates available from the file and photo information
getDate(self) This returns a string of the date when the photo was taken/created
previewPhoto(self) This returns a string of the filename of the previewed thumbnail
The properties and their respective descriptions are as follows:
Properties Description
self.filename The filename of the photo
self.filevalid This is set to True if the file is opened successfully
self.exifvalid This is set to True if the Photo contains EXIF information
self.exif_info This contains the EXIF information from the photo
To test the new class, we will create some test code to confirm that everything is working as we expect; see the following section.
There's more…
We previously created the class Photo. Now we can add some test code to our module to ensure that it functions as we expect. We can use the __name__ ="__main__" attribute as before to detect whether the module has been run directly or not.
We can add the succeeding section of code at the end of the photohandler.py script to produce the following test application which looks as follows:
The Photo View Demo application
Add the following code at the end of photohandler.py: #Module test code
def dispPreview(aPhoto): """Create a test GUI""" import tkinter as TK #Define the app window app = TK.Tk()
app.title("Photo View Demo")
#Define TK objects
# create an empty canvas object the same size as the image canvas = TK.Canvas(app, width=previewsize[0],
#(including xyscroll bars) photoInfo=TK.Variable() lbPhotoInfo=TK.Listbox(app,listvariable=photoInfo, height=18,width=45, font=("monospace",10)) yscroll=TK.Scrollbar(command=lbPhotoInfo.yview, orient=TK.VERTICAL) xscroll=TK.Scrollbar(command=lbPhotoInfo.xview, orient=TK.HORIZONTAL) lbPhotoInfo.configure(xscrollcommand=xscroll.set, yscrollcommand=yscroll.set) lbPhotoInfo.grid(row=0,column=1,sticky=TK.N+TK.S) yscroll.grid(row=0,column=2,sticky=TK.N+TK.S) xscroll.grid(row=1,column=1,sticky=TK.N+TK.E+TK.W)
# Generate the preview image
preview_filename = aPhoto.previewPhoto()
photoImg = TK.PhotoImage(file=preview_filename) # anchor image to NW corner
canvas.create_image(0,0, anchor=TK.NW, image=photoImg)
# Populate infoList with dates and exif data infoList=[]
for key,value in aPhoto.filedates.items(): infoList.append(key.ljust(25) + value) if aPhoto.exifvalid:
for key,value in aPhoto.exif_info.items(): infoList.append(key.ljust(25) + str(value)) # Set listvariable with the infoList
photoInfo.set(tuple(infoList)) app.mainloop()
def main():
"""called only when run directly, allowing module testing""" import sys
#Check the arguments
if len(sys.argv) == ARG_LENGTH: print ("Command: %s" %(sys.argv)) #Create an instance of the Photo class viewPhoto = Photo(sys.argv[ARG_IMAGEFILE]) #Test the module by running a GUI
else:
print ("Usage: photohandler.py imagefile") if __name__=='__main__':
main() #End
The previous test code will run the main() function, which takes the filename of a photo to use and create a new Photo object called viewPhoto. If viewPhoto is opened successfully, we will call dispPreview() to display the image and its details.
The dispPreview() function creates four Tkinter widgets to be displayed: a Canvas to load the thumbnail image, a Listbox widget to display the photo information, and two scroll bars to control the Listbox. First, we create a Canvas widget the size of the thumbnail image (previewsize).
Next, we create photoInfo, which will be our listvariable parameter linked to the Listbox widget. Since Tkinter doesn't provide a ListVar() function to create a suitable item, we use the generic type TK.Variable() and then ensure we convert it to a tuple type before setting the value. The Listbox widget gets added; we need to make sure that the listvariable parameter is set to photoInfo and also set the font to monospace. This will allow us to line up our data values using spaces, as monospace is a fixed width font, so each character takes up the same width as any other.
We define the two scroll bars, linking them to the Listbox widget, by setting the Scrollbar command parameters for vertical and horizontal scroll bars to lbPhotoInfo.yview and lbPhotoInfo.xview. Then, we adjust the parameters of the Listbox using the following command:
lbPhotoInfo.configure(xscrollcommand=xscroll.set, yscrollcommand=yscroll.set)
The configure command allows us to add or change the widget's parameters after it has been created, in this case linking the two scroll bars so the Listbox widget can also control them if the user scrolls within the list.
As before, we make use of the grid layout to ensure that the Listbox widget has the two scroll bars placed correctly next to it and the Canvas widget is to the left of the Listbox widget. We now use the Photo object to create the preview.ppm thumbnail file (using the aPhoto. previewPhoto() function) and create a TK.PhotoImage object that can then be added to the Canvas widget with the following command:
Finally, we use the date information that the Photo class gathers and the EXIF information (ensuring it is valid first) to populate the Listbox widget. We do this by converting each item into a list of strings that are spaced out using .ljust(25)—it adds a left justification to the name and pads it out to make the string 25 characters wide. Once we have the list, we convert it to a tuple type and set the listvariable (photoInfo) parameter.
As always, we call app.mainloop() to start the monitoring for events to respond to.