Using Python to Get XMP Faces From Picasa Tagged Images

Posted:04/06/2015 3:21PM

Using Python to Get XMP Faces From Picasa Tagged Images

Mike Mclain discusses how to use Python to Get XMP Faces From Picasa Tagged Images

Preface:

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.

Configuring XMP Image Tags in Picasa:

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:


Opening The Options Menu Within Picasa.

Opening The Options Menu Within Picasa.

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:


Enabling Embedded XMP Image Tags Within Picasa.

Enabling Embedded XMP Image Tags Within Picasa.

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.

Processing 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:


Viewing Embedded XMP Within a Text Editor.

Viewing Embedded XMP Within a Text Editor.

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

Conclusion:

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!

Comments:

comments powered by Disqus