def __init__(self, host): AnalysisModule.__init__(self, host) self.items = weakref.WeakKeyDictionary() self.files = weakref.WeakValueDictionary() self.ctrl = QtGui.QWidget() self.ui = Ui_Form() self.ui.setupUi(self.ctrl) self.atlas = None self.canvas = Canvas(name='MosaicEditor') self._elements_ = OrderedDict([ ('File Loader', {'type': 'fileInput', 'size': (200, 300), 'host': self}), ('Mosaic', {'type': 'ctrl', 'object': self.ctrl, 'pos': ('right',), 'size': (600, 100)}), ('Canvas', {'type': 'ctrl', 'object': self.canvas.ui.view, 'pos': ('bottom', 'Mosaic'), 'size': (600, 800)}), ('ItemList', {'type': 'ctrl', 'object': self.canvas.ui.canvasCtrlWidget, 'pos': ('right', 'Canvas'), 'size': (200, 400)}), ('ItemCtrl', {'type': 'ctrl', 'object': self.canvas.ui.canvasItemCtrl, 'pos': ('bottom', 'ItemList'), 'size': (200, 400)}), ]) self.initializeElements() self.clear(ask=False) self.ui.fileLoader = self.getElement('File Loader', create=True) self.ui.fileLoader.ui.fileTree.hide() try: self.ui.fileLoader.setBaseClicked() # get the currently selected directory in the DataManager except: pass for a in atlas.listAtlases(): self.ui.atlasCombo.addItem(a) # Add buttons to the canvas control panel self.btnBox = QtGui.QWidget() self.btnLayout = QtGui.QGridLayout() self.btnLayout.setContentsMargins(0, 0, 0, 0) self.btnBox.setLayout(self.btnLayout) l = self.canvas.ui.gridLayout l.addWidget(self.btnBox, l.rowCount(), 0, 1, l.columnCount()) self.saveBtn = QtGui.QPushButton("Save ...") self.saveBtn.clicked.connect(self.saveClicked) self.btnLayout.addWidget(self.saveBtn, 0, 0) self.clearBtn = QtGui.QPushButton("Clear All") self.clearBtn.clicked.connect(lambda: self.clear(ask=True)) self.btnLayout.addWidget(self.clearBtn, 0, 1) self.canvas.sigItemTransformChangeFinished.connect(self.itemMoved) self.ui.atlasCombo.currentIndexChanged.connect(self.atlasComboChanged) self.ui.normalizeBtn.clicked.connect(self.normalizeImages) self.ui.tileShadingBtn.clicked.connect(self.rescaleImages) self.ui.mosaicApplyScaleBtn.clicked.connect(self.updateScaling) self.ui.mosaicFlipLRBtn.clicked.connect(self.flipLR) self.ui.mosaicFlipUDBtn.clicked.connect(self.flipUD) self.imageMax = 0.0
class MosaicEditor(AnalysisModule): # Version number for save format. # increment minor version number for backward-compatible changes # increment major version number for backward-incompatible changes _saveVersion = (2, 0) # Types that appear in the dropdown menu as addable items # Use MosaicEditor.registerItemType to add to this list. _addTypes = OrderedDict() # used to allow external modules to add their own ui elements to the mosaic editor _extensions = OrderedDict() @classmethod def addExtension(cls, name, spec): """Add a specification for a UI element to add to newly created MosaicEditor instances. Format is: { 'type': 'ctrl', 'builder': callable, 'pos': ('right', 'Canvas'), 'size': (200, 400), } Where *callable* will be called with the MosaicEditor as its argument and must return a QWidget to be inserted into the UI. """ cls._extensions[name] = spec def __init__(self, host): AnalysisModule.__init__(self, host=host) self.items = weakref.WeakKeyDictionary() self.files = weakref.WeakValueDictionary() self.ctrl = QtGui.QWidget() self.ui = Ui_Form() self.ui.setupUi(self.ctrl) self.atlas = None self.canvas = Canvas(name='MosaicEditor') self._elements_ = OrderedDict([ ('File Loader', { 'type': 'fileInput', 'size': (200, 300), 'host': self }), ('Mosaic', { 'type': 'ctrl', 'object': self.ctrl, 'pos': ('right', ), 'size': (600, 100) }), ('Canvas', { 'type': 'ctrl', 'object': self.canvas.ui.view, 'pos': ('bottom', 'Mosaic'), 'size': (600, 800) }), ('ItemList', { 'type': 'ctrl', 'object': self.canvas.ui.canvasCtrlWidget, 'pos': ('right', 'Canvas'), 'size': (200, 400) }), ('ItemCtrl', { 'type': 'ctrl', 'object': self.canvas.ui.canvasItemCtrl, 'pos': ('bottom', 'ItemList'), 'size': (200, 400) }), ]) for name, spec in self._extensions.items(): builder = spec.pop('builder', None) if builder is not None: spec['object'] = builder(self) self._elements_[name] = spec self.initializeElements() self.clear(ask=False) self.ui.fileLoader = self.getElement('File Loader', create=True) self.ui.fileLoader.ui.fileTree.hide() try: self.ui.fileLoader.setBaseClicked( ) # get the currently selected directory in the DataManager except: pass for a in atlas.listAtlases(): self.ui.atlasCombo.addItem(a) # Add buttons to the canvas control panel self.btnBox = QtGui.QWidget() self.btnLayout = QtGui.QGridLayout() self.btnLayout.setContentsMargins(0, 0, 0, 0) self.btnBox.setLayout(self.btnLayout) l = self.canvas.ui.gridLayout l.addWidget(self.btnBox, l.rowCount(), 0, 1, l.columnCount()) self.addCombo = QtGui.QComboBox() self.addCombo.currentIndexChanged.connect(self._addItemChanged) self.btnLayout.addWidget(self.addCombo, 0, 0, 1, 2) self.addCombo.addItem('Add item..') self.saveBtn = QtGui.QPushButton("Save ...") self.saveBtn.clicked.connect(self.saveClicked) self.btnLayout.addWidget(self.saveBtn, 1, 0) self.clearBtn = QtGui.QPushButton("Clear All") self.clearBtn.clicked.connect(lambda: self.clear(ask=True)) self.btnLayout.addWidget(self.clearBtn, 1, 1) self.canvas.sigItemTransformChangeFinished.connect(self.itemMoved) self.ui.atlasCombo.currentIndexChanged.connect(self.atlasComboChanged) self.ui.normalizeBtn.clicked.connect(self.normalizeImages) self.ui.tileShadingBtn.clicked.connect(self.rescaleImages) self.ui.mosaicApplyScaleBtn.clicked.connect(self.updateScaling) self.ui.mosaicFlipLRBtn.clicked.connect(self.flipLR) self.ui.mosaicFlipUDBtn.clicked.connect(self.flipUD) self.imageMax = 0.0 for menuString in self._addTypes: self.addCombo.addItem(menuString) @classmethod def registerItemType(cls, itemclass, menuString=None): """Add an item type to the list of addable items. """ if menuString is None: menuString = itemclass.typeName() if itemclass.__name__ not in items.itemTypes(): items.registerItemType(itemclass) cls._addTypes[menuString] = itemclass.__name__ def _addItemChanged(self, index): # User requested to create and add a new item if index <= 0: return itemtype = self._addTypes[self.addCombo.currentText()] self.addCombo.setCurrentIndex(0) self.addItem(type=itemtype) def atlasComboChanged(self, ind): if ind == 0: self.closeAtlas() return name = self.ui.atlasCombo.currentText() self.loadAtlas(name) def closeAtlas(self): if self.atlas is not None: self.atlas.close() self.atlas = None while True: ch = self.ui.atlasLayout.takeAt(0) if ch is None: break ch = ch.widget() ch.hide() ch.setParent(None) def loadAtlas(self, name): name = str(name) self.closeAtlas() cls = atlas.getAtlasClass(name) obj = cls() ctrl = obj.ctrlWidget(host=self) self.ui.atlasLayout.addWidget(ctrl, 0, 0) self.atlas = ctrl def loadFileRequested(self, files): if files is None: return for f in files: if f.shortName().endswith('.mosaic'): self.loadStateFile(f.name()) continue if f in self.files: ## Do not allow loading the same file more than once item = self.files[f] item.show() # just show the file; but do not load it continue if f.isFile(): # add specified files item = self.addFile(f) elif f.isDir(): # Directories are more complicated if self.dataModel.dirType( f ) == 'Cell': # If it is a cell, just add the cell "Marker" to the plot item = self.canvas.addFile(f) else: # in all other directory types, look for MetaArray files filesindir = glob.glob(f.name() + '/*.ma') for fd in filesindir: # add files in the directory (ma files: e.g., images, videos) try: fdh = DataManager.getFileHandle( fd) # open file to get handle. except IOError: continue # just skip file item = self.addFile(fdh) if len(filesindir) == 0: # add protocol sequences item = self.addFile(f) self.canvas.autoRange() def addFile(self, f, name=None, inheritTransform=True): """Load a file and add it to the canvas. The new item will inherit the user transform from the previous item (chronologocally) if it does not already have a user transform specified. """ item = self.canvas.addFile(f, name=name) self.canvas.selectItem(item) if isinstance(item, list): item = item[0] self.items[item] = f self.files[f] = item try: item.timestamp = f.info()['__timestamp__'] except: item.timestamp = None ## load or guess user transform for this item if inheritTransform and not item.hasUserTransform( ) and item.timestamp is not None: ## Record the timestamp for this file, see what is the most recent transformation to copy best = None for i2 in self.items: if i2 is item: continue if i2.timestamp is None: continue if i2.timestamp < item.timestamp: if best is None or i2.timestamp > best.timestamp: best = i2 if best is not None: trans = best.saveTransform() item.restoreTransform(trans) return item def addItem(self, item=None, type=None, **kwds): """Add an item to the MosaicEditor canvas. May provide either *item* which is a CanvasItem or QGraphicsItem instance, or *type* which is a string specifying the type of item to create and add. """ if isinstance(item, QtGui.QGraphicsItem): return self.canvas.addGraphicsItem(item, **kwds) else: return self.canvas.addItem(item, type, **kwds) def rescaleImages(self): """ Apply corrections to the images and rescale the data. This does the following: 1. compute mean image over entire selected group 2. smooth the mean image heavily. 3. rescale the images and correct for field flatness from the average image 4. apply the scale. Use the min/max mosaic button to readjust the display scale after this automatic operation if the scaling is not to your liking. """ nsel = len(self.canvas.selectedItems()) if nsel == 0: return # print dir(self.selectedItems()[0].data) nxm = self.canvas.selectedItems()[0].data.shape meanImage = np.zeros((nxm[0], nxm[1])) nhistbins = 100 # generate a histogram of the global levels in the image (all images selected) hm = np.histogram( np.dstack([x.data for x in self.canvas.selectedItems()]), nhistbins) print hm #$meanImage = np.mean(self.selectedItems().asarray(), axis=0) n = 0 self.imageMax = 0.0 print 'nsel: ', nsel for i in range(nsel): try: meanImage = meanImage + np.array( self.canvas.selectedItems()[i].data) imagemax = np.amax(np.amax(meanImage, axis=1), axis=0) if imagemax > self.imageMax: self.imageMax = imagemax n = n + 1 except: print 'image i = %d failed' % i print 'file name: ', self.canvas.selectedItems()[i].name print 'expected shape of nxm: ', nxm print ' but got data shape: ', self.canvas.selectedItems( )[i].data.shape meanImage = meanImage / n # np.mean(meanImage[0:n], axis=0) filtwidth = np.floor(nxm[0] / 10 + 1) blimg = scipy.ndimage.filters.gaussian_filter(meanImage, filtwidth, order=0, mode='reflect') #pg.image(blimg) m = np.argmax(hm[0]) # returns the index of the max count print 'm = ', m # now rescale each individually # rescaling is done against the global histogram, to keep the gain constant. for i in range(nsel): d = np.array(self.canvas.selectedItems()[i].data) # hmd = np.histogram(d, 512) # return (count, bins) xh = d.shape # capture shape just in case it is not right (have data that is NOT !!) # flatten the illumination using the blimg average illumination pattern newImage = d # / blimg[0:xh[0], 0:xh[1]] # (d - imin)/(blimg - imin) # rescale image. hn = np.histogram(newImage, bins=hm[1]) # use bins from global image n = np.argmax(hn[0]) newImage = (hm[1][m] / hn[1][n]) * newImage # rescale to the global max. self.canvas.selectedItems()[i].updateImage(newImage) # self.canvas.selectedItems()[i].levelRgn.setRegion([0, 2.0]) self.canvas.selectedItems()[i].levelRgn.setRegion( [0., self.imageMax]) print "MosaicEditor::self imageMax: ", self.imageMax def normalizeImages(self): self.canvas.view.autoRange() def updateScaling(self): """ Set all the selected images to have the scaling in the editor bar (absolute values) """ nsel = len(self.canvas.selectedItems()) if nsel == 0: return for i in range(nsel): self.canvas.selectedItems()[i].levelRgn.setRegion([ self.ui.mosaicDisplayMin.value(), self.ui.mosaicDisplayMax.value() ]) def flipUD(self): """ flip each image array up/down, in place. Do not change position. Note: arrays are rotated, so use lr to do ud, etc. """ nsel = len(self.canvas.selectedItems()) if nsel == 0: return for i in range(nsel): self.canvas.selectedItems()[i].data = np.fliplr( self.canvas.selectedItems()[i].data) self.canvas.selectedItems()[i].graphicsItem().updateImage( self.canvas.selectedItems()[i].data) # print dir(self.canvas.selectedItems()[i]) def flipLR(self): """ Flip each image array left/right, in place. Do not change position. """ nsel = len(self.canvas.selectedItems()) if nsel == 0: return for i in range(nsel): self.canvas.selectedItems()[i].data = np.flipud( self.canvas.selectedItems()[i].data) self.canvas.selectedItems()[i].graphicsItem().updateImage( self.canvas.selectedItems()[i].data) def itemMoved(self, canvas, item): """Save an item's transformation if the user has moved it. This is saved in the 'userTransform' attribute; the original position data is not affected.""" fh = self.items.get(item, None) if not hasattr(fh, 'setInfo'): fh = None try: item.storeUserTransform(fh) except Exception as ex: if len(ex.args) > 1 and ex.args[ 1] == 1: ## this means the item has no file handle to store position return raise def getLoadedFiles(self): """Return a list of all file handles that have been loaded""" return self.items.values() def clear(self, ask=True): """Remove all loaded data and reset to the default state. If ask is True (and there are items loaded), then the user is prompted before clearing. If the user declines, then this method returns False. """ if ask and len(self.items) > 0: response = QtGui.QMessageBox.question( self.clearBtn, "Warning", "Really clear all items?", QtGui.QMessageBox.Ok | QtGui.QMessageBox.Cancel) if response != QtGui.QMessageBox.Ok: return False self.canvas.clear() self.items.clear() self.files.clear() self.lastSaveFile = None return True def saveState(self, relativeTo=None): """Return a serializable representation of the current state of the MosaicEditor. This includes the list of all items, their current visibility and parameters, and the view configuration. """ items = list(self.canvas.items) items.sort(key=lambda i: i.zValue()) return OrderedDict([ ('contents', 'MosaicEditor_save'), ('version', self._saveVersion), ('rootPath', relativeTo.name() if relativeTo is not None else ''), ('items', [item.saveState(relativeTo=relativeTo) for item in items]), ('view', self.canvas.view.getState()), ]) def saveStateFile(self, filename): dh = DataManager.getDirHandle(os.path.dirname(filename)) state = self.saveState(relativeTo=dh) json.dump(state, open(filename, 'w'), indent=4, cls=Encoder) def restoreState(self, state, rootPath=None): if state.get('contents', None) != 'MosaicEditor_save': raise TypeError( "This does not appear to be MosaicEditor save data.") if state['version'][0] > self._saveVersion[0]: raise TypeError( "Save data has version %d.%d, but this MosaicEditor only supports up to version %d.x." % (state['version'][0], state['version'][1], self._saveVersion[0])) if not self.clear(): return root = state['rootPath'] if root == '': # data was stored with no root path; filenames should be absolute root = None else: # data was stored with no root path; filenames should be relative to the loaded file root = DataManager.getHandle(rootPath) loadfail = [] for itemState in state['items']: fname = itemState.get('filename') if fname is None: # create item from scratch and restore state itemtype = itemState.get('type') if itemtype not in items.itemTypes(): # warn the user later on that we could not load this item loadfail.append((itemState.get('name'), 'Unknown item type "%s"' % itemtype)) continue item = self.addItem(type=itemtype, name=itemState['name']) else: # create item by loading file and restore state if root is None: fh = DataManager.getHandle(fh) else: fh = root[fname] item = self.addFile(fh, name=itemState['name'], inheritTransform=False) item.restoreState(itemState) self.canvas.view.setState(state['view']) if len(loadfail) > 0: msg = "\n".join(["%s: %s" % m for m in loadfail]) raise Exception("Failed to load some items:\n%s" % msg) def loadStateFile(self, filename): state = json.load(open(filename, 'r')) self.restoreState(state, rootPath=os.path.dirname(filename)) def saveClicked(self): base = self.ui.fileLoader.baseDir() if self.lastSaveFile is None: path = base.name() else: path = self.lastSaveFile filename = QtGui.QFileDialog.getSaveFileName( None, "Save mosaic file", path, "Mosaic files (*.mosaic)") if filename == '': return if not filename.endswith('.mosaic'): filename += '.mosaic' self.lastSaveFile = filename self.saveStateFile(filename) def quit(self): self.files = None self.items = None self.canvas.clear()