Posted:04/06/2015 3:21PM
Mike Mclain discusses how to use Python to Get XMP Faces From Picasa Tagged Images
Over the last year I have been (albeit slowly) working on digitizing the family photos I inherited (I estimate that there are somewhere around 6,000 to 10,000 photos collectively). Likewise, in addition to digitizing (these photos) I have also been utilizing Google Picasa 3 to add facial tags (to these photos) in order to preserve any accompanying historical information given the (albeit morbid) reality that time will inevitably obscure such information via death (noting that I myself am having a hard time finding someone alive who can identify the people within some of the photos I inherited).
Nevertheless, while this information is somewhat tangential (since this article is neither about document digitization nor about the importance of cataloging historical information), the prevalent attribute that arises (from such discussion) is the concept of sharing or distributing such photos to family members via the Internet. Conversely, the implementation of this objective is where I ultimately hit a bit of a wall, since (given the overall size and hereditary diversity of my Picasa archives) such objectives were not easily obtained (to my own personal satisfaction) through the sole usage of a free photo service
Likewise, based upon such realities (and upon sniffing around Googles method of saving tag information) I decided to begin working on creating a Python based Picasa web gallery generator that had in browser tag support (since Picasas web exportation is far from being satisfactory given my particular needs). Now, while I will not go into substantial detail surrounding the dynamics of this particular generator (since I am currently in the early development stages); however, I did run into a slight snag (along with found virtually no web documentation) when it came to decoding the Picasa XMP tag format. Conversely, after playing around with tag orientations for a day and figuring out the encoding scheme, I figured that I would share the fruits of my labor here.
Note: I have incorporated a number of screenshots into this article (since a picture is typically worth a 1000 words) and I would like to briefly mention that these screenshots are being presented (within this article) under the fair use doctrine since their inclusion is strictly educational.
To begin, because Picasa appears to have a number of photo tag encoding methods (I have witness Picasa save tag information within ini files) and the XMP method I wanted to use encoded information within the image (rather than in an ini file). Steps must be taken to ensure that Picasa utilizes the XMP file encoding method over the ini encoding method and this can be done by:
First navigating the Picasa tools menu and selecting the options submenu, like so:
Next, (once the options dialogue is open) the "Name Tags" tab should be selected and afterwards the "Store name tags in photo" checkbox should be checked, like so:
noting that, upon pressing the okay button to confirm this action, the process of Picasa writing all tag information to image files could take a lengthy period of time depending on how many files have to be updated with XMP tags.
Now that the XMP configuration aspect is out of the way (although XMP support is still reportedly somewhat buggy within Picasa), it should be pointed out that XMP (despite its introduction by Adobe in 2001 and later acceptance by Microsoft) still has that infancy standard feel to it (since not many people actively discuss it and few photo organizers fully support it) but it seems to be gaining some traction in recent years. Likewise, the standard itself can be seemingly surmised to be nothing more than an embedded (within the image) XML file (noting that all the specifications associated with the standard are somewhat outlined here) and this information can be easily observed upon opening a XMP encoded image within a text editor (like Notepad++), like so:
Likewise, upon extracting this embedded partial XMP data (from the image) and styling (admittedly viewing information in a text editor is rather gimmicky), a more presentable XMP packet can be observed, like so:
<?xpacket begin="..." id="W5M0MpCehiHzreSzNTczkc9d"?> <x:xmpmeta xmlns:x="adobe:ns:meta/" x:xmptk="XMP Core 5.1.2"> <rdf:RDF xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"> <rdf:Description xmlns:mwg-rs="http://www.metadataworkinggroup.com/schemas/regions/" xmlns:stArea="http://ns.adobe.com/xmp/sType/Area#" xmlns:stDim="http://ns.adobe.com/xap/1.0/sType/Dimensions#" xmlns:xmp="http://ns.adobe.com/xap/1.0/" rdf:about="" xmp:ModifyDate="2015-01-26T01:47:57-05:00" > <mwg-rs:Regions rdf:parseType="Resource"> <mwg-rs:AppliedToDimensions stDim:w="17376" stDim:h="21552" stDim:unit="pixel" /> <mwg-rs:RegionList> <rdf:Bag> <rdf:li> <rdf:Description mwg-rs:Name="Name Tag 1" mwg-rs:Type="Face"> <mwg-rs:Area stArea:x="0.305018" stArea:y="0.536888" stArea:w="0.321708" stArea:h="0.311247" stArea:unit="normalized" /> </rdf:Description> </rdf:li> <rdf:li> <rdf:Description mwg-rs:Name="Name Tag 2" mwg-rs:Type="Face"> <mwg-rs:Area stArea:x="0.713945" stArea:y="0.517817" stArea:w="0.296098" stArea:h="0.286841" stArea:unit="normalized" /> </rdf:Description> </rdf:li> <rdf:li> <rdf:Description mwg-rs:Name="Name Tag 3" mwg-rs:Type="Face"> <mwg-rs:Area stArea:x="0.44924" stArea:y="0.167803" stArea:w="0.321708" stArea:h="0.31185" stArea:unit="normalized" /> </rdf:Description> </rdf:li> </rdf:Bag> </mwg-rs:RegionList> </mwg-rs:Regions> </rdf:Description> </rdf:RDF> </x:xmpmeta> <?xpacket end="w"?>
in which the only information of particular relevance (to the task of extracting XMP name tags) is the XML children of the "<rdf:Bag>"
tag (or more particularly the information stored within the"<rdf:Description>"
tag).
Now (at this point), it might be tempting to utilize the PIL or similar Python libraries in order to obtain this embedded XMP XML; however, such utilities seem to be somewhat limited when it comes to XMP nametags (although your mileage will likely vary depending on what libraries you ultimately decide to utilize, but PIL does seem to be currently ill-equipped for this particular task), thus I opted for a more direct approach, like so:
# This function will extract the XMP Bag Tag def Get_XMP_Bag_Tag(file): # initialize our return data file_data = None try: # attempt to open the file as binary file_as_binary = open(file,'rb') # if it opened try to read the file file_data = file_as_binary.read() # close the file afterward done file_as_binary.close() except: # if we sell the open the file abort return False, None # if the file is empty abort if file_data is None: return False, None # using the file data, attempt to locate the starting XMP XML Bag tag xmp_start = file_data.find('<rdf:Bag') # also try and locate the ending XMP XML Bag tag xmp_end = file_data.find('</rdf:Bag') # if the tag is found, -1 is used and we get "" else we get data xmp_bag = file_data[xmp_start:xmp_end+len("</rdf:Bag>")] # if nothing is found abort if xmp_bag == "": return False, None # if we found something, return tag information return True, xmp_bag
which simply opens and reads the image file into memory, attempts to find the XMP bag tags, and then extracts any information contained between the two tags prior to returning this information back to the caller.
Likewise (as it might be expected), processing this XML information (in order to extract the tag information) becomes the act of programmers preference (some methods are better than others), and I decided to utilize the Python lxml
etree
class to achieve this objective (but feel free to pick whatever method works best for your particular application).
Conversely (as a result of my XML processing selection), I had to add additional XML data (to the acquired XML data) in order to get the etree
class to process the image tag data correctly, while the overall extraction of the XMP tags (afterwards) was relatively straightforward, like so:
# Import a Python XML processing class from lxml import html, etree # extract the XMP BAG information using the previous function found, value = Get_XMP_Bag_Tag(file) # if data was found, then process this data if found: # Because lxml has strict XML syntax standards, a XML root with namespaces # must be provided rawxml = """<root xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns:mwg-rs="http://www.metadataworkinggroup.com/schemas/regions/" xmlns:stArea="http://ns.adobe.com/xmp/sType/Area#" xmlns:stDim="http://ns.adobe.com/xap/1.0/sType/Dimensions#" xmlns:xmp="http://ns.adobe.com/xap/1.0/"> %s </root>""" % value # adding a root also added a extra child we must ignore via [0] # getchildren() will return all rdf:li tags root = etree.fromstring(rawxml).getchildren()[0].getchildren() # making a array to hold tag information tags = [] # iterate through each rdf:li tag for li in list(root): # dig into rdf:li via [0] to access the child rdf:Description tag li2 = li[0] # extract the XMP tag name name = li2.get('{http://www.metadataworkinggroup.com/schemas/regions/}Name') # every rdf:Description has 1 child mwg-rs:Area that defines the tag location # extract this information via getchildren() and [0] li3 = li2.getchildren()[0] # the extract the normalized center X, Y coordinates # and the total rectangle size x = li3.get('{http://ns.adobe.com/xmp/sType/Area#}x') y = li3.get('{http://ns.adobe.com/xmp/sType/Area#}y') w = li3.get('{http://ns.adobe.com/xmp/sType/Area#}w') h = li3.get('{http://ns.adobe.com/xmp/sType/Area#}h') # save the information into the tag array tags.append((name,float(x),float(y),float(w),float(h))) # do something useful here
At this point, the overall process of identifying, extracting, and processing the XMP tag information should seem (at least to an experienced programmer) relatively straightforward; however, a minor caveat exist here when it comes to translating the normalized XMP tag rectangle (or coordinates that describe the physical location of the tag within the image) into a web friendly (left, top, right, bottom) rectangle utilized by the HTML image map element.
Noting that I use the term minor caveat here sardonically since the normalization and relative encoding of the XMP tag (x, y, width, height) is either not documented or is so currently obfuscated within existing documentation that determining that the provided XMP X,Y coordinates are actually referring to the center of the rectangle rather than to a corner of the rectangle (which is traditional) is a frustrating process for the uninitiated.
Nevertheless (with this information known), converting the XMP X,Y coordinates into a web friendly (left, top, right, bottom) coordinates can be accomplished easily, like so:
# Permit floating-point division from __future__ import division # import image class for demonstrating image loading import Image # define your JPG image here TheImagePath = "Demo_XMP.jpg" # open the image TheImage = Image.open(TheImagePath) # extract the width and height of the image in pixels img_width, img_height = TheImage.size # assuming tags were defined previously, process each tag found for name, x, y, width, height in tags: # convert normalized XMP x, y center coordinates into relative pixel coordinates center_x = x*img_width center_y = y*img_height # convert normalized XMP width and height into relative pixel coordinates total_width = width*img_width total_height = height*img_height # calculate half width and height in order to determine rectangular coordinates half_width = total_width/2.0 half_height = total_height/2.0 # calculate web rectangular coordinates from center coordinates top = center_y-half_height bottom = center_y+half_height left= center_x-half_width right = center_x+half_width # do something useful here
While the process of converting the normalized XMP X, Y coordinate system into a web friendly (left, top, right, bottom) coordinate system is (ultimately) relatively straightforward (once an understanding surrounding the XMP encoding scheme is obtained); however, given the overall lack of information available on this topic, I figured such discussion might prove to be beneficial for somebody seeking to work with XMP tags in the near future.
Enjoy!
By Mike Mclain