classdef OMETiffWriter_gy < handle %OMETiffWriter_gy Utility for writing OME-TIFF files %Gary Yellen, 2014-10-31 % First call the Constructor: % OTW = OMETiffWriter_gy(filename, experimentType, experimentDescription) % Then all the metadata must be defined before any writing occurs: % OTW.addImageAnnotation(type,description,value) % these are collected and later added to all the image series % type = 'Comment', 'Double', 'Timestamp', 'Boolean', 'XML' % other non-image annotations can be added with OTW.addAnnotation % but then the annotation refs must be done manually % by reference to OTW.metadata % Now define the Image series, one at a time: % OTW.defineSeries(acqdate, imageDescription, dimOrder, sizeXYZCT, ... % datatype, frameDeltaT) % acqdate in a valid MATLAB datestring format (or with a T separating date and time) % dimOrder as a string: 'XYCTZ', 'XYTCZ' etc. % sizeXYZCT is a 1-D array of sizes, always in XYZCT order % datatype is 'double', 'float', 'uint16', etc. % frameDeltaT: if non-zero, the time offset between frames % (channels assumed simultaneous; only XYCTZ or XYCZT order) % After each series is defined, other metadata can be set manually % e.g. OTW.metadata.setImageInstrumentRef, etc. % Channels can be specified by: % OTW.defineChannel(chname, series, channelNumber) % and channel metadata can be set manually % Metadata are finalized using the following 3 commands in order: % OTW.finalizeImageAnnotations() % - adds refs to all series for all annots added using addImageAnnotation % OTW.defineModuloAlongT(series, start, step, stop) [all in ns] % - used for FLIMfit. Must be done last! % OTW.finalizeMetadata() % % DATA WRITING: image planes for all images, in correct order, are % added by invoking: % OTW.writePlane(data) % - data must be in correct format (uint16, double) % If multiple image sets are used with different formats or sizes, use % OTW.writePlane(data,firstInSet) % - firstInSet is true when the format changes, otherwise 0. % % FINALLY: close the Tiff writer with OTW.close % properties (SetAccess = private) filename experimentType experimentDescription metadata coremetadata specialNamespace % keep track of annotation numbers and tag as C0, D3, etc because % sometime annotations are created and numbered out of our control % (Modulo?) nImageAnnotations % matrix (comments, doubles, timestamps, booleans, xmls) nOtherAnnotations % matrix (comments, doubles, timestamps, booleans, xmls) nSeries nPlanes % this keeps count as the images are defined nPlanesWritten % this keeps count as the images are written OMEXMLService dimensionOrderEnumHandler experimentTypeEnumHandler pixelTypeEnumHandler tagstruct sMetadata % string version of the metadata end % properties properties (SetAccess = private, GetAccess = private) twriter % instance of the MATLAB Tiff writer end properties (Constant) enumAnnotType = struct('Comment',1, 'Double',2, 'Timestamp',3, 'Boolean',4, 'XML',5); annotTypes={'Comment','Double','Timestamp','Boolean','XML'}; end % properties (Constant) methods function otw = OMETiffWriter_gy(filename, experimentType, experimentDescription) otw.filename = filename; otw.specialNamespace = 'gydFLIM'; otw.OMEXMLService = loci.formats.services.OMEXMLServiceImpl(); otw.metadata = otw.OMEXMLService.createOMEXMLMetadata(); otw.metadata.createRoot(); otw.dimensionOrderEnumHandler = ome.xml.model.enums.handlers.DimensionOrderEnumHandler(); otw.experimentTypeEnumHandler = ome.xml.model.enums.handlers.ExperimentTypeEnumHandler(); otw.experimentType = otw.experimentTypeEnumHandler.getEnumeration(experimentType); otw.pixelTypeEnumHandler = ome.xml.model.enums.handlers.PixelTypeEnumHandler(); otw.nImageAnnotations = [0 0 0 0 0]; otw.nOtherAnnotations = [0 0 0 0 0]; otw.nPlanes = 0; otw.nPlanesWritten = 0; otw.nSeries = 0; otw.experimentDescription = experimentDescription; java.lang.System.setProperty('javax.xml.transform.TransformerFactory', ... 'com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl'); end function series = defineSeries(otw, acqdate, description, dimensionOrder, szXYZCT, datatype, frameDeltaT) series = otw.nSeries; sSeries = num2str(series); otw.metadata.setImageID(['Image:' sSeries],series); otw.metadata.setImageAcquisitionDate(otw.toTimestamp(acqdate),series); otw.metadata.setImageDescription(description,series); otw.metadata.setPixelsID(['Pixels:' sSeries], series); otw.metadata.setPixelsBinDataBigEndian (java.lang.Boolean.FALSE, series, 0); % (java.lang.Boolean.TRUE, 0, 0); NOT SURE ABOUT THIRD PARAMETER USAGE if series==0 % first series gets linked to the experiment (?) otw.metadata.setExperimentType(otw.experimentType,series); otw.metadata.setExperimentID('Experiment:0', 0); % number is ExperimenterIndex otw.metadata.setExperimentDescription(otw.experimentDescription, series); end otw.metadata.setImageExperimentRef('Experiment:0', series); otw.metadata.setPixelsType(otw.pixelTypeEnumHandler.getEnumeration(datatype), series); otw.metadata.setPixelsDimensionOrder(otw.dimensionOrderEnumHandler.getEnumeration(dimensionOrder), series); % utility functions toInt = @(x) ome.xml.model.primitives.PositiveInteger(java.lang.Integer(x)); toNNInt = @(x) ome.xml.model.primitives.NonNegativeInteger(java.lang.Integer(x)); % set dimension sizes otw.metadata.setPixelsSizeX(toInt(szXYZCT(1)), series); otw.metadata.setPixelsSizeY(toInt(szXYZCT(2)), series); otw.metadata.setPixelsSizeZ(toInt(szXYZCT(3)), series); otw.metadata.setPixelsSizeC(toInt(szXYZCT(4)), series); otw.metadata.setPixelsSizeT(toInt(szXYZCT(5)), series); planecount = prod(szXYZCT(3:5)); % need to create the (summary) TiffData entries ourselves plane=0; % not sure, but think this can be used otw.metadata.setTiffDataIFD(toNNInt(otw.nPlanes), series, plane); %otw.metadata.setUUIDFileName(filename, series, plane); %otw.metadata.setUUIDValue(uuid, series, plane); otw.metadata.setTiffDataPlaneCount(toNNInt(planecount), series, plane); otw.nPlanes = otw.nPlanes + planecount; if frameDeltaT>0 % only write Plane entries if frameDeltaT>0 % only use this if the dimension order is XYCTZ or XYCZT nChannels = szXYZCT(4); for k=0:planecount-1 channel = mod(k,nChannels); timept = floor(k/nChannels); % increments after each channel set switch dimensionOrder case 'XYCTZ' otw.metadata.setPlaneTheT(toNNInt(mod(timept,szXYZCT(5))),series,k); otw.metadata.setPlaneTheZ(toNNInt(floor(timept/szXYZCT(5))),series,k); case 'XYCZT' otw.metadata.setPlaneTheT(toNNInt(floor(timept/szXYZCT(3))),series,k); otw.metadata.setPlaneTheZ(toNNInt(mod(timept,szXYZCT(3))),series,k); otherwise break; % don't write plane entries! end otw.metadata.setPlaneTheC(toNNInt(channel),series,k); otw.metadata.setPlaneDeltaT(java.lang.Double(frameDeltaT*timept),series,k); end end otw.nSeries = otw.nSeries + 1; end function defineChannel(otw, name, series, channelNumber) toInt = @(x) ome.xml.model.primitives.PositiveInteger(java.lang.Integer(x)); otw.metadata.setChannelID(['Channel:' num2str(series) num2str(channelNumber)], series, channelNumber); otw.metadata.setChannelName(name, series, channelNumber); otw.metadata.setChannelSamplesPerPixel(toInt(1), series, channelNumber); end function finalizeImageAnnotations(otw) % attach the collected image annotations to every series for series=0:otw.nSeries-1 for type=1:5 % all the different types atype = OMETiffWriter_gy.annotTypes{type}; atag = atype(1); % first letter of the type for k=0:otw.nImageAnnotations(type)-1 otw.metadata.setImageAnnotationRef(['Annotation:' atag num2str(k)], series, k); end end end end function finalizeMetadata(otw) % now get the metadata string otw.sMetadata = otw.OMEXMLService.getOMEXML(otw.metadata); end function writePlane(otw,data,varargin) % optional argument: firstInSet (T/F), only needed after the % first series. if ~isempty(varargin) firstInSet = varargin{1}; else firstInSet = 0; end % the data must already be in the correct format (uint16, % double, etc) if otw.nPlanesWritten==0 % use the MATLAB Tiff facility otw.twriter = Tiff(otw.filename,'w8'); % write a BigTiff % some of these values are placeholders, because the order is important (!) otw.tagstruct = struct('Photometric',Tiff.Photometric.MinIsBlack, ... 'BitsPerSample', 8, 'SamplesPerPixel', 1, ... 'ImageLength', 64, 'ImageWidth', 64, ... 'RowsPerStrip', 64, ... 'PlanarConfiguration', Tiff.PlanarConfiguration.Chunky, ... 'Software', 'MATLAB', ... 'SampleFormat', 'UInt', ... 'Compression', Tiff.Compression.LZW); end if otw.nPlanesWritten==0 || firstInSet otw.tagstruct.ImageLength = size(data,1); otw.tagstruct.ImageWidth = size(data,2); otw.tagstruct.RowsPerStrip = size(data,1); dtype = class(data); switch dtype case 'double' otw.tagstruct.BitsPerSample = 64; otw.tagstruct.SampleFormat = Tiff.SampleFormat.IEEEFP; case 'single' otw.tagstruct.BitsPerSample = 32; otw.tagstruct.SampleFormat = Tiff.SampleFormat.IEEEFP; otherwise otw.tagstruct.BitsPerSample = ... % extract from the name str2double(dtype(strfind(dtype,'int')+3:end)); if dtype(1)=='u' otw.tagstruct.SampleFormat = Tiff.SampleFormat.UInt; else otw.tagstruct.SampleFormat = Tiff.SampleFormat.Int; end end %disp(['Bits changed to ' num2str(otw.tagstruct.BitsPerSample)]); end if otw.nPlanesWritten==0 % first IFD gets the XML header ts0 = otw.tagstruct; ts0.ImageDescription = char(otw.sMetadata); otw.twriter.setTag(ts0); else % all the others get a simple otw.twriter.setTag(otw.tagstruct); end otw.twriter.write(data); otw.twriter.writeDirectory(); otw.nPlanesWritten = otw.nPlanesWritten+1; end function close(otw) if ~isempty(otw.twriter) otw.twriter.close(); end otw.twriter=[]; end function idx = addImageAnnotation(otw,annType,description,value) % these will automatically get added to all the image series atype = OMETiffWriter_gy.enumAnnotType.(annType); atag = annType(1); % use the first letter to tag the annotations idx = otw.nImageAnnotations(atype); % previous number of this type switch annType case 'Comment' otw.commentAnnotation(idx, description, value); case 'Double' otw.doubleAnnotation(idx, description, value); case 'Timestamp' otw.timestampAnnotation(idx, description, value); case 'Boolean' otw.booleanAnnotation(idx, description, value); case 'XML' otw.xmlAnnotation(idx, description, value); end idx = idx + 1; % new number of this type otw.nImageAnnotations(atype) = idx; end function [idx,atag] = addAnnotation(otw,annType,description,value) % these DO NOT automatically get added: caller must add the ref atype = otw.enumAnnotType.(annType); atag = annType(1); % use the first letter to tag the annotations idx = otw.nOtherAnnotations(atype); % previous number of this type switch annType case 'Comment' otw.commentAnnotation(idx, description, value); case 'Double' otw.doubleAnnotation(idx, description, value); case 'Timestamp' otw.timestampAnnotation(idx, description, value); case 'Boolean' otw.booleanAnnotation(idx, description, value); case 'XML' otw.xmlAnnotation(idx, description, value); end idx = idx + 1; % new number of this type otw.nOtherAnnotations(atype) = idx; end function defineModuloAlongT(otw, series, startval, stepval, endval) % values in nanoseconds %% for the moduloAlongT annotation: coreMetadata = loci.formats.CoreMetadata(); % see the loci.formats.Modulo javadoc for details of what fields can be set % this will be turned into a Modulo XML annotation later coreMetadata.moduloT.start = startval; coreMetadata.moduloT.step = stepval; coreMetadata.moduloT.end = endval; coreMetadata.moduloT.type = loci.formats.FormatTools.LIFETIME; coreMetadata.moduloT.unit = 'ns'; otw.OMEXMLService.addModuloAlong(otw.metadata, coreMetadata, series); end function str = toString(otw,cellStrings) str=''; for k=1:numel(cellStrings) str = [str cellStrings{k}]; if k'],idx); end end function doubleAnnotation(otw,idx,description,value) otw.metadata.setDoubleAnnotationID(['Annotation:D' num2str(idx)],idx) otw.metadata.setDoubleAnnotationDescription(description,idx); otw.metadata.setDoubleAnnotationNamespace(otw.specialNamespace,idx); otw.metadata.setDoubleAnnotationValue(java.lang.Double(value),idx); end function timestampAnnotation(otw,idx,description,value) otw.metadata.setTimestampAnnotationID(['Annotation:T' num2str(idx)],idx) otw.metadata.setTimestampAnnotationDescription(description,idx); otw.metadata.setTimestampAnnotationNamespace(otw.specialNamespace,idx); otw.metadata.setTimestampAnnotationValue(toTimestamp(otw,value),idx); end function booleanAnnotation(otw,idx,description,value) otw.metadata.setBooleanAnnotationID(['Annotation:B' num2str(idx)],idx) otw.metadata.setBooleanAnnotationDescription(description,idx); otw.metadata.setBooleanAnnotationNamespace(otw.specialNamespace,idx); otw.metadata.setBooleanAnnotationValue(java.lang.Boolean(value),idx); end function xmlAnnotation(otw,idx,descr,value) % value is an XML DocumentImpl; convert it to a string stringOut=java.io.StringWriter; formatter=org.apache.xml.serialize.OutputFormat(value); serial=org.apache.xml.serialize.XMLSerializer(stringOut,formatter); serial.asDOMSerializer(); serial.serialize(value.getDocumentElement()); str = char(stringOut.toString); % eliminate the xml version header pos = strfind(str,'?>'); str = str(pos+3:end); otw.metadata.setXMLAnnotationID(['Annotation:X' num2str(idx)],idx) otw.metadata.setXMLAnnotationDescription(descr,idx); otw.metadata.setXMLAnnotationNamespace(otw.specialNamespace,idx); otw.metadata.setXMLAnnotationValue(str,idx); end function val = toTimestamp(otw, x) val = ome.xml.model.primitives.Timestamp(datestr(datenumT(x),'yyyy-mm-ddTHH:MM:SS.FFF')); end end % methods end function dnum = datenumT(dstr) % handles the case of datestrings in ISO8601 format % no provision for timezones, though % gy 20141029 if numel(dstr)>10 && strcmp('T',dstr(11)) dstr(11)=' '; end dnum=datenum(dstr); end