# Python-bioformats is distributed under the GNU General Public
# License, but this file is licensed under the more permissive BSD
# license. See the accompanying file LICENSE for details.
#
# Copyright (c) 2009-2014 Broad Institute
# All rights reserved.
"""omexml.py read and write OME xml
"""
from __future__ import absolute_import, unicode_literals
import xml.etree.ElementTree
from xml.etree import cElementTree as ElementTree
import sys
if sys.version_info.major == 3:
from io import StringIO
uenc = 'unicode'
else:
from cStringIO import StringIO
uenc = 'utf-8'
import datetime
import logging
from functools import reduce
logger = logging.getLogger(__file__)
import re
import uuid
def xsd_now():
'''Return the current time in xsd:dateTime format'''
return datetime.datetime.now().isoformat()
DEFAULT_NOW = xsd_now()
#
# The namespaces
#
NS_BINARY_FILE = "http://www.openmicroscopy.org/Schemas/BinaryFile/2013-06"
NS_ORIGINAL_METADATA = "openmicroscopy.org/OriginalMetadata"
NS_DEFAULT = "http://www.openmicroscopy.org/Schemas/{ns_key}/2013-06"
NS_RE = r"http://www.openmicroscopy.org/Schemas/(?P<ns_key>.*)/[0-9/-]"
default_xml = """<?xml version="1.0" encoding="UTF-8"?>
<OME xmlns="{ns_ome_default}s"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.openmicroscopy.org/Schemas/OME/2013-06 http://www.openmicroscopy.org/Schemas/OME/2012-03/ome.xsd">
<Image ID="Image:0" Name="default.png">
<AcquisitionDate>%(DEFAULT_NOW)s</AcquisitionDate>
<Pixels DimensionOrder="XYCTZ"
ID="Pixels:0"
SizeC="1"
SizeT="1"
SizeX="512"
SizeY="512"
SizeZ="1"
Type="uint8">
<Channel ID="Channel:0:0" SamplesPerPixel="1">
<LightPath/>
</Channel>
<BinData xmlns="%(NS_BINARY_FILE)s"
BigEndian="false" Length="0"/>
</Pixels>
</Image>
<StructuredAnnotations xmlns="{ns_sa_default}s"/>
</OME>""".format(ns_ome_default=NS_DEFAULT.format(ns_key='ome'), ns_sa_default=NS_DEFAULT.format(ns_key='sa'))
#
# These are the OME-XML pixel types - not all supported by subimager
#
PT_INT8 = "int8"
PT_INT16 = "int16"
PT_INT32 = "int32"
PT_UINT8 = "uint8"
PT_UINT16 = "uint16"
PT_UINT32 = "uint32"
PT_FLOAT = "float"
PT_BIT = "bit"
PT_DOUBLE = "double"
PT_COMPLEX = "complex"
PT_DOUBLECOMPLEX = "double-complex"
#
# The allowed dimension types
#
DO_XYZCT = "XYZCT"
DO_XYZTC = "XYZTC"
DO_XYCTZ = "XYCTZ"
DO_XYCZT = "XYCZT"
DO_XYTCZ = "XYTCZ"
DO_XYTZC = "XYTZC"
#
# Original metadata corresponding to TIFF tags
# The text for these can be found in
# loci.formats.in.BaseTiffReader.initStandardMetadata
#
'''IFD # 254'''
OM_NEW_SUBFILE_TYPE = "NewSubfileType"
'''IFD # 256'''
OM_IMAGE_WIDTH = "ImageWidth"
'''IFD # 257'''
OM_IMAGE_LENGTH = "ImageLength"
'''IFD # 258'''
OM_BITS_PER_SAMPLE = "BitsPerSample"
'''IFD # 262'''
OM_PHOTOMETRIC_INTERPRETATION = "PhotometricInterpretation"
PI_WHITE_IS_ZERO = "WhiteIsZero"
PI_BLACK_IS_ZERO = "BlackIsZero"
PI_RGB = "RGB"
PI_RGB_PALETTE = "Palette"
PI_TRANSPARENCY_MASK = "Transparency Mask"
PI_CMYK = "CMYK"
PI_Y_CB_CR = "YCbCr"
PI_CIE_LAB = "CIELAB"
PI_CFA_ARRAY = "Color Filter Array"
'''BioFormats infers the image type from the photometric interpretation'''
OM_METADATA_PHOTOMETRIC_INTERPRETATION = "MetaDataPhotometricInterpretation"
MPI_RGB = "RGB"
MPI_MONOCHROME = "Monochrome"
MPI_CMYK = "CMYK"
'''IFD # 263'''
OM_THRESHHOLDING = "Threshholding" # (sic)
'''IFD # 264 (but can be 265 if the orientation = 8)'''
OM_CELL_WIDTH = "CellWidth"
'''IFD # 265'''
OM_CELL_LENGTH = "CellLength"
'''IFD # 266'''
OM_FILL_ORDER = "FillOrder"
'''IFD # 279'''
OM_DOCUMENT_NAME = "Document Name"
'''IFD # 271'''
OM_MAKE = "Make"
'''IFD # 272'''
OM_MODEL = "Model"
'''IFD # 274'''
OM_ORIENTATION = "Orientation"
'''IFD # 277'''
OM_SAMPLES_PER_PIXEL = "SamplesPerPixel"
'''IFD # 280'''
OM_MIN_SAMPLE_VALUE = "MinSampleValue"
'''IFD # 281'''
OM_MAX_SAMPLE_VALUE = "MaxSampleValue"
'''IFD # 282'''
OM_X_RESOLUTION = "XResolution"
'''IFD # 283'''
OM_Y_RESOLUTION = "YResolution"
'''IFD # 284'''
OM_PLANAR_CONFIGURATION = "PlanarConfiguration"
PC_CHUNKY = "Chunky"
PC_PLANAR = "Planar"
'''IFD # 286'''
OM_X_POSITION = "XPosition"
'''IFD # 287'''
OM_Y_POSITION = "YPosition"
'''IFD # 288'''
OM_FREE_OFFSETS = "FreeOffsets"
'''IFD # 289'''
OM_FREE_BYTECOUNTS = "FreeByteCounts"
'''IFD # 290'''
OM_GRAY_RESPONSE_UNIT = "GrayResponseUnit"
'''IFD # 291'''
OM_GRAY_RESPONSE_CURVE = "GrayResponseCurve"
'''IFD # 292'''
OM_T4_OPTIONS = "T4Options"
'''IFD # 293'''
OM_T6_OPTIONS = "T6Options"
'''IFD # 296'''
OM_RESOLUTION_UNIT = "ResolutionUnit"
'''IFD # 297'''
OM_PAGE_NUMBER = "PageNumber"
'''IFD # 301'''
OM_TRANSFER_FUNCTION = "TransferFunction"
'''IFD # 305'''
OM_SOFTWARE = "Software"
'''IFD # 306'''
OM_DATE_TIME = "DateTime"
'''IFD # 315'''
OM_ARTIST = "Artist"
'''IFD # 316'''
OM_HOST_COMPUTER = "HostComputer"
'''IFD # 317'''
OM_PREDICTOR = "Predictor"
'''IFD # 318'''
OM_WHITE_POINT = "WhitePoint"
'''IFD # 322'''
OM_TILE_WIDTH = "TileWidth"
'''IFD # 323'''
OM_TILE_LENGTH = "TileLength"
'''IFD # 324'''
OM_TILE_OFFSETS = "TileOffsets"
'''IFD # 325'''
OM_TILE_BYTE_COUNT = "TileByteCount"
'''IFD # 332'''
OM_INK_SET = "InkSet"
'''IFD # 33432'''
OM_COPYRIGHT = "Copyright"
#
# Well row/column naming conventions
#
NC_LETTER = "letter"
NC_NUMBER = "number"
def page_name_original_metadata(index):
'''Get the key name for the page name metadata data for the indexed tiff page
These are TIFF IFD #'s 285+
index - zero-based index of the page
'''
return "PageName #%d" % index
def get_text(node):
'''Get the contents of text nodes in a parent node'''
return node.text
def set_text(node, text):
'''Set the text of a parent'''
node.text = text
def qn(namespace, tag_name):
'''Return the qualified name for a given namespace and tag name
This is the ElementTree representation of a qualified name
'''
return "{%s}%s" % (namespace, tag_name)
def split_qn(qn):
'''Split a qualified tag name or return None if namespace not present'''
m = re.match('\{(.*)\}(.*)', qn)
return m.group(1), m.group(2) if m else None
def get_namespaces(node):
'''Get top-level XML namespaces from a node.'''
ns_lib = {'ome': None, 'sa': None, 'spw': None}
for child in node.iter():
ns = split_qn(child.tag)[0]
match = re.match(NS_RE, ns)
if match:
ns_key = match.group('ns_key').lower()
ns_lib[ns_key] = ns
return ns_lib
def get_float_attr(node, attribute):
'''Cast an element attribute to a float or return None if not present'''
attr = node.get(attribute)
return None if attr is None else float(attr)
def get_int_attr(node, attribute):
'''Cast an element attribute to an int or return None if not present'''
attr = node.get(attribute)
return None if attr is None else int(attr)
def make_text_node(parent, namespace, tag_name, text):
'''Either make a new node and add the given text or replace the text
parent - the parent node to the node to be created or found
namespace - the namespace of the node's qualified name
tag_name - the tag name of the node's qualified name
text - the text to be inserted
'''
qname = qn(namespace, tag_name)
node = parent.find(qname)
if node is None:
node = ElementTree.SubElement(parent, qname)
set_text(node, text)
[docs]class OMEXML(object):
'''Reads and writes OME-XML with methods to get and set it.
The OMEXML class has four main purposes: to parse OME-XML, to output
OME-XML, to provide a structured mechanism for inspecting OME-XML and to
let the caller create and modify OME-XML.
There are two ways to invoke the constructor. If you supply XML as a string
or unicode string, the constructor will parse it and will use it as the
base for any inspection and modification. If you don't supply XML, you'll
get a bland OME-XML object which has a one-channel image. You can modify
it programatically and get the modified OME-XML back out by calling to_xml.
There are two ways to get at the XML. The arduous way is to get the
root_node of the DOM and explore it yourself using the DOM API
(http://docs.python.org/library/xml.dom.html#module-xml.dom). The easy way,
where it's supported is to use properties on OMEXML and on some of its
derived objects. For instance:
>>> o = OMEXML()
>>> print o.image().AcquisitionDate
will get you the date that image # 0 was acquired.
>>> o = OMEXML()
>>> o.image().Name = "MyImage"
will set the image name to "MyImage".
You can add and remove objects using the "count" properties. Each of these
handles hooking up and removing orphaned elements for you and should be
less error prone than creating orphaned elements and attaching them. For
instance, to create a three-color image:
>>> o = OMEXML()
>>> o.image().Pixels.channel_count = 3
>>> o.image().Pixels.Channel(0).Name = "Red"
>>> o.image().Pixels.Channel(1).Name = "Green"
>>> o.image().Pixels.Channel(2).Name = "Blue"
See the `OME-XML schema documentation <http://git.openmicroscopy.org/src/develop/components/specification/Documentation/Generated/OME-2011-06/ome.html>`_.
'''
def __init__(self, xml=None):
if xml is None:
xml = default_xml
if isinstance(xml, str):
xml = xml.encode("utf-8")
self.dom = ElementTree.ElementTree(ElementTree.fromstring(xml))
# determine OME namespaces
self.ns = get_namespaces(self.dom.getroot())
if self.ns['ome'] is None:
raise Exception("Error: String not in OME-XML format")
def __str__(self):
#
# need to register the ome namespace because BioFormats expects
# that namespace to be the default or to be explicitly named "ome"
#
for ns_key in ["ome", "sa", "spw"]:
ns = self.ns.get(ns_key) or NS_DEFAULT.format(ns_key=ns_key)
ElementTree.register_namespace(ns_key, ns)
ElementTree.register_namespace("om", NS_ORIGINAL_METADATA)
result = StringIO()
ElementTree.ElementTree(self.root_node).write(result,
encoding=uenc,
method="xml")
return result.getvalue()
def to_xml(self, indent="\t", newline="\n", encoding=uenc):
return str(self)
def get_ns(self, key):
return self.ns[key]
@property
def root_node(self):
return self.dom.getroot()
def get_image_count(self):
'''The number of images (= series) specified by the XML'''
return len(self.root_node.findall(qn(self.ns['ome'], "Image")))
def set_image_count(self, value):
'''Add or remove image nodes as needed'''
assert value > 0
root = self.root_node
if self.image_count > value:
image_nodes = root.find(qn(self.ns['ome'], "Image"))
for image_node in image_nodes[value:]:
root.remove(image_node)
while(self.image_count < value):
new_image = self.Image(ElementTree.SubElement(root, qn(self.ns['ome'], "Image")))
new_image.ID = str(uuid.uuid4())
new_image.Name = "default.png"
new_image.AcquisitionDate = xsd_now()
new_pixels = self.Pixels(
ElementTree.SubElement(new_image.node, qn(self.ns['ome'], "Pixels")))
new_pixels.ID = str(uuid.uuid4())
new_pixels.DimensionOrder = DO_XYCTZ
new_pixels.PixelType = PT_UINT8
new_pixels.SizeC = 1
new_pixels.SizeT = 1
new_pixels.SizeX = 512
new_pixels.SizeY = 512
new_pixels.SizeZ = 1
new_channel = self.Channel(
ElementTree.SubElement(new_pixels.node, qn(self.ns['ome'], "Channel")))
new_channel.ID = "Channel%d:0" % self.image_count
new_channel.Name = new_channel.ID
new_channel.SamplesPerPixel = 1
image_count = property(get_image_count, set_image_count)
@property
def plates(self):
return self.PlatesDucktype(self.root_node)
@property
def structured_annotations(self):
'''Return the structured annotations container
returns a wrapping of OME/StructuredAnnotations. It creates
the element if it doesn't exist.
'''
node = self.root_node.find(qn(self.ns['sa'], "StructuredAnnotations"))
if node is None:
node = ElementTree.SubElement(
self.root_node, qn(self.ns['sa'], "StructuredAnnotations"))
return self.StructuredAnnotations(node)
[docs] class Image(object):
'''Representation of the OME/Image element'''
def __init__(self, node):
'''Initialize with the DOM Image node'''
self.node = node
self.ns = get_namespaces(self.node)
def get_ID(self):
return self.node.get("ID")
def set_ID(self, value):
self.node.set("ID", value)
ID = property(get_ID, set_ID)
def get_Name(self):
return self.node.get("Name")
def set_Name(self, value):
self.node.set("Name", value)
Name = property(get_Name, set_Name)
def get_AcquisitionDate(self):
'''The date in ISO-8601 format'''
acquired_date = self.node.find(qn(self.ns["ome"], "AcquisitionDate"))
if acquired_date is None:
return None
return get_text(acquired_date)
def set_AcquisitionDate(self, date):
acquired_date = self.node.find(qn(self.ns["ome"], "AcquisitionDate"))
if acquired_date is None:
acquired_date = ElementTree.SubElement(
self.node, qn(self.ns["ome"], "AcquisitionDate"))
set_text(acquired_date, date)
AcquisitionDate = property(get_AcquisitionDate, set_AcquisitionDate)
@property
def Pixels(self):
'''The OME/Image/Pixels element.
Example:
>>> md = bioformats.omexml.OMEXML(xml)
>>> pixels = omemetadata.image(i).Pixels
>>> channel_count = pixels.SizeC
>>> stack_count = pixels.SizeZ
>>> timepoint_count = pixels.SizeT
'''
return OMEXML.Pixels(self.node.find(qn(self.ns['ome'], "Pixels")))
[docs] def image(self, index=0):
'''Return an image node by index'''
return self.Image(self.root_node.findall(qn(self.ns['ome'], "Image"))[index])
class Channel(object):
'''The OME/Image/Pixels/Channel element'''
def __init__(self, node):
self.node = node
self.ns = get_namespaces(node)
def get_ID(self):
return self.node.get("ID")
def set_ID(self, value):
self.node.set("ID", value)
ID = property(get_ID, set_ID)
def get_Name(self):
return self.node.get("Name")
def set_Name(self, value):
self.node.set("Name", value)
Name = property(get_Name, set_Name)
def get_SamplesPerPixel(self):
return get_int_attr(self.node, "SamplesPerPixel")
def set_SamplesPerPixel(self, value):
self.node.set("SamplesPerPixel", str(value))
SamplesPerPixel = property(get_SamplesPerPixel, set_SamplesPerPixel)
class Plane(object):
'''The OME/Image/Pixels/Plane element
The Plane element represents one 2-dimensional image plane. It
has the Z, C and T indices of the plane and optionally has the
X, Y, Z, exposure time and a relative time delta.
'''
def __init__(self, node):
self.node = node
self.ns = get_namespaces(self.node)
def get_TheZ(self):
'''The Z index of the plane'''
return get_int_attr(self.node, "TheZ")
def set_TheZ(self, value):
self.node.set("TheZ", str(value))
TheZ = property(get_TheZ, set_TheZ)
def get_TheC(self):
'''The channel index of the plane'''
return get_int_attr(self.node, "TheC")
def set_TheC(self, value):
self.node.set("TheC", str(value))
TheC = property(get_TheC, set_TheC)
def get_TheT(self):
'''The T index of the plane'''
return get_int_attr(self.node, "TheT")
def set_TheT(self, value):
self.node.set("TheT", str(value))
TheT = property(get_TheT, set_TheT)
def get_DeltaT(self):
'''# of seconds since the beginning of the experiment'''
return get_float_attr(self.node, "DeltaT")
def set_DeltaT(self, value):
self.node.set("DeltaT", str(value))
DeltaT = property(get_DeltaT, set_DeltaT)
@property
def ExposureTime(self):
'''Units are seconds. Duration of acquisition????'''
exposure_time = self.node.get("ExposureTime")
if exposure_time is not None:
return float(exposure_time)
return None
def get_PositionX(self):
'''X position of stage'''
position_x = self.node.get("PositionX")
if position_x is not None:
return float(position_x)
return None
def set_PositionX(self, value):
self.node.set("PositionX", str(value))
PositionX = property(get_PositionX, set_PositionX)
def get_PositionY(self):
'''Y position of stage'''
return get_float_attr(self.node, "PositionY")
def set_PositionY(self, value):
self.node.set("PositionY", str(value))
PositionY = property(get_PositionY, set_PositionY)
def get_PositionZ(self):
'''Z position of stage'''
return get_float_attr(self.node, "PositionZ")
def set_PositionZ(self, value):
self.node.set("PositionZ", str(value))
PositionZ = property(get_PositionZ, set_PositionZ)
class Pixels(object):
'''The OME/Image/Pixels element
The Pixels element represents the pixels in an OME image and, for
an OME-XML encoded image, will actually contain the base-64 encoded
pixel data. It has the X, Y, Z, C, and T extents of the image
and it specifies the channel interleaving and channel depth.
'''
def __init__(self, node):
self.node = node
self.ns = get_namespaces(self.node)
def get_ID(self):
return self.node.get("ID")
def set_ID(self, value):
self.node.set("ID", value)
ID = property(get_ID, set_ID)
def get_DimensionOrder(self):
'''The ordering of image planes in the file
A 5-letter code indicating the ordering of pixels, from the most
rapidly varying to least. Use the DO_* constants (for instance
DO_XYZCT) to compare and set this.
'''
return self.node.get("DimensionOrder")
def set_DimensionOrder(self, value):
self.node.set("DimensionOrder", value)
DimensionOrder = property(get_DimensionOrder, set_DimensionOrder)
def get_PixelType(self):
'''The pixel bit type, for instance PT_UINT8
The pixel type specifies the datatype used to encode pixels
in the image data. You can use the PT_* constants to compare
and set the pixel type.
'''
return self.node.get("Type")
def set_PixelType(self, value):
self.node.set("Type", value)
PixelType = property(get_PixelType, set_PixelType)
def get_SizeX(self):
'''The dimensions of the image in the X direction in pixels'''
return get_int_attr(self.node, "SizeX")
def set_SizeX(self, value):
self.node.set("SizeX", str(value))
SizeX = property(get_SizeX, set_SizeX)
def get_SizeY(self):
'''The dimensions of the image in the Y direction in pixels'''
return get_int_attr(self.node, "SizeY")
def set_SizeY(self, value):
self.node.set("SizeY", str(value))
SizeY = property(get_SizeY, set_SizeY)
def get_SizeZ(self):
'''The dimensions of the image in the Z direction in pixels'''
return get_int_attr(self.node, "SizeZ")
def set_SizeZ(self, value):
self.node.set("SizeZ", str(value))
SizeZ = property(get_SizeZ, set_SizeZ)
def get_SizeT(self):
'''The dimensions of the image in the T direction in pixels'''
return get_int_attr(self.node, "SizeT")
def set_SizeT(self, value):
self.node.set("SizeT", str(value))
SizeT = property(get_SizeT, set_SizeT)
def get_SizeC(self):
'''The dimensions of the image in the C direction in pixels'''
return get_int_attr(self.node, "SizeC")
def set_SizeC(self, value):
self.node.set("SizeC", str(value))
SizeC = property(get_SizeC, set_SizeC)
def get_channel_count(self):
'''The number of channels in the image
You can change the number of channels in the image by
setting the channel_count:
pixels.channel_count = 3
pixels.Channel(0).Name = "Red"
...
'''
return len(self.node.findall(qn(self.ns['ome'], "Channel")))
def set_channel_count(self, value):
assert value > 0
channel_count = self.channel_count
if channel_count > value:
channels = self.node.findall(qn(self.ns['ome'], "Channel"))
for channel in channels[value:]:
self.node.remove(channel)
else:
for _ in range(channel_count, value):
new_channel = OMEXML.Channel(
ElementTree.SubElement(self.node, qn(self.ns['ome'], "Channel")))
new_channel.ID = str(uuid.uuid4())
new_channel.Name = new_channel.ID
new_channel.SamplesPerPixel = 1
channel_count = property(get_channel_count, set_channel_count)
def Channel(self, index=0):
'''Get the indexed channel from the Pixels element'''
channel = self.node.findall(qn(self.ns['ome'], "Channel"))[index]
return OMEXML.Channel(channel)
def get_plane_count(self):
'''The number of planes in the image
An image with only one plane or an interleaved color plane will
often not have any planes.
You can change the number of planes in the image by
setting the plane_count:
pixels.plane_count = 3
pixels.Plane(0).TheZ=pixels.Plane(0).TheC=pixels.Plane(0).TheT=0
...
'''
return len(self.node.findall(qn(self.ns['ome'], "Plane")))
def set_plane_count(self, value):
assert value >= 0
plane_count = self.plane_count
if plane_count > value:
planes = self.node.findall(qn(self.ns['ome'], "Plane"))
for plane in planes[value:]:
self.node.remove(plane)
else:
for _ in range(plane_count, value):
new_plane = OMEXML.Plane(
ElementTree.SubElement(self.node, qn(self.ns['ome'], "Plane")))
plane_count = property(get_plane_count, set_plane_count)
def Plane(self, index=0):
'''Get the indexed plane from the Pixels element'''
plane = self.node.findall(qn(self.ns['ome'], "Plane"))[index]
return OMEXML.Plane(plane)
class StructuredAnnotations(dict):
'''The OME/StructuredAnnotations element
Structured annotations let OME-XML represent metadata from other file
formats, for example the tag metadata in TIFF files. The
StructuredAnnotations element is a container for the structured
annotations.
Images can have structured annotation references. These match to
the IDs of structured annotations in the StructuredAnnotations
element. You can get the structured annotations in an OME-XML document
using a dictionary interface to StructuredAnnotations.
Pragmatically, TIFF tag metadata is stored as key/value pairs in
OriginalMetadata annotations - in the context of CellProfiler,
callers will be using these to read tag data that's not represented
in OME-XML such as the bits per sample and min and max sample values.
'''
def __init__(self, node):
self.node = node
self.ns = get_namespaces(self.node)
def __getitem__(self, key):
for child in self.node:
if child.get("ID") == key:
return child
raise IndexError('ID "%s" not found' % key)
def __contains__(self, key):
return self.has_key(key)
def keys(self):
return filter(lambda x: x is not None,
[child.get("ID") for child in self.node])
def has_key(self, key):
for child in self.node:
if child.get("ID") == key:
return True
return False
def add_original_metadata(self, key, value):
'''Create an original data key/value pair
key - the original metadata's key name, for instance OM_PHOTOMETRIC_INTERPRETATION
value - the value, for instance, "RGB"
returns the ID for the structured annotation.
'''
xml_annotation = ElementTree.SubElement(
self.node, qn(self.ns['sa'], "XMLAnnotation"))
node_id = str(uuid.uuid4())
xml_annotation.set("ID", node_id)
xa_value = ElementTree.SubElement(xml_annotation, qn(self.ns['sa'], "Value"))
ov = ElementTree.SubElement(
xa_value, qn(NS_ORIGINAL_METADATA, "OriginalMetadata"))
ov_key = ElementTree.SubElement(ov, qn(NS_ORIGINAL_METADATA, "Key"))
set_text(ov_key, key)
ov_value = ElementTree.SubElement(
ov, qn(NS_ORIGINAL_METADATA, "Value"))
set_text(ov_value, value)
return node_id
def iter_original_metadata(self):
'''An iterator over the original metadata in structured annotations
returns (<annotation ID>, (<key, value>))
where <annotation ID> is the ID attribute of the annotation (which
can be used to tie an annotation to an image)
<key> is the original metadata key, typically one of the
OM_* names of a TIFF tag
<value> is the value for the metadata
'''
#
# Here's the XML we're traversing:
#
# <StructuredAnnotations>
# <XMLAnnotation>
# <Value>
# <OriginalMetadta>
# <Key>Foo</Key>
# <Value>Bar</Value>
# </OriginalMetadata>
# </Value>
# </XMLAnnotation>
# </StructuredAnnotations>
#
for annotation_node in self.node.findall(qn(self.ns['sa'], "XMLAnnotation")):
# <XMLAnnotation/>
annotation_id = annotation_node.get("ID")
for xa_value_node in annotation_node.findall(qn(self.ns['sa'], "Value")):
# <Value/>
for om_node in xa_value_node.findall(
qn(NS_ORIGINAL_METADATA, "OriginalMetadata")):
# <OriginalMetadata>
key_node = om_node.find(qn(NS_ORIGINAL_METADATA, "Key"))
value_node = om_node.find(qn(NS_ORIGINAL_METADATA, "Value"))
if key_node is not None and value_node is not None:
key_text = get_text(key_node)
value_text = get_text(value_node)
if key_text is not None and value_text is not None:
yield annotation_id, (key_text, value_text)
else:
logger.warn("Original metadata was missing key or value:" + om_node.toxml())
return
def has_original_metadata(self, key):
'''True if there is an original metadata item with the given key'''
return any([k == key
for annotation_id, (k, v)
in self.iter_original_metadata()])
def get_original_metadata_value(self, key, default=None):
'''Return the value for a particular original metadata key
key - key to search for
default - default value to return if not found
'''
for annotation_id, (k, v) in self.iter_original_metadata():
if k == key:
return v
return default
def get_original_metadata_refs(self, ids):
'''For a given ID, get the matching original metadata references
ids - collection of IDs to match
returns a dictionary of key to value
'''
d = {}
for annotation_id, (k,v) in self.iter_original_metadata():
if annotation_id in ids:
d[k] = v
return d
@property
def OriginalMetadata(self):
return OMEXML.OriginalMetadata(self)
class OriginalMetadata(dict):
'''View original metadata as a dictionary
Original metadata holds "vendor-specific" metadata including TIFF
tag values.
'''
def __init__(self, sa):
'''Initialized with the structured_annotations class instance'''
self.sa = sa
def __getitem__(self, key):
return self.sa.get_original_metadata_value(key)
def __setitem__(self, key, value):
self.sa.add_original_metadata(key, value)
def __contains__(self, key):
return self.has_key(key)
def __iter__(self):
for annotation_id, (key, value) in self.sa.iter_original_metadata():
yield key
def __len__(self):
return len(list(self.sa_iter_original_metadata()))
def keys(self):
return [key
for annotation_id, (key, value)
in self.sa.iter_original_metadata()]
def has_key(self, key):
for annotation_id, (k, value) in self.sa.iter_original_metadata():
if k == key:
return True
return False
def iteritems(self):
for annotation_id, (key, value) in self.sa.iter_original_metadata():
yield key, value
class PlatesDucktype(object):
'''It looks like a list of plates'''
def __init__(self, root):
self.root = root
self.ns = get_namespaces(self.root)
def __getitem__(self, key):
plates = self.root.findall(qn(self.ns['spw'], "Plate"))
if isinstance(key, slice):
return [OMEXML.Plate(plate) for plate in plates[key]]
return OMEXML.Plate(plates[key])
def __len__(self):
return len(self.root.findall(qn(self.ns['spw'], "Plate")))
def __iter__(self):
for plate in self.root.iterfind(qn(self.ns['spw'], "Plate")):
yield OMEXML.Plate(plate)
def newPlate(self, name, plate_id = str(uuid.uuid4())):
new_plate_node = ElementTree.SubElement(
self.root, qn(self.ns['spw'], "Plate"))
new_plate = OMEXML.Plate(new_plate_node)
new_plate.ID = plate_id
new_plate.Name = name
return new_plate
class Plate(object):
'''The SPW:Plate element
This represents the plate element of the SPW schema:
http://www.openmicroscopy.org/Schemas/SPW/2007-06/
'''
def __init__(self, node):
self.node = node
self.ns = get_namespaces(self.node)
def get_ID(self):
return self.node.get("ID")
def set_ID(self, value):
self.node.set("ID", value)
ID = property(get_ID, set_ID)
def get_Name(self):
return self.node.get("Name")
def set_Name(self, value):
self.node.set("Name", value)
Name = property(get_Name, set_Name)
def get_Status(self):
return self.node.get("Status")
def set_Status(self, value):
self.node.set("Status", value)
Status = property(get_Status, set_Status)
def get_ExternalIdentifier(self):
return self.node.get("ExternalIdentifier")
def set_ExternalIdentifier(self, value):
return self.node.set("ExternalIdentifier", value)
ExternalIdentifier = property(get_ExternalIdentifier, set_ExternalIdentifier)
def get_ColumnNamingConvention(self):
# Consider a default if not defined of NC_NUMBER
return self.node.get("ColumnNamingConvention")
def set_ColumnNamingConvention(self, value):
assert value in (NC_LETTER, NC_NUMBER)
self.node.set("ColumnNamingConvention", value)
ColumnNamingConvention = property(get_ColumnNamingConvention,
set_ColumnNamingConvention)
def get_RowNamingConvention(self):
# Consider a default if not defined of NC_LETTER
return self.node.get("RowNamingConvention")
def set_RowNamingConvention(self, value):
assert value in (NC_LETTER, NC_NUMBER)
self.node.set("RowNamingConvention", value)
RowNamingConvention = property(get_RowNamingConvention,
set_RowNamingConvention)
def get_WellOriginX(self):
return get_float_attr(self.node, "WellOriginX")
def set_WellOriginX(self, value):
self.node.set("WellOriginX", str(value))
WellOriginX = property(get_WellOriginX, set_WellOriginX)
def get_WellOriginY(self):
return get_float_attr(self.node, "WellOriginY")
def set_WellOriginY(self, value):
self.node.set("WellOriginY", str(value))
WellOriginY = property(get_WellOriginY, set_WellOriginY)
def get_Rows(self):
return get_int_attr(self.node, "Rows")
def set_Rows(self, value):
self.node.set("Rows", str(value))
Rows = property(get_Rows, set_Rows)
def get_Columns(self):
return get_int_attr(self.node, "Columns")
def set_Columns(self, value):
self.node.set("Columns", str(value))
Columns = property(get_Columns, set_Columns)
def get_Description(self):
description = self.node.find(qn(self.ns['spw'], "Description"))
if description is None:
return None
return get_text(description)
def set_Description(self, text):
make_text_node(self.node, NS_SPW, "Description", test)
Description = property(get_Description, set_Description)
def get_Well(self):
'''The well dictionary / list'''
return OMEXML.WellsDucktype(self)
Well = property(get_Well)
def get_well_name(self, well):
'''Get a well's name, using the row and column convention'''
result = "".join([
"%02d" % (i+1) if convention == NC_NUMBER
else "ABCDEFGHIJKLMNOP"[i]
for i, convention
in ((well.Row, self.RowNamingConvention or NC_LETTER),
(well.Column, self.ColumnNamingConvention or NC_NUMBER))])
return result
class WellsDucktype(dict):
'''The WellsDucktype lets you retrieve and create wells
The WellsDucktype looks like a dictionary but lets you reference
the wells in a plate using indexing. Types of indexes:
list indexing: e.g. plate.Well[14] gets the 14th well as it appears
in the XML
dictionary_indexing:
by well name - e.g. plate.Well["A08"]
by row and column - e.g. plate.Well[1,3] (B03)
by ID - e.g. plate.Well["Well:0:0:0"]
If the ducktype is unable to parse a well name, it assumes you're
using an ID.
'''
def __init__(self, plate):
self.plate_node = plate.node
self.plate = plate
self.ns = get_namespaces(self.plate_node)
def __len__(self):
return len(self.plate_node.findall(qn(self.ns['spw'], "Well")))
def __getitem__(self, key):
all_wells = self.plate_node.findall(qn(self.ns['spw'], "Well"))
if isinstance(key, slice):
return [OMEXML.Well(w) for w in all_wells[key]]
if hasattr(key, "__len__") and len(key) == 2:
well = OMEXML.Well(None)
for w in all_wells:
well.node = w
if well.Row == key[0] and well.Column == key[1]:
return well
if isinstance(key, int):
return OMEXML.Well(all_wells[key])
well = OMEXML.Well(None)
for w in all_wells:
well.node = w
if self.plate.get_well_name(well) == key:
return well
if well.ID == key:
return well
return None
def __iter__(self):
'''Return the standard name for all wells on the plate
for instance, 'B03' for a well with Row=1, Column=2 for a plate
with the standard row and column naming convention
'''
all_wells = self.plate_node.findall(qn(self.ns['spw'], "Well"))
well = OMEXML.Well(None)
for w in all_wells:
well.node = w
yield self.plate.get_well_name(well)
def new(self, row, column, well_id = str(uuid.uuid4())):
'''Create a new well at the given row and column
row - index of well's row
column - index of well's column
well_id - the ID attribute for the well
'''
well_node = ElementTree.SubElement(
self.plate_node, qn(self.ns['spw'], "Well"))
well = OMEXML.Well(well_node)
well.Row = row
well.Column = column
well.ID = well_id
return well
class Well(object):
def __init__(self, node):
self.node = node
def get_Column(self):
return get_int_attr(self.node, "Column")
def set_Column(self, value):
self.node.set("Column", str(value))
Column = property(get_Column, set_Column)
def get_Row(self):
return get_int_attr(self.node, "Row")
def set_Row(self, value):
self.node.set("Row", str(value))
Row = property(get_Row, set_Row)
def get_ID(self):
return self.node.get("ID")
def set_ID(self, value):
self.node.set("ID", value)
ID = property(get_ID, set_ID)
def get_Sample(self):
return OMEXML.WellSampleDucktype(self.node)
Sample = property(get_Sample)
def get_ExternalDescription(self):
return self.node.get("ExternalDescription")
def set_ExternalDescription(self, value):
return self.node.set("ExternalDescription", value)
ExternalDescription = property(get_ExternalDescription, set_ExternalDescription)
def get_ExternalIdentifier(self):
return self.node.get("ExternalIdentifier")
def set_ExternalIdentifier(self, value):
return self.node.set("ExternalIdentifier", value)
ExternalIdentifier = property(get_ExternalIdentifier, set_ExternalIdentifier)
def get_Color(self):
return int(self.node.get("Color"))
def set_Color(self, value):
self.node.set("Color", str(value))
class WellSampleDucktype(list):
'''The WellSample elements in a well
This is made to look like an indexable list so that you can do
things like:
wellsamples[0:2]
'''
def __init__(self, well_node):
self.well_node = well_node
self.ns = get_namespaces(self.well_node)
def __len__(self):
return len(self.well_node.findall(qn(self.ns['spw'], "WellSample")))
def __getitem__(self, key):
all_samples = self.well_node.findall(qn(self.ns['spw'], "WellSample"))
if isinstance(key, slice):
return [OMEXML.WellSample(s)
for s in all_samples[key]]
return OMEXML.WellSample(all_samples[int(key)])
def __iter__(self):
'''Iterate through the well samples.'''
all_samples = self.well_node.findall(qn(self.ns['spw'], "WellSample"))
for s in all_samples:
yield OMEXML.WellSample(s)
def new(self, wellsample_id = str(uuid.uuid4()), index = None):
'''Create a new well sample
'''
if index is None:
index = reduce(max, [s.Index for s in self], -1) + 1
new_node = ElementTree.SubElement(
self.well_node, qn(self.ns['spw'], "WellSample"))
s = OMEXML.WellSample(new_node)
s.ID = wellsample_id
s.Index = index
class WellSample(object):
'''The WellSample is a location within a well'''
def __init__(self, node):
self.node = node
self.ns = get_namespaces(self.node)
def get_ID(self):
return self.node.get("ID")
def set_ID(self, value):
self.node.set("ID", value)
ID = property(get_ID, set_ID)
def get_PositionX(self):
return get_float_attr(self.node, "PositionX")
def set_PositionX(self, value):
self.node.set("PositionX", str(value))
PositionX = property(get_PositionX, set_PositionX)
def get_PositionY(self):
return get_float_attr(self.node, "PositionY")
def set_PositionY(self, value):
self.node.set("PositionY", str(value))
PositionY = property(get_PositionY, set_PositionY)
def get_Timepoint(self):
return self.node.get("Timepoint")
def set_Timepoint(self, value):
if isinstance(value, datetime.datetime):
value = value.isoformat()
self.node.set("Timepoint", value)
Timepoint = property(get_Timepoint, set_Timepoint)
def get_Index(self):
return get_int_attr(self.node, "Index")
def set_Index(self, value):
self.node.set("Index", str(value))
Index = property(get_Index, set_Index)
def get_ImageRef(self):
'''Get the ID of the image of this site'''
ref = self.node.find(qn(self.ns['spw'], "ImageRef"))
if ref is None:
return None
return ref.get("ID")
def set_ImageRef(self, value):
'''Add a reference to the image of this site'''
ref = self.node.find(qn(self.ns['spw'], "ImageRef"))
if ref is None:
ref = ElementTree.SubElement(self.node, qn(self.ns['spw'], "ImageRef"))
ref.set("ID", value)
ImageRef = property(get_ImageRef, set_ImageRef)