class ImageStack(object): def __init__(self, data = None, mdh = None, filename = None, queueURI = None, events = [], titleStub='Untitled Image', haveGUI=True): ''' Create an Image Stack. This is a essentially a wrapper of the image data and any ascociated metadata. The class can be given a ndarray like* data source, or alternatively supports loading from file, or from a PYME task queue URI, in which case individual slices will be fetched from the server as required. For details on the file type support, see the Load method. You should provide one of the 'data' or 'filename' parmeters, with all other parameters being optional. Parameters: data Image data. Something that supports ndarray like slicing and exposes a .shape parameter, something implementing the PYME.Analysis.DataSources interface, or a list of either of the above. Dimensionality can be between 1 and 4, with the dimensions being interpreted as x, y, z/t, colour. A mangled (will support slicing, but not necessarily other array operations) version of the data will be stored as the .data member of the class. mdh something derived from PYME.Acquire.MetaDataHandler.MDHandlerBase If None, and empty one will be created. filename filename of the data to load (see Load), or PYME queue identifier queueURI PYRO URI of the task server. This exists to optionally speed up loading from a queue by eliminating the PYRO nameserver lookup. The queue name itself should be passed in the filename, with a leading QUEUE://. events An array of time series events (TODO - more doc) haveGUI Whether we have a wx GUI available, so that we can display dialogs asking for, e.g. a file name if no data or filename is supplied, or for missing metadata entries ''' global nUntitled self.data = data #image data self.mdh = mdh #metadata (a MetaDataHandler class) self.events = events #events self.queueURI = queueURI self.filename = filename self.haveGUI = haveGUI #default 'mode' / image type - see PYME/DSView/modules/__init__.py self.mode = 'LM' self.saved = False self.volatile = False #is the data likely to change and need refreshing? #s408 if (data is None): #if we've supplied data, use that, otherwise load from file self.Load(filename) #do the necessary munging to get the data in the format we want it self.SetData(self.data) #generate a placeholder filename / window title if self.filename == None: self.filename = '%s %d' % (titleStub, nUntitled[titleStub]) nUntitled[titleStub] += 1 self.seriesName = self.filename #generate some empty metadata if we don't have any if self.mdh == None: self.mdh = MetaDataHandler.NestedClassMDHandler() #hack to make spectral data behave right - doesn't really belong here if 'Spectrum.Wavelengths' in self.mdh.getEntryNames(): self.xvals = self.mdh['Spectrum.Wavelengths'] self.xlabel = 'Wavelength [nm]' #if we have 1D data, plot as graph rather than image if self.data.shape[1] == 1: self.mode = 'graph' #add ourselves to the list of open images openImages[self.filename] = self def SetData(self, data): #the data does not need to be a numpy array - it could also be, eg., queue data #on a remote server - wrap so that is is indexable like an array self.data = dataWrap.Wrap(data) @property def voxelsize(self): '''Returns voxel size, in nm, as a 3-tuple. Expects metadata voxel size to be in um''' try: return 1e3*self.mdh['voxelsize.x'], 1e3*self.mdh['voxelsize.y'], 1e3*self.mdh['voxelsize.z'] except: return 1,1,1 @property def pixelSize(self): try: return 1e3*self.mdh['voxelsize.x'] except: return 1 @pixelSize.setter def pixelSize(self, value): self.mdh['voxelsize.x'] = .001*value self.mdh['voxelsize.y'] = .001*value @property def sliceSize(self): try: return 1e3*self.mdh['voxelsize.z'] except: return 1 @sliceSize.setter def sliceSize(self, value): self.mdh['voxelsize.z'] = .001*value @property def names(self): '''Return the names of the colour channels''' try: return self.mdh['ChannelNames'] except: return ['Chan %d'% d for d in range(self.data.shape[3])] @names.setter def names(self, value): self.mdh['ChannelNames'] = value @property def imgBounds(self): '''Return the bounds (or valid area) of the image in nm as (x0, y0, x1, y1, z0, z1)''' try: return ImageBounds(self.mdh['ImageBounds.x0'],self.mdh['ImageBounds.y0'],self.mdh['ImageBounds.x1'],self.mdh['ImageBounds.y1'],self.mdh['ImageBounds.z0'],self.mdh['ImageBounds.z1']) except: return ImageBounds(0, 0, self.pixelSize*self.data.shape[0], self.pixelSize*self.data.shape[1],0, self.sliceSize*self.data.shape[2]) @imgBounds.setter def imgBounds(self, value): self.mdh['ImageBounds.x0'] = value.x0 self.mdh['ImageBounds.y0'] = value.y0 self.mdh['ImageBounds.x1'] = value.x1 self.mdh['ImageBounds.y1'] = value.y1 self.mdh['ImageBounds.z0'] = value.z0 self.mdh['ImageBounds.z1'] = value.z1 @property def origin(self): #the origin, in nm from the camera - used for overlaying with different ROIs if 'Origin.x' in self.mdh.getEntryNames(): return self.mdh['Origin.x'], self.mdh['Origin.y'], self.mdh['Origin.z'] elif 'Camera.ROIPosX' in self.mdh.getEntryNames(): #has ROI information try: voxx, voxy = 1e3*self.mdh['voxelsize.x'], 1e3*self.mdh['voxelsize.y'] except AttributeError: voxx = self.pixelSize voxy = voxx ox = (self.mdh['Camera.ROIPosX'] - 1)*voxx oy = (self.mdh['Camera.ROIPosY'] - 1)*voxy oz = 0 if 'AcquisitionType' in self.mdh.getEntryNames() and self.mdh['AcquisitionType'] == 'Stack': oz = self.mdh['StackSettings.StartPos']*1e3 elif 'Positioning.PIFoc' in self.mdh.getEntryNames(): oz = self.mdh['Positioning.PIFoc']*1e3 return ox, oy, oz elif 'Source.Camera.ROIPosX' in self.mdh.getEntryNames(): #a rendered image with information about the source ROI voxx, voxy = 1e3*self.mdh['Source.voxelsize.x'], 1e3*self.mdh['Source.voxelsize.y'] ox = (self.mdh['Source.Camera.ROIPosX'] - 1)*voxx oy = (self.mdh['Source.Camera.ROIPosY'] - 1)*voxy return ox, oy, 0 else: return 0,0, 0 def LoadQueue(self, filename): '''Load data from a remote PYME.ParallelTasks.HDFTaskQueue queue using Pyro. Parameters: filename the name of the queue ''' import Pyro.core from PYME.Analysis.DataSources import TQDataSource from PYME.misc.computerName import GetComputerName compName = GetComputerName() if self.queueURI == None: #do a lookup taskQueueName = 'TaskQueues.%s' % compName self.tq = Pyro.core.getProxyForURI('PYRONAME://' + taskQueueName) else: self.tq = Pyro.core.getProxyForURI(self.queueURI) self.seriesName = filename[len('QUEUE://'):] self.dataSource = TQDataSource.DataSource(self.seriesName, self.tq) self.data = self.dataSource #this will get replaced with a wrapped version self.mdh = MetaDataHandler.QueueMDHandler(self.tq, self.seriesName) MetaData.fillInBlanks(self.mdh, self.dataSource) #self.timer.WantNotification.append(self.dsRefresh) self.events = self.dataSource.getEvents() def Loadh5(self, filename): '''Load PYMEs semi-custom HDF5 image data format. Offloads all the hard work to the HDFDataSource class''' import tables from PYME.Analysis.DataSources import HDFDataSource, BGSDataSource from PYME.Analysis.LMVis import inpFilt #open hdf5 file self.dataSource = HDFDataSource.DataSource(filename, None) #chain on a background subtraction data source, so we can easily do #background subtraction in the GUI the same way as in the analysis self.data = BGSDataSource.DataSource(self.dataSource) #this will get replaced with a wrapped version if 'MetaData' in self.dataSource.h5File.root: #should be true the whole time self.mdh = MetaData.TIRFDefault self.mdh.copyEntriesFrom(MetaDataHandler.HDFMDHandler(self.dataSource.h5File)) else: self.mdh = MetaData.TIRFDefault wx.MessageBox("Carrying on with defaults - no gaurantees it'll work well", 'ERROR: No metadata found in file ...', wx.OK) print("ERROR: No metadata fond in file ... Carrying on with defaults - no gaurantees it'll work well") #attempt to estimate any missing parameters from the data itself MetaData.fillInBlanks(self.mdh, self.dataSource) #calculate the name to use when we do batch analysis on this from PYME.ParallelTasks.relativeFiles import getRelFilename self.seriesName = getRelFilename(filename) #try and find a previously performed analysis fns = filename.split(os.path.sep) cand = os.path.sep.join(fns[:-2] + ['analysis',] + fns[-2:]) + 'r' print(cand) if False:#os.path.exists(cand): h5Results = tables.openFile(cand) if 'FitResults' in dir(h5Results.root): self.fitResults = h5Results.root.FitResults[:] self.resultsSource = inpFilt.h5rSource(h5Results) self.resultsMdh = MetaData.TIRFDefault self.resultsMdh.copyEntriesFrom(MetaDataHandler.HDFMDHandler(h5Results)) self.events = self.dataSource.getEvents() def LoadHTTP(self, filename): '''Load PYMEs semi-custom HDF5 image data format. Offloads all the hard work to the HDFDataSource class''' import tables from PYME.Analysis.DataSources import HTTPDataSource, BGSDataSource #from PYME.Analysis.LMVis import inpFilt #open hdf5 file self.dataSource = HTTPDataSource.DataSource(filename) #chain on a background subtraction data source, so we can easily do #background subtraction in the GUI the same way as in the analysis self.data = BGSDataSource.DataSource(self.dataSource) #this will get replaced with a wrapped version #try: #should be true the whole time self.mdh = MetaData.TIRFDefault self.mdh.copyEntriesFrom(self.dataSource.getMetadata()) #except: # self.mdh = MetaData.TIRFDefault # wx.MessageBox("Carrying on with defaults - no gaurantees it'll work well", 'ERROR: No metadata found in file ...', wx.OK) # print("ERROR: No metadata fond in file ... Carrying on with defaults - no gaurantees it'll work well") #attempt to estimate any missing parameters from the data itself MetaData.fillInBlanks(self.mdh, self.dataSource) #calculate the name to use when we do batch analysis on this #from PYME.ParallelTasks.relativeFiles import getRelFilename self.seriesName = filename self.events = self.dataSource.getEvents() def LoadKdf(self, filename): '''load khorus formatted data - pretty much deprecated by now''' import PYME.cSMI as cSMI self.data = cSMI.CDataStack_AsArray(cSMI.CDataStack(filename), 0).squeeze() self.mdh = MetaData.TIRFDefault try: #try and get metadata from the .log file lf = open(os.path.splitext(filename)[0] + '.log') from PYME.DSView import logparser lp = logparser.logparser() log = lp.parse(lf.read()) lf.close() self.mdh.setEntry('voxelsize.z', log['PIEZOS']['Stepsize']) except: pass from PYME.ParallelTasks.relativeFiles import getRelFilename self.seriesName = getRelFilename(filename) self.mode = 'psf' def LoadPSF(self, filename): '''Load PYME .psf data. .psf files consist of a tuple containing the data and the voxelsize. ''' self.data, vox = numpy.load(filename) self.mdh = MetaData.ConfocDefault self.mdh.setEntry('voxelsize.x', vox.x) self.mdh.setEntry('voxelsize.y', vox.y) self.mdh.setEntry('voxelsize.z', vox.z) from PYME.ParallelTasks.relativeFiles import getRelFilename self.seriesName = getRelFilename(filename) self.mode = 'psf' def LoadNPY(self, filename): '''Load numpy .npy data. ''' mdfn = self.FindAndParseMetadata(filename) self.data = numpy.load(filename) from PYME.ParallelTasks.relativeFiles import getRelFilename self.seriesName = getRelFilename(filename) self.mode = 'default' def LoadDBL(self, filename): '''Load Bewersdorf custom STED data. ''' mdfn = self.FindAndParseMetadata(filename) self.data = numpy.memmap(filename, dtype='<f4', mode='r', offset=128, shape=(self.mdh['Camera.ROIWidth'],self.mdh['Camera.ROIHeight'],self.mdh['NumImages']), order='F') from PYME.ParallelTasks.relativeFiles import getRelFilename self.seriesName = getRelFilename(filename) self.mode = 'default' def FindAndParseMetadata(self, filename): '''Try and find and load a .xml or .md metadata file that might be ascociated with a given image filename. See the relevant metadatahandler classes for details.''' import xml.parsers.expat mdf = None xmlfn = os.path.splitext(filename)[0] + '.xml' xmlfnmc = os.path.splitext(filename)[0].split('__')[0] + '.xml' if os.path.exists(xmlfn): try: self.mdh = MetaDataHandler.NestedClassMDHandler(MetaData.TIRFDefault) self.mdh.copyEntriesFrom(MetaDataHandler.XMLMDHandler(xmlfn)) mdf = xmlfn except xml.parsers.expat.ExpatError: #fix for bug in which PYME .md was written with a .xml extension self.mdh = MetaDataHandler.NestedClassMDHandler(MetaData.BareBones) self.mdh.copyEntriesFrom(MetaDataHandler.SimpleMDHandler(xmlfn)) mdf = xmlfn elif os.path.exists(xmlfnmc): #this is a single colour channel of a pair self.mdh = MetaDataHandler.NestedClassMDHandler(MetaData.TIRFDefault) self.mdh.copyEntriesFrom(MetaDataHandler.XMLMDHandler(xmlfnmc)) mdf = xmlfnmc else: self.mdh = MetaDataHandler.NestedClassMDHandler(MetaData.BareBones) #check for simple metadata (python code with an .md extension which #fills a dictionary called md) mdfn = os.path.splitext(filename)[0] + '.md' if os.path.exists(mdfn): self.mdh.copyEntriesFrom(MetaDataHandler.SimpleMDHandler(mdfn)) mdf = mdfn elif filename.endswith('.lsm'): #read lsm metadata from PYME.gohlke.tifffile import TIFFfile tf = TIFFfile(filename) lsm_info = tf[0].cz_lsm_scan_information self.mdh['voxelsize.x'] = lsm_info['line_spacing'] self.mdh['voxelsize.y'] = lsm_info['line_spacing'] self.mdh['voxelsize.z'] = lsm_info['plane_spacing'] def lsm_pop(basename, dic): for k, v in dic.items(): if isinstance(v, list): #print k, v for i, l_i in enumerate(v): #print i, l_i, basename lsm_pop(basename + k + '.' + k[:-1] + '%i.' %i, l_i) else: self.mdh[basename + k] = v lsm_pop('LSM.', lsm_info) elif filename.endswith('.dbl'): #Bewersdorf lab STED mdfn = filename[:-4] + '.txt' entrydict = {} try: #try to read in extra metadata if possible with open(mdfn, 'r') as mf: for line in mf: s = line.split(':') if len(s) == 2: entrydict[s[0]] = s[1] except IOError: pass # vx, vy = entrydict['Pixel size (um)'].split('x') # self.mdh['voxelsize.x'] = float(vx) # self.mdh['voxelsize.y'] = float(vy) # self.mdh['voxelsize.z'] = 0.2 #FIXME for stacks ... # # sx, sy = entrydict['Image format'].split('x') # self.mdh['Camera.ROIWidth'] = int(sx) # self.mdh['Camera.ROIHeight'] = int(sy) # # self.mdh['NumImages'] = int(entrydict['# Images']) with open(filename) as df: s = df.read(8) Z, X, Y, T = numpy.fromstring(s, '>u2') s = df.read(16) depth, width, height, elapsed = numpy.fromstring(s, '<f4') self.mdh['voxelsize.x'] = width/X self.mdh['voxelsize.y'] = height/Y self.mdh['voxelsize.z'] = depth self.mdh['Camera.ROIWidth'] = X self.mdh['Camera.ROIHeight'] = Y self.mdh['NumImages'] = Z*T def _sanitise_key(key): k = key.replace('#', 'Num') k = k.replace('(%)', '') k = k.replace('(', '') k = k.replace(')', '') k = k.replace('.', '') k = k.replace('/', '') k = k.replace('?', '') k = k.replace(' ', '') if not k[0].isalpha(): k = 's' + k return k for k, v in entrydict.items(): self.mdh['STED.%s'%_sanitise_key(k)] = v if self.haveGUI and not ('voxelsize.x' in self.mdh.keys() and 'voxelsize.y' in self.mdh.keys()): from PYME.DSView.voxSizeDialog import VoxSizeDialog dlg = VoxSizeDialog(None) #dlg.ShowModal() self.mdh.setEntry('voxelsize.x', dlg.GetVoxX()) self.mdh.setEntry('voxelsize.y', dlg.GetVoxY()) self.mdh.setEntry('voxelsize.z', dlg.GetVoxZ()) return mdf def LoadTiff(self, filename): #from PYME.FileUtils import readTiff from PYME.Analysis.DataSources import TiffDataSource mdfn = self.FindAndParseMetadata(filename) self.dataSource = TiffDataSource.DataSource(filename, None) self.dataSource = BufferedDataSource.DataSource(self.dataSource, min(self.dataSource.getNumSlices(), 50)) self.data = self.dataSource #this will get replaced with a wrapped version #if we have a multi channel data set, try and pull in all the channels if 'ChannelFiles' in self.mdh.getEntryNames(): try: from PYME.DSView.dataWrap import ListWrap #pull in all channels chans = [] for cf in self.mdh.getEntry('ChannelFiles'): cfn = os.path.join(os.path.split(filename)[0], cf) ds = TiffDataSource.DataSource(cfn, None) ds = BufferedDataSource.DataSource(ds, min(ds.getNumSlices(), 50)) chans.append(ds) self.data = ListWrap(chans) #this will get replaced with a wrapped version self.filename = mdfn except: pass elif 'ChannelNames' in self.mdh.getEntryNames() and len(self.mdh['ChannelNames']) == self.data.getNumSlices(): from PYME.DSView.dataWrap import ListWrap chans = [numpy.atleast_3d(self.data.getSlice(i)) for i in range(len(self.mdh['ChannelNames']))] self.data = ListWrap(chans) elif filename.endswith('.lsm') and 'LSM.images_number_channels' in self.mdh.keys() and self.mdh['LSM.images_number_channels'] > 1: from PYME.DSView.dataWrap import ListWrap nChans = self.mdh['LSM.images_number_channels'] chans = [] for n in range(nChans): ds = TiffDataSource.DataSource(filename, None, n) ds = BufferedDataSource.DataSource(ds, min(ds.getNumSlices(), 50)) chans.append(ds) self.data = ListWrap(chans) #self.data = readTiff.read3DTiff(filename) from PYME.ParallelTasks.relativeFiles import getRelFilename self.seriesName = getRelFilename(filename) self.mode = 'default' def LoadImageSeries(self, filename): #from PYME.FileUtils import readTiff from PYME.Analysis.DataSources import ImageSeriesDataSource self.dataSource = ImageSeriesDataSource.DataSource(filename, None) self.dataSource = BufferedDataSource.DataSource(self.dataSource, min(self.dataSource.getNumSlices(), 50)) self.data = self.dataSource #this will get replaced with a wrapped version #self.data = readTiff.read3DTiff(filename) self.FindAndParseMetadata(filename) from PYME.ParallelTasks.relativeFiles import getRelFilename self.seriesName = getRelFilename(filename) self.mode = 'default' def Load(self, filename=None): print r'> image.py: Loading file', filename if (filename == None): import wx #only introduce wx dependency here - so can be used non-interactively global lastdir fdialog = wx.FileDialog(None, 'Please select Data Stack to open ...', wildcard='Image Data|*.h5;*.tif;*.lsm;*.kdf;*.md;*.psf;*.npy;*.dbl|All files|*.*', style=wx.OPEN, defaultDir = lastdir) succ = fdialog.ShowModal() if (succ == wx.ID_OK): filename = fdialog.GetPath() lastdir = fdialog.GetDirectory() if not filename == None: if filename.startswith('QUEUE://'): self.LoadQueue(filename) elif filename.startswith('http://'): self.LoadHTTP(filename) elif filename.endswith('.h5'): self.Loadh5(filename) elif filename.endswith('.kdf'): self.LoadKdf(filename) elif filename.endswith('.psf'): #psf self.LoadPSF(filename) elif filename.endswith('.md'): #treat this as being an image series self.LoadImageSeries(filename) elif filename.endswith('.npy'): #treat this as being an image series self.LoadNPY(filename) elif filename.endswith('.dbl'): #treat this as being an image series self.LoadDBL(filename) else: #try tiff self.LoadTiff(filename) #self.SetTitle(filename) self.filename = filename self.saved = True def Save(self, filename=None, crop=False, view=None): import dataExporter ofn = self.filename if crop: dataExporter.CropExportData(view, self.mdh, self.events, self.seriesName) else: if 'defaultExt' in dir(self): self.filename = dataExporter.ExportData(self.data, self.mdh, self.events, defaultExt=self.defaultExt, filename=filename) else: self.filename = dataExporter.ExportData(self.data, self.mdh, self.events, filename=filename) #self.SetTitle(fn) if not (self.filename == None): self.saved = True openImages.pop(ofn) openImages[self.filename] = self else: self.filename = ofn
def LoadTiff(self, filename): #from PYME.FileUtils import readTiff from PYME.Analysis.DataSources import TiffDataSource mdfn = self.FindAndParseMetadata(filename) self.dataSource = TiffDataSource.DataSource(filename, None) self.dataSource = BufferedDataSource.DataSource( self.dataSource, min(self.dataSource.getNumSlices(), 50)) self.data = self.dataSource #this will get replaced with a wrapped version #if we have a multi channel data set, try and pull in all the channels if 'ChannelFiles' in self.mdh.getEntryNames(): try: from PYME.DSView.dataWrap import ListWrap #pull in all channels chans = [] for cf in self.mdh.getEntry('ChannelFiles'): cfn = os.path.join(os.path.split(filename)[0], cf) ds = TiffDataSource.DataSource(cfn, None) ds = BufferedDataSource.DataSource( ds, min(ds.getNumSlices(), 50)) chans.append(ds) self.data = ListWrap( chans) #this will get replaced with a wrapped version self.filename = mdfn except: pass elif 'ChannelNames' in self.mdh.getEntryNames() and len( self.mdh['ChannelNames']) == self.data.getNumSlices(): from PYME.DSView.dataWrap import ListWrap chans = [ numpy.atleast_3d(self.data.getSlice(i)) for i in range(len(self.mdh['ChannelNames'])) ] self.data = ListWrap(chans) elif filename.endswith( '.lsm') and 'LSM.images_number_channels' in self.mdh.keys( ) and self.mdh['LSM.images_number_channels'] > 1: from PYME.DSView.dataWrap import ListWrap nChans = self.mdh['LSM.images_number_channels'] chans = [] for n in range(nChans): ds = TiffDataSource.DataSource(filename, None, n) ds = BufferedDataSource.DataSource(ds, min(ds.getNumSlices(), 50)) chans.append(ds) self.data = ListWrap(chans) #self.data = readTiff.read3DTiff(filename) from PYME.ParallelTasks.relativeFiles import getRelFilename self.seriesName = getRelFilename(filename) self.mode = 'default'
def LoadTiff(self, filename): #from PYME.FileUtils import readTiff from PYME.Analysis.DataSources import TiffDataSource mdfn = self.FindAndParseMetadata(filename) self.dataSource = TiffDataSource.DataSource(filename, None) self.dataSource = BufferedDataSource.DataSource(self.dataSource, min(self.dataSource.getNumSlices(), 50)) self.data = self.dataSource #this will get replaced with a wrapped version #if we have a multi channel data set, try and pull in all the channels if 'ChannelFiles' in self.mdh.getEntryNames(): try: from PYME.DSView.dataWrap import ListWrap #pull in all channels chans = [] for cf in self.mdh.getEntry('ChannelFiles'): cfn = os.path.join(os.path.split(filename)[0], cf) ds = TiffDataSource.DataSource(cfn, None) ds = BufferedDataSource.DataSource(ds, min(ds.getNumSlices(), 50)) chans.append(ds) self.data = ListWrap(chans) #this will get replaced with a wrapped version self.filename = mdfn except: pass elif 'ChannelNames' in self.mdh.getEntryNames() and len(self.mdh['ChannelNames']) == self.data.getNumSlices(): from PYME.DSView.dataWrap import ListWrap chans = [numpy.atleast_3d(self.data.getSlice(i)) for i in range(len(self.mdh['ChannelNames']))] self.data = ListWrap(chans) elif filename.endswith('.lsm') and 'LSM.images_number_channels' in self.mdh.keys() and self.mdh['LSM.images_number_channels'] > 1: from PYME.DSView.dataWrap import ListWrap nChans = self.mdh['LSM.images_number_channels'] chans = [] for n in range(nChans): ds = TiffDataSource.DataSource(filename, None, n) ds = BufferedDataSource.DataSource(ds, min(ds.getNumSlices(), 50)) chans.append(ds) self.data = ListWrap(chans) #self.data = readTiff.read3DTiff(filename) from PYME.ParallelTasks.relativeFiles import getRelFilename self.seriesName = getRelFilename(filename) self.mode = 'default'
class ImageStack(object): def __init__(self, data=None, mdh=None, filename=None, queueURI=None, events=[], titleStub='Untitled Image', haveGUI=True): ''' Create an Image Stack. This is a essentially a wrapper of the image data and any ascociated metadata. The class can be given a ndarray like* data source, or alternatively supports loading from file, or from a PYME task queue URI, in which case individual slices will be fetched from the server as required. For details on the file type support, see the Load method. You should provide one of the 'data' or 'filename' parmeters, with all other parameters being optional. Parameters: data Image data. Something that supports ndarray like slicing and exposes a .shape parameter, something implementing the PYME.Analysis.DataSources interface, or a list of either of the above. Dimensionality can be between 1 and 4, with the dimensions being interpreted as x, y, z/t, colour. A mangled (will support slicing, but not necessarily other array operations) version of the data will be stored as the .data member of the class. mdh something derived from PYME.Acquire.MetaDataHandler.MDHandlerBase If None, and empty one will be created. filename filename of the data to load (see Load), or PYME queue identifier queueURI PYRO URI of the task server. This exists to optionally speed up loading from a queue by eliminating the PYRO nameserver lookup. The queue name itself should be passed in the filename, with a leading QUEUE://. events An array of time series events (TODO - more doc) haveGUI Whether we have a wx GUI available, so that we can display dialogs asking for, e.g. a file name if no data or filename is supplied, or for missing metadata entries ''' global nUntitled self.data = data #image data self.mdh = mdh #metadata (a MetaDataHandler class) self.events = events #events self.queueURI = queueURI self.filename = filename self.haveGUI = haveGUI #default 'mode' / image type - see PYME/DSView/modules/__init__.py self.mode = 'LM' self.saved = False self.volatile = False #is the data likely to change and need refreshing? #s408 if (data is None): #if we've supplied data, use that, otherwise load from file self.Load(filename) #do the necessary munging to get the data in the format we want it self.SetData(self.data) #generate a placeholder filename / window title if self.filename == None: self.filename = '%s %d' % (titleStub, nUntitled[titleStub]) nUntitled[titleStub] += 1 self.seriesName = self.filename #generate some empty metadata if we don't have any if self.mdh == None: self.mdh = MetaDataHandler.NestedClassMDHandler() #hack to make spectral data behave right - doesn't really belong here if 'Spectrum.Wavelengths' in self.mdh.getEntryNames(): self.xvals = self.mdh['Spectrum.Wavelengths'] self.xlabel = 'Wavelength [nm]' #if we have 1D data, plot as graph rather than image if self.data.shape[1] == 1: self.mode = 'graph' #add ourselves to the list of open images openImages[self.filename] = self def SetData(self, data): #the data does not need to be a numpy array - it could also be, eg., queue data #on a remote server - wrap so that is is indexable like an array self.data = dataWrap.Wrap(data) @property def voxelsize(self): '''Returns voxel size, in nm, as a 3-tuple. Expects metadata voxel size to be in um''' try: return 1e3 * self.mdh['voxelsize.x'], 1e3 * self.mdh[ 'voxelsize.y'], 1e3 * self.mdh['voxelsize.z'] except: return 1, 1, 1 @property def pixelSize(self): try: return 1e3 * self.mdh['voxelsize.x'] except: return 1 @pixelSize.setter def pixelSize(self, value): self.mdh['voxelsize.x'] = .001 * value self.mdh['voxelsize.y'] = .001 * value @property def sliceSize(self): try: return 1e3 * self.mdh['voxelsize.z'] except: return 1 @sliceSize.setter def sliceSize(self, value): self.mdh['voxelsize.z'] = .001 * value @property def names(self): '''Return the names of the colour channels''' try: return self.mdh['ChannelNames'] except: return ['Chan %d' % d for d in range(self.data.shape[3])] @names.setter def names(self, value): self.mdh['ChannelNames'] = value @property def imgBounds(self): '''Return the bounds (or valid area) of the image in nm as (x0, y0, x1, y1, z0, z1)''' try: return ImageBounds(self.mdh['ImageBounds.x0'], self.mdh['ImageBounds.y0'], self.mdh['ImageBounds.x1'], self.mdh['ImageBounds.y1'], self.mdh['ImageBounds.z0'], self.mdh['ImageBounds.z1']) except: return ImageBounds(0, 0, self.pixelSize * self.data.shape[0], self.pixelSize * self.data.shape[1], 0, self.sliceSize * self.data.shape[2]) @imgBounds.setter def imgBounds(self, value): self.mdh['ImageBounds.x0'] = value.x0 self.mdh['ImageBounds.y0'] = value.y0 self.mdh['ImageBounds.x1'] = value.x1 self.mdh['ImageBounds.y1'] = value.y1 self.mdh['ImageBounds.z0'] = value.z0 self.mdh['ImageBounds.z1'] = value.z1 @property def origin(self): #the origin, in nm from the camera - used for overlaying with different ROIs if 'Origin.x' in self.mdh.getEntryNames(): return self.mdh['Origin.x'], self.mdh['Origin.y'], self.mdh[ 'Origin.z'] elif 'Camera.ROIPosX' in self.mdh.getEntryNames(): #has ROI information try: voxx, voxy = 1e3 * self.mdh['voxelsize.x'], 1e3 * self.mdh[ 'voxelsize.y'] except AttributeError: voxx = self.pixelSize voxy = voxx ox = (self.mdh['Camera.ROIPosX'] - 1) * voxx oy = (self.mdh['Camera.ROIPosY'] - 1) * voxy oz = 0 if 'AcquisitionType' in self.mdh.getEntryNames( ) and self.mdh['AcquisitionType'] == 'Stack': oz = self.mdh['StackSettings.StartPos'] * 1e3 elif 'Positioning.PIFoc' in self.mdh.getEntryNames(): oz = self.mdh['Positioning.PIFoc'] * 1e3 return ox, oy, oz elif 'Source.Camera.ROIPosX' in self.mdh.getEntryNames(): #a rendered image with information about the source ROI voxx, voxy = 1e3 * self.mdh['Source.voxelsize.x'], 1e3 * self.mdh[ 'Source.voxelsize.y'] ox = (self.mdh['Source.Camera.ROIPosX'] - 1) * voxx oy = (self.mdh['Source.Camera.ROIPosY'] - 1) * voxy return ox, oy, 0 else: return 0, 0, 0 def LoadQueue(self, filename): '''Load data from a remote PYME.ParallelTasks.HDFTaskQueue queue using Pyro. Parameters: filename the name of the queue ''' import Pyro.core from PYME.Analysis.DataSources import TQDataSource from PYME.misc.computerName import GetComputerName compName = GetComputerName() if self.queueURI == None: #do a lookup taskQueueName = 'TaskQueues.%s' % compName self.tq = Pyro.core.getProxyForURI('PYRONAME://' + taskQueueName) else: self.tq = Pyro.core.getProxyForURI(self.queueURI) self.seriesName = filename[len('QUEUE://'):] self.dataSource = TQDataSource.DataSource(self.seriesName, self.tq) self.data = self.dataSource #this will get replaced with a wrapped version self.mdh = MetaDataHandler.QueueMDHandler(self.tq, self.seriesName) MetaData.fillInBlanks(self.mdh, self.dataSource) #self.timer.WantNotification.append(self.dsRefresh) self.events = self.dataSource.getEvents() def Loadh5(self, filename): '''Load PYMEs semi-custom HDF5 image data format. Offloads all the hard work to the HDFDataSource class''' import tables from PYME.Analysis.DataSources import HDFDataSource, BGSDataSource from PYME.Analysis.LMVis import inpFilt #open hdf5 file self.dataSource = HDFDataSource.DataSource(filename, None) #chain on a background subtraction data source, so we can easily do #background subtraction in the GUI the same way as in the analysis self.data = BGSDataSource.DataSource( self.dataSource) #this will get replaced with a wrapped version if 'MetaData' in self.dataSource.h5File.root: #should be true the whole time self.mdh = MetaData.TIRFDefault self.mdh.copyEntriesFrom( MetaDataHandler.HDFMDHandler(self.dataSource.h5File)) else: self.mdh = MetaData.TIRFDefault wx.MessageBox( "Carrying on with defaults - no gaurantees it'll work well", 'ERROR: No metadata found in file ...', wx.OK) print( "ERROR: No metadata fond in file ... Carrying on with defaults - no gaurantees it'll work well" ) #attempt to estimate any missing parameters from the data itself MetaData.fillInBlanks(self.mdh, self.dataSource) #calculate the name to use when we do batch analysis on this from PYME.ParallelTasks.relativeFiles import getRelFilename self.seriesName = getRelFilename(filename) #try and find a previously performed analysis fns = filename.split(os.path.sep) cand = os.path.sep.join(fns[:-2] + [ 'analysis', ] + fns[-2:]) + 'r' print(cand) if False: #os.path.exists(cand): h5Results = tables.openFile(cand) if 'FitResults' in dir(h5Results.root): self.fitResults = h5Results.root.FitResults[:] self.resultsSource = inpFilt.h5rSource(h5Results) self.resultsMdh = MetaData.TIRFDefault self.resultsMdh.copyEntriesFrom( MetaDataHandler.HDFMDHandler(h5Results)) self.events = self.dataSource.getEvents() def LoadHTTP(self, filename): '''Load PYMEs semi-custom HDF5 image data format. Offloads all the hard work to the HDFDataSource class''' import tables from PYME.Analysis.DataSources import HTTPDataSource, BGSDataSource #from PYME.Analysis.LMVis import inpFilt #open hdf5 file self.dataSource = HTTPDataSource.DataSource(filename) #chain on a background subtraction data source, so we can easily do #background subtraction in the GUI the same way as in the analysis self.data = BGSDataSource.DataSource( self.dataSource) #this will get replaced with a wrapped version #try: #should be true the whole time self.mdh = MetaData.TIRFDefault self.mdh.copyEntriesFrom(self.dataSource.getMetadata()) #except: # self.mdh = MetaData.TIRFDefault # wx.MessageBox("Carrying on with defaults - no gaurantees it'll work well", 'ERROR: No metadata found in file ...', wx.OK) # print("ERROR: No metadata fond in file ... Carrying on with defaults - no gaurantees it'll work well") #attempt to estimate any missing parameters from the data itself MetaData.fillInBlanks(self.mdh, self.dataSource) #calculate the name to use when we do batch analysis on this #from PYME.ParallelTasks.relativeFiles import getRelFilename self.seriesName = filename self.events = self.dataSource.getEvents() def LoadKdf(self, filename): '''load khorus formatted data - pretty much deprecated by now''' import PYME.cSMI as cSMI self.data = cSMI.CDataStack_AsArray(cSMI.CDataStack(filename), 0).squeeze() self.mdh = MetaData.TIRFDefault try: #try and get metadata from the .log file lf = open(os.path.splitext(filename)[0] + '.log') from PYME.DSView import logparser lp = logparser.logparser() log = lp.parse(lf.read()) lf.close() self.mdh.setEntry('voxelsize.z', log['PIEZOS']['Stepsize']) except: pass from PYME.ParallelTasks.relativeFiles import getRelFilename self.seriesName = getRelFilename(filename) self.mode = 'psf' def LoadPSF(self, filename): '''Load PYME .psf data. .psf files consist of a tuple containing the data and the voxelsize. ''' self.data, vox = numpy.load(filename) self.mdh = MetaData.ConfocDefault self.mdh.setEntry('voxelsize.x', vox.x) self.mdh.setEntry('voxelsize.y', vox.y) self.mdh.setEntry('voxelsize.z', vox.z) from PYME.ParallelTasks.relativeFiles import getRelFilename self.seriesName = getRelFilename(filename) self.mode = 'psf' def LoadNPY(self, filename): '''Load numpy .npy data. ''' mdfn = self.FindAndParseMetadata(filename) self.data = numpy.load(filename) from PYME.ParallelTasks.relativeFiles import getRelFilename self.seriesName = getRelFilename(filename) self.mode = 'default' def LoadDBL(self, filename): '''Load Bewersdorf custom STED data. ''' mdfn = self.FindAndParseMetadata(filename) self.data = numpy.memmap(filename, dtype='<f4', mode='r', offset=128, shape=(self.mdh['Camera.ROIWidth'], self.mdh['Camera.ROIHeight'], self.mdh['NumImages']), order='F') from PYME.ParallelTasks.relativeFiles import getRelFilename self.seriesName = getRelFilename(filename) self.mode = 'default' def FindAndParseMetadata(self, filename): '''Try and find and load a .xml or .md metadata file that might be ascociated with a given image filename. See the relevant metadatahandler classes for details.''' import xml.parsers.expat mdf = None xmlfn = os.path.splitext(filename)[0] + '.xml' xmlfnmc = os.path.splitext(filename)[0].split('__')[0] + '.xml' if os.path.exists(xmlfn): try: self.mdh = MetaDataHandler.NestedClassMDHandler( MetaData.TIRFDefault) self.mdh.copyEntriesFrom(MetaDataHandler.XMLMDHandler(xmlfn)) mdf = xmlfn except xml.parsers.expat.ExpatError: #fix for bug in which PYME .md was written with a .xml extension self.mdh = MetaDataHandler.NestedClassMDHandler( MetaData.BareBones) self.mdh.copyEntriesFrom( MetaDataHandler.SimpleMDHandler(xmlfn)) mdf = xmlfn elif os.path.exists( xmlfnmc): #this is a single colour channel of a pair self.mdh = MetaDataHandler.NestedClassMDHandler( MetaData.TIRFDefault) self.mdh.copyEntriesFrom(MetaDataHandler.XMLMDHandler(xmlfnmc)) mdf = xmlfnmc else: self.mdh = MetaDataHandler.NestedClassMDHandler(MetaData.BareBones) #check for simple metadata (python code with an .md extension which #fills a dictionary called md) mdfn = os.path.splitext(filename)[0] + '.md' if os.path.exists(mdfn): self.mdh.copyEntriesFrom(MetaDataHandler.SimpleMDHandler(mdfn)) mdf = mdfn elif filename.endswith('.lsm'): #read lsm metadata from PYME.gohlke.tifffile import TIFFfile tf = TIFFfile(filename) lsm_info = tf[0].cz_lsm_scan_information self.mdh['voxelsize.x'] = lsm_info['line_spacing'] self.mdh['voxelsize.y'] = lsm_info['line_spacing'] self.mdh['voxelsize.z'] = lsm_info['plane_spacing'] def lsm_pop(basename, dic): for k, v in dic.items(): if isinstance(v, list): #print k, v for i, l_i in enumerate(v): #print i, l_i, basename lsm_pop( basename + k + '.' + k[:-1] + '%i.' % i, l_i) else: self.mdh[basename + k] = v lsm_pop('LSM.', lsm_info) elif filename.endswith('.dbl'): #Bewersdorf lab STED mdfn = filename[:-4] + '.txt' entrydict = {} try: #try to read in extra metadata if possible with open(mdfn, 'r') as mf: for line in mf: s = line.split(':') if len(s) == 2: entrydict[s[0]] = s[1] except IOError: pass # vx, vy = entrydict['Pixel size (um)'].split('x') # self.mdh['voxelsize.x'] = float(vx) # self.mdh['voxelsize.y'] = float(vy) # self.mdh['voxelsize.z'] = 0.2 #FIXME for stacks ... # # sx, sy = entrydict['Image format'].split('x') # self.mdh['Camera.ROIWidth'] = int(sx) # self.mdh['Camera.ROIHeight'] = int(sy) # # self.mdh['NumImages'] = int(entrydict['# Images']) with open(filename) as df: s = df.read(8) Z, X, Y, T = numpy.fromstring(s, '>u2') s = df.read(16) depth, width, height, elapsed = numpy.fromstring(s, '<f4') self.mdh['voxelsize.x'] = width / X self.mdh['voxelsize.y'] = height / Y self.mdh['voxelsize.z'] = depth self.mdh['Camera.ROIWidth'] = X self.mdh['Camera.ROIHeight'] = Y self.mdh['NumImages'] = Z * T def _sanitise_key(key): k = key.replace('#', 'Num') k = k.replace('(%)', '') k = k.replace('(', '') k = k.replace(')', '') k = k.replace('.', '') k = k.replace('/', '') k = k.replace('?', '') k = k.replace(' ', '') if not k[0].isalpha(): k = 's' + k return k for k, v in entrydict.items(): self.mdh['STED.%s' % _sanitise_key(k)] = v if self.haveGUI and not ('voxelsize.x' in self.mdh.keys() and 'voxelsize.y' in self.mdh.keys()): from PYME.DSView.voxSizeDialog import VoxSizeDialog dlg = VoxSizeDialog(None) #dlg.ShowModal() self.mdh.setEntry('voxelsize.x', dlg.GetVoxX()) self.mdh.setEntry('voxelsize.y', dlg.GetVoxY()) self.mdh.setEntry('voxelsize.z', dlg.GetVoxZ()) return mdf def LoadTiff(self, filename): #from PYME.FileUtils import readTiff from PYME.Analysis.DataSources import TiffDataSource mdfn = self.FindAndParseMetadata(filename) self.dataSource = TiffDataSource.DataSource(filename, None) self.dataSource = BufferedDataSource.DataSource( self.dataSource, min(self.dataSource.getNumSlices(), 50)) self.data = self.dataSource #this will get replaced with a wrapped version #if we have a multi channel data set, try and pull in all the channels if 'ChannelFiles' in self.mdh.getEntryNames(): try: from PYME.DSView.dataWrap import ListWrap #pull in all channels chans = [] for cf in self.mdh.getEntry('ChannelFiles'): cfn = os.path.join(os.path.split(filename)[0], cf) ds = TiffDataSource.DataSource(cfn, None) ds = BufferedDataSource.DataSource( ds, min(ds.getNumSlices(), 50)) chans.append(ds) self.data = ListWrap( chans) #this will get replaced with a wrapped version self.filename = mdfn except: pass elif 'ChannelNames' in self.mdh.getEntryNames() and len( self.mdh['ChannelNames']) == self.data.getNumSlices(): from PYME.DSView.dataWrap import ListWrap chans = [ numpy.atleast_3d(self.data.getSlice(i)) for i in range(len(self.mdh['ChannelNames'])) ] self.data = ListWrap(chans) elif filename.endswith( '.lsm') and 'LSM.images_number_channels' in self.mdh.keys( ) and self.mdh['LSM.images_number_channels'] > 1: from PYME.DSView.dataWrap import ListWrap nChans = self.mdh['LSM.images_number_channels'] chans = [] for n in range(nChans): ds = TiffDataSource.DataSource(filename, None, n) ds = BufferedDataSource.DataSource(ds, min(ds.getNumSlices(), 50)) chans.append(ds) self.data = ListWrap(chans) #self.data = readTiff.read3DTiff(filename) from PYME.ParallelTasks.relativeFiles import getRelFilename self.seriesName = getRelFilename(filename) self.mode = 'default' def LoadImageSeries(self, filename): #from PYME.FileUtils import readTiff from PYME.Analysis.DataSources import ImageSeriesDataSource self.dataSource = ImageSeriesDataSource.DataSource(filename, None) self.dataSource = BufferedDataSource.DataSource( self.dataSource, min(self.dataSource.getNumSlices(), 50)) self.data = self.dataSource #this will get replaced with a wrapped version #self.data = readTiff.read3DTiff(filename) self.FindAndParseMetadata(filename) from PYME.ParallelTasks.relativeFiles import getRelFilename self.seriesName = getRelFilename(filename) self.mode = 'default' def Load(self, filename=None): print r'> image.py: Loading file', filename if (filename == None): import wx #only introduce wx dependency here - so can be used non-interactively global lastdir fdialog = wx.FileDialog( None, 'Please select Data Stack to open ...', wildcard= 'Image Data|*.h5;*.tif;*.lsm;*.kdf;*.md;*.psf;*.npy;*.dbl|All files|*.*', style=wx.OPEN, defaultDir=lastdir) succ = fdialog.ShowModal() if (succ == wx.ID_OK): filename = fdialog.GetPath() lastdir = fdialog.GetDirectory() if not filename == None: if filename.startswith('QUEUE://'): self.LoadQueue(filename) elif filename.startswith('http://'): self.LoadHTTP(filename) elif filename.endswith('.h5'): self.Loadh5(filename) elif filename.endswith('.kdf'): self.LoadKdf(filename) elif filename.endswith('.psf'): #psf self.LoadPSF(filename) elif filename.endswith( '.md'): #treat this as being an image series self.LoadImageSeries(filename) elif filename.endswith( '.npy'): #treat this as being an image series self.LoadNPY(filename) elif filename.endswith( '.dbl'): #treat this as being an image series self.LoadDBL(filename) else: #try tiff self.LoadTiff(filename) #self.SetTitle(filename) self.filename = filename self.saved = True def Save(self, filename=None, crop=False, view=None): import dataExporter ofn = self.filename if crop: dataExporter.CropExportData(view, self.mdh, self.events, self.seriesName) else: if 'defaultExt' in dir(self): self.filename = dataExporter.ExportData( self.data, self.mdh, self.events, defaultExt=self.defaultExt, filename=filename) else: self.filename = dataExporter.ExportData(self.data, self.mdh, self.events, filename=filename) #self.SetTitle(fn) if not (self.filename == None): self.saved = True openImages.pop(ofn) openImages[self.filename] = self else: self.filename = ofn