diff --git a/screening/resource/test-data/MicroscopyImageDropboxTest/C01_256x256.png b/screening/resource/test-data/MicroscopyImageDropboxTest/C01_256x256.png new file mode 100644 index 0000000000000000000000000000000000000000..64f766e96cdd8e30306eaacf593c2d2aee6be593 Binary files /dev/null and b/screening/resource/test-data/MicroscopyImageDropboxTest/C01_256x256.png differ diff --git a/screening/resource/test-data/MicroscopyImageDropboxTest/C01_512x512.png b/screening/resource/test-data/MicroscopyImageDropboxTest/C01_512x512.png new file mode 100644 index 0000000000000000000000000000000000000000..928a67adf2a62e66427878eb9d5c36816dc218a1 Binary files /dev/null and b/screening/resource/test-data/MicroscopyImageDropboxTest/C01_512x512.png differ diff --git a/screening/resource/test-data/MicroscopyImageDropboxTest/C01_Default.png b/screening/resource/test-data/MicroscopyImageDropboxTest/C01_Default.png new file mode 100644 index 0000000000000000000000000000000000000000..8eb9f9719b1c18624d51d26d21abeb258051b8e8 Binary files /dev/null and b/screening/resource/test-data/MicroscopyImageDropboxTest/C01_Default.png differ diff --git a/screening/resource/test-data/MicroscopyImageDropboxTest/C1_256x256.png b/screening/resource/test-data/MicroscopyImageDropboxTest/C1_256x256.png new file mode 100644 index 0000000000000000000000000000000000000000..606a7eec9f055c41eb7ef8b53db4d921c0ec5b62 Binary files /dev/null and b/screening/resource/test-data/MicroscopyImageDropboxTest/C1_256x256.png differ diff --git a/screening/resource/test-data/MicroscopyImageDropboxTest/C1_512x512.png b/screening/resource/test-data/MicroscopyImageDropboxTest/C1_512x512.png new file mode 100644 index 0000000000000000000000000000000000000000..759383221ff7738a2f996458b77ff5b4b773df8e Binary files /dev/null and b/screening/resource/test-data/MicroscopyImageDropboxTest/C1_512x512.png differ diff --git a/screening/resource/test-data/MicroscopyImageDropboxTest/C1_Default.png b/screening/resource/test-data/MicroscopyImageDropboxTest/C1_Default.png new file mode 100644 index 0000000000000000000000000000000000000000..23e66f27906dc14733b7503b70c3375b533ee66b Binary files /dev/null and b/screening/resource/test-data/MicroscopyImageDropboxTest/C1_Default.png differ diff --git a/screening/resource/test-data/MicroscopyImageDropboxTest/Merged_256x256.png b/screening/resource/test-data/MicroscopyImageDropboxTest/Merged_256x256.png new file mode 100644 index 0000000000000000000000000000000000000000..2720997e003ca22d9f7bb25262296beaf24d2270 Binary files /dev/null and b/screening/resource/test-data/MicroscopyImageDropboxTest/Merged_256x256.png differ diff --git a/screening/resource/test-data/MicroscopyImageDropboxTest/Merged_512x512.png b/screening/resource/test-data/MicroscopyImageDropboxTest/Merged_512x512.png new file mode 100644 index 0000000000000000000000000000000000000000..2eabfbfef7400e116804337e26ce5bbf375f7c3c Binary files /dev/null and b/screening/resource/test-data/MicroscopyImageDropboxTest/Merged_512x512.png differ diff --git a/screening/resource/test-data/MicroscopyImageDropboxTest/Merged_Default.png b/screening/resource/test-data/MicroscopyImageDropboxTest/Merged_Default.png new file mode 100644 index 0000000000000000000000000000000000000000..ef6a273c888eeaf379ffbfa4643054577ee83c9d Binary files /dev/null and b/screening/resource/test-data/MicroscopyImageDropboxTest/Merged_Default.png differ diff --git a/screening/resource/test-data/MicroscopyImageDropboxTest/aarons_example/pontia/Experiment/Experiment_properties.oix b/screening/resource/test-data/MicroscopyImageDropboxTest/aarons_example/pontia/Experiment/Experiment_properties.oix new file mode 100755 index 0000000000000000000000000000000000000000..cb61c432b3edb182a53360f10881caee7a143a15 --- /dev/null +++ b/screening/resource/test-data/MicroscopyImageDropboxTest/aarons_example/pontia/Experiment/Experiment_properties.oix @@ -0,0 +1,9 @@ +<?xml version="1.0" encoding="UTF-8" standalone="no"?> +<obitXML version="1"> + <Experiment description="This is an ND2 file with one series." name="Test_Experiment" + openBISIdentifier="/TEST/TEST-PROJECT/MICROSCOPY_IMAGE_DROPBOX_TEST" openBISSpaceIdentifier="/TEST"> + <MicroscopyFile fileSize="2.8 MiB" name="example.nd2" + openBISExperimentIdentifier="/TEST/TEST-PROJECT/MICROSCOPY_IMAGE_DROPBOX_TEST" + openBISSpaceIdentifier="/TEST" relativeFileName="pontia/Experiment/example.nd2"/> + </Experiment> +</obitXML> \ No newline at end of file diff --git a/screening/resource/test-data/MicroscopyImageDropboxTest/aarons_example/pontia/Experiment/example.nd2 b/screening/resource/test-data/MicroscopyImageDropboxTest/aarons_example/pontia/Experiment/example.nd2 new file mode 100644 index 0000000000000000000000000000000000000000..9d014d17219caaae04d8d7e2b252442f92aa9d5f Binary files /dev/null and b/screening/resource/test-data/MicroscopyImageDropboxTest/aarons_example/pontia/Experiment/example.nd2 differ diff --git a/screening/resource/test-data/MicroscopyImageDropboxTest/aarons_example/pontia/data_structure.ois b/screening/resource/test-data/MicroscopyImageDropboxTest/aarons_example/pontia/data_structure.ois new file mode 100755 index 0000000000000000000000000000000000000000..b7a04663bf01671a0097f517df0adef2ff971334 --- /dev/null +++ b/screening/resource/test-data/MicroscopyImageDropboxTest/aarons_example/pontia/data_structure.ois @@ -0,0 +1 @@ +pontia/Experiment/Experiment_properties.oix diff --git a/screening/sourceTest/core-plugins/MicroscopyImageDropboxTest/1/dss/drop-boxes/MicroscopyImageDropboxTest-drop-box/BioFormatsProcessor.py b/screening/sourceTest/core-plugins/MicroscopyImageDropboxTest/1/dss/drop-boxes/MicroscopyImageDropboxTest-drop-box/BioFormatsProcessor.py new file mode 100644 index 0000000000000000000000000000000000000000..569644f4d566b99f01cd1899d8c0dc6ccc81e2b9 --- /dev/null +++ b/screening/sourceTest/core-plugins/MicroscopyImageDropboxTest/1/dss/drop-boxes/MicroscopyImageDropboxTest-drop-box/BioFormatsProcessor.py @@ -0,0 +1,418 @@ +""" +Created on Feb 6, 2014 + +@author: Aaron Ponti +""" + +from loci.formats import FormatTools +from loci.formats import ChannelSeparator +from loci.formats import ChannelFiller +from loci.formats import MetadataTools + +class BioFormatsProcessor: + """The BioFormatsProcessor class scans a file using the bio-formats library and + extracts relevant metadata information for registration.""" + + # File path + _filePath = None + + # LOCI reader object + _reader = None + + # OME metadata store object + _metadataStore = None + + # Number of series in the file + _nSeries = 0 + + # Array of metadata disctionaries (one per series) + _metadata = [] + + + def __init__(self, filePath): + # Microscopy file path + self._filePath = filePath + + # Initialize the LOCI reader + self._reader = ChannelSeparator(ChannelFiller()) + self._reader.setMetadataStore(MetadataTools.createOMEXMLMetadata()) + + # Set the filename + self._reader.setId(self._filePath) + + # Store a reference to the OME metadata store + self._metadataStore = self._reader.getMetadataStore() + + # Initialize the metadata structure + self._initMetadata() + + + def extractMetadata(self): + """Extract and store needed metadata""" + + # Dataset sizes + self._getDatasetSizes() + + # Data types + self._getDataTypes() + + # Voxel sizes + self._getVoxelSizes() + + # Acquisition dates + self._getAcquisitionDates() + + # Mark thumbnails + self._markThumbnails() + + # Channel names + self._getChannelNames() + + # Channel colors + self._getChannelColors() + + # Get wavelengths + self._getWavelengths() + + # Get time stamps + self._getTimeStamps() + + + def getMetadata(self): + """ + Return the extracted metadata. + """ + + return self._metadata + + + def getNumSeries(self): + """ + Return the number of series in file. + """ + + return self._nSeries + + + # # # + # # # PRIVATE METHODS + # # # + + def _initMetadata(self): + + # Initialize metadata dictionary + metadata = {'seriesNumber' : 0, + 'nImages' : 0, + 'sizeX' : 0, + 'sizeY' : 0, + 'sizeZ' : 0, + 'sizeC' : 0, + 'sizeT' : 0, + 'voxelSizeX' : 0, + 'voxelSizeY' : 0, + 'voxelSizeZ' : 0, + 'datatype' : None, + 'isLittleEndian' : False, + 'isSigned' : False, + 'isThumbnail' : False, + 'acquisitionDate' : '', + 'channelNames' : [], + 'channelColors' : [], + 'emWavelengths' : [], + 'exWavelengths' : [], + 'NumericalAperture' : 0, + 'timestamps' : []} + + # Get and store the number of series + self._nSeries = self._reader.getSeriesCount() + + # Initialize metadata dictionaries (one per series) + n = 0 + while n < self._nSeries: + self._metadata.append(metadata.copy()) + n += 1 + + + def _getAcquisitionDates(self): + """ + Extract the acquisition dates from all series. + """ + + # Get and store all acquisition dates + for n in range(self._nSeries): + + # Change to current series + self._reader.setSeries(n) + + acqDate = self._metadataStore.getImageAcquisitionDate(n) + if acqDate is None: + acqDate = '' + else: + acqDate = acqDate.getValue() + + self._metadata[n]['acquisitionDate'] = acqDate + + + def _getChannelColors(self): + """ + Extracts the colors as RGBA vectors of all channels from all series. + """ + + # Get and store all channel names + for n in range(self._nSeries): + + # Initialize + self._metadata[n]['channelColors'] = [] + + # Change to series + self._reader.setSeries(n) + + # Get the number of channels in this series + nChannels = self._reader.getSizeC() + + for ch in range(nChannels): + + # Get the channel color + color = self._metadataStore.getChannelColor(n, ch) + if color is None: + colorsRGBA = [255, 255, 255, 255] + else: + colorsRGBA = [color.getRed(), color.getGreen(), + color.getBlue(), color.getAlpha()] + + # Store color for current channel in current series + self._metadata[n]['channelColors'].append(colorsRGBA) + + + def _getChannelNames(self): + """ + Extracts the names of all channels from all series. + """ + + # Get and store all channel names + for n in range(self._nSeries): + + # Initialize + self._metadata[n]['channelNames'] = [] + + # Change to series + self._reader.setSeries(n) + + # Get the number of channels in this series + nChannels = self._reader.getSizeC() + + for ch in range(nChannels): + + # Get channel name + name = self._metadataStore.getChannelName(n, ch) + if name is None: + name = "No name" + + # Remove 0-byte at the end of the string if present + if name.endswith('\x00'): + name = name[:-1] + + # Store name for current channel in current series + self._metadata[n]['channelNames'].append(name) + + + def _getDatasetSizes(self): + """ + Extract dataset sizes for all series. + """ + + # Get and store all dataset sizes for all series + for n in range(self._nSeries): + + # Change to current series + self._reader.setSeries(n) + + # Get sizes + self._metadata[n]['nImages'] = self._reader.getImageCount(); + self._metadata[n]['sizeX'] = self._reader.getSizeX(); + self._metadata[n]['sizeY'] = self._reader.getSizeY(); + self._metadata[n]['sizeZ'] = self._reader.getSizeZ(); + self._metadata[n]['sizeC'] = self._reader.getSizeC(); + self._metadata[n]['sizeT'] = self._reader.getSizeT(); + + + def _getDataTypes(self): + """ + Get data types for all series. + """ + + # Get and store all dataset sizes for all series + for n in range(self._nSeries): + + # Change to current series + self._reader.setSeries(n) + + # Get the pixel type + pixelType = self._reader.getPixelType() + + # Bytes per pixel + BytesPerPixel = FormatTools.getBytesPerPixel(pixelType) + if BytesPerPixel == 1: + datatype = 'uint8' + elif BytesPerPixel == 2: + datatype = 'uint16' + elif BytesPerPixel == 4: + # This is 32-bit floating point + datatype = 'single' + else: + datatype = "unsupported" + + # Is the data type signed? + isSigned = FormatTools.isSigned(pixelType) + + # Endianity + isLittleEndian = self._reader.isLittleEndian() + + # Store data type information + self._metadata[n]['datatype'] = datatype + self._metadata[n]['isLittleEndian'] = isLittleEndian + self._metadata[n]['isSigned'] = isSigned + + + def _getNAs(self): + """ + Get the numerical aperture for all series. + """ + + # Get and store all numerical apertures + for n in range(self._nSeries): + + # Initialize + self._metadata[n]['NumericalAperture'] = [] + + # Change to series + self._reader.setSeries(n) + + # Get the number of instruments and objectives for this series + # Expected: 1 and 1. + nInstr = self._metadataStore.getInstrumentCount() + nObj = self._metadataStore.getObjectiveCount() + + for i in range(nInstr): + for o in range(nObj): + NA = self._metadataStore.getObjectiveLensNA(i, o) + self._metadata[n]['NumericalAperture'].append(NA) + + + def _getTimeStamps(self): + """ + Extracts the timestamps from all series. + """ + + # Get and store all channel names + for n in range(self._nSeries): + + # Initialize + self._metadata[n]['timestamps'] = [] + + # Change to series + self._reader.setSeries(n) + + # Number of time stamps to retrieve + nTimepoints = self._reader.getSizeT() + + # Iterate over the keys and get the values + for tp in range(nTimepoints): + + option = 'timestamp ' + str(tp) + t = self._reader.getSeriesMetadataValue(option) + if t is None: + t = "NaN" + + # Store timestamps for current series + self._metadata[n]['timestamps'].append(t) + + + def _getVoxelSizes(self): + """ + Extract voxel sizes for all series. + """ + + # Get and store all voxel sizes for all series + for n in range(self._nSeries): + + # Change to current series + self._reader.setSeries(n) + + # Voxel size X + voxelX = self._metadataStore.getPixelsPhysicalSizeX(n) + if voxelX is None: + voxelX = 0; + else: + voxelX = voxelX.getValue() + + self._metadata[n]['voxelSizeX'] = voxelX + + # Voxel size Y + voxelY = self._metadataStore.getPixelsPhysicalSizeY(n) + if voxelY is None: + voxelY = 0; + else: + voxelY = voxelY.getValue() + + self._metadata[n]['voxelSizeY'] = voxelY + + # Voxel size Z + voxelZ = self._metadataStore.getPixelsPhysicalSizeZ(n) + if voxelZ is None: + voxelZ = 0; + else: + voxelZ = voxelZ.getValue() + + self._metadata[n]['voxelSizeZ'] = voxelZ + + + def _getWavelengths(self): + """ + Extracts the excitation and emission wavelengths of all channels + from all series. + """ + + # Get and store all channel names + for n in range(self._nSeries): + + # Initialize + self._metadata[n]['exWavelengths'] = [] + self._metadata[n]['emWavelengths'] = [] + + # Change to series + self._reader.setSeries(n) + + # Get the number of channels in this series + nChannels = self._reader.getSizeC() + + for ch in range(nChannels): + + # Get and store emission wavelength for current channel in + # current series + em = self._metadataStore.getChannelEmissionWavelength(n, ch) + if em is None: + em = "NaN" + self._metadata[n]['emWavelengths'].append(em) + + # Get and store excitation wavelength for current channel in + # current series + ex = self._metadataStore.getChannelExcitationWavelength(n, ch) + if ex is None: + ex = "NaN" + self._metadata[n]['exWavelengths'].append(ex) + + + def _markThumbnails(self): + """ + Mark which series contain thumbnails. + """ + + # Mark all series whether they are thumbnails or not + for n in range(self._nSeries): + + # Change to current series + self._reader.setSeries(n) + + # Check if thumbnail + self._metadata[n]['isThumbnail'] = self._reader.isThumbnailSeries() diff --git a/screening/sourceTest/core-plugins/MicroscopyImageDropboxTest/1/dss/drop-boxes/MicroscopyImageDropboxTest-drop-box/MicroscopyDropbox.py b/screening/sourceTest/core-plugins/MicroscopyImageDropboxTest/1/dss/drop-boxes/MicroscopyImageDropboxTest-drop-box/MicroscopyDropbox.py new file mode 100644 index 0000000000000000000000000000000000000000..282a4271fc79e50c8471b5562ebaec2e8a1a478b --- /dev/null +++ b/screening/sourceTest/core-plugins/MicroscopyImageDropboxTest/1/dss/drop-boxes/MicroscopyImageDropboxTest-drop-box/MicroscopyDropbox.py @@ -0,0 +1,22 @@ +""" +@author: Aaron Ponti +""" + +import os +import logging +import re + +from Processor import Processor + + +def process(transaction): + """Dropbox entry point. + + @param transaction, the transaction object + """ + + # Create a Processor + processor = Processor(transaction) + + # Run + processor.run() diff --git a/screening/sourceTest/core-plugins/MicroscopyImageDropboxTest/1/dss/drop-boxes/MicroscopyImageDropboxTest-drop-box/MicroscopySingleDatasetConfig.py b/screening/sourceTest/core-plugins/MicroscopyImageDropboxTest/1/dss/drop-boxes/MicroscopyImageDropboxTest-drop-box/MicroscopySingleDatasetConfig.py new file mode 100644 index 0000000000000000000000000000000000000000..9f7a3988de02caac79e3e6475e950f6a6b63b200 --- /dev/null +++ b/screening/sourceTest/core-plugins/MicroscopyImageDropboxTest/1/dss/drop-boxes/MicroscopyImageDropboxTest-drop-box/MicroscopySingleDatasetConfig.py @@ -0,0 +1,197 @@ +""" +Created on Feb 20, 2014 + +@author: Aaron Ponti +""" + +import re +from ch.systemsx.cisd.openbis.dss.etl.dto.api import SimpleImageDataConfig +from ch.systemsx.cisd.openbis.dss.etl.dto.api import SimpleImageContainerDataConfig +from ch.systemsx.cisd.openbis.dss.etl.dto.api import ChannelColor +from ch.systemsx.cisd.openbis.dss.etl.dto.api import ImageIdentifier +from ch.systemsx.cisd.openbis.dss.etl.dto.api import ImageMetadata +from ch.systemsx.cisd.openbis.dss.etl.dto.api import OriginalDataStorageFormat +from ch.systemsx.cisd.openbis.dss.etl.dto.api import ChannelColorRGB +from ch.systemsx.cisd.openbis.dss.etl.dto.api import Channel + +class MicroscopySingleDatasetConfig(SimpleImageContainerDataConfig): + """Image data configuration class for multi datasets image files.""" + + # MetadataReader used to extract relevant metadata information using + # the LOCI bio-formats library. + _metadataReader = None + + # Number of the series to register (for a multi-series dataset. + _seriesNum = 0 + + def __init__(self, metadataReader, seriesNum=0): + """Constructor. + + @param metadataReader: MetadataReader object (with extracted metadata) + @param seriesNum: Int Number of the series to register. All other series in + the file will be ignored. + """ + + # Store the MetadataReader + self._metadataReader = metadataReader + + # Store the series number + self._seriesNum = seriesNum + + # This is microscopy data + self.setMicroscopyData(True) + + # Store raw data in original form + self.setOriginalDataStorageFormat(OriginalDataStorageFormat.UNCHANGED) + + # Set the image library + self.setImageLibrary("BioFormats") + + # Disable thumbnail generation by ImageMagick + self.setUseImageMagicToGenerateThumbnails(False) + + # Enable thumbnail generation + self.setGenerateThumbnails(True) + + # Set the recognized extensions + self.setRecognizedImageExtensions(['lsm', 'stk', 'lif', 'nd2']) + + # Set the dataset type + self.setDataSetType("MICROSCOPY_IMG") + + def createChannel(self, channelCode): + """Create a channel from the channelCode with the name as read from + the file via the MetadataReader. + + @param channelCode Code of the channel as generated by extractImagesMetadata(). + """ + + # Get the indices of series and channel from the channel code + (seriesIndx, channelIndx) = self._getSeriesAndChannelNumbers(channelCode) + + if seriesIndx != self._seriesNum: + return + + # Try extracting the channel colors for the given series + try: + channelNames = self._metadataReader.getMetadata()[seriesIndx]['channelNames'] + except IndexError: + raise("Could not channel name for series " + str(seriesIndx) + " from MetadataReader.") + + # Try extracting the name + try: + name = channelNames[channelIndx] + except: + raise("Could not extract name with index " + channelIndx) + + # In case no name was found, assign default name + if name == "": + name = "No name" + + # Return the color + return Channel(channelCode, name) + + + def extractImagesMetadata(self, imagePath, imageIdentifiers): + """Overrides extractImagesMetadata method making sure to store + both series and channel indices in the channel code to be reused + later to extract color information and other metadata. + + The channel code is in the form SERIES-(\d+)_CHANNEL-(\d+). + + Only metadata for the relevant series number is returned! + + @param imagePath Full path to the file to process + @param imageIdentifiers Array of ImageIdentifier's + + @see constructor. + """ + + # Initialize array of metadata entries + metaData = [] + + # Iterate over all image identifiers + for id in imageIdentifiers: + + # Extract the info from the image identifier + ch = id.colorChannelIndex + plane = id.focalPlaneIndex + series = id.seriesIndex + timepoint = id.timeSeriesIndex + + # Make sure to process only the relevant series + if series != self._seriesNum: + continue + + # Build the channel code + channelCode = "SERIES-" + str(series) + "_CHANNEL-" + str(ch) + + # Initialize a new ImageMetadata object + imageMetadata = ImageMetadata(); + + # Fill in all information + imageMetadata.imageIdentifier = id + imageMetadata.seriesNumber = series + imageMetadata.timepoint = timepoint + imageMetadata.depth = plane + imageMetadata.channelCode = channelCode + imageMetadata.well = "IGNORED" + imageMetadata.tileNumber = 1 + + # Append metadata for current image + metaData.append(imageMetadata) + return metaData + + + def getChannelColorRGB(self, channelCode): + """Returns a ChannelColorRGB instantiated with the RGB color components + extracted from the file by the MetadataReader. + + @param channelCode Code of the channel as generated by extractImagesMetadata(). + """ + + # Get the indices of series and channel from the channel code + (seriesIndx, channelIndx) = self._getSeriesAndChannelNumbers(channelCode) + + if seriesIndx != self._seriesNum: + return + + # Try extracting the channel colors for the given series + try: + channelColors = \ + self._metadataReader.getMetadata()[seriesIndx]['channelColors'] + except IndexError: + raise("Could not extract channel colors for series " + str(seriesIndx) + " from MetadataReader.") + + # Try extracting the color + try: + color = channelColors[channelIndx] + R = color[0] + G = color[1] + B = color[2] + except: + raise("Could not extract color with index " + channelIndx) + + return ChannelColorRGB(R, G, B) + + + def _getSeriesAndChannelNumbers(self, channelCode): + """Extract series and channel number from channel code in + the form SERIES-(\d+)_CHANNEL-(\d+) to a tuple + (seriesIndx, channelIndx). + + @param channelCode Code of the channel as generated by extractImagesMetadata(). + """ + + # Get the indices of series and channel from the channel code + p = re.compile("SERIES-(\d+)_CHANNEL-(\d+)") + m = p.match(channelCode) + if m is None or len(m.groups()) != 2: + raise Exception("Could not extract series and channel number!") + + # Now assign the indices + seriesIndx = int(m.group(1)) + channelIndx = int(m.group(2)) + + # Return them + return seriesIndx, channelIndx diff --git a/screening/sourceTest/core-plugins/MicroscopyImageDropboxTest/1/dss/drop-boxes/MicroscopyImageDropboxTest-drop-box/Processor.py b/screening/sourceTest/core-plugins/MicroscopyImageDropboxTest/1/dss/drop-boxes/MicroscopyImageDropboxTest-drop-box/Processor.py new file mode 100644 index 0000000000000000000000000000000000000000..8364cc88b1fdb383d317f85ddf4eac69e8b3f435 --- /dev/null +++ b/screening/sourceTest/core-plugins/MicroscopyImageDropboxTest/1/dss/drop-boxes/MicroscopyImageDropboxTest-drop-box/Processor.py @@ -0,0 +1,169 @@ +""" +Created on Feb 20, 2014 + +@author: Aaron Ponti +""" + +import java.io.File +import os +import re +import xml.etree.ElementTree as xml +from datetime import datetime +from BioFormatsProcessor import BioFormatsProcessor +from MicroscopySingleDatasetConfig import MicroscopySingleDatasetConfig + +class Processor: + """The Processor class performs all steps required for registering datasets + from the assigned dropbox folder.""" + + # A transaction object passed by openBIS + _transaction = None + + # The incoming folder to process (a java.io.File object) + _incoming = "" + + # Constructor + def __init__(self, transaction): + + # Store arguments + self._transaction = transaction + self._incoming = transaction.getIncoming() + + def getCustomTimeStamp(self): + """Create an univocal time stamp based on the current date and time + (works around incomplete API of Jython 2.5).""" + + t = datetime.now() + return t.strftime("%y%d%m%H%M%S") + unicode(t)[20:] + + def getSubFolders(self): + """Return a list of subfolders of the passed incoming directory. + + @return list of subfolders (String) + """ + + incomingStr = self._incoming.getAbsolutePath() + return [name for name in os.listdir(incomingStr) + if os.path.isdir(os.path.join(incomingStr, name))] + + def createExperiment(self, expId, expType): + # Create the experiment + exp = self._transaction.createNewExperiment(expId, expType) + if not exp: + raise Exception("Could not create experiment " + expId + "!") + + return exp + + def processExperiment(self, experimentNode, openBISExpType): + """Register an IExperimentUpdatable based on the Experiment XML node. + + @param experimentNode An XML node corresponding to an Experiment + @param openBISExpType The experiment type + @return IExperimentUpdatable experiment + """ + + # Get the openBIS identifier + openBISIdentifier = experimentNode.attrib.get("openBISIdentifier") + + openBISExperiment = self.createExperiment(openBISIdentifier, openBISExpType) + if not openBISExperiment: + raise Exception("Could not create experiment " + openBISIdentifier) + return openBISExperiment + + + def processMicroscopyFile(self, microscopyFileNode, openBISExperiment, propertiesFile): + """Register the Microscopy File using the parsed properties file. + + @param microscopyFileNode An XML node corresponding to a microscopy + file (dataset) + @param openBISExperiment An ISample object representing an Experiment + """ + + # Assign the file to the dataset (we will use the absolute path) + relativeFileName = microscopyFileNode.attrib.get("relativeFileName") + fileName = os.path.join(self._incoming.getAbsolutePath(), relativeFileName) + + # Instantiate a BioFormatsProcessor + bioFormatsProcessor = BioFormatsProcessor(fileName) + + # Extract and store metadata + bioFormatsProcessor.extractMetadata() + + # Log the number of series found + num_series = bioFormatsProcessor.getNumSeries() + singleDatasetConfig = MicroscopySingleDatasetConfig(bioFormatsProcessor) + dataset = self._transaction.createNewImageDataSet(singleDatasetConfig, java.io.File(fileName)) + self._transaction.moveFile(fileName, dataset) + sample = self._transaction.createNewSampleWithGeneratedCode("TEST", "MICROSCOPY_SAMPLE") + sample.setExperiment(openBISExperiment) + dataset.setSample(sample) + + + def register(self, tree, propertiesFile): + """Register the Experiment using the parsed properties file. + + @param tree ElementTree parsed from the properties XML file + """ + + # Get the root node (obitXML) + root = tree.getroot() + + # Iterate over the children (Experiments) + for experimentNode in root: + # The tag of the immediate children of the root experimentNode + # must be Experiment + if experimentNode.tag != "Experiment": + raise Exception("Expected Experiment node, found " + experimentNode.tag) + + # Process an Experiment XML node and get/create an IExperimentUpdatable + openBISExperiment = self.processExperiment(experimentNode, "MICROSCOPY_EXPERIMENT") + + # Process children of the Experiment + for microscopyFileNode in experimentNode: + if microscopyFileNode.tag != "MicroscopyFile": + raise Exception("Expected MicroscopyFile node (found " + microscopyFileNode.tag + "!") + + # Process the MicroscopyFile node + self.processMicroscopyFile(microscopyFileNode, openBISExperiment, propertiesFile) + + + def run(self): + """Run the registration.""" + + # Make sure that incoming is a folder + if not self._incoming.isDirectory(): + raise Exception("Incoming MUST be a folder!") + + # There must be just one subfolder: the user subfolder + subFolders = self.getSubFolders() + if len(subFolders) != 1: + raise Exception("Expected user subfolder!") + + # Set the user folder + userFolder = os.path.join(self._incoming.getAbsolutePath(), subFolders[0]) + + # In the user subfolder we must find the data_structure.ois file + dataFileName = os.path.join(userFolder, "data_structure.ois") + if not os.path.exists(dataFileName): + raise Exception("File data_structure.ois not found!") + + # Now read the data structure file and store all the pointers to + # the properties files. The paths are stored relative to self._incoming, + # so we can easily build the full file paths. + propertiesFileList = [] + f = open(dataFileName) + try: + for line in f: + line = re.sub('[\r\n]', '', line) + propertiesFile = os.path.join(self._incoming.getAbsolutePath(), line) + propertiesFileList.append(propertiesFile) + finally: + f.close() + + # Process (and ultimately register) all experiments + for propertiesFile in propertiesFileList: + # Read the properties file into an ElementTree + tree = xml.parse(propertiesFile) + + # Now register the experiment + self.register(tree, propertiesFile) diff --git a/screening/sourceTest/core-plugins/MicroscopyImageDropboxTest/1/dss/drop-boxes/MicroscopyImageDropboxTest-drop-box/plugin.properties b/screening/sourceTest/core-plugins/MicroscopyImageDropboxTest/1/dss/drop-boxes/MicroscopyImageDropboxTest-drop-box/plugin.properties new file mode 100644 index 0000000000000000000000000000000000000000..70ba6f9993bd9e36b2a3e78abffce26fb7406a2c --- /dev/null +++ b/screening/sourceTest/core-plugins/MicroscopyImageDropboxTest/1/dss/drop-boxes/MicroscopyImageDropboxTest-drop-box/plugin.properties @@ -0,0 +1,23 @@ +# The directory to watch for new data sets +incoming-dir = ${incoming-root-dir}/incoming-MicroscopyImageDropboxTest + +# The handler class. +top-level-data-set-handler = ch.systemsx.cisd.openbis.dss.etl.jython.v2.JythonPlateDataSetHandlerV2 + +# The script to execute, reloaded and recompiled each time a file/folder is placed in the dropbox +script-path = MicroscopyDropbox.py + +# The appropriate storage processor +storage-processor = ch.systemsx.cisd.openbis.dss.etl.MicroscopyStorageProcessor + +# Defines how the drop box decides if a folder is ready to process: either by a 'marker-file' or a time out which is called 'auto-detection' +# The time out is set globally in the service.properties and is called 'quiet-period'. This means when the number of seconds is over and no changes have +# been made to the incoming folder the drop will start to register. The marker file must have the following naming schema: '.MARKER_is_finished_<incoming_folder_name>' +#incoming-data-completeness-condition = marker-file +incoming-data-completeness-condition = auto-detection + +# Storage processor +storage-processor.data-source = imaging-db + +# Enable development mode +development-mode = true diff --git a/screening/sourceTest/java/ch/systemsx/cisd/openbis/screening/systemtests/AbstractImageDropboxTestCase.java b/screening/sourceTest/java/ch/systemsx/cisd/openbis/screening/systemtests/AbstractImageDropboxTestCase.java index b94231feca6206610d1aec0826162ace7ff82438..ccffd57692511c8af48f13cc69d29b50cb2ff1d7 100644 --- a/screening/sourceTest/java/ch/systemsx/cisd/openbis/screening/systemtests/AbstractImageDropboxTestCase.java +++ b/screening/sourceTest/java/ch/systemsx/cisd/openbis/screening/systemtests/AbstractImageDropboxTestCase.java @@ -18,10 +18,19 @@ package ch.systemsx.cisd.openbis.screening.systemtests; import java.io.File; import java.io.IOException; +import java.util.Arrays; +import java.util.List; import org.apache.commons.io.FileUtils; import org.testng.annotations.BeforeTest; +import ch.systemsx.cisd.openbis.generic.shared.basic.dto.AbstractExternalData; +import ch.systemsx.cisd.openbis.generic.shared.basic.dto.DataSetKind; +import ch.systemsx.cisd.openbis.generic.shared.basic.dto.DataSetRelatedEntities; +import ch.systemsx.cisd.openbis.generic.shared.basic.dto.Experiment; +import ch.systemsx.cisd.openbis.generic.shared.dto.identifier.ExperimentIdentifier; +import ch.systemsx.cisd.openbis.generic.shared.dto.identifier.ExperimentIdentifierFactory; + /** * * @author Franz-Josef Elmer @@ -31,10 +40,15 @@ public abstract class AbstractImageDropboxTestCase extends AbstractScreeningSyst @BeforeTest public void dropAnExampleDataSet() throws Exception { + registerAdditionalOpenbisMetaData(); File exampleDataSet = createTestDataContents(); moveFileToIncoming(exampleDataSet); waitUntilDataSetImported(FINISHED_POST_REGISTRATION_CONDITION); } + + protected void registerAdditionalOpenbisMetaData() + { + } private File createTestDataContents() throws IOException { @@ -56,6 +70,49 @@ public abstract class AbstractImageDropboxTestCase extends AbstractScreeningSyst { return 60; } + + protected AbstractExternalData getRegisteredContainerDataSet() + { + List<AbstractExternalData> dataSets = getRegisteredDataSets(); + for (AbstractExternalData dataSet : dataSets) + { + if (dataSet.getDataSetType().getDataSetKind().equals(DataSetKind.CONTAINER)) + { + return dataSet; + } + } + fail("No container data set found: " + dataSets); + return null; // never reached but needed for the compiler + } + + protected List<AbstractExternalData> getRegisteredDataSets() + { + String code = translateIntoCamelCase(getClass().getSimpleName()).toUpperCase(); + ExperimentIdentifier identifier = ExperimentIdentifierFactory.parse("/TEST/TEST-PROJECT/" + code); + Experiment experiment = commonServer.getExperimentInfo(sessionToken, identifier); + List<AbstractExternalData> dataSets + = commonServer.listRelatedDataSets(sessionToken, new DataSetRelatedEntities(Arrays.asList(experiment)), false); + return dataSets; + } + private String translateIntoCamelCase(String string) + { + StringBuilder builder = new StringBuilder(); + for (char c : string.toCharArray()) + { + if (Character.isUpperCase(c)) + { + if (builder.length() > 0) + { + builder.append('_'); + } + builder.append(Character.toLowerCase(c)); + } else + { + builder.append(c); + } + } + return builder.toString(); + } } diff --git a/screening/sourceTest/java/ch/systemsx/cisd/openbis/screening/systemtests/MicroscopyImageDropboxTest.java b/screening/sourceTest/java/ch/systemsx/cisd/openbis/screening/systemtests/MicroscopyImageDropboxTest.java new file mode 100644 index 0000000000000000000000000000000000000000..44a293edb0c57e4dac367f88d93bbcff2255c473 --- /dev/null +++ b/screening/sourceTest/java/ch/systemsx/cisd/openbis/screening/systemtests/MicroscopyImageDropboxTest.java @@ -0,0 +1,98 @@ +/* + * Copyright 2014 ETH Zuerich, SIS + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package ch.systemsx.cisd.openbis.screening.systemtests; + +import java.io.File; +import java.util.Collections; + +import org.testng.annotations.Test; + +import ch.systemsx.cisd.openbis.generic.shared.ICommonServer; +import ch.systemsx.cisd.openbis.generic.shared.basic.dto.AbstractExternalData; +import ch.systemsx.cisd.openbis.generic.shared.basic.dto.DataSetKind; +import ch.systemsx.cisd.openbis.generic.shared.basic.dto.DataSetType; +import ch.systemsx.cisd.openbis.generic.shared.basic.dto.ExperimentType; +import ch.systemsx.cisd.openbis.generic.shared.basic.dto.NewAttachment; +import ch.systemsx.cisd.openbis.generic.shared.basic.dto.SampleType; +import ch.systemsx.cisd.openbis.generic.shared.dto.identifier.ProjectIdentifierFactory; + +/** + * + * + * @author Franz-Josef Elmer + */ +public class MicroscopyImageDropboxTest extends AbstractImageDropboxTestCase +{ + + @Override + protected void registerAdditionalOpenbisMetaData() + { + System.out.println("MicroscopyImageDropboxTest.registerAdditionalOpenbisMetaData()"); + commonServer = (ICommonServer) applicationContext + .getBean(ch.systemsx.cisd.openbis.generic.shared.ResourceNames.COMMON_SERVER); + sessionToken = commonServer.tryAuthenticate("admin", "a").getSessionToken(); + ExperimentType experimentType = new ExperimentType(); + experimentType.setCode("MICROSCOPY_EXPERIMENT"); + commonServer.registerExperimentType(sessionToken, experimentType); + SampleType sampleType = new SampleType(); + sampleType.setCode("MICROSCOPY_SAMPLE"); + sampleType.setGeneratedCodePrefix("M-"); + commonServer.registerSampleType(sessionToken, sampleType); + DataSetType dataSetType = new DataSetType("MICROSCOPY_IMG"); + dataSetType.setDataSetKind(DataSetKind.PHYSICAL); + commonServer.registerDataSetType(sessionToken, dataSetType); + dataSetType = new DataSetType("MICROSCOPY_IMG_CONTAINER"); + dataSetType.setDataSetKind(DataSetKind.CONTAINER); + commonServer.registerDataSetType(sessionToken, dataSetType); + commonServer.registerSpace(sessionToken, "TEST", null); + commonServer.registerProject(sessionToken, ProjectIdentifierFactory.parse("/TEST/TEST-PROJECT"), "", + null, Collections.<NewAttachment>emptySet()); + } + + @Override + protected String getDataFolderToDrop() + { + return "aarons_example"; + } + + @Test + public void test() + { + AbstractExternalData dataSet = getRegisteredContainerDataSet(); + ImageChecker imageChecker = new ImageChecker(); + imageChecker.check(new File(getTestDataFolder(), "Merged_Default.png"), + new ImageLoader(dataSet, sessionToken)); + imageChecker.check(new File(getTestDataFolder(), "Merged_256x256.png"), + new ImageLoader(dataSet, sessionToken).mode("thumbnail256x256")); + imageChecker.check(new File(getTestDataFolder(), "Merged_512x512.png"), + new ImageLoader(dataSet, sessionToken).mode("thumbnail512x512")); + imageChecker.check(new File(getTestDataFolder(), "C1_Default.png"), + new ImageLoader(dataSet, sessionToken).channel("SERIES-0-CHANNEL-1")); + imageChecker.check(new File(getTestDataFolder(), "C1_256x256.png"), + new ImageLoader(dataSet, sessionToken).channel("SERIES-0-CHANNEL-1").mode("thumbnail256x256")); + imageChecker.check(new File(getTestDataFolder(), "C1_512x512.png"), + new ImageLoader(dataSet, sessionToken).channel("SERIES-0-CHANNEL-1").mode("thumbnail512x512")); + imageChecker.check(new File(getTestDataFolder(), "C01_Default.png"), + new ImageLoader(dataSet, sessionToken).channel("SERIES-0-CHANNEL-0").channel("SERIES-0-CHANNEL-1")); + imageChecker.check(new File(getTestDataFolder(), "C01_256x256.png"), + new ImageLoader(dataSet, sessionToken).channel("SERIES-0-CHANNEL-0").channel("SERIES-0-CHANNEL-1").mode("thumbnail256x256")); + imageChecker.check(new File(getTestDataFolder(), "C01_512x512.png"), + new ImageLoader(dataSet, sessionToken).channel("SERIES-0-CHANNEL-0").channel("SERIES-0-CHANNEL-1").mode("thumbnail512x512")); + + } + +} diff --git a/screening/sourceTest/java/ch/systemsx/cisd/openbis/screening/systemtests/SimpleImageDropboxTest.java b/screening/sourceTest/java/ch/systemsx/cisd/openbis/screening/systemtests/SimpleImageDropboxTest.java index c368f8605c620732d7b540ddb7fa7466c7495eb6..b485912efe64e0125d182b86401bc508c41265ef 100644 --- a/screening/sourceTest/java/ch/systemsx/cisd/openbis/screening/systemtests/SimpleImageDropboxTest.java +++ b/screening/sourceTest/java/ch/systemsx/cisd/openbis/screening/systemtests/SimpleImageDropboxTest.java @@ -17,18 +17,10 @@ package ch.systemsx.cisd.openbis.screening.systemtests; import java.io.File; -import java.util.List; import org.testng.annotations.Test; -import ch.systemsx.cisd.openbis.generic.shared.basic.TechId; import ch.systemsx.cisd.openbis.generic.shared.basic.dto.AbstractExternalData; -import ch.systemsx.cisd.openbis.generic.shared.basic.dto.DataSetRelatedEntities; -import ch.systemsx.cisd.openbis.generic.shared.basic.dto.Experiment; -import ch.systemsx.cisd.openbis.generic.shared.basic.dto.ListSampleCriteria; -import ch.systemsx.cisd.openbis.generic.shared.basic.dto.Sample; -import ch.systemsx.cisd.openbis.generic.shared.dto.identifier.ExperimentIdentifier; -import ch.systemsx.cisd.openbis.generic.shared.dto.identifier.ExperimentIdentifierFactory; /** * @@ -47,17 +39,7 @@ public class SimpleImageDropboxTest extends AbstractImageDropboxTestCase @Test public void test() throws Exception { - ExperimentIdentifier identifier = ExperimentIdentifierFactory.parse("/TEST/TEST-PROJECT/SIMPLE_IMAGE_DROPBOX_TEST"); - Experiment experiment = commonServer.getExperimentInfo(sessionToken, identifier); - ListSampleCriteria sampleCriteria = ListSampleCriteria.createForExperiment(TechId.create(experiment)); - List<Sample> samples = commonServer.listSamples(sessionToken, sampleCriteria); - Sample plate = samples.get(0); - assertEquals("/TEST/SIMPLE_IMAGE_DROPBOX_TEST", plate.getIdentifier()); - assertEquals(1, samples.size()); - List<AbstractExternalData> dataSets2 - = commonServer.listRelatedDataSets(sessionToken, new DataSetRelatedEntities(samples), false); - AbstractExternalData dataSet = dataSets2.get(0); - assertEquals(1, dataSets2.size()); + AbstractExternalData dataSet = getRegisteredContainerDataSet(); ImageChecker imageChecker = new ImageChecker(); imageChecker.check(new File(getTestDataFolder(), "1_1_Merged_Default.png"), new ImageLoader(dataSet, sessionToken));