def __init__(self, name, shape, dtype, axistags="not none"): OutputSlot.__init__(self,name) self.meta.shape = shape self.meta.dtype = dtype self._data = None self.meta.axistags = axistags self._lock = threading.Lock()
class OpDetectMissing(Operator): ''' Sub-Operator for detection of missing image content ''' InputVolume = InputSlot() PatchSize = InputSlot(value=128) HaloSize = InputSlot(value=30) DetectionMethod = InputSlot(value='classic') NHistogramBins = InputSlot(value=_defaultBinSize) OverloadDetector = InputSlot(value='') #histograms: ndarray, shape: nHistograms x (NHistogramBins.value + 1) # the last column holds the label, i.e. {0: negative, 1: positive} TrainingHistograms = InputSlot() Output = OutputSlot() Detector = OutputSlot(stype=Opaque) ### PRIVATE class attributes ### _manager = None ### PRIVATE attributes ### _inputRange = (0, 255) _needsTraining = True _felzenOpts = {"firstSamples": 250, "maxRemovePerStep": 0, "maxAddPerStep": 250, "maxSamples": 1000, "nTrainingSteps": 4} def __init__(self, *args, **kwargs): super(OpDetectMissing, self).__init__(*args, **kwargs) self.TrainingHistograms.setValue(_defaultTrainingHistograms()) def propagateDirty(self, slot, subindex, roi): if slot == self.InputVolume: self.Output.setDirty(roi) if slot == self.TrainingHistograms: OpDetectMissing._needsTraining = True if slot == self.NHistogramBins: OpDetectMissing._needsTraining = \ OpDetectMissing._manager.has(self.NHistogramBins.value) if slot == self.PatchSize or slot == self.HaloSize: self.Output.setDirty() if slot == self.OverloadDetector: s = self.OverloadDetector.value self.loads(s) self.Output.setDirty() def setupOutputs(self): self.Output.meta.assignFrom(self.InputVolume.meta) self.Output.meta.dtype = np.uint8 # determine range of input if self.InputVolume.meta.dtype == np.uint8: r = (0, 255) elif self.InputVolume.meta.dtype == np.uint16: r = (0, 65535) else: #FIXME hardcoded range, use np.iinfo r = (0, 255) self._inputRange = r self.Detector.meta.shape = (1,) def execute(self, slot, subindex, roi, result): if slot == self.Detector: result = self.dumps() return result # sanity check assert self.DetectionMethod.value in ['svm', 'classic'], \ "Unknown detection method '{}'".format(self.DetectionMethod.value) # prefill result resultZYXCT = vigra.taggedView( result, self.InputVolume.meta.axistags).withAxes(*'zyxct') # acquire data data = self.InputVolume.get(roi).wait() dataZYXCT = vigra.taggedView( data, self.InputVolume.meta.axistags).withAxes(*'zyxct') # walk over time and channel axes for t in range(dataZYXCT.shape[4]): for c in range(dataZYXCT.shape[3]): resultZYXCT[..., c, t] = \ self._detectMissing(dataZYXCT[..., c, t]) return result def _detectMissing(self, data): ''' detects missing regions and labels each missing region with 1 :param data: 3d data with axistags 'zyx' :type data: array-like ''' assert data.axistags.index('z') == 0 \ and data.axistags.index('y') == 1 \ and data.axistags.index('x') == 2 \ and len(data.shape) == 3, \ "Data must be 3d with axis 'zyx'." result = np.zeros(data.shape, dtype=np.uint8) patchSize = self.PatchSize.value haloSize = self.HaloSize.value if patchSize is None or not patchSize > 0: raise ValueError("PatchSize must be a positive integer") if haloSize is None or haloSize < 0: raise ValueError("HaloSize must be a non-negative integer") maxZ = data.shape[0] # walk over slices for z in range(maxZ): patches, slices = _patchify(data[z, :, :], patchSize, haloSize) hists = [] # walk over patches for patch in patches: (hist, _) = np.histogram( patch, bins=self.NHistogramBins.value, range=self._inputRange, density=True) hists.append(hist) hists = np.vstack(hists) pred = self.predict(hists, method=self.DetectionMethod.value) for i, p in enumerate(pred): if p > 0: #patch is classified as missing result[z, slices[i][0], slices[i][1]] |= 1 return result def train(self, force=False): ''' trains with samples drawn from slot TrainingHistograms (retrains only if bin size is currently untrained or force is True) ''' # return early if unneccessary if not force and not OpDetectMissing._needsTraining and \ OpDetectMissing._manager.has(self.NHistogramBins.value): return #return if we don't have svms if not havesklearn: return logger.debug("Training for {} histogram bins ...".format( self.NHistogramBins.value)) if self.DetectionMethod.value == 'classic' or not havesklearn: # no need to train this return histograms = self.TrainingHistograms[:].wait() logger.debug("Finished loading histogram data of shape {}.".format( histograms.shape)) assert histograms.shape[1] >= self.NHistogramBins.value+1 and \ len(histograms.shape) == 2, \ "Training data has wrong shape (expected: (n,{}), got: {}.".format( self.NHistogramBins.value+1, histograms.shape) labels = histograms[:, self.NHistogramBins.value] histograms = histograms[:, :self.NHistogramBins.value] neg_inds = np.where(labels == 0)[0] pos_inds = np.setdiff1d(np.arange(len(labels)), neg_inds) pos = histograms[pos_inds] neg = histograms[neg_inds] npos = len(pos) nneg = len(neg) #prepare for 10-fold cross-validation nfolds = 10 cfp = np.zeros((nfolds,)) cfn = np.zeros((nfolds,)) cprec = np.zeros((nfolds,)) crec = np.zeros((nfolds,)) pos_random = np.random.permutation(len(pos)) neg_random = np.random.permutation(len(neg)) logger.debug( "Starting training with " + "{} negative patches and {} positive patches...".format( len(neg), len(pos))) self._felzenszwalbTraining(neg, pos) logger.debug("Finished training.") OpDetectMissing._needsTraining = False def _felzenszwalbTraining(self, negative, positive): ''' we want to train on a 'hard' subset of the training data, see FELZENSZWALB ET AL.: OBJECT DETECTION WITH DISCRIMINATIVELY TRAINED PART-BASED MODELS (4.4), PAMI 32/9 ''' #TODO sanity checks n = (self.PatchSize.value + self.HaloSize.value)**2 method = self.DetectionMethod.value # set options for Felzenszwalb training firstSamples = self._felzenOpts["firstSamples"] maxRemovePerStep = self._felzenOpts["maxRemovePerStep"] maxAddPerStep = self._felzenOpts["maxAddPerStep"] maxSamples = self._felzenOpts["maxSamples"] nTrainingSteps = self._felzenOpts["nTrainingSteps"] # initial choice of training samples (initNegative, choiceNegative, _, _) = \ _chooseRandomSubset(negative, min(firstSamples, len(negative))) (initPositive, choicePositive, _, _) = \ _chooseRandomSubset(positive, min(firstSamples, len(positive))) # setup for parallel training samples = [negative, positive] choice = [choiceNegative, choicePositive] S_t = [initNegative, initPositive] finished = [False, False] ### BEGIN SUBROUTINE ### def felzenstep(x, cache, ind): case = ("positive" if ind > 0 else "negative") + " set" pred = self.predict(x, method=method) hard = np.where(pred != ind)[0] easy = np.setdiff1d(list(range(len(x))), hard) logger.debug(" {}: currently {} hard and {} easy samples".format( case, len(hard), len(easy))) # shrink the cache easyInCache = np.intersect1d(easy, cache) if len(easy) > 0 else [] if len(easyInCache) > 0: (removeFromCache, _, _, _) = _chooseRandomSubset( easyInCache, min(len(easyInCache), maxRemovePerStep)) cache = np.setdiff1d(cache, removeFromCache) logger.debug(" {}: shrunk the cache by {} elements".format( case, len(removeFromCache))) # grow the cache temp = len(cache) addToCache = _chooseRandomSubset( hard, min(len(hard), maxAddPerStep))[0] cache = np.union1d(cache, addToCache) addedHard = len(cache)-temp logger.debug(" {}: grown the cache by {} elements".format( case, addedHard)) if len(cache) > maxSamples: logger.debug( " {}: Cache to big, removing elements.".format(case)) cache = _chooseRandomSubset(cache, maxSamples)[0] # apply the cache C = x[cache] return (C, cache, addedHard == 0) ### END SUBROUTINE ### ### BEGIN PARALLELIZATION FUNCTION ### def partFun(i): (C, newChoice, newFinished) = felzenstep(samples[i], choice[i], i) S_t[i] = C choice[i] = newChoice finished[i] = newFinished ### END PARALLELIZATION FUNCTION ### for k in range(nTrainingSteps): logger.debug( "Felzenszwalb Training " + "(step {}/{}): {} hard negative samples, {}".format( k+1, nTrainingSteps, len(S_t[0]), len(S_t[1])) + "hard positive samples.") self.fit(S_t[0], S_t[1], method=method) pool = RequestPool() for i in range(len(S_t)): req = Request(partial(partFun, i)) pool.add(req) pool.wait() pool.clean() if np.all(finished): #already have all hard examples in training set break self.fit(S_t[0], S_t[1], method=method) logger.debug(" Finished Felzenszwalb Training.")
class OpFormattedDataExport(Operator): """ Wraps OpExportSlot, but with optional preprocessing: - cut out a subregion - renormalize the data - convert to a different dtype - transpose axis order """ TransactionSlot = InputSlot( ) # To apply all settings in one 'transaction', # disconnect this slot and reconnect it when all slots are ready # This avoids multiple calls to setupOutputs when setting several optional slots in a row. Input = InputSlot() # Subregion params: 'None' can be provided for any axis, in which case it means 'full range' for that axis RegionStart = InputSlot(optional=True) RegionStop = InputSlot(optional=True) # Normalization params InputMin = InputSlot(optional=True) InputMax = InputSlot(optional=True) ExportMin = InputSlot(optional=True) ExportMax = InputSlot(optional=True) ExportDtype = InputSlot(optional=True) OutputAxisOrder = InputSlot(optional=True) # File settings OutputFilenameFormat = InputSlot( value=os.path.expanduser('~') + os.sep + 'RESULTS_{roi}' ) # A format string allowing {roi}, {x_start}, {x_stop}, etc. OutputInternalPath = InputSlot(value='exported_data') OutputFormat = InputSlot(value='hdf5') ConvertedImage = OutputSlot() # Not yet re-ordered ImageToExport = OutputSlot( ) # Preview of the pre-processed image that will be exported ExportPath = OutputSlot( ) # Location of the saved file after export is complete. FormatSelectionErrorMsg = OutputSlot( ) # True or False depending on whether or not the currently selected format can support the current export data. ALL_FORMATS = OpExportSlot.ALL_FORMATS # Simplified block diagram: -> ConvertedImage -> FormatSelectionErrorMsg # / / # Input -> opSubRegion -> opDrangeInjection -> opNormalizeAndConvert -> opReorderAxes -> opExportSlot -> ExportPath # \ # -> ImageToExport def __init__(self, *args, **kwargs): super(OpFormattedDataExport, self).__init__(*args, **kwargs) self._dirty = True opSubRegion = OpSubRegion(parent=self) opSubRegion.Input.connect(self.Input) self._opSubRegion = opSubRegion # If normalization parameters are provided, we inject a 'drange' # metadata item for downstream operators/gui to use. opDrangeInjection = OpMetadataInjector(parent=self) opDrangeInjection.Input.connect(opSubRegion.Output) self._opDrangeInjection = opDrangeInjection # Normalization and dtype conversion are performed in one step # using an OpPixelOperator. opNormalizeAndConvert = OpPixelOperator(parent=self) opNormalizeAndConvert.Input.connect(opDrangeInjection.Output) self._opNormalizeAndConvert = opNormalizeAndConvert # ConvertedImage shows the full result but WITHOUT axis reordering. self.ConvertedImage.connect(self._opNormalizeAndConvert.Output) opReorderAxes = OpReorderAxes(parent=self) opReorderAxes.Input.connect(opNormalizeAndConvert.Output) self._opReorderAxes = opReorderAxes self.ImageToExport.connect(opReorderAxes.Output) self._opExportSlot = OpExportSlot(parent=self) self._opExportSlot.Input.connect(opReorderAxes.Output) self._opExportSlot.OutputFormat.connect(self.OutputFormat) self.ExportPath.connect(self._opExportSlot.ExportPath) self.FormatSelectionErrorMsg.connect( self._opExportSlot.FormatSelectionErrorMsg) self.progressSignal = self._opExportSlot.progressSignal def setupOutputs(self): # Prepare subregion operator total_roi = roiFromShape(self.Input.meta.shape) total_roi = list(map(tuple, total_roi)) # Default to full roi new_start, new_stop = total_roi if self.RegionStart.ready(): # RegionStart is permitted to contain 'None' values, which we replace with zeros new_start = [x or 0 for x in self.RegionStart.value] if self.RegionStop.ready(): # RegionStop is permitted to contain 'None' values, # which we replace with the full extent of the corresponding axis new_stop = [ x_extent[0] or x_extent[1] for x_extent in zip(self.RegionStop.value, total_roi[1]) ] clipped_start = numpy.maximum(0, new_start) clipped_stop = numpy.minimum(total_roi[1], new_stop) if (clipped_start != new_start).any() or (clipped_stop != new_stop).any(): warnings.warn( "The ROI you are attempting to export exceeds the extents of your dataset. Clipping to dataset bounds." ) new_start, new_stop = tuple(clipped_start), tuple(clipped_stop) # If we're in the process of switching input data, # then the roi dimensionality might not match up. # Just leave the roi disconnected for now. if len(self.Input.meta.shape) != len(new_start) or \ len(self.Input.meta.shape) != len(new_stop): self._opSubRegion.Roi.disconnect() elif (not self._opSubRegion.Roi.ready() or self._opSubRegion.Roi.value != (new_start, new_stop)): self._opSubRegion.Roi.setValue((new_start, new_stop)) # Set up normalization and dtype conversion export_dtype = self.Input.meta.dtype if self.ExportDtype.ready(): export_dtype = self.ExportDtype.value need_normalize = (self.InputMin.ready() and self.InputMax.ready() and self.ExportMin.ready() and self.ExportMax.ready()) if need_normalize: minVal, maxVal = self.InputMin.value, self.InputMax.value outputMinVal, outputMaxVal = self.ExportMin.value, self.ExportMax.value # Force a drange onto the input slot metadata. # opNormalizeAndConvert is an OpPixelOperator, # which transforms the drange correctly in this case. self._opDrangeInjection.Metadata.setValue( {'drange': (minVal, maxVal)}) def normalize(a): numerator = numpy.float64(outputMaxVal) - numpy.float64( outputMinVal) denominator = numpy.float64(maxVal) - numpy.float64(minVal) if denominator != 0.0: frac = numpy.float32(numerator / denominator) else: # Denominator was zero. The user is probably just temporarily changing the values. frac = numpy.float32(0.0) result = numpy.asarray(outputMinVal + (a - minVal) * frac, export_dtype) return result self._opNormalizeAndConvert.Function.setValue(normalize) # The OpPixelOperator sets the drange correctly using the function we give it. output_drange = self._opNormalizeAndConvert.Output.meta.drange assert type(output_drange[0]) == export_dtype assert type(output_drange[1]) == export_dtype else: # We have no drange to set. # If the original slot metadata had a drange, # it will be propagated downstream anyway. self._opDrangeInjection.Metadata.setValue({}) # No normalization: just identity function with dtype conversion self._opNormalizeAndConvert.Function.setValue( lambda a: numpy.asarray(a, export_dtype)) # Use user-provided axis order if specified user_provided = False if self.OutputAxisOrder.ready(): try: self._opReorderAxes.AxisOrder.setValue( self.OutputAxisOrder.value) user_provided = True except KeyError: # FIXME: Why does the above line fail sometimes? warnings.warn("Ignoring invalid axis order setting") if not user_provided: if self.Input.meta.original_axistags is None: axiskeys = self.Input.meta.getAxisKeys() else: axiskeys = self.Input.meta.getOriginalAxisKeys() self._opReorderAxes.AxisOrder.setValue(''.join(axiskeys)) # Provide the coordinate offset, but only for the axes that are present in the output image tagged_input_offset = collections.defaultdict( lambda: -1, list(zip(self.Input.meta.getAxisKeys(), new_start))) output_axes = self._opReorderAxes.AxisOrder.value output_offset = [tagged_input_offset[axis] for axis in output_axes] output_offset = tuple([x for x in output_offset if x != -1]) self._opExportSlot.CoordinateOffset.setValue(output_offset) # Obtain values for possible name fields known_keys = {'roi': list(self._opSubRegion.Roi.value)} roi = numpy.array(self._opSubRegion.Roi.value) for key, (start, stop) in zip(self.Input.meta.getAxisKeys(), roi.transpose()): known_keys[key + '_start'] = start known_keys[key + '_stop'] = stop # Blank the internal path while we update the external path # to avoid invalid intermediate states of ExportPath self._opExportSlot.OutputInternalPath.setValue("") # use partial formatting to fill in non-coordinate name fields name_format = self.OutputFilenameFormat.value partially_formatted_path = format_known_keys(name_format, known_keys) self._opExportSlot.OutputFilenameFormat.setValue( partially_formatted_path) internal_dataset_format = self.OutputInternalPath.value partially_formatted_dataset_name = format_known_keys( internal_dataset_format, known_keys) self._opExportSlot.OutputInternalPath.setValue( partially_formatted_dataset_name) def execute(self, slot, subindex, roi, result): assert False, "Shouldn't get here" def propagateDirty(self, slot, subindex, roi): self._dirty = True def run_export(self): self._opExportSlot.run_export() def run_export_to_array(self): return self._opExportSlot.run_export_to_array()
class _OpLabelImage(Operator): """ Produces labeled 5D volumes. If multiple time slices and/or channels are present, each time/channel combo is treated as a separate volume for labeling, which are then stacked at the output. """ Input = InputSlot() BackgroundLabels = InputSlot( optional=True) # Must be a list: one for each channel of the volume. Output = OutputSlot() # Schematic: # # BackgroundLabels -> opBgTimeSlicer -> opBgChannelSlicer ---- # \ # Input ------------> opTimeSlicer ---> opChannelSlicer -----> opLabelers -> opChannelStacker -> opTimeStacker -> Output def __init__(self, *args, **kwargs): """ Set up the internal pipeline. Since each labeling operator can only handle a single time and channel, we split the volume along time and channel axes to produce N 3D volumes, where N=T*C. The volumes are combined again into a 5D volume on the output using stackers. See ascii schematic in comments above for an overview. """ super(_OpLabelImage, self).__init__(*args, **kwargs) self.opTimeSlicer = OpMultiArraySlicer2(parent=self) self.opTimeSlicer.AxisFlag.setValue("t") self.opTimeSlicer.Input.connect(self.Input) assert self.opTimeSlicer.Slices.level == 1 self.opChannelSlicer = OperatorWrapper(OpMultiArraySlicer2, parent=self) self.opChannelSlicer.AxisFlag.setValue("c") self.opChannelSlicer.Input.connect(self.opTimeSlicer.Slices) assert self.opChannelSlicer.Slices.level == 2 class OpWrappedVigraLabelVolume(Operator): """ This quick hack is necessary because there's not currently a way to wrap an OperatorWrapper. We need to double-wrap OpVigraLabelVolume, so we need this operator to provide the first level of wrapping. """ Input = InputSlot(level=1) BackgroundValue = InputSlot(optional=True, level=1) Output = OutputSlot(level=1) def __init__(self, *args, **kwargs): super(OpWrappedVigraLabelVolume, self).__init__(*args, **kwargs) self._innerOperator = OperatorWrapper(OpVigraLabelVolume, parent=self) self._innerOperator.Input.connect(self.Input) self._innerOperator.BackgroundValue.connect( self.BackgroundValue) self.Output.connect(self._innerOperator.Output) def execute(self, slot, subindex, roi, destination): assert False, "Shouldn't get here." def propagateDirty(self, slot, subindex, roi): pass # Nothing to do... # Wrap OpVigraLabelVolume TWICE. self.opLabelers = OperatorWrapper(OpWrappedVigraLabelVolume, parent=self) assert self.opLabelers.Input.level == 2 self.opLabelers.Input.connect(self.opChannelSlicer.Slices) # The background labels will be converted to a VigraArray with axistags 'tc' so they can # be distributed to the labeling operators via slicers in the same manner as the input data. # Here, we set up the slicers that will distribute the background labels to the appropriate labelers. self.opBgTimeSlicer = OpMultiArraySlicer2(parent=self) self.opBgTimeSlicer.AxisFlag.setValue("t") assert self.opBgTimeSlicer.Slices.level == 1 self.opBgChannelSlicer = OperatorWrapper(OpMultiArraySlicer2, parent=self) self.opBgChannelSlicer.AxisFlag.setValue("c") self.opBgChannelSlicer.Input.connect(self.opBgTimeSlicer.Slices) assert self.opBgChannelSlicer.Slices.level == 2 assert self.opLabelers.BackgroundValue.level == 2 self.opLabelers.BackgroundValue.connect(self.opBgChannelSlicer.Slices) self.opChannelStacker = OperatorWrapper(OpMultiArrayStacker, parent=self) self.opChannelStacker.AxisFlag.setValue("c") assert self.opLabelers.Output.level == 2 assert self.opChannelStacker.Images.level == 2 self.opChannelStacker.Images.connect(self.opLabelers.Output) self.opTimeStacker = OpMultiArrayStacker(parent=self) self.opTimeStacker.AxisFlag.setValue("t") assert self.opChannelStacker.Output.level == 1 assert self.opTimeStacker.Images.level == 1 self.opTimeStacker.Images.connect(self.opChannelStacker.Output) # Connect our outputs self.Output.connect(self.opTimeStacker.Output) def setupOutputs(self): assert set(self.Input.meta.getTaggedShape().keys()) == set( "txyzc" ), "OpLabelImage requires all txyzc axes to be present in the input." # These slots couldn't be configured in __init__ because Input wasn't connected yet. self.opChannelStacker.AxisIndex.setValue( self.Input.meta.axistags.index("c")) self.opTimeStacker.AxisIndex.setValue( self.Input.meta.axistags.index("t")) taggedShape = self.Input.meta.getTaggedShape() if self.BackgroundLabels.ready(): # Turn this list into an array with axistags='tc' that can be sliced by time and channel, # just like the input data bgLabelList = self.BackgroundLabels.value assert ( len(bgLabelList) == taggedShape["c"] ), "If background labels are provided, there must be one for each input channel" bgLabelVolume = numpy.ndarray(shape=(taggedShape["t"], taggedShape["c"]), dtype=numpy.uint32) # Duplicate the bg label list for all time slices bgLabelVolume[...] = bgLabelList bgLabelVolume = bgLabelVolume.view(vigra.VigraArray) bgLabelVolume.axistags = vigra.defaultAxistags("tc") self.opBgTimeSlicer.Input.setValue(bgLabelVolume) else: self.opBgTimeSlicer.Input.disconnect() def execute(self, slot, subindex, roi, destination): assert False, "Shouldn't get here." def propagateDirty(self, slot, subindex, roi): pass # Nothing to do...
class OpIntegralImage(Operator): """ Computes the integral image of the input volume. For multi-channel volumes, the integral image for each channel is computed independently. The integral image operation is equivalent to: output = input_image.copy() for i in range(a.ndim): np.add.accumulate(output, axis=i, out=output) (That is, simply integrate over all axes of the volume.) But here, we use iiboost.computeIntegralImage() because it seems to be faster than the above numpy code. """ Input = InputSlot() Output = OutputSlot() def setupOutputs(self): assert len(self.Input.meta.shape ) == 4, "Data must be exactly 3D+c (no time axis)" assert self.Input.meta.getAxisKeys()[-1] == 'c' self.Output.meta.assignFrom(self.Input.meta) self.Output.meta.dtype = numpy.float32 if self.Input.meta.channel_names: self.Output.meta.channel_names = [ "Integrated " + name for name in self.Input.meta.channel_names ] def execute(self, slot, subindex, roi, result): def compute_for_channel(output_channel, input_channel): input_roi = numpy.array((roi.start, roi.stop)) input_roi[:, -1] = (input_channel, input_channel + 1) input_req = self.Input(*input_roi) # If possible, use the result array itself as a scratch area if self.Input.meta.dtype == result.dtype: input_req.writeInto(result[..., output_channel:output_channel + 1]) input_data = input_req.wait() input_data = input_data.astype(numpy.float32, order='C', copy=False) input_data = input_data[..., 0] # drop channel axis result[..., output_channel] = computeIntegralImage(input_data) pool = RequestPool() for output_channel, input_channel in enumerate( range(roi.start[-1], roi.stop[-1])): pool.add( Request( partial(compute_for_channel, output_channel, input_channel))) pool.wait() def propagateDirty(self, slot, subindex, roi): self.Output.setDirty(roi.start, roi.stop)
class OpObjectExtraction(Operator): """The top-level operator for the object extraction applet. Computes object features and object center images. """ name = "Object Extraction" RawImage = InputSlot() BinaryImage = InputSlot() BackgroundLabels = InputSlot() # which features to compute. # nested dictionary with format: # dict[plugin_name][feature_name][parameter_name] = parameter_value # for example {"Standard Object Features": {"Mean in neighborhood":{"margin": (5, 5, 2)}}} Features = InputSlot(rtype=List, stype=Opaque, value={}) LabelImage = OutputSlot() ObjectCenterImage = OutputSlot() # the computed features. # nested dictionary with format: # dict[plugin_name][feature_name] = feature_value RegionFeatures = OutputSlot(stype=Opaque, rtype=List) # pass through the 'Features' input slot ComputedFeatureNames = OutputSlot(rtype=List, stype=Opaque) BlockwiseRegionFeatures = OutputSlot( ) # For compatibility with tracking workflow, the RegionFeatures output # has rtype=List, indexed by t. # For other workflows, output has rtype=ArrayLike, indexed by (t) LabelInputHdf5 = InputSlot(optional=True) LabelOutputHdf5 = OutputSlot() CleanLabelBlocks = OutputSlot() RegionFeaturesCacheInput = InputSlot(optional=True) RegionFeaturesCleanBlocks = OutputSlot() # Schematic: # # BackgroundLabels LabelImage # \ / # BinaryImage ---> opLabelImage ---> opRegFeats ---> opRegFeatsAdaptOutput ---> RegionFeatures # / \ # RawImage-------------------------- BinaryImage ---> opObjectCenterImage --> opCenterCache --> ObjectCenterImage def __init__(self, *args, **kwargs): super(OpObjectExtraction, self).__init__(*args, **kwargs) # internal operators self._opLabelImage = OpCachedLabelImage(parent=self) self._opRegFeats = OpCachedRegionFeatures(parent=self) self._opRegFeatsAdaptOutput = OpAdaptTimeListRoi(parent=self) self._opObjectCenterImage = OpObjectCenterImage(parent=self) # connect internal operators self._opLabelImage.Input.connect(self.BinaryImage) self._opLabelImage.InputHdf5.connect(self.LabelInputHdf5) self._opLabelImage.BackgroundLabels.connect(self.BackgroundLabels) self._opRegFeats.RawImage.connect(self.RawImage) self._opRegFeats.LabelImage.connect(self._opLabelImage.Output) self._opRegFeats.Features.connect(self.Features) self.RegionFeaturesCleanBlocks.connect(self._opRegFeats.CleanBlocks) self._opRegFeats.CacheInput.connect(self.RegionFeaturesCacheInput) self._opRegFeatsAdaptOutput.Input.connect(self._opRegFeats.Output) self._opObjectCenterImage.BinaryImage.connect(self.BinaryImage) self._opObjectCenterImage.RegionCenters.connect( self._opRegFeatsAdaptOutput.Output) self._opCenterCache = OpCompressedCache(parent=self) self._opCenterCache.Input.connect(self._opObjectCenterImage.Output) # connect outputs self.LabelImage.connect(self._opLabelImage.Output) self.ObjectCenterImage.connect(self._opCenterCache.Output) self.RegionFeatures.connect(self._opRegFeatsAdaptOutput.Output) self.BlockwiseRegionFeatures.connect(self._opRegFeats.Output) self.LabelOutputHdf5.connect(self._opLabelImage.OutputHdf5) self.CleanLabelBlocks.connect(self._opLabelImage.CleanBlocks) self.ComputedFeatureNames.connect(self.Features) # As soon as input data is available, check its constraints self.RawImage.notifyReady(self._checkConstraints) self.BinaryImage.notifyReady(self._checkConstraints) def _checkConstraints(self, *args): if self.RawImage.ready() and self.BinaryImage.ready(): rawTaggedShape = self.RawImage.meta.getTaggedShape() binTaggedShape = self.BinaryImage.meta.getTaggedShape() rawTaggedShape['c'] = None binTaggedShape['c'] = None if dict(rawTaggedShape) != dict(binTaggedShape): logger.info("Raw data and other data must have equal dimensions (different channels are okay).\n"\ "Your datasets have shapes: {} and {}".format( self.RawImage.meta.shape, self.BinaryImage.meta.shape )) msg = "Raw data and other data must have equal dimensions (different channels are okay).\n"\ "Your datasets have shapes: {} and {}".format( self.RawImage.meta.shape, self.BinaryImage.meta.shape ) raise DatasetConstraintError("Object Extraction", msg) def setupOutputs(self): taggedShape = self.RawImage.meta.getTaggedShape() for k in taggedShape.keys(): if k == 't' or k == 'c': taggedShape[k] = 1 else: taggedShape[k] = 256 self._opCenterCache.BlockShape.setValue(tuple(taggedShape.values())) def execute(self, slot, subindex, roi, result): assert False, "Shouldn't get here." def propagateDirty(self, inputSlot, subindex, roi): pass def setInSlot(self, slot, subindex, roi, value): assert slot == self.LabelInputHdf5 or slot == self.RegionFeaturesCacheInput, "Invalid slot for setInSlot(): {}".format( slot.name)
class OpRegionFeatures3d(Operator): """Produces region features for a 3d image. The image MUST have xyzc axes, and is permitted to have t axis of dim 1. Inputs: * RawVolume : the raw data on which to compute features * LabelVolume : a volume of connected components for each object in the raw data. * Features : a nested dictionary of features to compute. Features[plugin name][feature name][parameter name] = parameter value Outputs: * Output : a nested dictionary of features. Output[plugin name][feature name] = numpy.ndarray """ RawVolume = InputSlot() LabelVolume = InputSlot() Features = InputSlot(rtype=List, stype=Opaque) Output = OutputSlot() def setupOutputs(self): if self.LabelVolume.meta.axistags != self.RawVolume.meta.axistags: raise Exception('raw and label axis tags do not match') taggedOutputShape = self.LabelVolume.meta.getTaggedShape() taggedRawShape = self.RawVolume.meta.getTaggedShape() if not np.all( list( taggedOutputShape.get(k, 0) == taggedRawShape.get(k, 0) for k in "txyz")): raise Exception("shapes do not match. label volume shape: {}." " raw data shape: {}".format( self.LabelVolume.meta.shape, self.RawVolume.meta.shape)) if taggedOutputShape.get('t', 1) != 1: raise Exception('this operator cannot handle multiple time slices') if set(taggedOutputShape.keys()) - set('t') != set('xyzc'): raise Exception("Input volumes must have xyzc axes.") # Remove the spatial dims (keep t if present) del taggedOutputShape['x'] del taggedOutputShape['y'] del taggedOutputShape['z'] del taggedOutputShape['c'] self.Output.meta.shape = tuple(taggedOutputShape.values()) self.Output.meta.axistags = vigra.defaultAxistags("".join( taggedOutputShape.keys())) # The features for the entire block (in xyz) are provided for the requested tc coordinates. self.Output.meta.dtype = object def execute(self, slot, subindex, roi, result): assert len(roi.start) == len(roi.stop) == len(self.Output.meta.shape) assert slot == self.Output # Process ENTIRE volume rawVolume = self.RawVolume[:].wait() labelVolume = self.LabelVolume[:].wait() # Convert to 4D (preserve axis order) axes4d = self.RawVolume.meta.getTaggedShape().keys() axes4d = filter(lambda k: k in 'xyzc', axes4d) rawVolume = rawVolume.view(vigra.VigraArray) rawVolume.axistags = self.RawVolume.meta.axistags rawVolume4d = rawVolume.withAxes(*axes4d) labelVolume = labelVolume.view(vigra.VigraArray) labelVolume.axistags = self.LabelVolume.meta.axistags labelVolume4d = labelVolume.withAxes(*axes4d) assert np.prod(roi.stop - roi.start) == 1 acc = self._extract(rawVolume4d, labelVolume4d) result[tuple(roi.start)] = acc return result def compute_extent(self, i, image, mincoords, maxcoords, axes, margin): """Make a slicing to extract object i from the image.""" #find the bounding box (margin is always 'xyz' order) result = [None] * 3 minx = max(mincoords[i][axes.x] - margin[axes.x], 0) miny = max(mincoords[i][axes.y] - margin[axes.y], 0) # Coord<Minimum> and Coord<Maximum> give us the [min,max] # coords of the object, but we want the bounding box: [min,max), so add 1 maxx = min(maxcoords[i][axes.x] + 1 + margin[axes.x], image.shape[axes.x]) maxy = min(maxcoords[i][axes.y] + 1 + margin[axes.y], image.shape[axes.y]) result[axes.x] = slice(minx, maxx) result[axes.y] = slice(miny, maxy) try: minz = max(mincoords[i][axes.z] - margin[axes.z], 0) maxz = min(maxcoords[i][axes.z] + 1 + margin[axes.z], image.shape[axes.z]) except: minz = 0 maxz = 1 result[axes.z] = slice(minz, maxz) return result def compute_rawbbox(self, image, extent, axes): """essentially returns image[extent], preserving all channels.""" key = copy(extent) key.insert(axes.c, slice(None)) return image[tuple(key)] def _extract(self, image, labels): if not (image.ndim == labels.ndim == 4): raise Exception("both images must be 4D. raw image shape: {}" " label image shape: {}".format( image.shape, labels.shape)) # FIXME: maybe simplify? taggedShape should be easier here class Axes(object): x = image.axistags.index('x') y = image.axistags.index('y') z = image.axistags.index('z') c = image.axistags.index('c') axes = Axes() slc3d = [slice(None)] * 4 # FIXME: do not hardcode slc3d[axes.c] = 0 labels = labels[slc3d] logger.debug("Computing default features") feature_names = self.Features([]).wait() # do global features logger.debug("computing global features") extra_features_computed = False global_features = {} selected_vigra_features = [] for plugin_name, feature_dict in feature_names.iteritems(): plugin = pluginManager.getPluginByName(plugin_name, "ObjectFeatures") if plugin_name == "Standard Object Features": #expand the feature list by our default features logger.debug( "attaching default features {} to vigra features {}". format(default_features, feature_dict)) selected_vigra_features = feature_dict.keys() feature_dict.update(default_features) extra_features_computed = True global_features[plugin_name] = plugin.plugin_object.compute_global( image, labels, feature_dict, axes) extrafeats = {} if extra_features_computed: for feat_key in default_features: feature = None if feat_key in selected_vigra_features: #we wanted that feature independently feature = global_features["Standard Object Features"][ feat_key] else: feature = global_features["Standard Object Features"].pop( feat_key) feature_names["Standard Object Features"].pop(feat_key) extrafeats[feat_key] = feature else: logger.debug("default features not computed, computing separately") extrafeats_acc = vigra.analysis.extractRegionFeatures( image[slc3d].squeeze().astype(np.float32), labels.squeeze(), default_features.keys(), ignoreLabel=0) #remove the 0th object, we'll add it again later for k, v in extrafeats_acc.iteritems(): extrafeats[k] = v[1:] if len(v.shape) == 1: extrafeats[k] = extrafeats[k].reshape(extrafeats[k].shape + (1, )) extrafeats = dict( (k.replace(' ', ''), v) for k, v in extrafeats.iteritems()) mincoords = extrafeats["Coord<Minimum>"] maxcoords = extrafeats["Coord<Maximum>"] nobj = mincoords.shape[0] # local features: loop over all objects def dictextend(a, b): for key in b: a[key].append(b[key]) return a local_features = defaultdict(lambda: defaultdict(list)) margin = max_margin(feature_names) has_local_features = {} for plugin_name, feature_dict in feature_names.iteritems(): has_local_features[plugin_name] = False for features in feature_dict.itervalues(): if 'margin' in features: has_local_features[plugin_name] = True break if np.any(margin) > 0: #starting from 0, we stripped 0th background object in global computation for i in range(0, nobj): logger.debug("processing object {}".format(i)) extent = self.compute_extent(i, image, mincoords, maxcoords, axes, margin) rawbbox = self.compute_rawbbox(image, extent, axes) #it's i+1 here, because the background has label 0 binary_bbox = np.where(labels[tuple(extent)] == i + 1, 1, 0).astype(np.bool) for plugin_name, feature_dict in feature_names.iteritems(): if not has_local_features[plugin_name]: continue plugin = pluginManager.getPluginByName( plugin_name, "ObjectFeatures") feats = plugin.plugin_object.compute_local( rawbbox, binary_bbox, feature_dict, axes) local_features[plugin_name] = dictextend( local_features[plugin_name], feats) logger.debug("computing done, removing failures") # remove local features that failed for pname, pfeats in local_features.iteritems(): for key in pfeats.keys(): value = pfeats[key] try: pfeats[key] = np.vstack( list(v.reshape(1, -1) for v in value)) except: logger.warn('feature {} failed'.format(key)) del pfeats[key] # merge the global and local features logger.debug("removed failed, merging") all_features = {} plugin_names = set(global_features.keys()) | set(local_features.keys()) for name in plugin_names: d1 = global_features.get(name, {}) d2 = local_features.get(name, {}) all_features[name] = dict(d1.items() + d2.items()) all_features[default_features_key] = extrafeats # reshape all features for pfeats in all_features.itervalues(): for key, value in pfeats.iteritems(): if value.shape[0] != nobj: raise Exception( 'feature {} does not have enough rows, {} instead of {}' .format(key, value.shape[0], nobj)) # because object classification operator expects nobj to # include background. FIXME: we should change that assumption. value = np.vstack((np.zeros(value.shape[1]), value)) value = value.astype(np.float32) #turn Nones into numpy.NaNs assert value.dtype == np.float32 assert value.shape[0] == nobj + 1 assert value.ndim == 2 pfeats[key] = value logger.debug("merged, returning") return all_features def propagateDirty(self, slot, subindex, roi): if slot is self.Features: self.Output.setDirty(slice(None)) else: axes = self.RawVolume.meta.getTaggedShape().keys() dirtyStart = collections.OrderedDict(zip(axes, roi.start)) dirtyStop = collections.OrderedDict(zip(axes, roi.stop)) # Remove the spatial and channel dims (keep t, if present) del dirtyStart['x'] del dirtyStart['y'] del dirtyStart['z'] del dirtyStart['c'] del dirtyStop['x'] del dirtyStop['y'] del dirtyStop['z'] del dirtyStop['c'] self.Output.setDirty(dirtyStart.values(), dirtyStop.values())
class OpUnblockedArrayCache(Operator, ManagedBlockedCache): """ This cache operator stores the results of all requests that pass through it, in exactly the same blocks that were requested. - If there are any overlapping requests, then the data for the overlapping portion will be stored multiple times, except for the special case where the new request happens to fall ENTIRELY within an existing block of data. - If any portion of a stored block is marked dirty, the entire block is discarded. Unlike other caches, this cache does not impose its own blocking on the data. Instead, it is assumed that the downstream operators have chosen some reasonable blocking. Hopefully the downstream operators are reasonably consistent in the blocks they request data with, since every unique result is cached separately. """ Input = InputSlot(allow_mask=True) CompressionEnabled = InputSlot( value=False) # If True, compression will be enabled for certain dtypes Output = OutputSlot(allow_mask=True) def __init__(self, *args, **kwargs): super(OpUnblockedArrayCache, self).__init__(*args, **kwargs) self._lock = RequestLock() self._resetBlocks() # Now that we're initialized, it's safe to register with the memory manager self.registerWithMemoryManager() def _standardize_roi(self, start, stop): # We use rois as dict keys. # For comparison purposes, all rois in the dict keys are assumed to be tuple-of-tuples-of-int start = tuple(map(int, start)) stop = tuple(map(int, stop)) return (start, stop) def setupOutputs(self): self.Output.meta.assignFrom(self.Input.meta) def execute(self, slot, subindex, roi, result): with self._lock: # Does this roi happen to fit ENTIRELY within an existing stored block? outer_rois = containing_rois(self._block_data.keys(), (roi.start, roi.stop)) if len(outer_rois) > 0: # Use the first one we found block_roi = self._standardize_roi(*outer_rois[0]) block_relative_roi = numpy.array( (roi.start, roi.stop)) - block_roi[0] self.Output.stype.copy_data( result, self._block_data[block_roi][roiToSlice( *block_relative_roi)]) return # Standardize roi for usage as dict key block_roi = self._standardize_roi(roi.start, roi.stop) # Get lock for this block (create first if necessary) with self._lock: if block_roi not in self._block_locks: self._block_locks[block_roi] = RequestLock() block_lock = self._block_locks[block_roi] # Handle identical simultaneous requests with block_lock: try: # Extra [:] here is in case we are decompressing from a chunkedarray self.Output.stype.copy_data(result, self._block_data[block_roi][:]) return except KeyError: # Not yet stored: Request it now. # We attach a special attribute to the array to allow the upstream operator # to optionally tell us not to bother caching the data. self.Input(roi.start, roi.stop).writeInto(result).block() if self.Input.meta.dontcache: # The upstream operator says not to bother caching the data. # (For example, see OpCacheFixer.) return if self.CompressionEnabled.value and numpy.dtype( result.dtype) in [ numpy.dtype(numpy.uint8), numpy.dtype(numpy.uint32), numpy.dtype(numpy.float32) ]: compressed_block = vigra.ChunkedArrayCompressed( result.shape, vigra.Compression.LZ4, result.dtype) compressed_block[:] = result block_storage_data = compressed_block else: block_storage_data = result.copy() with self._lock: # Store the data. # First double-check that the block wasn't removed from the # cache while we were requesting it. # (Could have happened via propagateDirty() or eventually the arrayCacheMemoryMgr) if block_roi in self._block_locks: self._block_data[block_roi] = block_storage_data self._last_access_times[block_roi] = time.time() def propagateDirty(self, slot, subindex, roi): dirty_roi = self._standardize_roi(roi.start, roi.stop) maximum_roi = roiFromShape(self.Input.meta.shape) maximum_roi = self._standardize_roi(*maximum_roi) if dirty_roi == maximum_roi: # Optimize the common case: # Everything is dirty, so no need to loop self._resetBlocks() else: # FIXME: This is O(N) for now. # We should speed this up by maintaining a bookkeeping data structure in execute(). for block_roi in self._block_data.keys(): if getIntersection(block_roi, dirty_roi, assertIntersect=False): self.freeBlock(block_roi) self.Output.setDirty(roi.start, roi.stop) ## ## OpManagedCache interface implementation ## def usedMemory(self): total = 0.0 for k in self._block_data.keys(): try: block = self._block_data[k] bytes_per_pixel = numpy.dtype(block.dtype).itemsize portion = block.size * bytes_per_pixel except (KeyError, AttributeError): # what could have happened and why it's fine # * block was deleted (then it does not occupy memory) # * block is not array data (then we don't know how # much memory it ouccupies) portion = 0.0 total += portion return total def fractionOfUsedMemoryDirty(self): # dirty memory is discarded immediately return 0.0 def lastAccessTime(self): return super(OpUnblockedArrayCache, self).lastAccessTime() def getBlockAccessTimes(self): with self._lock: # needs to be locked because dicts must not change size # during iteration l = [(k, self._last_access_times[k]) for k in self._last_access_times] return l def freeMemory(self): used = self.usedMemory() self._resetBlocks() return used def freeBlock(self, key): with self._lock: if key not in self._block_locks: return 0 block = self._block_data[key] bytes_per_pixel = numpy.dtype(block.dtype).itemsize mem = block.size * bytes_per_pixel del self._block_data[key] del self._block_locks[key] del self._last_access_times[key] return mem def freeDirtyMemory(self): return 0.0 def _resetBlocks(self): with self._lock: self._block_data = {} self._block_locks = {} self._last_access_times = collections.defaultdict(float)
class OpTrainVectorwiseClassifierBlocked(Operator): Images = InputSlot(level=1) Labels = InputSlot(level=1) ClassifierFactory = InputSlot() MaxLabel = InputSlot() Classifier = OutputSlot() # Images[N] --- MaxLabel ------ # \ \ # Labels[N] --> opFeatureMatrixCaches ---(FeatureImage[N])---> opConcatenateFeatureImages ---(label+feature matrix)---> OpTrainFromFeatures ---(Classifier)---> def __init__(self, *args, **kwargs): super(OpTrainVectorwiseClassifierBlocked, self).__init__(*args, **kwargs) self.progressSignal = OrderedSignal() self._opFeatureMatrixCaches = OperatorWrapper(OpFeatureMatrixCache, parent=self) self._opFeatureMatrixCaches.LabelImage.connect(self.Labels) self._opFeatureMatrixCaches.FeatureImage.connect(self.Images) self._opConcatenateFeatureMatrices = OpConcatenateFeatureMatrices( parent=self) self._opConcatenateFeatureMatrices.FeatureMatrices.connect( self._opFeatureMatrixCaches.LabelAndFeatureMatrix) self._opConcatenateFeatureMatrices.ProgressSignals.connect( self._opFeatureMatrixCaches.ProgressSignal) self._opTrainFromFeatures = OpTrainClassifierFromFeatureVectors( parent=self) self._opTrainFromFeatures.ClassifierFactory.connect( self.ClassifierFactory) self._opTrainFromFeatures.LabelAndFeatureMatrix.connect( self._opConcatenateFeatureMatrices.ConcatenatedOutput) self._opTrainFromFeatures.MaxLabel.connect(self.MaxLabel) self.Classifier.connect(self._opTrainFromFeatures.Classifier) # Progress reporting def _handleFeatureProgress(progress): # Note that these progress messages will probably appear out-of-order. # See comments in OpFeatureMatrixCache logger.debug("Training: {:02}% (Computing features)".format( int(progress))) self.progressSignal(0.8 * progress) self._opConcatenateFeatureMatrices.progressSignal.subscribe( _handleFeatureProgress) def _handleTrainingComplete(): logger.debug("Training: 100% (Complete)") self.progressSignal(100.0) self._opTrainFromFeatures.trainingCompleteSignal.subscribe( _handleTrainingComplete) def cleanUp(self): self.progressSignal.clean() self.Classifier.disconnect() super(OpTrainVectorwiseClassifierBlocked, self).cleanUp() def setupOutputs(self): pass # Nothing to do; our output is connected to an internal operator. def execute(self, slot, subindex, roi, result): assert False, "Shouldn't get here..." def propagateDirty(self, slot, subindex, roi): pass
class OpTrainPixelwiseClassifierBlocked(Operator): Images = InputSlot(level=1) Labels = InputSlot(level=1) ClassifierFactory = InputSlot() nonzeroLabelBlocks = InputSlot(level=1) MaxLabel = InputSlot() Classifier = OutputSlot() def __init__(self, *args, **kwargs): super(OpTrainPixelwiseClassifierBlocked, self).__init__(*args, **kwargs) self.progressSignal = OrderedSignal() # Normally, lane removal does not trigger a dirty notification. # But in this case, if the lane contained any label data whatsoever, # the classifier needs to be marked dirty. # We know which slots contain (or contained) label data because they have # been 'touched' at some point (they became dirty at some point). self._touched_slots = set() def handle_new_lane(multislot, index, newlength): def handle_dirty_lane(slot, roi): self._touched_slots.add(slot) multislot[index].notifyDirty(handle_dirty_lane) self.Labels.notifyInserted(handle_new_lane) def handle_remove_lane(multislot, index, newlength): # If the lane we're removing contained # label data, then mark the downstream dirty if multislot[index] in self._touched_slots: self.Classifier.setDirty() self._touched_slots.remove(multislot[index]) self.Labels.notifyRemove(handle_remove_lane) def setupOutputs(self): for slot in [self.Images, self.Labels]: assert all( [s.meta.getAxisKeys()[-1] == "c" for s in slot] ), f"This opearator assumes channel is the last axis. problem: {slot}" self.Classifier.meta.dtype = object self.Classifier.meta.shape = (1, ) # Special metadata for downstream operators using the classifier self.Classifier.meta.classifier_factory = self.ClassifierFactory.value def cleanUp(self): self.progressSignal.clean() super(OpTrainPixelwiseClassifierBlocked, self).cleanUp() def execute(self, slot, subindex, roi, result): classifier_factory = self.ClassifierFactory.value assert issubclass( type(classifier_factory), LazyflowPixelwiseClassifierFactoryABC ), ("Factory is of type {}, which does not satisfy the LazyflowPixelwiseClassifierFactoryABC interface." "".format(type(classifier_factory))) # Accumulate all non-zero blocks of each image into lists label_data_blocks = [] image_data_blocks = [] for image_slot, label_slot, nonzero_block_slot in zip( self.Images, self.Labels, self.nonzeroLabelBlocks): block_slicings = nonzero_block_slot.value for block_slicing in block_slicings: # Get labels block_label_roi = sliceToRoi(block_slicing, label_slot.meta.shape) block_label_data = label_slot(*block_label_roi).wait() # Shrink roi to bounding box of actual label pixels bb_roi_within_block = nonzero_bounding_box(block_label_data) block_label_bb_roi = bb_roi_within_block + block_label_roi[0] # Double-check that there is at least 1 non-zero label in the block. if (block_label_bb_roi[1] > block_label_bb_roi[0]).all(): # Ask for the halo needed by the classifier axiskeys = image_slot.meta.getAxisKeys() halo_shape = classifier_factory.get_halo_shape(axiskeys) assert len(halo_shape) == len(block_label_roi[0]) assert halo_shape[ -1] == 0, "Didn't expect a non-zero halo for channel dimension." # Expand block by halo, but keep clipped to image bounds padded_label_roi, bb_roi_within_padded = enlargeRoiForHalo( *block_label_bb_roi, shape=label_slot.meta.shape, sigma=halo_shape, window=1, return_result_roi=True, ) # Copy labels to new array, which has size == bounding-box + halo padded_label_data = numpy.zeros( padded_label_roi[1] - padded_label_roi[0], label_slot.meta.dtype) padded_label_data[roiToSlice( *bb_roi_within_padded)] = block_label_data[roiToSlice( *bb_roi_within_block)] padded_image_roi = numpy.array(padded_label_roi) assert (padded_image_roi[:, -1] == [0, 1]).all() num_channels = image_slot.meta.shape[-1] padded_image_roi[:, -1] = [0, num_channels] # Ensure the results are plain ndarray, not VigraArray, # which some classifiers might have trouble with. padded_image_data = numpy.asarray( image_slot(*padded_image_roi).wait()) label_data_blocks.append(padded_label_data) image_data_blocks.append(padded_image_data) if len(image_data_blocks) == 0: result[0] = None else: channel_names = self.Images[0].meta.channel_names axistags = self.Images[0].meta.axistags logger.debug("Training new pixelwise classifier: {}".format( classifier_factory.description)) classifier = classifier_factory.create_and_train_pixelwise( image_data_blocks, label_data_blocks, axistags, channel_names) result[0] = classifier if classifier is not None: assert issubclass( type(classifier), LazyflowPixelwiseClassifierABC ), ("Classifier is of type {}, which does not satisfy the LazyflowPixelwiseClassifierABC interface." "".format(type(classifier))) def propagateDirty(self, slot, subindex, roi): self.Classifier.setDirty()
class OpFormattedDataExport(Operator): """ Wraps OpExportSlot, but with optional preprocessing: - cut out a subregion - renormalize the data - convert to a different dtype - transpose axis order """ TransactionSlot = InputSlot( ) # To apply all settings in one 'transaction', # disconnect this slot and reconnect it when all slots are ready # This avoids multiple calls to setupOutputs when setting several optional slots in a row. Input = InputSlot() # Subregion params: 'None' can be provided for any axis, in which case it means 'full range' for that axis RegionStart = InputSlot(optional=True) RegionStop = InputSlot(optional=True) # Normalization params InputMin = InputSlot(optional=True) InputMax = InputSlot(optional=True) ExportMin = InputSlot(optional=True) ExportMax = InputSlot(optional=True) ExportDtype = InputSlot(optional=True) OutputAxisOrder = InputSlot(optional=True) # File settings OutputFilenameFormat = InputSlot( value=os.path.expanduser('~') + os.sep + 'RESULTS_{roi}' ) # A format string allowing {roi}, {x_start}, {x_stop}, etc. OutputInternalPath = InputSlot(value='exported_data') OutputFormat = InputSlot(value='hdf5') ConvertedImage = OutputSlot() # Not yet re-ordered ImageToExport = OutputSlot( ) # Preview of the pre-processed image that will be exported ExportPath = OutputSlot( ) # Location of the saved file after export is complete. FormatSelectionIsValid = OutputSlot( ) # True or False depending on whether or not the currently selected format can support the current export data. ALL_FORMATS = OpExportSlot.ALL_FORMATS # Simplified block diagram: -> ConvertedImage -> FormatSelectionIsValid # / / # Input -> opSubRegion -> opDrangeInjection -> opNormalizeAndConvert -> opReorderAxes -> opExportSlot -> ExportPath # \ # -> ImageToExport def __init__(self, *args, **kwargs): super(OpFormattedDataExport, self).__init__(*args, **kwargs) self._dirty = True opSubRegion = OpSubRegion(parent=self) opSubRegion.Input.connect(self.Input) self._opSubRegion = opSubRegion # If normalization parameters are provided, we inject a 'drange' # metadata item for downstream operators/gui to use. opDrangeInjection = OpMetadataInjector(parent=self) opDrangeInjection.Input.connect(opSubRegion.Output) self._opDrangeInjection = opDrangeInjection # Normalization and dtype conversion are performed in one step # using an OpPixelOperator. opNormalizeAndConvert = OpPixelOperator(parent=self) opNormalizeAndConvert.Input.connect(opDrangeInjection.Output) self._opNormalizeAndConvert = opNormalizeAndConvert # ConvertedImage shows the full result but WITHOUT axis reordering. self.ConvertedImage.connect(self._opNormalizeAndConvert.Output) opReorderAxes = OpReorderAxes(parent=self) opReorderAxes.Input.connect(opNormalizeAndConvert.Output) self._opReorderAxes = opReorderAxes self.ImageToExport.connect(opReorderAxes.Output) self._opExportSlot = OpExportSlot(parent=self) self._opExportSlot.Input.connect(opReorderAxes.Output) self._opExportSlot.OutputFormat.connect(self.OutputFormat) self.ExportPath.connect(self._opExportSlot.ExportPath) self.FormatSelectionIsValid.connect( self._opExportSlot.FormatSelectionIsValid) self.progressSignal = self._opExportSlot.progressSignal def setupOutputs(self): # Prepare subregion operator total_roi = roiFromShape(self.Input.meta.shape) total_roi = map(tuple, total_roi) # Default to full roi new_start, new_stop = total_roi if self.RegionStart.ready(): # RegionStart is permitted to contain 'None' values, which we replace with zeros new_start = map(lambda x: x or 0, self.RegionStart.value) if self.RegionStop.ready(): # RegionStop is permitted to contain 'None' values, # which we replace with the full extent of the corresponding axis new_stop = map(lambda (x, extent): x or extent, zip(self.RegionStop.value, total_roi[1])) else: self._opSubRegion.Stop.setValue(tuple(total_roi[1])) if not self._opSubRegion.Start.ready() or \ not self._opSubRegion.Stop.ready() or \ self._opSubRegion.Start.value != new_start or \ self._opSubRegion.Stop.value != new_stop: # Disconnect first to ensure that the start/stop slots are applied together (atomically) self._opSubRegion.Stop.disconnect() # Provide the coordinate offset, but only for the axes that are present in the output image tagged_input_offset = collections.defaultdict( lambda: -1, zip(self.Input.meta.getAxisKeys(), new_start)) output_axes = self._opReorderAxes.AxisOrder.value output_offset = [tagged_input_offset[axis] for axis in output_axes] output_offset = tuple(filter(lambda x: x != -1, output_offset)) self._opExportSlot.CoordinateOffset.setValue(output_offset) self._opSubRegion.Start.setValue(tuple(new_start)) self._opSubRegion.Stop.setValue(tuple(new_stop)) # Set up normalization and dtype conversion export_dtype = self.Input.meta.dtype if self.ExportDtype.ready(): export_dtype = self.ExportDtype.value need_normalize = (self.InputMin.ready() and self.InputMax.ready() and self.ExportMin.ready() and self.ExportMax.ready()) if need_normalize: minVal, maxVal = self.InputMin.value, self.InputMax.value outputMinVal, outputMaxVal = self.ExportMin.value, self.ExportMax.value # Force a drange onto the input slot metadata. # opNormalizeAndConvert is an OpPixelOperator, # which transforms the drange correctly in this case. self._opDrangeInjection.Metadata.setValue( {'drange': (minVal, maxVal)}) def normalize(a): numerator = numpy.float64(outputMaxVal) - numpy.float64( outputMinVal) denominator = numpy.float64(maxVal) - numpy.float64(minVal) if denominator != 0.0: frac = numpy.float32(numerator / denominator) else: # Denominator was zero. The user is probably just temporarily changing the values. frac = numpy.float32(0.0) result = numpy.asarray(outputMinVal + (a - minVal) * frac, export_dtype) return result self._opNormalizeAndConvert.Function.setValue(normalize) # The OpPixelOperator sets the drange correctly using the function we give it. output_drange = self._opNormalizeAndConvert.Output.meta.drange assert type(output_drange[0]) == export_dtype assert type(output_drange[1]) == export_dtype else: # We have no drange to set. # If the original slot metadata had a drange, # it will be propagated downstream anyway. self._opDrangeInjection.Metadata.setValue({}) # No normalization: just identity function with dtype conversion self._opNormalizeAndConvert.Function.setValue( lambda a: numpy.asarray(a, export_dtype)) # Use user-provided axis order if specified if self.OutputAxisOrder.ready(): self._opReorderAxes.AxisOrder.setValue(self.OutputAxisOrder.value) else: axistags = self.Input.meta.axistags self._opReorderAxes.AxisOrder.setValue("".join( tag.key for tag in axistags)) # Obtain values for possible name fields roi = [ tuple(self._opSubRegion.Start.value), tuple(self._opSubRegion.Stop.value) ] known_keys = {'roi': roi} # Blank the internal path while we update the external path # to avoid invalid intermediate states of ExportPath self._opExportSlot.OutputInternalPath.setValue("") # use partial formatting to fill in non-coordinate name fields name_format = self.OutputFilenameFormat.value partially_formatted_path = format_known_keys(name_format, known_keys) self._opExportSlot.OutputFilenameFormat.setValue( partially_formatted_path) internal_dataset_format = self.OutputInternalPath.value partially_formatted_dataset_name = format_known_keys( internal_dataset_format, known_keys) self._opExportSlot.OutputInternalPath.setValue( partially_formatted_dataset_name) def execute(self, slot, subindex, roi, result): assert False, "Shouldn't get here" def propagateDirty(self, slot, subindex, roi): self._dirty = True def run_export(self): self._opExportSlot.run_export()
class OpSingleBlockObjectPrediction(Operator): RawImage = InputSlot() BinaryImage = InputSlot() SelectedFeatures = InputSlot(rtype=List, stype=Opaque) Classifier = InputSlot() LabelsCount = InputSlot() ObjectwisePredictions = OutputSlot(stype=Opaque, rtype=List) PredictionImage = OutputSlot() ProbabilityChannelImage = OutputSlot() BlockwiseRegionFeatures = OutputSlot() # Indexed by (t,c) # Schematic: # # RawImage -----> opRawSubRegion ------ _______________________ # \ / \ # BinaryImage --> opBinarySubRegion --> opExtract --(features)--> opPredict --(map)--> opPredictionImage --via execute()--> PredictionImage # / \ / / # SelectedFeatures----- \ Classifier / # \ / # (labels)---------------------------> opProbabilityChannelsToImage # +----------------------------------------------------------------+ # | input_shape = RawImage.meta.shape | # | | # | | # | | # | | # | | # | | # | halo_shape = blockshape + 2*halo_padding | # | +------------------------+ | # | | halo_roi | | # | | (for internal pipeline)| | # | | | | # | | +------------------+ | | # | | | block_roi | | | # | | | (output shape) | | | # | | | | | | # | | | | | | # | | | | | | # | | +------------------+ | | # | | | | # | | | | # | | | | # | +------------------------+ | # | | # | | # | | # | | # | | # | | # | | # +----------------------------------------------------------------+ def __init__(self, block_roi, halo_padding, *args, **kwargs): super(self.__class__, self).__init__(*args, **kwargs) self.block_roi = block_roi # In global coordinates self._halo_padding = halo_padding self._opBinarySubRegion = OpSubRegion(parent=self) self._opBinarySubRegion.Input.connect(self.BinaryImage) self._opRawSubRegion = OpSubRegion(parent=self) self._opRawSubRegion.Input.connect(self.RawImage) self._opExtract = OpObjectExtraction(parent=self) self._opExtract.BinaryImage.connect(self._opBinarySubRegion.Output) self._opExtract.RawImage.connect(self._opRawSubRegion.Output) self._opExtract.Features.connect(self.SelectedFeatures) self.BlockwiseRegionFeatures.connect( self._opExtract.BlockwiseRegionFeatures) self._opExtract._opRegFeats._opCache.name = "blockwise-regionfeats-cache" self._opPredict = OpObjectPredict(parent=self) self._opPredict.Features.connect(self._opExtract.RegionFeatures) self._opPredict.SelectedFeatures.connect(self.SelectedFeatures) self._opPredict.Classifier.connect(self.Classifier) self._opPredict.LabelsCount.connect(self.LabelsCount) self.ObjectwisePredictions.connect(self._opPredict.Predictions) self._opPredictionImage = OpRelabelSegmentation(parent=self) self._opPredictionImage.Image.connect(self._opExtract.LabelImage) self._opPredictionImage.Features.connect( self._opExtract.RegionFeatures) self._opPredictionImage.ObjectMap.connect(self._opPredict.Predictions) self._opPredictionCache = OpArrayCache(parent=self) self._opPredictionCache.Input.connect(self._opPredictionImage.Output) self._opProbabilityChannelsToImage = OpMultiRelabelSegmentation( parent=self) self._opProbabilityChannelsToImage.Image.connect( self._opExtract.LabelImage) self._opProbabilityChannelsToImage.ObjectMaps.connect( self._opPredict.ProbabilityChannels) self._opProbabilityChannelsToImage.Features.connect( self._opExtract.RegionFeatures) self._opProbabilityChannelStacker = OpMultiArrayStacker(parent=self) self._opProbabilityChannelStacker.Images.connect( self._opProbabilityChannelsToImage.Output) self._opProbabilityChannelStacker.AxisFlag.setValue('c') self._opProbabilityCache = OpArrayCache(parent=self) self._opProbabilityCache.Input.connect( self._opProbabilityChannelStacker.Output) def setupOutputs(self): tagged_input_shape = self.RawImage.meta.getTaggedShape() self._halo_roi = self.computeHaloRoi( tagged_input_shape, self._halo_padding, self.block_roi) # In global coordinates # Output roi in our own coordinates (i.e. relative to the halo start) self._output_roi = self.block_roi - self._halo_roi[0] halo_start, halo_stop = map(tuple, self._halo_roi) self._opRawSubRegion.Roi.setValue((halo_start, halo_stop)) # Binary image has only 1 channel. Adjust halo subregion. assert self.BinaryImage.meta.getTaggedShape()['c'] == 1 c_index = self.BinaryImage.meta.axistags.channelIndex binary_halo_roi = numpy.array(self._halo_roi) binary_halo_roi[:, c_index] = (0, 1) # Binary has only 1 channel. binary_halo_start, binary_halo_stop = map(tuple, binary_halo_roi) self._opBinarySubRegion.Roi.setValue( (binary_halo_start, binary_halo_stop)) self.PredictionImage.meta.assignFrom( self._opPredictionImage.Output.meta) self.PredictionImage.meta.shape = tuple( numpy.subtract(self.block_roi[1], self.block_roi[0])) self.ProbabilityChannelImage.meta.assignFrom( self._opProbabilityChannelStacker.Output.meta) probability_shape = numpy.subtract(self.block_roi[1], self.block_roi[0]) probability_shape[ -1] = self._opProbabilityChannelStacker.Output.meta.shape[-1] self.ProbabilityChannelImage.meta.shape = tuple(probability_shape) # Cache the entire block self._opPredictionCache.blockShape.setValue( self._opPredictionCache.Input.meta.shape) self._opProbabilityCache.blockShape.setValue( self._opProbabilityCache.Input.meta.shape) # Forward dirty regions to our own output self._opPredictionImage.Output.notifyDirty(self._handleDirtyPrediction) def execute(self, slot, subindex, roi, destination): assert slot is self.PredictionImage or slot is self.ProbabilityChannelImage, "Unknown input slot" assert (numpy.array(roi.stop) <= slot.meta.shape).all(), "Roi is out-of-bounds" # Extract from the output (discard halo) halo_offset = numpy.subtract(self.block_roi[0], self._halo_roi[0]) adjusted_roi = (halo_offset + roi.start, halo_offset + roi.stop) if slot is self.PredictionImage: return self._opPredictionCache.Output( *adjusted_roi).writeInto(destination).wait() elif slot is self.ProbabilityChannelImage: return self._opProbabilityCache.Output( *adjusted_roi).writeInto(destination).wait() def propagateDirty(self, slot, subindex, roi): """ Nothing to do here because dirty notifications are propagated through our internal pipeline and forwarded to our output via our notifyDirty handler. """ pass def _handleDirtyPrediction(self, slot, roi): """ Foward dirty notifications from our internal output slot to the external one, but first discard the halo and offset the roi to compensate for the halo. """ # Discard halo. dirtyRoi is in internal coordinates (i.e. relative to halo start) dirtyRoi = getIntersection((roi.start, roi.stop), self._output_roi, assertIntersect=False) if dirtyRoi is not None: halo_offset = numpy.subtract(self.block_roi[0], self._halo_roi[0]) adjusted_roi = dirtyRoi - halo_offset # adjusted_roi is in output coordinates (relative to output block start) self.PredictionImage.setDirty(*adjusted_roi) # Expand to all channels and set channel image dirty adjusted_roi[:, -1] = (0, self.ProbabilityChannelImage.meta.shape[-1]) self.ProbabilityChannelImage.setDirty(*adjusted_roi) @classmethod def computeHaloRoi(cls, tagged_dataset_shape, halo_padding, block_roi): block_roi = numpy.array(block_roi) block_start, block_stop = block_roi channel_index = tagged_dataset_shape.keys().index('c') block_start[channel_index] = 0 block_stop[channel_index] = tagged_dataset_shape['c'] # Compute halo and clip to dataset bounds halo_start = block_start - halo_padding halo_start = numpy.maximum(halo_start, (0, ) * len(halo_start)) halo_stop = block_stop + halo_padding halo_stop = numpy.minimum(halo_stop, tagged_dataset_shape.values()) halo_roi = (halo_start, halo_stop) return halo_roi
class OpBlockwiseObjectClassification(Operator): """ Handles prediction ONLY. Training must be provided externally and loaded via the serializer. """ RawImage = InputSlot() BinaryImage = InputSlot() Classifier = InputSlot() LabelsCount = InputSlot() SelectedFeatures = InputSlot(rtype=List, stype=Opaque) BlockShape3dDict = InputSlot(value={ 'x': 512, 'y': 512, 'z': 512 }) # A dict of SPATIAL block dims HaloPadding3dDict = InputSlot(value={ 'x': 64, 'y': 64, 'z': 64 }) # A dict of spatial block dims PredictionImage = OutputSlot() ProbabilityChannelImage = OutputSlot() BlockwiseRegionFeatures = OutputSlot() def __init__(self, *args, **kwargs): super(self.__class__, self).__init__(*args, **kwargs) self._blockPipelines = {} # indexed by blockstart self._lock = RequestLock() def setupOutputs(self): # Check for preconditions. if self.RawImage.ready() and self.BinaryImage.ready(): rawTaggedShape = self.RawImage.meta.getTaggedShape() binTaggedShape = self.BinaryImage.meta.getTaggedShape() rawTaggedShape['c'] = None binTaggedShape['c'] = None if dict(rawTaggedShape) != dict(binTaggedShape): msg = "Raw data and other data must have equal dimensions (different channels are okay).\n"\ "Your datasets have shapes: {} and {}".format( self.RawImage.meta.shape, self.BinaryImage.meta.shape ) raise DatasetConstraintError("Blockwise Object Classification", msg) self._block_shape_dict = self.BlockShape3dDict.value self._halo_padding_dict = self.HaloPadding3dDict.value self.PredictionImage.meta.assignFrom(self.RawImage.meta) self.PredictionImage.meta.dtype = numpy.uint8 # Ultimately determined by meta.mapping_dtype from OpRelabelSegmentation prediction_tagged_shape = self.RawImage.meta.getTaggedShape() prediction_tagged_shape['c'] = 1 self.PredictionImage.meta.shape = tuple( prediction_tagged_shape.values()) block_shape = self._getFullShape(self._block_shape_dict) self.PredictionImage.meta.ideal_blockshape = block_shape raw_ruprp = self.RawImage.meta.ram_usage_per_requested_pixel binary_ruprp = self.BinaryImage.meta.ram_usage_per_requested_pixel prediction_ruprp = max(raw_ruprp, binary_ruprp) self.PredictionImage.meta.ram_usage_per_requested_pixel = prediction_ruprp self.ProbabilityChannelImage.meta.assignFrom(self.RawImage.meta) self.ProbabilityChannelImage.meta.dtype = numpy.float32 prediction_channels_tagged_shape = self.RawImage.meta.getTaggedShape() prediction_channels_tagged_shape['c'] = self.LabelsCount.value self.ProbabilityChannelImage.meta.shape = tuple( prediction_channels_tagged_shape.values()) self.ProbabilityChannelImage.meta.ram_usage_per_requested_pixel = prediction_ruprp region_feature_output_shape = (numpy.array( self.PredictionImage.meta.shape) + block_shape - 1) // block_shape self.BlockwiseRegionFeatures.meta.shape = tuple( region_feature_output_shape) self.BlockwiseRegionFeatures.meta.dtype = object self.BlockwiseRegionFeatures.meta.axistags = self.PredictionImage.meta.axistags def execute(self, slot, subindex, roi, destination): if slot == self.PredictionImage or slot == self.ProbabilityChannelImage: return self._executePredictionImage(slot, roi, destination) elif slot == self.BlockwiseRegionFeatures: return self._executeBlockwiseRegionFeatures(roi, destination) else: assert False, "Unknown output slot: {}".format(slot.name) def _executePredictionImage(self, slot, roi, destination): roi_one_channel = numpy.array((roi.start, roi.stop)) roi_one_channel[..., -1] = (0, 1) # Determine intersecting blocks block_shape = self._getFullShape(self.BlockShape3dDict.value) block_starts = getIntersectingBlocks(block_shape, roi_one_channel) block_starts = map(tuple, block_starts) # Ensure that block pipelines exist (create first if necessary) for block_start in block_starts: self._ensurePipelineExists(block_start) # Retrieve result from each block, and write into the appropriate region of the destination pool = RequestPool() for block_start in block_starts: opBlockPipeline = self._blockPipelines[block_start] block_roi = opBlockPipeline.block_roi block_intersection = getIntersection(block_roi, roi_one_channel) block_relative_intersection = numpy.subtract( block_intersection, block_roi[0]) destination_relative_intersection = numpy.subtract( block_intersection, roi_one_channel[0]) block_slot = opBlockPipeline.PredictionImage if slot == self.ProbabilityChannelImage: block_slot = opBlockPipeline.ProbabilityChannelImage # Add channels back to roi block_relative_intersection[..., -1] = (roi.start[-1], roi.stop[-1]) destination_relative_intersection[..., -1] = (0, roi.stop[-1] - roi.start[-1]) # Request the data destination_slice = roiToSlice(*destination_relative_intersection) req = block_slot(*block_relative_intersection) req.writeInto(destination[destination_slice]) pool.add(req) pool.wait() return destination def _executeBlockwiseRegionFeatures(self, roi, destination): """ Provide data for the BlockwiseRegionFeatures slot. Note: Each block produces a single element of this slot's output. Construct requested roi coordinates accordingly. e.g. if block_shape is (1,10,10,10,1), the features for the block starting at (1,20,30,40,5) should be requested via roi [(1,2,3,4,5),(2,3,4,5,6)] Note: It is assumed that you will request these features for debug purposes, AFTER requesting the prediction image. Therefore, it is considered an error to request features that are not already computed. """ axiskeys = self.RawImage.meta.getAxisKeys() # Find the corresponding block start coordinates block_shape = self._getFullShape(self.BlockShape3dDict.value) pixel_roi = numpy.array(block_shape) * (roi.start, roi.stop) block_starts = getIntersectingBlocks(block_shape, pixel_roi) block_starts = map(tuple, block_starts) # TODO: Parallelize this? for block_start in block_starts: assert block_start in self._blockPipelines, "Not allowed to request region features for blocks that haven't yet been processed." # See note above # Discard spatial axes to get (t,c) index for region slot roi tagged_block_start = zip(axiskeys, block_start) tagged_block_start_tc = filter(lambda (k, v): k in 'tc', tagged_block_start) block_start_tc = map(lambda (k, v): v, tagged_block_start_tc) block_roi_tc = (block_start_tc, block_start_tc + numpy.array([1, 1])) block_roi_t = (block_roi_tc[0][:-1], block_roi_tc[1][:-1]) assert sys.version_info.major == 2, "Alert! This loop has not been tested "\ "under python 3. Please remove this assetion and be wary of any strnage behavior you encounter" destination_start = numpy.array( block_start) // block_shape - roi.start destination_stop = destination_start + numpy.array( [1] * len(axiskeys)) opBlockPipeline = self._blockPipelines[block_start] req = opBlockPipeline.BlockwiseRegionFeatures(*block_roi_t) destination_without_channel = destination[roiToSlice( destination_start, destination_stop)] destination_with_channel = destination_without_channel[ ..., block_roi_tc[0][-1]:block_roi_tc[1][-1]] req.writeInto(destination_with_channel) req.wait() return destination def _ensurePipelineExists(self, block_start): if block_start in self._blockPipelines: return with self._lock: if block_start in self._blockPipelines: return logger.debug("Creating pipeline for block: {}".format(block_start)) block_shape = self._getFullShape(self._block_shape_dict) halo_padding = self._getFullShape(self._halo_padding_dict) input_shape = self.RawImage.meta.shape block_stop = getBlockBounds(input_shape, block_shape, block_start)[1] block_roi = (block_start, block_stop) # Instantiate pipeline opBlockPipeline = OpSingleBlockObjectPrediction(block_roi, halo_padding, parent=self) opBlockPipeline.RawImage.connect(self.RawImage) opBlockPipeline.BinaryImage.connect(self.BinaryImage) opBlockPipeline.Classifier.connect(self.Classifier) opBlockPipeline.LabelsCount.connect(self.LabelsCount) opBlockPipeline.SelectedFeatures.connect(self.SelectedFeatures) # Forward dirtyness opBlockPipeline.PredictionImage.notifyDirty( bind(self._handleDirtyBlock, block_start)) self._blockPipelines[block_start] = opBlockPipeline def get_blockshape(self): return self._getFullShape(self.BlockShape3dDict.value) def get_block_roi(self, block_start): block_shape = self._getFullShape(self._block_shape_dict) input_shape = self.RawImage.meta.shape block_stop = getBlockBounds(input_shape, block_shape, block_start)[1] block_roi = (block_start, block_stop) return block_roi def is_in_block(self, block_start, coord): block_roi = self.get_block_roi(block_start) coord_roi = (coord, TinyVector(coord) + 1) intersection = getIntersection(block_roi, coord_roi, False) return (intersection is not None) def _getFullShape(self, spatialShapeDict): # 't' should match raw input # 'c' should be 1 (output image has exactly 1 channel) # xyz come from spatialShapeDict axiskeys = self.RawImage.meta.getAxisKeys() shape = [0] * len(axiskeys) for i, k in enumerate(axiskeys): if k in 'xyz': shape[i] = spatialShapeDict[k] elif k == 'c': shape[i] = 1 elif k == 't': shape[i] = 1 else: assert False, "Unknown axis key: '{}'".format(k) return shape def _deleteAllPipelines(self): logger.debug("Deleting all pipelines.") oldBlockPipelines = self._blockPipelines self._blockPipelines = {} with self._lock: for opBlockPipeline in oldBlockPipelines.values(): opBlockPipeline.cleanUp() def propagateDirty(self, slot, subindex, roi): if slot == self.BlockShape3dDict or slot == self.HaloPadding3dDict: self._deleteAllPipelines() self.PredictionImage.setDirty(slice(None)) def _handleDirtyBlock(self, block_start, slot, roi): # Convert roi from block coords to global coords block_relative_roi = (roi.start, roi.stop) global_roi = block_relative_roi + numpy.array(block_start) logger.debug("Setting roi dirty: {}".format(global_roi)) self.PredictionImage.setDirty(*global_roi)
class OpVectorwiseClassifierPredict(Operator): Image = InputSlot() LabelsCount = InputSlot() Classifier = InputSlot() # An entire prediction request is skipped if the mask is all zeros for the requested roi. # Otherwise, the request is serviced as usual and the mask is ignored. PredictionMask = InputSlot(optional=True) PMaps = OutputSlot() def __init__(self, *args, **kwargs): super( OpVectorwiseClassifierPredict, self ).__init__(*args, **kwargs) # Make sure the entire image is dirty if the prediction mask is removed. self.PredictionMask.notifyUnready( lambda s: self.PMaps.setDirty() ) def setupOutputs(self): assert self.Image.meta.getAxisKeys()[-1] == 'c' nlabels = max(self.LabelsCount.value, 1) #we'll have at least 2 labels once we actually predict something #not setting it to 0 here is friendlier to possible downstream #ilastik operators, setting it to 2 causes errors in pixel classification #(live prediction doesn't work when only two labels are present) self.PMaps.meta.assignFrom( self.Image.meta ) self.PMaps.meta.dtype = numpy.float32 self.PMaps.meta.shape = self.Image.meta.shape[:-1]+(nlabels,) # FIXME: This assumes that channel is the last axis self.PMaps.meta.drange = (0.0, 1.0) ideal_blockshape = self.Image.meta.ideal_blockshape if ideal_blockshape is None: ideal_blockshape = (0,) * len( self.Image.meta.shape ) ideal_blockshape = list(ideal_blockshape) ideal_blockshape[-1] = self.PMaps.meta.shape[-1] self.PMaps.meta.ideal_blockshape = tuple(ideal_blockshape) output_channels = nlabels input_channels = self.Image.meta.shape[-1] # Temporarily consumed RAM includes the following: # >> result array: 4 * N output_channels # >> (times 2 due to temporary variable) # >> input data allocation ram_per_pixel = 4.0 * output_channels * 2 + self.Image.meta.dtype().nbytes * input_channels ram_per_pixel = max( ram_per_pixel, self.Image.meta.ram_usage_per_requested_pixel ) self.PMaps.meta.ram_usage_per_requested_pixel = ram_per_pixel def execute(self, slot, subindex, roi, result): classifier = self.Classifier.value # Training operator may return 'None' if there was no data to train with skip_prediction = (classifier is None) # Shortcut: If the mask is totally zero, skip this request entirely if not skip_prediction and self.PredictionMask.ready(): mask_roi = numpy.array((roi.start, roi.stop)) mask_roi[:,-1:] = [[0],[1]] start, stop = map(tuple, mask_roi) mask = self.PredictionMask( start, stop ).wait() skip_prediction = not numpy.any(mask) del mask if skip_prediction: result[:] = 0.0 return result assert issubclass(type(classifier), LazyflowVectorwiseClassifierABC), \ "Classifier is of type {}, which does not satisfy the LazyflowVectorwiseClassifierABC interface."\ "".format( type(classifier) ) key = roi.toSlice() newKey = key[:-1] newKey += (slice(0,self.Image.meta.shape[-1],None),) input_data = self.Image[newKey].wait() shape=input_data.shape prod = numpy.prod(shape[:-1]) features = input_data.reshape((prod, shape[-1])) probabilities = classifier.predict_probabilities( features ) assert probabilities.shape[1] <= self.PMaps.meta.shape[-1], \ "Error: Somehow the classifier has more label classes than expected:"\ " Got {} classes, expected {} classes"\ .format( probabilities.shape[1], self.PMaps.meta.shape[-1] ) # We're expecting a channel for each label class. # If we didn't provide at least one sample for each label, # we may get back fewer channels. if probabilities.shape[1] < self.PMaps.meta.shape[-1]: # Copy to an array of the correct shape # This is slow, but it's an unusual case assert probabilities.shape[-1] == len(classifier.known_classes) full_probabilities = numpy.zeros( probabilities.shape[:-1] + (self.PMaps.meta.shape[-1],), dtype=numpy.float32 ) for i, label in enumerate(classifier.known_classes): full_probabilities[:, label-1] = probabilities[:, i] probabilities = full_probabilities # Reshape to image probabilities.shape = shape[:-1] + (self.PMaps.meta.shape[-1],) # Copy only the prediction channels the client requested. result[...] = probabilities[...,roi.start[-1]:roi.stop[-1]] return result def propagateDirty(self, slot, subindex, roi): if slot == self.Classifier: self.logger.debug("classifier changed, setting dirty") self.PMaps.setDirty() elif slot == self.Image: self.PMaps.setDirty() elif slot == self.PredictionMask: self.PMaps.setDirty(roi.start, roi.stop)
class OpPixelwiseClassifierPredict(Operator): Image = InputSlot() LabelsCount = InputSlot() Classifier = InputSlot() # An entire prediction request is skipped if the mask is all zeros for the requested roi. # Otherwise, the request is serviced as usual and the mask is ignored. PredictionMask = InputSlot(optional=True) PMaps = OutputSlot() def __init__(self, *args, **kwargs): super( OpPixelwiseClassifierPredict, self ).__init__(*args, **kwargs) # Make sure the entire image is dirty if the prediction mask is removed. self.PredictionMask.notifyUnready( lambda s: self.PMaps.setDirty() ) def setupOutputs(self): assert self.Image.meta.getAxisKeys()[-1] == 'c' nlabels = max(self.LabelsCount.value, 1) #we'll have at least 2 labels once we actually predict something #not setting it to 0 here is friendlier to possible downstream #ilastik operators, setting it to 2 causes errors in pixel classification #(live prediction doesn't work when only two labels are present) self.PMaps.meta.dtype = numpy.float32 self.PMaps.meta.axistags = copy.copy(self.Image.meta.axistags) self.PMaps.meta.shape = self.Image.meta.shape[:-1]+(nlabels,) # FIXME: This assumes that channel is the last axis self.PMaps.meta.drange = (0.0, 1.0) def execute(self, slot, subindex, roi, result): classifier = self.Classifier.value # Training operator may return 'None' if there was no data to train with skip_prediction = (classifier is None) # Shortcut: If the mask is totally zero, skip this request entirely if not skip_prediction and self.PredictionMask.ready(): mask_roi = numpy.array((roi.start, roi.stop)) mask_roi[:,-1:] = [[0],[1]] start, stop = map(tuple, mask_roi) mask = self.PredictionMask( start, stop ).wait() skip_prediction = not numpy.any(mask) if skip_prediction: result[:] = 0.0 return result assert issubclass(type(classifier), LazyflowPixelwiseClassifierABC), \ "Classifier is of type {}, which does not satisfy the LazyflowPixelwiseClassifierABC interface."\ "".format( type(classifier) ) upstream_roi = (roi.start, roi.stop) # Ask for the halo needed by the classifier axiskeys = self.Image.meta.getAxisKeys() halo_shape = classifier.get_halo_shape(axiskeys) assert len(halo_shape) == len( upstream_roi[0] ) assert halo_shape[-1] == 0, "Didn't expect a non-zero halo for channel dimension." # Expand block by halo, then clip to image bounds upstream_roi = numpy.array( upstream_roi ) upstream_roi[0] -= halo_shape upstream_roi[1] += halo_shape upstream_roi = getIntersection( upstream_roi, roiFromShape(self.Image.meta.shape) ) upstream_roi = numpy.asarray( upstream_roi ) # Determine how to extract the data from the result (without the halo) downstream_roi = numpy.array((roi.start, roi.stop)) downstream_channels = self.PMaps.meta.shape[-1] roi_within_result = downstream_roi - upstream_roi[0] roi_within_result[:,-1] = [0, downstream_channels] # Request all upstream channels input_channels = self.Image.meta.shape[-1] upstream_roi[:,-1] = [0, input_channels] # Request the data input_data = self.Image(*upstream_roi).wait() probabilities = classifier.predict_probabilities_pixelwise( input_data ) # We're expecting a channel for each label class. # If we didn't provide at least one sample for each label, # we may get back fewer channels. if probabilities.shape[-1] != self.PMaps.meta.shape[-1]: # Copy to an array of the correct shape # This is slow, but it's an unusual case assert probabilities.shape[-1] == len(classifier.known_classes) full_probabilities = numpy.zeros( probabilities.shape[:-1] + (self.PMaps.meta.shape[-1],), dtype=numpy.float32 ) for i, label in enumerate(classifier.known_classes): full_probabilities[..., label-1] = probabilities[..., i] probabilities = full_probabilities # Extract requested region (discard halo) probabilities = probabilities[ roiToSlice(*roi_within_result) ] # Copy only the prediction channels the client requested. result[...] = probabilities[...,roi.start[-1]:roi.stop[-1]] return result def propagateDirty(self, slot, subindex, roi): if slot == self.Classifier: self.logger.debug("classifier changed, setting dirty") self.PMaps.setDirty() elif slot == self.Image: self.PMaps.setDirty() elif slot == self.PredictionMask: self.PMaps.setDirty(roi.start, roi.stop)
class OpDivisionFeatures(Operator): """Computes division features on a 5D volume.""" LabelVolume = InputSlot() DivisionFeatureNames = InputSlot(rtype=List, stype=Opaque) RegionFeaturesVigra = InputSlot() BlockwiseDivisionFeatures = OutputSlot() def __init__(self, *args, **kwargs): super(OpDivisionFeatures, self).__init__(*args, **kwargs) def setupOutputs(self): taggedShape = self.LabelVolume.meta.getTaggedShape() if set(taggedShape.keys()) != set('txyzc'): raise Exception("Input volumes must have txyzc axes.") self.BlockwiseDivisionFeatures.meta.shape = tuple([taggedShape['t']]) self.BlockwiseDivisionFeatures.meta.axistags = vigra.defaultAxistags( "t") self.BlockwiseDivisionFeatures.meta.dtype = object ndim = 3 if np.any(list(taggedShape.get(k, 0) == 1 for k in "xyz")): ndim = 2 self.featureManager = FeatureManager( scales=config.image_scale, n_best=config.n_best_successors, com_name_cur=config.com_name_cur, com_name_next=config.com_name_next, size_name=config.size_name, delim=config.delim, template_size=config.template_size, ndim=ndim, size_filter=config.size_filter, squared_distance_default=config.squared_distance_default) def execute(self, slot, subindex, roi, result): assert len(roi.start) == len(roi.stop) == len( self.BlockwiseDivisionFeatures.meta.shape) assert slot == self.BlockwiseDivisionFeatures taggedShape = self.LabelVolume.meta.getTaggedShape() timeIndex = taggedShape.keys().index('t') import time start = time.time() vroi_start = len(self.LabelVolume.meta.shape) * [ 0, ] vroi_stop = list(self.LabelVolume.meta.shape) assert len(roi.start) == 1 froi_start = roi.start[0] froi_stop = roi.stop[0] vroi_stop[timeIndex] = roi.stop[0] assert timeIndex == 0 vroi_start[timeIndex] = roi.start[0] if roi.stop[0] + 1 < self.LabelVolume.meta.shape[timeIndex]: vroi_stop[timeIndex] = roi.stop[0] + 1 froi_stop = roi.stop[0] + 1 vroi = [ slice(vroi_start[i], vroi_stop[i]) for i in range(len(vroi_start)) ] feats = self.RegionFeaturesVigra[slice(froi_start, froi_stop)].wait() labelVolume = self.LabelVolume[vroi].wait() divisionFeatNames = self.DivisionFeatureNames[( )].wait()[config.features_division_name] for t in range(roi.stop[0] - roi.start[0]): result[t] = {} feats_cur = feats[t][config.features_vigra_name] if t + 1 < froi_stop - froi_start: feats_next = feats[t + 1][config.features_vigra_name] img_next = labelVolume[t + 1, ...] else: feats_next = None img_next = None res = self.featureManager.computeFeatures_at( feats_cur, feats_next, img_next, divisionFeatNames) result[t][config.features_division_name] = res stop = time.time() logger.debug( "TIMING: computing division features took {:.3f}s".format(stop - start)) return result def propagateDirty(self, slot, subindex, roi): if slot is self.DivisionFeatureNames: self.BlockwiseDivisionFeatures.setDirty(slice(None)) elif slot is self.RegionFeaturesVigra: self.BlockwiseDivisionFeatures.setDirty(roi) else: axes = self.LabelVolume.meta.getTaggedShape().keys() dirtyStart = collections.OrderedDict(zip(axes, roi.start)) dirtyStop = collections.OrderedDict(zip(axes, roi.stop)) # Remove the spatial and channel dims (keep t, if present) del dirtyStart['x'] del dirtyStart['y'] del dirtyStart['z'] del dirtyStart['c'] del dirtyStop['x'] del dirtyStop['y'] del dirtyStop['z'] del dirtyStop['c'] self.BlockwiseDivisionFeatures.setDirty(dirtyStart.values(), dirtyStop.values())
class OpClassifierPredict(Operator): Image = InputSlot() LabelsCount = InputSlot() Classifier = InputSlot() # An entire prediction request is skipped if the mask is all zeros for the requested roi. # Otherwise, the request is serviced as usual and the mask is ignored. PredictionMask = InputSlot(optional=True) PMaps = OutputSlot() def __init__(self, *args, **kwargs): super(OpClassifierPredict, self).__init__(*args, **kwargs) self._mode = None self._prediction_op = None def setupOutputs(self): # Construct an inner operator depending on the type of classifier we'll be using. # We don't want to access the classifier directly here because that would trigger the full computation already. # Instead, we require the factory to be passed along with the classifier metadata. try: classifier_factory = self.Classifier.meta.classifier_factory except KeyError: raise Exception( "Classifier slot must include classifier factory as metadata.") if issubclass(classifier_factory.__class__, LazyflowVectorwiseClassifierFactoryABC): new_mode = "vectorwise" elif issubclass(classifier_factory.__class__, LazyflowPixelwiseClassifierFactoryABC): new_mode = "pixelwise" else: raise Exception("Unknown classifier factory type: {}".format( type(classifier_factory))) if new_mode == self._mode: return if self._mode is not None: self.PMaps.disconnect() self._prediction_op.cleanUp() self._mode = new_mode if self._mode == "vectorwise": self._prediction_op = OpVectorwiseClassifierPredict(parent=self) elif self._mode == "pixelwise": self._prediction_op = OpPixelwiseClassifierPredict(parent=self) self._prediction_op.PredictionMask.connect(self.PredictionMask) self._prediction_op.Image.connect(self.Image) self._prediction_op.LabelsCount.connect(self.LabelsCount) self._prediction_op.Classifier.connect(self.Classifier) self.PMaps.connect(self._prediction_op.PMaps) def execute(self, slot, subindex, roi, result): assert False, "Shouldn't get here..." def propagateDirty(self, slot, subindex, roi): if slot == self.Classifier: self.PMaps.setDirty()
class OpCarving(Operator): name = "Carving" category = "interactive segmentation" # I n p u t s # #MST of preprocessed Graph MST = InputSlot() # These three slots are for display only. # All computation is done with the MST. OverlayData = InputSlot( optional=True ) # Display-only: Available to the GUI in case the input data was preprocessed in some way but you still want to see the 'raw' data. InputData = InputSlot() # The data used by preprocessing (display only) FilteredInputData = InputSlot() # The output of the preprocessing filter #write the seeds that the users draw into this slot WriteSeeds = InputSlot() #trigger an update by writing into this slot Trigger = InputSlot(value=numpy.zeros((1, ), dtype=numpy.uint8)) #number between 0.0 and 1.0 #bias of the background #FIXME: correct name? BackgroundPriority = InputSlot(value=0.95) LabelNames = OutputSlot(stype='list') #a number between 0 and 256 #below the number, no background bias will be applied to the edge weights NoBiasBelow = InputSlot(value=64) # uncertainty type UncertaintyType = InputSlot() # O u t p u t s # #current object + background Segmentation = OutputSlot() Supervoxels = OutputSlot() Uncertainty = OutputSlot() #contains an array with the object labels done so far, one label for each #object DoneSegmentation = OutputSlot() CurrentObjectName = OutputSlot(stype='string') AllObjectNames = OutputSlot(rtype=List, stype=Opaque) #current object has an actual segmentation HasSegmentation = OutputSlot(stype='bool') #Hint Overlay HintOverlay = OutputSlot() #Pmap Overlay PmapOverlay = OutputSlot() MstOut = OutputSlot() #: User-defined prefix for autogenerated object names ObjectPrefix = OutputSlot(stype='string') def __init__(self, graph=None, hintOverlayFile=None, pmapOverlayFile=None, parent=None): super(OpCarving, self).__init__(graph=graph, parent=parent) self.opLabelArray = OpDenseLabelArray(parent=self) #self.opLabelArray.EraserLabelValue.setValue( 100 ) self.opLabelArray.MetaInput.connect(self.InputData) self._hintOverlayFile = hintOverlayFile self._mst = None self.has_seeds = False # keeps track of whether or not there are seeds currently loaded, either drawn by the user or loaded from a saved object self.LabelNames.setValue(["Background", "Object"]) #supervoxels of finished and saved objects self._done_seg_lut = None self._hints = None self._pmap = None if hintOverlayFile is not None: try: f = h5py.File(hintOverlayFile, "r") except Exception as e: logger.info("Could not open hint overlay '%s'" % hintOverlayFile) raise e self._hints = f["/hints"].value[numpy.newaxis, :, :, :, numpy.newaxis] if pmapOverlayFile is not None: try: f = h5py.File(pmapOverlayFile, "r") except Exception as e: raise RuntimeError("Could not open pmap overlay '%s'" % pmapOverlayFile) self._pmap = f["/data"].value[numpy.newaxis, :, :, :, numpy.newaxis] self._setCurrObjectName("<not saved yet>") self.HasSegmentation.setValue(False) # keep track of a set of object names that have changed since # the last serialization of this object to disk self._dirtyObjects = set() self.preprocessingApplet = None self._opMstCache = OpValueCache(parent=self) self.MstOut.connect(self._opMstCache.Output) self.InputData.notifyReady(self._checkConstraints) self.ObjectPrefix.setValue(DEFAULT_LABEL_PREFIX) def _checkConstraints(self, *args): slot = self.InputData numChannels = slot.meta.getTaggedShape()['c'] if numChannels != 1: raise DatasetConstraintError( "Carving", "Input image must have exactly one channel. " + "You attempted to add a dataset with {} channels".format( numChannels)) sh = slot.meta.shape ax = slot.meta.axistags if len(slot.meta.shape) != 5: # Raise a regular exception. This error is for developers, not users. raise RuntimeError("was expecting a 5D dataset, got shape=%r" % (sh, )) if slot.meta.getTaggedShape()['t'] != 1: raise DatasetConstraintError( "Carving", "Input image must not have more than one time slice. " + "You attempted to add a dataset with {} time slices".format( slot.meta.getTaggedShape()['t'])) for i in range(1, 4): if not ax[i].isSpatial(): # This is for developers. Don't need a user-friendly error. raise RuntimeError("%d-th axis %r is not spatial" % (i, ax[i])) def clearLabel(self, label_value): self.opLabelArray.DeleteLabel.setValue(label_value) if self._mst is not None: self._mst.clearSeed(label_value) self.opLabelArray.DeleteLabel.setValue(-1) def _clearLabels(self): #clear the labels self.opLabelArray.DeleteLabel.setValue(2) self.opLabelArray.DeleteLabel.setValue(1) self.opLabelArray.DeleteLabel.setValue(-1) if self._mst is not None: self._mst.clearSeeds() self.has_seeds = False def _setCurrObjectName(self, n): """ Sets the current object name to n. """ self._currObjectName = n self.CurrentObjectName.setValue(n) def _buildDone(self): """ Builds the done segmentation anew, for example after saving an object or deleting an object. """ if self._mst is None: return with Timer() as timer: self._done_seg_lut = numpy.zeros(self._mst.numNodes + 1, dtype=numpy.int32) logger.info("building 'done' lut") for name, objectSupervoxels in self._mst.object_lut.items(): if name == self._currObjectName: continue assert name in self._mst.object_names, "%s not in self._mst.object_names, keys are %r" % ( name, list(self._mst.object_names.keys())) self._done_seg_lut[objectSupervoxels] = self._mst.object_names[ name] logger.info("building the 'done' luts took {} seconds".format( timer.seconds())) def dataIsStorable(self): if self._mst is None: return False nodeSeeds = self._mst.gridSegmentor.getNodeSeeds() fg_seedNum = len(numpy.where(nodeSeeds == 2)[0]) bg_seedNum = len(numpy.where(nodeSeeds == 1)[0]) if not (fg_seedNum > 0 and bg_seedNum > 0): return False else: return True def setupOutputs(self): self.Segmentation.meta.assignFrom(self.InputData.meta) self.Segmentation.meta.dtype = numpy.uint32 self.Supervoxels.meta.assignFrom(self.Segmentation.meta) self.DoneSegmentation.meta.assignFrom(self.Segmentation.meta) self.HintOverlay.meta.assignFrom(self.InputData.meta) self.PmapOverlay.meta.assignFrom(self.InputData.meta) self.Uncertainty.meta.assignFrom(self.InputData.meta) self.Uncertainty.meta.dtype = numpy.uint8 self.Trigger.meta.shape = (1, ) self.Trigger.meta.dtype = numpy.uint8 if self._mst is not None: objects = list(self._mst.object_names.keys()) self.AllObjectNames.meta.shape = (len(objects), ) else: self.AllObjectNames.meta.shape = (0, ) self.AllObjectNames.meta.dtype = object def connectToPreprocessingApplet(self, applet): self.PreprocessingApplet = applet # def updatePreprocessing(self): # if self.PreprocessingApplet is None or self._mst is None: # return #FIXME: why were the following lines needed ? # if len(self._mst.object_names)==0: # self.PreprocessingApplet.enableWriteprotect(True) # else: # self.PreprocessingApplet.enableWriteprotect(False) def hasCurrentObject(self): """ Returns current object name. None if it is not set. """ #FIXME: This is misleading. Having a current object and that object having #a name is not the same thing. return self._currObjectName def currentObjectName(self): """ Returns current object name. Return "" if no current object """ assert self._currObjectName is not None, "FIXME: This function should either return '' or None. Why does it sometimes return one and then the other?" return self._currObjectName def hasObjectWithName(self, name): """ Returns True if object with name is existent. False otherwise. """ return name in self._mst.object_lut def doneObjectNamesForPosition(self, position3d): """ Returns a list of names of objects which occupy a specific 3D position. List is empty if there are no objects present. """ assert len(position3d) == 3 #find the supervoxel that was clicked sv = self._mst.supervoxelUint32[position3d] names = [] for name, objectSupervoxels in self._mst.object_lut.items(): if numpy.sum(sv == objectSupervoxels) > 0: names.append(name) logger.info("click on %r, supervoxel=%d: %r" % (position3d, sv, names)) return names @Operator.forbidParallelExecute def attachVoxelLabelsToObject(self, name, fgVoxels, bgVoxels): """ Attaches Voxellabes to an object called name. """ self._mst.object_seeds_fg_voxels[name] = fgVoxels self._mst.object_seeds_bg_voxels[name] = bgVoxels @Operator.forbidParallelExecute def clearCurrentLabeling(self, trigger_recompute=True): """ Clears the current labeling. """ self._clearLabels() self._mst.gridSegmentor.clearSeeds() #lut_segmentation = self._mst.segmentation.lut[:] #lut_segmentation[:] = 0 #lut_seeds = self._mst.seeds.lut[:] #lut_seeds[:] = 0 #self.HasSegmentation.setValue(False) self.Trigger.setDirty(slice(None)) def loadObject_impl(self, name): """ Loads a single object called name to be the currently edited object. Its not part of the done segmentation anymore. """ assert self._mst is not None logger.info("[OpCarving] load object %s (opCarving=%d, mst=%d)" % (name, id(self), id(self._mst))) assert name in self._mst.object_lut assert name in self._mst.object_seeds_fg_voxels assert name in self._mst.object_seeds_bg_voxels assert name in self._mst.bg_priority assert name in self._mst.no_bias_below #lut_segmentation = self._mst.segmentation.lut[:] #lut_objects = self._mst.objects.lut[:] #lut_seeds = self._mst.seeds.lut[:] ## clean seeds #lut_seeds[:] = 0 # set foreground and background seeds fgVoxelsSeedPos = self._mst.object_seeds_fg_voxels[name] bgVoxelsSeedPos = self._mst.object_seeds_bg_voxels[name] fgArraySeedPos = numpy.array(fgVoxelsSeedPos) bgArraySeedPos = numpy.array(bgVoxelsSeedPos) self._mst.setSeeds(fgArraySeedPos, bgArraySeedPos) # load the actual segmentation fgNodes = self._mst.object_lut[name] self._mst.setResulFgObj(fgNodes[0]) #newSegmentation = numpy.ones(len(lut_objects), dtype=numpy.int32) #newSegmentation[ self._mst.object_lut[name] ] = 2 #lut_segmentation[:] = newSegmentation self._setCurrObjectName(name) self.HasSegmentation.setValue(True) #now that 'name' is no longer part of the set of finished objects, rebuild the done overlay self._buildDone() return (fgVoxelsSeedPos, bgVoxelsSeedPos) def loadObject(self, name): logger.info("want to load object with name = %s" % name) if not self.hasObjectWithName(name): logger.info(" --> no such object '%s'" % name) return False if self.hasCurrentObject(): self.saveCurrentObject() self._clearLabels() fgVoxels, bgVoxels = self.loadObject_impl(name) fg_bounding_box_start = numpy.array(list(map(numpy.min, fgVoxels))) fg_bounding_box_stop = 1 + numpy.array(list(map(numpy.max, fgVoxels))) bg_bounding_box_start = numpy.array(list(map(numpy.min, bgVoxels))) bg_bounding_box_stop = 1 + numpy.array(list(map(numpy.max, bgVoxels))) bounding_box_start = numpy.minimum(fg_bounding_box_start, bg_bounding_box_start) bounding_box_stop = numpy.maximum(fg_bounding_box_stop, bg_bounding_box_stop) bounding_box_slicing = roiToSlice(bounding_box_start, bounding_box_stop) bounding_box_shape = tuple(bounding_box_stop - bounding_box_start) dtype = self.opLabelArray.Output.meta.dtype # Convert coordinates to be relative to bounding box fgVoxels = numpy.array(fgVoxels) fgVoxels = fgVoxels - numpy.array([bounding_box_start]).transpose() fgVoxels = list(fgVoxels) bgVoxels = numpy.array(bgVoxels) bgVoxels = bgVoxels - numpy.array([bounding_box_start]).transpose() bgVoxels = list(bgVoxels) with Timer() as timer: logger.info("Loading seeds....") z = numpy.zeros(bounding_box_shape, dtype=dtype) logger.info("Allocating seed array took {} seconds".format( timer.seconds())) z[fgVoxels] = 2 z[bgVoxels] = 1 self.WriteSeeds[(slice(0, 1), ) + bounding_box_slicing + (slice(0, 1), )] = z[numpy.newaxis, :, :, :, numpy.newaxis] logger.info("Loading seeds took a total of {} seconds".format( timer.seconds())) #restore the correct parameter values mst = self._mst assert name in mst.object_lut assert name in mst.object_seeds_fg_voxels assert name in mst.object_seeds_bg_voxels assert name in mst.bg_priority assert name in mst.no_bias_below assert name in mst.bg_priority assert name in mst.no_bias_below self.BackgroundPriority.setValue(mst.bg_priority[name]) self.NoBiasBelow.setValue(mst.no_bias_below[name]) #self.updatePreprocessing() # The entire segmentation layer needs to be refreshed now. self.Segmentation.setDirty() return True @Operator.forbidParallelExecute def deleteObject_impl(self, name): """ Deletes an object called name. """ #lut_seeds = self._mst.seeds.lut[:] # clean seeds #lut_seeds[:] = 0 del self._mst.object_lut[name] del self._mst.object_seeds_fg_voxels[name] del self._mst.object_seeds_bg_voxels[name] del self._mst.bg_priority[name] del self._mst.no_bias_below[name] #delete it from object_names, as it indicates #whether the object exists if name in self._mst.object_names: del self._mst.object_names[name] self._setCurrObjectName("<not saved yet>") #now that 'name' has been deleted, rebuild the done overlay self._buildDone() #self.updatePreprocessing() def deleteObject(self, name): logger.info("want to delete object with name = %s" % name) if not self.hasObjectWithName(name): logger.info(" --> no such object '%s'" % name) return False self.deleteObject_impl(name) #clear the user labels self._clearLabels() # trigger a re-computation self.Trigger.setDirty(slice(None)) self._dirtyObjects.add(name) objects = list(self._mst.object_names.keys()) logger.info("save: len = {}".format(len(objects))) self.AllObjectNames.meta.shape = (len(objects), ) self.HasSegmentation.setValue(False) return True @Operator.forbidParallelExecute def saveCurrentObject(self): """ Saves the objects which is currently edited. """ if self._currObjectName: name = copy.copy(self._currObjectName) logger.info("saving object %s" % self._currObjectName) self.saveCurrentObjectAs(self._currObjectName) self.HasSegmentation.setValue(False) return name return "" @Operator.forbidParallelExecute def saveCurrentObjectAs(self, name): """ Saves current object as name. """ seed = 2 logger.info(" --> Saving object %r from seed %r" % (name, seed)) if name in self._mst.object_names: objNr = self._mst.object_names[name] else: # find free objNr if len(list(self._mst.object_names.values())) > 0: objNr = numpy.max( numpy.array(list(self._mst.object_names.values()))) + 1 else: objNr = 1 sVseg = self._mst.getSuperVoxelSeg() sVseed = self._mst.getSuperVoxelSeeds() self._mst.object_names[name] = objNr self._mst.bg_priority[name] = self.BackgroundPriority.value self._mst.no_bias_below[name] = self.NoBiasBelow.value self._mst.object_lut[name] = numpy.where(sVseg == 2) self._setCurrObjectName("<not saved yet>") self.HasSegmentation.setValue(False) objects = list(self._mst.object_names.keys()) self.AllObjectNames.meta.shape = (len(objects), ) #now that 'name' is no longer part of the set of finished objects, rebuild the done overlay self._buildDone() #self._clearLabels() #self._mst.clearSegmentation() #self.clearCurrentLabeling() #self._mst.gridSegmentor.clearSeeds() #self.Trigger.setDirty(slice(None)) #self.updatePreprocessing() def get_label_voxels(self): #the voxel coordinates of fg and bg labels if not self.opLabelArray.NonzeroBlocks.ready(): return (None, None) nonzeroSlicings = self.opLabelArray.NonzeroBlocks[:].wait()[0] coors1 = [[], [], []] coors2 = [[], [], []] for sl in nonzeroSlicings: a = self.opLabelArray.Output[sl].wait() w1 = numpy.where(a == 1) w2 = numpy.where(a == 2) w1 = [w1[i] + sl[i].start for i in range(1, 4)] w2 = [w2[i] + sl[i].start for i in range(1, 4)] for i in range(3): coors1[i].append(w1[i]) coors2[i].append(w2[i]) for i in range(3): if len(coors1[i]) > 0: coors1[i] = numpy.concatenate(coors1[i], 0) else: coors1[i] = numpy.ndarray((0, ), numpy.int32) if len(coors2[i]) > 0: coors2[i] = numpy.concatenate(coors2[i], 0) else: coors2[i] = numpy.ndarray((0, ), numpy.int32) return (coors2, coors1) def saveObjectAs(self, name): # first, save the object under "name" self.saveCurrentObjectAs(name) # Sparse label array automatically shifts label values down 1 sVseed = self._mst.getSuperVoxelSeeds() #fgVoxels = numpy.where(sVseed==2) #bgVoxels = numpy.where(sVseed==1) fgVoxels, bgVoxels = self.get_label_voxels() self.attachVoxelLabelsToObject(name, fgVoxels=fgVoxels, bgVoxels=bgVoxels) self._clearLabels() # trigger a re-computation self.Trigger.setDirty(slice(None)) self._dirtyObjects.add(name) self._mst.gridSegmentor.clearSeeds() self._mst.clearSegmentation() self.clearCurrentLabeling() def getMaxUncertaintyPos(self, label): # FIXME: currently working on uncertainties = self._mst.uncertainty.lut segmentation = self._mst.segmentation.lut uncertainty_fg = numpy.where(segmentation == label, uncertainties, 0) index_max_uncert = numpy.argmax(uncertainty_fg, axis=0) pos = self._mst.regionCenter[index_max_uncert, :] return pos def execute(self, slot, subindex, roi, result): self._mst = self.MST.value if slot == self.AllObjectNames: ret = list(self._mst.object_names.keys()) return ret sl = roi.toSlice() if slot == self.Segmentation: #avoid data being copied temp = self._mst.getVoxelSegmentation(roi=roi) temp.shape = (1, ) + temp.shape + (1, ) elif slot == self.Supervoxels: #avoid data being copied temp = self._mst.supervoxelUint32[sl[1:4]] temp.shape = (1, ) + temp.shape + (1, ) elif slot == self.DoneSegmentation: #avoid data being copied if self._done_seg_lut is None: result[0, :, :, :, 0] = 0 return result else: temp = self._done_seg_lut[self._mst.supervoxelUint32[sl[1:4]]] temp.shape = (1, ) + temp.shape + (1, ) elif slot == self.HintOverlay: if self._hints is None: result[:] = 0 return result else: result[:] = self._hints[roi.toSlice()] return result elif slot == self.PmapOverlay: if self._pmap is None: result[:] = 0 return result else: result[:] = self._pmap[roi.toSlice()] return result elif slot == self.Uncertainty: temp = self._mst.uncertainty[sl[1:4]] temp.shape = (1, ) + temp.shape + (1, ) else: raise RuntimeError("unknown slot") return temp #avoid copying data def setInSlot(self, slot, subindex, roi, value): key = roi.toSlice() if slot == self.WriteSeeds: with Timer() as timer: logger.info("Writing seeds to label array") self.opLabelArray.LabelSinkInput[roi.toSlice()] = value logger.info( "Writing seeds to label array took {} seconds".format( timer.seconds())) assert self._mst is not None # Important: mst.seeds will requires erased values to be 255 (a.k.a -1) #value[:] = numpy.where(value == 100, 255, value) seedVal = value.max() with Timer() as timer: logger.info("Writing seeds to MST") if hasattr(key, '__len__'): self._mst.addSeeds(roi=roi, brushStroke=value.squeeze()) else: raise RuntimeError("when is this part of the code called") self._mst.seeds[key] = value logger.info("Writing seeds to MST took {} seconds".format( timer.seconds())) self.has_seeds = True else: raise RuntimeError("unknown slots") def propagateDirty(self, slot, subindex, roi): if slot == self.Trigger or \ slot == self.BackgroundPriority or \ slot == self.NoBiasBelow or \ slot == self.UncertaintyType: if self._mst is None: return if not self.BackgroundPriority.ready(): return if not self.NoBiasBelow.ready(): return bgPrio = self.BackgroundPriority.value noBiasBelow = self.NoBiasBelow.value logger.info( "compute new carving results with bg priority = %f, no bias below %d" % (bgPrio, noBiasBelow)) t1 = time.time() labelCount = 2 params = dict() params["prios"] = [1.0, bgPrio, 1.0] params["uncertainty"] = self.UncertaintyType.value params["noBiasBelow"] = noBiasBelow unaries = numpy.zeros((self._mst.numNodes + 1, labelCount + 1), dtype=numpy.float32) self._mst.run(unaries, **params) logger.info(" ... carving took %f sec." % (time.time() - t1)) self.Segmentation.setDirty(slice(None)) self.DoneSegmentation.setDirty(slice(None)) hasSeg = numpy.any(self._mst.hasSeg) #hasSeg = numpy.any(self._mst.segmentation.lut > 0 ) self.HasSegmentation.setValue(hasSeg) elif slot == self.MST: self._opMstCache.Input.disconnect() self._mst = self.MST.value self._opMstCache.Input.setValue(self._mst) elif slot == self.OverlayData or \ slot == self.InputData or \ slot == self.FilteredInputData or \ slot == self.WriteSeeds: pass else: assert False, "Unknown input slot: {}".format(slot.name)
class OpBaseClassifierPredict(Operator): Image = InputSlot() LabelsCount = InputSlot() Classifier = InputSlot() # An entire prediction request is skipped if the mask is all zeros for the requested roi. # Otherwise, the request is serviced as usual and the mask is ignored. PredictionMask = InputSlot(optional=True) PMaps = OutputSlot() def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) # Make sure the entire image is dirty if the prediction mask is removed. self.PredictionMask.notifyUnready(lambda s: self.PMaps.setDirty()) def setupOutputs(self): assert self.Image.meta.getAxisKeys()[-1] == "c" nlabels = max( self.LabelsCount.value, 1 ) # we'll have at least 2 labels once we actually predict something # not setting it to 0 here is friendlier to possible downstream # ilastik operators, setting it to 2 causes errors in pixel classification # (live prediction doesn't work when only two labels are present) self.PMaps.meta.assignFrom(self.Image.meta) self.PMaps.meta.dtype = numpy.float32 self.PMaps.meta.shape = self.Image.meta.shape[:-1] + ( nlabels, ) # FIXME: This assumes that channel is the last axis self.PMaps.meta.drange = (0.0, 1.0) def execute(self, slot, subindex, roi, result): classifier = self.Classifier.value # Training operator may return 'None' if there was no data to train with if classifier is None: result[:] = 0.0 return result # Shortcut: If the mask is totally zero, skip this request entirely mask = None if self.PredictionMask.ready(): mask_roi = numpy.array((roi.start, roi.stop)) num_channels_in_mask = self.PredictionMask.meta.getTaggedShape( )["c"] mask_roi[:, -1:] = [[0], [num_channels_in_mask]] start, stop = list(map(tuple, mask_roi)) multichannel_mask = self.PredictionMask(start, stop).wait() # create a single-channel merged mask, which has 0 iff all PredictionMask channels are 0 mask = multichannel_mask[..., 0:1] > 0 for c in range(1, num_channels_in_mask): mask = numpy.logical_or(mask, multichannel_mask[..., c:c + 1]) if not numpy.any(mask): logger.debug(f"Skipping masked block {roi}") result[:] = 0.0 return result probabilities = self._calculate_probabilities(roi) # We're expecting a channel for each label class. # If we didn't provide at least one sample for each label, # we may get back fewer channels. if probabilities.shape[-1] != self.PMaps.meta.shape[-1]: # Copy to an array of the correct shape # This is slow, but it's an unusual case assert probabilities.shape[-1] == len(classifier.known_classes) full_probabilities = numpy.zeros(probabilities.shape[:-1] + (self.PMaps.meta.shape[-1], ), dtype=numpy.float32) for i, label in enumerate(classifier.known_classes): full_probabilities[..., label - 1] = probabilities[..., i] probabilities = full_probabilities # Cancel out masked pixels. if mask is not None: probabilities *= mask # Copy only the prediction channels the client requested. result[...] = probabilities[..., roi.start[-1]:roi.stop[-1]] return result @abstractmethod def _calculate_probabilities(roi): """Returns the channel-wise probability maps calculated on roi""" pass def propagateDirty(self, slot, subindex, roi): if slot == self.Classifier: self.logger.debug("classifier changed, setting dirty") self.PMaps.setDirty() elif slot == self.Image: self.PMaps.setDirty() elif slot == self.PredictionMask: self.PMaps.setDirty()
class OpCachedRegionFeatures(Operator): """Caches the region features computed by OpRegionFeatures.""" RawImage = InputSlot() LabelImage = InputSlot() CacheInput = InputSlot(optional=True) Features = InputSlot(rtype=List, stype=Opaque) Output = OutputSlot() CleanBlocks = OutputSlot() # Schematic: # # RawImage ----- blockshape=(t,)=(1,) # \ \ # LabelImage ----> OpRegionFeatures ----> OpArrayCache --> Output # \ # --> CleanBlocks def __init__(self, *args, **kwargs): super(OpCachedRegionFeatures, self).__init__(*args, **kwargs) # Hook up the labeler self._opRegionFeatures = OpRegionFeatures(parent=self) self._opRegionFeatures.RawImage.connect(self.RawImage) self._opRegionFeatures.LabelImage.connect(self.LabelImage) self._opRegionFeatures.Features.connect(self.Features) # Hook up the cache. self._opCache = OpArrayCache(parent=self) self._opCache.Input.connect(self._opRegionFeatures.Output) # Hook up our output slots self.Output.connect(self._opCache.Output) self.CleanBlocks.connect(self._opCache.CleanBlocks) def setupOutputs(self): assert self.LabelImage.meta.axistags == self.RawImage.meta.axistags taggedOutputShape = self.LabelImage.meta.getTaggedShape() taggedRawShape = self.RawImage.meta.getTaggedShape() if not np.all( list( taggedOutputShape.get(k, 0) == taggedRawShape.get(k, 0) for k in "txyz")): raise DatasetConstraintError( "Object Extraction", "Raw Image and Label Image shapes do not match.\n"\ "Label Image shape: {}. Raw Image shape: {}"\ "".format(self.LabelImage.meta.shape, self.RawVolume.meta.shape)) # Every value in the regionfeatures output is cached seperately as it's own "block" blockshape = (1, ) * len(self._opRegionFeatures.Output.meta.shape) self._opCache.blockShape.setValue(blockshape) def setInSlot(self, slot, subindex, roi, value): assert slot == self.CacheInput slicing = roiToSlice(roi.start, roi.stop) self._opCache.Input[slicing] = value def execute(self, slot, subindex, roi, destination): assert False, "Shouldn't get here." def propagateDirty(self, slot, subindex, roi): pass # Nothing to do...
class OpTrainClassifierBlocked(Operator): """ Owns two child training operators, for 'vectorwise' and 'pixelwise' classifier types. Chooses which one to use based on the type of ClassifierFactory provided as input. """ Images = InputSlot(level=1) Labels = InputSlot(level=1) ClassifierFactory = InputSlot() nonzeroLabelBlocks = InputSlot(level=1) # Used only in the pixelwise case. MaxLabel = InputSlot() Classifier = OutputSlot() def __init__(self, *args, **kwargs): super(OpTrainClassifierBlocked, self).__init__(*args, **kwargs) self.progressSignal = OrderedSignal() self._mode = None # Fully connect the vectorwise training operator self._opVectorwiseTrain = OpTrainVectorwiseClassifierBlocked( parent=self) self._opVectorwiseTrain.Images.connect(self.Images) self._opVectorwiseTrain.Labels.connect(self.Labels) self._opVectorwiseTrain.ClassifierFactory.connect( self.ClassifierFactory) self._opVectorwiseTrain.MaxLabel.connect(self.MaxLabel) self._opVectorwiseTrain.progressSignal.subscribe(self.progressSignal) # Fully connect the pixelwise training operator self._opPixelwiseTrain = OpTrainPixelwiseClassifierBlocked(parent=self) self._opPixelwiseTrain.Images.connect(self.Images) self._opPixelwiseTrain.Labels.connect(self.Labels) self._opPixelwiseTrain.ClassifierFactory.connect( self.ClassifierFactory) self._opPixelwiseTrain.nonzeroLabelBlocks.connect( self.nonzeroLabelBlocks) self._opPixelwiseTrain.MaxLabel.connect(self.MaxLabel) self._opPixelwiseTrain.progressSignal.subscribe(self.progressSignal) def setupOutputs(self): # Construct an inner operator depending on the type of classifier we'll be creating. classifier_factory = self.ClassifierFactory.value if issubclass(type(classifier_factory), LazyflowVectorwiseClassifierFactoryABC): new_mode = "vectorwise" elif issubclass(type(classifier_factory), LazyflowPixelwiseClassifierFactoryABC): new_mode = "pixelwise" else: raise Exception("Unknown classifier factory type: {}".format( type(classifier_factory))) if new_mode == self._mode: return self.Classifier.disconnect() self._mode = new_mode if self._mode == "vectorwise": self.Classifier.connect(self._opVectorwiseTrain.Classifier) elif self._mode == "pixelwise": self.Classifier.connect(self._opPixelwiseTrain.Classifier) def execute(self, slot, subindex, roi, result): assert False, "Shouldn't get here..." def propagateDirty(self, slot, subindex, roi): pass
class OpHessianEigenvectors(Operator): """ Operator to call iiboost's hessian eigenvector function. Takes a 3D 1-channel image as input and returns a 5D xyzij output, where the i,j axes are the eigenvector index and eigenvector element index, respectively. """ Input = InputSlot() Sigma = InputSlot(value=3.5) # FIXME: What is the right sigma to use? Output = OutputSlot() WINDOW_SIZE = 2.0 # Used to calculate halo def __init__(self, *args, **kwargs): super(OpHessianEigenvectors, self).__init__(*args, **kwargs) self.z_anisotropy_factor = 1.0 def setupOutputs(self): assert len(self.Input.meta.shape ) == 4, "Data must be exactly 3D+c (no time axis)" assert self.Input.meta.getAxisKeys()[-1] == 'c' assert self.Input.meta.shape[-1] == 1, "Input must be 1-channel" self.Output.meta.assignFrom(self.Input.meta) self.Output.meta.dtype = numpy.float32 self.Output.meta.shape = self.Input.meta.shape[:-1] + (3, 3) # axistags: start with input, drop channel and append i,j input_axistags = copy.copy(self.Input.meta.axistags) tag_list = [tag for tag in input_axistags] tag_list = tag_list[:-1] tag_list.append(vigra.AxisInfo('i', description='eigenvector index')) tag_list.append( vigra.AxisInfo('j', description='eigenvector component')) self.Output.meta.axistags = vigra.AxisTags(tag_list) # Calculate anisotropy factor. x_tag = self.Input.meta.axistags['x'] z_tag = self.Input.meta.axistags['z'] self.z_anisotropy_factor = 1.0 if z_tag.resolution != 0.0 and x_tag.resolution != 0.0: self.z_anisotropy_factor = z_tag.resolution / x_tag.resolution logger.debug("Anisotropy factor: {}/{} = {}".format( z_tag.resolution, x_tag.resolution, self.z_anisotropy_factor)) def execute(self, slot, subindex, roi, result): # Remove i,j slices from roi, append channel slice to roi. input_roi = (tuple(roi.start[:-2]) + (0, ), tuple(roi.stop[:-2]) + (1, )) enlarged_roi, result_roi = self._enlarge_roi_for_halo(*input_roi) # Request input input_data = self.Input(*enlarged_roi).wait() # Drop singleton channel axis input_data = input_data[..., 0] # We need a uint8 array, in C-order. input_data = input_data.astype(numpy.uint8, order='C', copy=False) # Compute. (Note that we drop the eigenvectors = computeEigenVectorsOfHessianImage( input_data, zAnisotropyFactor=self.z_anisotropy_factor, sigma=self.Sigma.value) # sanity checks... assert (eigenvectors.shape[:-2] == (numpy.array(enlarged_roi[1]) - enlarged_roi[0])[:-1]).all(), \ "eigenvector image has unexpected shape: {}".format( eigenvectors.shape ) assert eigenvectors.shape[-2:] == (3, 3) # Copy to output. result[:] = eigenvectors[roiToSlice( *result_roi)][..., slice(roi.start[-1], roi.stop[-1])] def propagateDirty(self, slot, subindex, roi): if slot is self.Sigma: self.Output.setDirty(slice(None)) elif slot is self.Input: enlarged_roi, _ = self._enlarge_roi_for_halo(roi.start, roi.stop) dirty_start = tuple(enlarged_roi[0, :-1]) + (0, 0) dirty_stop = tuple(enlarged_roi[1, :-1]) + (3, 3) self.Output.setDirty(dirty_start, dirty_stop) else: assert False, 'Unhandled dirty slot: {}'.format(slot) def _enlarge_roi_for_halo(self, start, stop): """ Given a roi of INPUT coordinates (3D+c, not 3D+ij), enlarge it with an appropriate halo. Also return the "result roi". (See enlargeRoiForHalo() docs for details.) """ assert len(self.Input.meta.shape ) == 4, "Data must be exactly 3D+c (no time axis)" assert self.Input.meta.getAxisKeys()[-1] == 'c' spatial_axes = (True, True, True, False) # don't enlarge channel roi enlarged_roi, result_roi = enlargeRoiForHalo(start, stop, self.Input.meta.shape, self.Sigma.value, window=self.WINDOW_SIZE, enlarge_axes=spatial_axes, return_result_roi=True) return enlarged_roi, result_roi
class OpArrayCache(OpCache): """ Allocates a block of memory as large as Input.meta.shape (==Output.meta.shape) with the same dtype in order to be able to cache results. blockShape: dirty regions are tracked with a granularity of blockShape """ name = "ArrayCache" description = "numpy.ndarray caching class" category = "misc" DefaultBlockSize = 64 #Input Input = InputSlot() blockShape = InputSlot(value=DefaultBlockSize) fixAtCurrent = InputSlot(value=False) #Output CleanBlocks = OutputSlot() Output = OutputSlot() loggingName = __name__ + ".OpArrayCache" logger = logging.getLogger(loggingName) traceLogger = logging.getLogger("TRACE." + loggingName) # Block states IN_PROCESS = 0 DIRTY = 1 CLEAN = 2 FIXED_DIRTY = 3 def __init__(self, *args, **kwargs): super(OpArrayCache, self).__init__(*args, **kwargs) self._origBlockShape = self.DefaultBlockSize self._last_access = None self._blockShape = None self._dirtyShape = None self._blockState = None self._dirtyState = None self._fixed = False self._cache = None self._lock = Lock() self._cacheLock = Lock() self._lazyAlloc = True self._cacheHits = 0 self._has_fixed_dirty_blocks = False self._memory_manager = ArrayCacheMemoryMgr.instance self._running = 0 def usedMemory(self): if self._cache is not None: return self._cache.nbytes else: return 0 def _blockShapeForIndex(self, index): if self._cache is None: return None cacheShape = numpy.array(self._cache.shape) blockStart = index * self._blockShape blockStop = numpy.minimum(blockStart + self._blockShape, cacheShape) def fractionOfUsedMemoryDirty(self): if self.Output.meta.shape is None: return 0 totAll = numpy.prod(self.Output.meta.shape) totDirty = 0 if self._blockState is None: return 0 for i, v in enumerate(self._blockState.ravel()): sh = self._blockShapeForIndex(i) if sh is None: continue if v == self.DIRTY or v == self.FIXED_DIRTY: totDirty += numpy.prod(sh) return totDirty / float(totAll) def lastAccessTime(self): return self._last_access def generateReport(self, report): report.name = self.name report.fractionOfUsedMemoryDirty = self.fractionOfUsedMemoryDirty() report.usedMemory = self.usedMemory() report.lastAccessTime = self.lastAccessTime() report.dtype = self.Output.meta.dtype report.type = type(self) report.id = id(self) def _freeMemory(self, refcheck=True): with self._cacheLock: freed = self.usedMemory() if self._cache is not None: fshape = self._cache.shape try: self._cache.resize((1, ), refcheck=refcheck) except ValueError: freed = 0 self.logger.warn( "OpArrayCache: freeing failed due to view references") if freed > 0: self.logger.debug( "OpArrayCache: freed cache of shape:{}".format(fshape)) self._lock.acquire() self._blockState[:] = OpArrayCache.DIRTY del self._cache self._cache = None self._lock.release() return freed def _allocateManagementStructures(self): shape = self.Output.meta.shape if type(self._origBlockShape) != tuple: self._blockShape = (self._origBlockShape, ) * len(shape) else: self._blockShape = self._origBlockShape self._blockShape = numpy.minimum(self._blockShape, shape) self._dirtyShape = numpy.ceil(1.0 * numpy.array(shape) / numpy.array(self._blockShape)).astype( numpy.int) self.logger.debug( "Configured OpArrayCache with shape={}, blockShape={}, dirtyShape={}, origBlockShape={}" .format(shape, self._blockShape, self._dirtyShape, self._origBlockShape)) #if a request has been submitted to get a block, the request object #is stored within this array self._blockQuery = numpy.ndarray(self._dirtyShape, dtype=object) #keep track of the dirty state of each block self._blockState = OpArrayCache.DIRTY * numpy.ones( self._dirtyShape, numpy.uint8) self._blockState[:] = OpArrayCache.DIRTY self._dirtyState = OpArrayCache.CLEAN def _allocateCache(self): with self._cacheLock: self._last_access = None self._cache_priority = 0 self._running = 0 if self._cache is None or (self._cache.shape != self.Output.meta.shape): mem = numpy.zeros(self.Output.meta.shape, dtype=self.Output.meta.dtype) self.logger.debug( "OpArrayCache: Allocating cache (size: %dbytes)" % mem.nbytes) if self._blockState is None: self._allocateManagementStructures() self._cache = mem self._memory_manager.add(self) def setupOutputs(self): self.CleanBlocks.meta.shape = (1, ) self.CleanBlocks.meta.dtype = object reconfigure = False if self.inputs["fixAtCurrent"].ready(): self._fixed = self.inputs["fixAtCurrent"].value if self.inputs["blockShape"].ready() and self.inputs["Input"].ready(): newBShape = self.inputs["blockShape"].value if self._origBlockShape != newBShape and self.inputs[ "Input"].ready(): reconfigure = True self._origBlockShape = newBShape self._blockShape = newBShape inputSlot = self.inputs["Input"] self.outputs["Output"].meta.assignFrom(inputSlot.meta) shape = self.Output.meta.shape if reconfigure and shape is not None: self._lock.acquire() self._allocateManagementStructures() if not self._lazyAlloc: self._allocateCache() self._lock.release() def propagateDirty(self, slot, subindex, roi): shape = self.Output.meta.shape key = roi.toSlice() if slot == self.inputs["Input"]: start, stop = sliceToRoi(key, shape) with self._lock: if self._blockState is not None: blockStart = numpy.floor(1.0 * start / self._blockShape) blockStop = numpy.ceil(1.0 * stop / self._blockShape) blockKey = roiToSlice(blockStart, blockStop) if self._fixed: # Remember that this block became dirty while we were fixed # so we can notify downstream operators when we become unfixed. self._blockState[blockKey] = OpArrayCache.FIXED_DIRTY self._has_fixed_dirty_blocks = True else: self._blockState[blockKey] = OpArrayCache.DIRTY if not self._fixed: self.outputs["Output"].setDirty(key) if slot == self.inputs["fixAtCurrent"]: if self.inputs["fixAtCurrent"].ready(): self._fixed = self.inputs["fixAtCurrent"].value if not self._fixed and self.Output.meta.shape is not None and self._has_fixed_dirty_blocks: # We've become unfixed, so we need to notify downstream # operators of every block that became dirty while we were fixed. # Convert all FIXED_DIRTY states into DIRTY states with self._lock: cond = ( self._blockState[...] == OpArrayCache.FIXED_DIRTY) self._blockState[...] = fastWhere( cond, OpArrayCache.DIRTY, self._blockState, numpy.uint8) self._has_fixed_dirty_blocks = False newDirtyBlocks = numpy.transpose(numpy.nonzero(cond)) # To avoid lots of setDirty notifications, we simply merge all the dirtyblocks into one single superblock. # This should be the best option in most cases, but could be bad in some cases. # TODO: Optimize this by merging the dirty blocks via connected components or something. cacheShape = numpy.array(self.Output.meta.shape) dirtyStart = cacheShape dirtyStop = [0] * len(cacheShape) for index in newDirtyBlocks: blockStart = index * self._blockShape blockStop = numpy.minimum( blockStart + self._blockShape, cacheShape) dirtyStart = numpy.minimum(dirtyStart, blockStart) dirtyStop = numpy.maximum(dirtyStop, blockStop) if len(newDirtyBlocks > 0): self.Output.setDirty(dirtyStart, dirtyStop) def _updatePriority(self, new_access=None): if self._last_access is None: self._last_access = new_access or time.time() cur_time = time.time() delta = cur_time - self._last_access + 1e-9 self._last_access = cur_time new_prio = 0.5 * self._cache_priority + delta self._cache_priority = new_prio def execute(self, slot, subindex, roi, result): if slot == self.Output: return self._executeOutput(slot, subindex, roi, result) elif slot == self.CleanBlocks: return self._executeCleanBlocks(slot, subindex, roi, result) def _executeOutput(self, slot, subindex, roi, result): t = time.time() key = roi.toSlice() shape = self.Output.meta.shape start, stop = sliceToRoi(key, shape) self._lock.acquire() ch = self._cacheHits ch += 1 self._cacheHits = ch self._running += 1 if self._cache is None: self._allocateCache() cacheView = self._cache[:] #prevent freeing of cache during running this function blockStart = (1.0 * start / self._blockShape).floor() blockStop = (1.0 * stop / self._blockShape).ceil() blockKey = roiToSlice(blockStart, blockStop) blockSet = self._blockState[blockKey] # this is a little optimization to shortcut # many lines of python code when all data is # is already in the cache: if numpy.logical_or(blockSet == OpArrayCache.CLEAN, blockSet == OpArrayCache.FIXED_DIRTY).all(): result[:] = self._cache[roiToSlice(start, stop)] self._running -= 1 self._updatePriority() cacheView = None self._lock.release() return inProcessQueries = numpy.unique( numpy.extract(blockSet == OpArrayCache.IN_PROCESS, self._blockQuery[blockKey])) cond = (blockSet == OpArrayCache.DIRTY) tileWeights = fastWhere(cond, 1, 128**3, numpy.uint32) trueDirtyIndices = numpy.nonzero(cond) tileArray = drtile.test_DRTILE(tileWeights, 128**3).swapaxes(0, 1) dirtyRois = [] half = tileArray.shape[0] / 2 dirtyPool = RequestPool() for i in range(tileArray.shape[1]): drStart3 = tileArray[:half, i] drStop3 = tileArray[half:, i] drStart2 = drStart3 + blockStart drStop2 = drStop3 + blockStart drStart = drStart2 * self._blockShape drStop = drStop2 * self._blockShape shape = self.Output.meta.shape drStop = numpy.minimum(drStop, shape) drStart = numpy.minimum(drStart, shape) key3 = roiToSlice(drStart3, drStop3) key2 = roiToSlice(drStart2, drStop2) key = roiToSlice(drStart, drStop) if not self._fixed: dirtyRois.append([drStart, drStop]) req = self.inputs["Input"][key].writeInto(self._cache[key]) req.uncancellable = True #FIXME dirtyPool.add(req) self._blockQuery[key2] = weakref.ref(req) #sanity check: if (self._blockState[key2] != OpArrayCache.DIRTY).any(): logger.warning("original condition" + str(cond)) logger.warning("original tilearray {} {}".format( tileArray, tileArray.shape)) logger.warning("original tileWeights {} {}".format( tileWeights, tileWeights.shape)) logger.warning("sub condition {}".format( self._blockState[key2] == OpArrayCache.DIRTY)) logger.warning("START={}, STOP={}".format( drStart2, drStop2)) import h5py with h5py.File("test.h5", "w") as f: f.create_dataset("data", data=tileWeights) logger.warning( "%r \n %r \n %r\n %r\n %r \n%r" % (key2, blockKey, self._blockState[key2], self._blockState[blockKey][trueDirtyIndices], self._blockState[blockKey], tileWeights)) assert False self._blockState[key2] = OpArrayCache.IN_PROCESS # indicate the inprocessing state, by setting array to 0 (i.e. IN_PROCESS) if not self._fixed: blockSet[:] = fastWhere(cond, OpArrayCache.IN_PROCESS, blockSet, numpy.uint8) else: # Someone asked for some dirty blocks while we were fixed. # Mark these blocks to be signaled as dirty when we become unfixed blockSet[:] = fastWhere(cond, OpArrayCache.FIXED_DIRTY, blockSet, numpy.uint8) self._has_fixed_dirty_blocks = True self._lock.release() temp = itertools.count(0) #wait for all requests to finish dirtyPool.wait() if len(dirtyPool) > 0: # Signal that something was updated. # Note that we don't need to do this for the 'in process' queries (below) # because they are already in the dirtyPool in some other thread self.Output._sig_value_changed() dirtyPool.clean() # indicate the finished inprocess state (i.e. CLEAN) if not self._fixed and temp.next() == 0: with self._lock: blockSet[:] = fastWhere(cond, OpArrayCache.CLEAN, blockSet, numpy.uint8) self._blockQuery[blockKey] = fastWhere( cond, None, self._blockQuery[blockKey], object) inProcessPool = RequestPool() #wait for all in process queries for req in inProcessQueries: req = req() # get original req object from weakref if req is not None: inProcessPool.add(req) inProcessPool.wait() inProcessPool.clean() # finally, store results in result area self._lock.acquire() if self._cache is not None: result[:] = self._cache[roiToSlice(start, stop)] else: self.inputs["Input"][roiToSlice(start, stop)].writeInto(result).wait() self._running -= 1 self._updatePriority() cacheView = None self._lock.release() self.logger.debug("read %s took %f sec." % (roi.pprint(), time.time() - t)) def setInSlot(self, slot, subindex, roi, value): assert slot == self.inputs["Input"] ch = self._cacheHits ch += 1 self._cacheHits = ch start, stop = roi.start, roi.stop blockStart = numpy.ceil(1.0 * start / self._blockShape) blockStop = numpy.floor(1.0 * stop / self._blockShape) blockStop = numpy.where(stop == self.Output.meta.shape, self._dirtyShape, blockStop) blockKey = roiToSlice(blockStart, blockStop) if (self._blockState[blockKey] != OpArrayCache.CLEAN).any(): start2 = blockStart * self._blockShape stop2 = blockStop * self._blockShape stop2 = numpy.minimum(stop2, self.Output.meta.shape) key2 = roiToSlice(start2, stop2) self._lock.acquire() if self._cache is None: self._allocateCache() self._cache[key2] = value[roiToSlice(start2 - start, stop2 - start)] self._blockState[blockKey] = self._dirtyState self._blockQuery[blockKey] = None self._lock.release() def _executeCleanBlocks(self, slot, subindex, roi, destination): indexCols = numpy.where(self._blockState == OpArrayCache.CLEAN) clean_block_starts = numpy.array(indexCols).transpose() inputShape = self.Input.meta.shape clean_block_rois = map( partial(getBlockBounds, inputShape, self._blockShape), clean_block_starts) destination[0] = map(partial(map, TinyVector), clean_block_rois) return destination
class OpIIBoostFeatureSelection(Operator): """ This operator produces an output image with the following channels: channel 0: Raw Input (the input is just duplicated as an output channel) channels 1-10: The 9 elements of the hessian eigenvector matrix (a 3x3 matrix flattened into 9 channels) channels 11-11+N: The 'integral images' of any features provided by the standard OpFeatureSelection operator. This operator owns an instance of the standard OpFeatureSelection operator, and exposes the same slot interface so the GUI can configure that inner operator transparently. """ # All inputs are directly passed to internal OpFeatureSelection InputImage = InputSlot() Scales = InputSlot(value=ScalesList) FeatureIds = InputSlot(value=FeatureIds) SelectionMatrix = InputSlot(value=default_feature_matrix) FeatureListFilename = InputSlot(stype="str", optional=True) # This output is only for the GUI. It's taken directly from OpFeatureSelection. # Unlike the OutputImage slot, it provides the raw features, NOT the integral images. FeatureLayers = OutputSlot(level=1) # These outputs are taken from OpFeatureSelection, but we add to them. OutputImage = OutputSlot() CachedOutputImage = OutputSlot() def __init__(self, filter_implementation, *args, **kwargs): # Schematic for cached images is as follows. # # InputImage -> opFeatureSelection -> opIntegralImage_from_cache -> opIntegralImageCache --- # \ `-- (stacked via execute) -> CachedOutputImage # `-> opHessianEigenvectors -> opConvertToChannels -> opHessianEigenvectorCache -/ super(OpIIBoostFeatureSelection, self).__init__(*args, **kwargs) self.opFeatureSelection = OpFeatureSelection(filter_implementation, parent=self) self.opFeatureSelection.InputImage.connect(self.InputImage) self.opFeatureSelection.Scales.connect(self.Scales) self.opFeatureSelection.FeatureIds.connect(self.FeatureIds) self.opFeatureSelection.SelectionMatrix.connect(self.SelectionMatrix) self.opFeatureSelection.FeatureListFilename.connect( self.FeatureListFilename) self.FeatureLayers.connect(self.opFeatureSelection.FeatureLayers) self.WINDOW_SIZE = self.opFeatureSelection.WINDOW_SIZE # The "normal" pixel features are integrated. self.opIntegralImage = OpIntegralImage(parent=self) self.opIntegralImage.Input.connect(self.opFeatureSelection.OutputImage) self.opIntegralImage_from_cache = OpIntegralImage(parent=self) self.opIntegralImage_from_cache.Input.connect( self.opFeatureSelection.CachedOutputImage) # We use an UNBLOCKED cache to store integral features, because a blocked cache would service # requests by concatenating neighboring blocks. That is not a valid operation for integral images. self.opIntegralImageCache = OpUnblockedArrayCache(parent=self) self.opIntegralImageCache.Input.connect( self.opIntegralImage_from_cache.Output) # Note: OutputImage and CachedOutputImage are not directly connected. # Their data is obtained in execute(), below. self.opHessianEigenvectors = OpHessianEigenvectors(parent=self) self.opHessianEigenvectors.Input.connect(self.InputImage) # The operator above produces an image with weird axes, # so let's convert it to a multi-channel image for easy handling. self.opConvertToChannels = OpConvertEigenvectorsToChannels(parent=self) self.opConvertToChannels.Input.connect( self.opHessianEigenvectors.Output) # Create a cache for the hessian eigenvector image data self.opHessianEigenvectorCache = OpSlicedBlockedArrayCache(parent=self) self.opHessianEigenvectorCache.name = "opHessianEigenvectorCache" self.opHessianEigenvectorCache.Input.connect( self.opConvertToChannels.Output) self.opHessianEigenvectorCache.fixAtCurrent.setValue(False) self.InputImage.notifyReady(self.checkConstraints) self.input_axistags = None self.InputImage.notifyMetaChanged(self._handleMetaChanged) def checkConstraints(self, *args): tagged_shape = self.InputImage.meta.getTaggedShape() if 't' in tagged_shape: raise DatasetConstraintError( "IIBoost Pixel Classification: Feature Selection", "This classifier handles only 3D data. Your input data has a time dimension, which is not allowed." ) if not set('xyz').issubset(tagged_shape.keys()): raise DatasetConstraintError( "IIBoost Pixel Classification: Feature Selection", "This classifier handles only 3D data. Your input data does not have all three spatial dimensions (xyz)." ) def _handleMetaChanged(self, slot): if self.input_axistags != self.InputImage.meta.axistags: self.InputImage.setDirty(slice(None)) def setupOutputs(self): # Output shape is the same as the inner operator, # except with 10 extra channels (1 raw + 9 hessian eigenvector elements) output_shape = self.opIntegralImage.Output.meta.shape output_shape = output_shape[:-1] + (output_shape[-1] + 10, ) self.OutputImage.meta.assignFrom(self.opIntegralImage.Output.meta) self.CachedOutputImage.meta.assignFrom( self.opIntegralImageCache.Output.meta) self.OutputImage.meta.shape = output_shape self.CachedOutputImage.meta.shape = output_shape channel_names = ['Raw Data'] channel_names += [ 'Hessian Eigenvectors Element {}'.format(i) for i in range(9) ] channel_names += self.opIntegralImage.Output.meta.channel_names self.OutputImage.meta.channel_names = channel_names self.CachedOutputImage.meta.channel_names = channel_names # If we know the data resolution, fine-tune the hessian eigenvalue sigma x_tag = self.InputImage.meta.axistags['x'] if x_tag.resolution != 0.0: # This formula comes from Carlos Becker (email from 2015-03-11) hessian_ev_sigma = 3.5 / 6.8 * x_tag.resolution else: hessian_ev_sigma = 3.5 self.opHessianEigenvectors.Sigma.setValue(hessian_ev_sigma) # Copy the cache block settings from the standard pixel feature operator. self.opHessianEigenvectorCache.BlockShape.setValue( self.opFeatureSelection.opPixelFeatureCache.BlockShape.value) def propagateDirty(self, slot, subindex, roi): # All channels are dirty num_channels = self.OutputImage.meta.shape[-1] dirty_start = tuple(roi.start[:-1]) + (num_channels, ) dirty_stop = tuple(roi.stop[:-1]) + (num_channels, ) self.OutputImage.setDirty(dirty_start, dirty_stop) def execute(self, slot, subindex, roi, result): assert slot == self.OutputImage or slot == self.CachedOutputImage # Combine all three 'feature' images into one big result spatial_roi = (tuple(roi.start[:-1]), tuple(roi.stop[:-1])) raw_roi = (spatial_roi[0] + (0, ), spatial_roi[1] + (1, )) hess_ev_roi = (spatial_roi[0] + (0, ), spatial_roi[1] + (9, )) features_roi = (spatial_roi[0] + (0, ), spatial_roi[1] + (roi.stop[-1] - 10, )) # Raw request is the same in either case (there is no cache) raw_req = self.InputImage(*raw_roi) if self.InputImage.meta.dtype == self.OutputImage.meta.dtype: raw_req.writeInto(result[..., 0:1]) raw_req.wait() else: # Can't use writeInto because we need an implicit dtype cast here. result[..., 0:1] = raw_req.wait() # Pull the rest of the channels from different sources, depending on cached/uncached slot. if slot == self.OutputImage: hev_req = self.opConvertToChannels.Output(*hess_ev_roi).writeInto( result[..., 1:10]) feat_req = self.opIntegralImage.Output(*features_roi).writeInto( result[..., 10:]) elif slot == self.CachedOutputImage: hev_req = self.opHessianEigenvectorCache.Output( *hess_ev_roi).writeInto(result[..., 1:10]) feat_req = self.opIntegralImageCache.Output( *features_roi).writeInto(result[..., 10:]) hev_req.submit() feat_req.submit() hev_req.wait() feat_req.wait()
class OpAutocontextBatch(Operator): Classifiers = InputSlot(level=1) FeatureImage = InputSlot() MaxLabelValue = InputSlot() AutocontextIterations = InputSlot() PredictionProbabilities = OutputSlot() #PixelOnlyPredictions = OutputSlot() def __init__(self, *args, **kwargs): super(OpAutocontextBatch, self).__init__(*args, **kwargs) self.prediction_caches = None self.predictors = None #self.AutocontextIterations.notifyDirty(self.setupOperators) def setupOperators(self, *args, **kwargs): self.predictors = [] self.prediction_caches = [] #niter = len(self.Classifiers) niter = self.AutocontextIterations.value for i in range(niter): #predict = OperatorWrapper(OpPredictRandomForest, parent=self, parent=self) predict = OpPredictRandomForest(parent=self) self.predictors.append(predict) #prediction_cache = OperatorWrapper( OpSlicedBlockedArrayCache, parent=self, parent=self ) prediction_cache = OpSlicedBlockedArrayCache(parent=self) self.prediction_caches.append(prediction_cache) # Setup autocontext features self.autocontextFeatures = [] self.autocontextFeaturesMulti = [] self.autocontext_caches = [] self.featureStackers = [] for i in range(niter - 1): features = createAutocontextFeatureOperators(self, False) self.autocontextFeatures.append(features) opMulti = Op50ToMulti(parent=self) self.autocontextFeaturesMulti.append(opMulti) opStacker = OpMultiArrayStacker(parent=self) opStacker.inputs["AxisFlag"].setValue("c") opStacker.inputs["AxisIndex"].setValue(3) self.featureStackers.append(opStacker) autocontext_cache = OpSlicedBlockedArrayCache(parent=self) self.autocontext_caches.append(autocontext_cache) # connect the features to predictors for i in range(niter - 1): for ifeat, feat in enumerate(self.autocontextFeatures[i]): feat.inputs['Input'].connect(self.prediction_caches[i].Output) print "Multi: Connecting an output", "Input%.2d" % (ifeat) self.autocontextFeaturesMulti[i].inputs[ "Input%.2d" % (ifeat)].connect(feat.outputs["Output"]) # connect the pixel features to the same multislot print "Multi: Connecting an output", "Input%.2d" % (len( self.autocontextFeatures[i])) self.autocontextFeaturesMulti[i].inputs["Input%.2d" % (len( self.autocontextFeatures[i]))].connect(self.FeatureImage) # stack the autocontext features with pixel features self.featureStackers[i].inputs["Images"].connect( self.autocontextFeaturesMulti[i].outputs["Outputs"]) # cache the stacks self.autocontext_caches[i].inputs["Input"].connect( self.featureStackers[i].outputs["Output"]) self.autocontext_caches[i].inputs["fixAtCurrent"].setValue(False) for i in range(niter): self.predictors[i].inputs['Classifier'].connect( self.Classifiers[i]) self.predictors[i].inputs['LabelsCount'].connect( self.MaxLabelValue) self.prediction_caches[i].inputs["fixAtCurrent"].setValue(False) self.prediction_caches[i].inputs["Input"].connect( self.predictors[i].PMaps) self.predictors[0].inputs['Image'].connect(self.FeatureImage) for i in range(1, niter): self.predictors[i].inputs['Image'].connect( self.autocontext_caches[i - 1].outputs["Output"]) #self.PixelOnlyPredictions.connect(self.predictors[-1].PMaps) self.PredictionProbabilities.connect(self.predictors[0].PMaps) def setupOutputs(self): print "calling setupOutputs" if self.AutocontextIterations.ready() and self.predictors is None: self.setupOperators() # Set the blockshapes for each input image separately, depending on which axistags it has. axisOrder = [tag.key for tag in self.FeatureImage.meta.axistags] ## Pixel Cache blocks blockDimsX = { 't': (1, 1), 'z': (128, 256), 'y': (128, 256), 'x': (5, 5), 'c': (1000, 1000) } blockDimsY = { 't': (1, 1), 'z': (128, 256), 'y': (5, 5), 'x': (128, 256), 'c': (1000, 1000) } blockDimsZ = { 't': (1, 1), 'z': (5, 5), 'y': (128, 256), 'x': (128, 256), 'c': (1000, 1000) } innerBlockShapeX = tuple(blockDimsX[k][0] for k in axisOrder) outerBlockShapeX = tuple(blockDimsX[k][1] for k in axisOrder) innerBlockShapeY = tuple(blockDimsY[k][0] for k in axisOrder) outerBlockShapeY = tuple(blockDimsY[k][1] for k in axisOrder) innerBlockShapeZ = tuple(blockDimsZ[k][0] for k in axisOrder) outerBlockShapeZ = tuple(blockDimsZ[k][1] for k in axisOrder) for cache in self.prediction_caches: cache.inputs["innerBlockShape"].setValue( (innerBlockShapeX, innerBlockShapeY, innerBlockShapeZ)) cache.inputs["outerBlockShape"].setValue( (outerBlockShapeX, outerBlockShapeY, outerBlockShapeZ)) for cache in self.autocontext_caches: cache.innerBlockShape.setValue( (innerBlockShapeX, innerBlockShapeY, innerBlockShapeZ)) cache.outerBlockShape.setValue( (outerBlockShapeX, outerBlockShapeY, outerBlockShapeZ)) ''' def execute(self, slot, subindex, roi, result): if slot==self.PredictionProbabilities: #we shouldn't be here, it's for testing print "opBatchPredict, who is not ready?" print self.Classifiers.ready(), self.FeatureImage.ready(), self.AutocontextIterations.ready(), self.MaxLabelValue.ready() return ''' def setInSlot(self, slot, subindex, roi, value): # Nothing to do here: All inputs that support __setitem__ # are directly connected to internal operators. pass def propagateDirty(self, inputSlot, subindex, key): # Nothing to do here: All outputs are directly connected to # internal operators that handle their own dirty propagation. pass
class OpExplicitMulti(Operator): Output = OutputSlot(level=1) def setupOutputs(self): pass
class OpInputDataReader(Operator): """ This operator can read input data of any supported type. The data format is determined from the file extension. """ name = "OpInputDataReader" category = "Input" videoExts = ['ufmf', 'mmf', 'avi'] h5Exts = ['h5', 'hdf5', 'ilp'] npyExts = ['npy'] rawExts = ['dat', 'bin', 'raw'] blockwiseExts = ['json'] tiledExts = ['json'] tiffExts = ['tif', 'tiff'] vigraImpexExts = vigra.impex.listExtensions().split() SupportedExtensions = h5Exts + npyExts + rawExts + vigraImpexExts + blockwiseExts + videoExts if _supports_dvid: dvidExts = ['dvidvol'] SupportedExtensions += dvidExts # FilePath is inspected to determine data type. # For hdf5 files, append the internal path to the filepath, # e.g. /mydir/myfile.h5/internal/path/to/dataset # For stacks, provide a globstring, e.g. /mydir/input*.png # Other types are determined via file extension WorkingDirectory = InputSlot(stype='filestring', optional=True) FilePath = InputSlot(stype='filestring') # FIXME: Document this. SubVolumeRoi = InputSlot(optional=True) # (start, stop) Output = OutputSlot() loggingName = __name__ + ".OpInputDataReader" logger = logging.getLogger(loggingName) class DatasetReadError(Exception): pass def __init__(self, *args, **kwargs): super(OpInputDataReader, self).__init__(*args, **kwargs) self.internalOperators = [] self.internalOutput = None self._file = None def cleanUp(self): super(OpInputDataReader, self).cleanUp() if self._file is not None: self._file.close() self._file = None def setupOutputs(self): """ Inspect the file name and instantiate and connect an internal operator of the appropriate type. TODO: Handle datasets of non-standard (non-5d) dimensions. """ filePath = self.FilePath.value assert isinstance( filePath, (str, unicode )), "Error: filePath is not of type str. It's of type {}".format( type(filePath)) # Does this look like a relative path? useRelativePath = not isUrl(filePath) and not os.path.isabs(filePath) if useRelativePath: # If using a relative path, we need both inputs before proceeding if not self.WorkingDirectory.ready(): return else: # Convert this relative path into an absolute path filePath = os.path.normpath( os.path.join(self.WorkingDirectory.value, filePath)).replace('\\', '/') # Clean up before reconfiguring if self.internalOperators: self.Output.disconnect() self.opInjector.cleanUp() for op in self.internalOperators[::-1]: op.cleanUp() self.internalOperators = [] self.internalOutput = None if self._file is not None: self._file.close() openFuncs = [ self._attemptOpenAsUfmf, self._attemptOpenAsMmf, self._attemptOpenAsDvidVolume, self._attemptOpenAsTiffStack, self._attemptOpenAsStack, self._attemptOpenAsHdf5, self._attemptOpenAsNpy, self._attemptOpenAsRawBinary, self._attemptOpenAsBlockwiseFileset, self._attemptOpenAsRESTfulBlockwiseFileset, self._attemptOpenAsTiledVolume, self._attemptOpenAsTiff, self._attemptOpenWithVigraImpex ] # Try every method of opening the file until one works. iterFunc = openFuncs.__iter__() while not self.internalOperators: try: openFunc = iterFunc.next() except StopIteration: break self.internalOperators, self.internalOutput = openFunc(filePath) if self.internalOutput is None: raise RuntimeError("Can't read " + filePath + " because it has an unrecognized format.") # If we've got a ROI, append a subregion operator. if self.SubVolumeRoi.ready(): self._opSubRegion = OpSubRegion(parent=self) self._opSubRegion.Roi.setValue(self.SubVolumeRoi.value) self._opSubRegion.Input.connect(self.internalOutput) self.internalOutput = self._opSubRegion.Output self.opInjector = OpMetadataInjector(parent=self) self.opInjector.Input.connect(self.internalOutput) # Add metadata for estimated RAM usage if the internal operator didn't already provide it. if self.internalOutput.meta.ram_per_pixelram_usage_per_requested_pixel is None: ram_per_pixel = self.internalOutput.meta.dtype().nbytes if 'c' in self.internalOutput.meta.getTaggedShape(): ram_per_pixel *= self.internalOutput.meta.getTaggedShape()['c'] self.opInjector.Metadata.setValue( {'ram_per_pixelram_usage_per_requested_pixel': ram_per_pixel}) else: # Nothing to add self.opInjector.Metadata.setValue({}) # Directly connect our own output to the internal output self.Output.connect(self.opInjector.Output) def _attemptOpenAsMmf(self, filePath): if '.mmf' in filePath: mmfReader = OpStreamingMmfReader(parent=self) mmfReader.FileName.setValue(filePath) return ([mmfReader], mmfReader.Output) ''' # Cache the frames we read frameShape = mmfReader.Output.meta.ideal_blockshape mmfCache = OpBlockedArrayCache( parent=self ) mmfCache.fixAtCurrent.setValue( False ) mmfCache.innerBlockShape.setValue( frameShape ) mmfCache.outerBlockShape.setValue( frameShape ) mmfCache.Input.connect( mmfReader.Output ) return ([mmfReader, mmfCache], mmfCache.Output) ''' else: return ([], None) def _attemptOpenAsUfmf(self, filePath): if '.ufmf' in filePath: ufmfReader = OpStreamingUfmfReader(parent=self) ufmfReader.FileName.setValue(filePath) return ([ufmfReader], ufmfReader.Output) # Cache the frames we read ''' frameShape = ufmfReader.Output.meta.ideal_blockshape ufmfCache = OpBlockedArrayCache( parent=self ) ufmfCache.fixAtCurrent.setValue( False ) ufmfCache.innerBlockShape.setValue( frameShape ) ufmfCache.outerBlockShape.setValue( frameShape ) ufmfCache.Input.connect( ufmfReader.Output ) return ([ufmfReader, ufmfCache], ufmfCache.Output) ''' else: return ([], None) def _attemptOpenAsTiffStack(self, filePath): if not ('*' in filePath or os.path.pathsep in filePath): return ([], None) try: opReader = OpTiffSequenceReader(parent=self) opReader.GlobString.setValue(filePath) return (opReader, opReader.Output) except OpTiffSequenceReader.WrongFileTypeError as ex: return ([], None) def _attemptOpenAsStack(self, filePath): if '*' in filePath or os.path.pathsep in filePath: stackReader = OpStackLoader(parent=self) stackReader.globstring.setValue(filePath) return ([stackReader], stackReader.stack) else: return ([], None) def _attemptOpenAsHdf5(self, filePath): # Check for an hdf5 extension h5Exts = OpInputDataReader.h5Exts + ['ilp'] h5Exts = ['.' + ex for ex in h5Exts] ext = None for x in h5Exts: if x in filePath: ext = x if ext is None: return ([], None) externalPath = filePath.split(ext)[0] + ext internalPath = filePath.split(ext)[1] if not os.path.exists(externalPath): raise OpInputDataReader.DatasetReadError( "Input file does not exist: " + externalPath) # Open the h5 file in read-only mode try: h5File = h5py.File(externalPath, 'r') except OpInputDataReader.DatasetReadError: raise except Exception as e: msg = "Unable to open HDF5 File: {}\n{}".format( externalPath, str(e)) raise OpInputDataReader.DatasetReadError(msg) else: if internalPath == '': possible_internal_paths = self._get_hdf5_dataset_names(h5File) if len(possible_internal_paths) == 1: internalPath = possible_internal_paths[0] elif len(possible_internal_paths) == 0: h5File.close() msg = "HDF5 file contains no datasets: {}".format( externalPath) raise OpInputDataReader.DatasetReadError(msg) else: h5File.close() msg = "When using hdf5, you must append the hdf5 internal path to the "\ "data set to your filename, e.g. myfile.h5/volume/data "\ "No internal path provided for dataset in file: {}".format( externalPath ) raise OpInputDataReader.DatasetReadError(msg) try: compression_setting = h5File[internalPath].compression except Exception as e: h5File.close() msg = "Error reading HDF5 File: {}\n{}".format( externalPath, e.msg) raise OpInputDataReader.DatasetReadError(msg) # If the h5 dataset is compressed, we'll have better performance # with a multi-process hdf5 access object. # (Otherwise, single-process is faster.) allow_multiprocess_hdf5 = "LAZYFLOW_MULTIPROCESS_HDF5" in os.environ and os.environ[ "LAZYFLOW_MULTIPROCESS_HDF5"] != "" if compression_setting is not None and allow_multiprocess_hdf5: h5File.close() h5File = MultiProcessHdf5File(externalPath, 'r') self._file = h5File h5Reader = OpStreamingHdf5Reader(parent=self) h5Reader.Hdf5File.setValue(h5File) try: h5Reader.InternalPath.setValue(internalPath) except OpStreamingHdf5Reader.DatasetReadError as e: msg = "Error reading HDF5 File: {}\n{}".format(externalPath, e.msg) raise OpInputDataReader.DatasetReadError(msg) return ([h5Reader], h5Reader.OutputImage) @staticmethod def _get_hdf5_dataset_names(h5_file): """ Helper function for _attemptOpenAsHdf5(). Returns the name of all datasets in the file with at least 2 axes. """ dataset_names = [] def accumulate_names(name, val): if type(val) == h5py._hl.dataset.Dataset and 2 <= len(val.shape): dataset_names.append('/' + name) h5_file.visititems(accumulate_names) return dataset_names def _attemptOpenAsNpy(self, filePath): fileExtension = os.path.splitext(filePath)[1].lower() fileExtension = fileExtension.lstrip('.') # Remove leading dot # Check for numpy extension if fileExtension not in OpInputDataReader.npyExts: return ([], None) else: try: # Create an internal operator npyReader = OpNpyFileReader(parent=self) npyReader.FileName.setValue(filePath) return ([npyReader], npyReader.Output) except OpNpyFileReader.DatasetReadError as e: raise OpInputDataReader.DatasetReadError(*e.args) def _attemptOpenAsRawBinary(self, filePath): fileExtension = os.path.splitext(filePath)[1].lower() fileExtension = fileExtension.lstrip('.') # Remove leading dot # Check for numpy extension if fileExtension not in OpInputDataReader.rawExts: return ([], None) else: try: # Create an internal operator opReader = OpRawBinaryFileReader(parent=self) opReader.FilePath.setValue(filePath) return ([opReader], opReader.Output) except OpRawBinaryFileReader.DatasetReadError as e: raise OpInputDataReader.DatasetReadError(*e.args) def _attemptOpenAsDvidVolume(self, filePath): """ Two ways to specify a dvid volume. 1) via a file that contains the hostname, uuid, and dataset name (1 per line) 2) as a url, e.g. http://localhost:8000/api/node/uuid/dataname """ if os.path.splitext(filePath)[1] == '.dvidvol': with open(filePath) as f: filetext = f.read() hostname, uuid, dataname = filetext.splitlines() opDvidVolume = OpDvidVolume(hostname, uuid, dataname, transpose_axes=True, parent=self) return [opDvidVolume], opDvidVolume.Output if '://' not in filePath: return ([], None) # not a url url_format = "^protocol://hostname/api/node/uuid/dataname(\\?query_string)?" for field in [ 'protocol', 'hostname', 'uuid', 'dataname', 'query_string' ]: url_format = url_format.replace(field, '(?P<' + field + '>[^?]+)') match = re.match(url_format, filePath) if not match: # DVID is the only url-based format we support right now. # So if it looks like the user gave a URL that isn't a valid DVID node, then error. raise OpInputDataReader.DatasetReadError( "Invalid URL format for DVID: {}".format(filePath)) fields = match.groupdict() try: query_string = fields['query_string'] query_args = {} if query_string: query_args = dict( map(lambda s: s.split('='), query_string.split('&'))) try: opDvidVolume = OpDvidVolume(fields['hostname'], fields['uuid'], fields['dataname'], query_args, transpose_axes=True, parent=self) return [opDvidVolume], opDvidVolume.Output except: # Maybe this is actually a roi opDvidRoi = OpDvidRoi(fields['hostname'], fields['uuid'], fields['dataname'], transpose_axes=True, parent=self) return [opDvidRoi], opDvidRoi.Output except OpDvidVolume.DatasetReadError as e: raise OpInputDataReader.DatasetReadError(*e.args) def _attemptOpenAsBlockwiseFileset(self, filePath): fileExtension = os.path.splitext(filePath)[1].lower() fileExtension = fileExtension.lstrip('.') # Remove leading dot if fileExtension in OpInputDataReader.blockwiseExts: opReader = OpBlockwiseFilesetReader(parent=self) try: # This will raise a SchemaError if this is the wrong type of json config. opReader.DescriptionFilePath.setValue(filePath) return ([opReader], opReader.Output) except JsonConfigParser.SchemaError: opReader.cleanUp() except OpBlockwiseFilesetReader.MissingDatasetError as e: raise OpInputDataReader.DatasetReadError(*e.args) return ([], None) def _attemptOpenAsRESTfulBlockwiseFileset(self, filePath): fileExtension = os.path.splitext(filePath)[1].lower() fileExtension = fileExtension.lstrip('.') # Remove leading dot if fileExtension in OpInputDataReader.blockwiseExts: opReader = OpRESTfulBlockwiseFilesetReader(parent=self) try: # This will raise a SchemaError if this is the wrong type of json config. opReader.DescriptionFilePath.setValue(filePath) return ([opReader], opReader.Output) except JsonConfigParser.SchemaError: opReader.cleanUp() except OpRESTfulBlockwiseFilesetReader.MissingDatasetError as e: raise OpInputDataReader.DatasetReadError(*e.args) return ([], None) def _attemptOpenAsTiledVolume(self, filePath): fileExtension = os.path.splitext(filePath)[1].lower() fileExtension = fileExtension.lstrip('.') # Remove leading dot if fileExtension in OpInputDataReader.tiledExts: opReader = OpCachedTiledVolumeReader(parent=self) try: # This will raise a SchemaError if this is the wrong type of json config. opReader.DescriptionFilePath.setValue(filePath) return ([opReader], opReader.SpecifiedOutput) except JsonConfigParser.SchemaError: opReader.cleanUp() return ([], None) def _attemptOpenAsTiff(self, filePath): fileExtension = os.path.splitext(filePath)[1].lower() fileExtension = fileExtension.lstrip('.') # Remove leading dot if fileExtension not in OpInputDataReader.tiffExts: return ([], None) if not os.path.exists(filePath): raise OpInputDataReader.DatasetReadError( "Input file does not exist: " + filePath) opReader = OpTiffReader(parent=self) opReader.Filepath.setValue(filePath) page_shape = opReader.Output.meta.ideal_blockshape # Cache the pages we read opCache = OpBlockedArrayCache(parent=self) opCache.fixAtCurrent.setValue(False) opCache.innerBlockShape.setValue(page_shape) opCache.outerBlockShape.setValue(page_shape) opCache.Input.connect(opReader.Output) return ([opReader, opCache], opCache.Output) def _attemptOpenWithVigraImpex(self, filePath): fileExtension = os.path.splitext(filePath)[1].lower() fileExtension = fileExtension.lstrip('.') # Remove leading dot if fileExtension not in OpInputDataReader.vigraImpexExts: return ([], None) if not os.path.exists(filePath): raise OpInputDataReader.DatasetReadError( "Input file does not exist: " + filePath) vigraReader = OpImageReader(parent=self) vigraReader.Filename.setValue(filePath) # Cache the image instead of reading the hard disk for every access. imageCache = OpBlockedArrayCache(parent=self) imageCache.Input.connect(vigraReader.Image) # 2D: Just one block for the whole image cacheBlockShape = vigraReader.Image.meta.shape taggedShape = vigraReader.Image.meta.getTaggedShape() if 'z' in taggedShape.keys(): # 3D: blocksize is one slice. taggedShape['z'] = 1 cacheBlockShape = tuple(taggedShape.values()) imageCache.fixAtCurrent.setValue(False) imageCache.innerBlockShape.setValue(cacheBlockShape) imageCache.outerBlockShape.setValue(cacheBlockShape) assert imageCache.Output.ready() return ([vigraReader, imageCache], imageCache.Output) def execute(self, slot, subindex, roi, result): assert False, "Shouldn't get here because our output is directly connected..." def propagateDirty(self, slot, subindex, roi): # Output slots are directly conncted to internal operators pass @classmethod def getInternalDatasets(cls, filePath): """ Search the given file for internal datasets, and return their internal paths as a list. For now, it is assumed that the file is an hdf5 file. Returns: A list of the internal datasets in the file, or None if the format doesn't support internal datasets. """ datasetNames = None ext = os.path.splitext(filePath)[1][1:] # HDF5. Other formats don't contain more than one dataset (as far as we're concerned). if ext in OpInputDataReader.h5Exts: datasetNames = [] # Open the file as a read-only so we can get a list of the internal paths with h5py.File(filePath, 'r') as f: # Define a closure to collect all of the dataset names in the file. def accumulateDatasetPaths(name, val): if type(val) == h5py._hl.dataset.Dataset and 3 <= len( val.shape) <= 5: datasetNames.append('/' + name) # Visit every group/dataset in the file f.visititems(accumulateDatasetPaths) return datasetNames
class OpMockPixelClassifier(Operator): """ This class is a simple stand-in for the real pixel classification operator. Uses hard-coded data shape and block shape. Provides hard-coded outputs. """ name = "OpMockPixelClassifier" LabelInputs = InputSlot( optional=True, level=1) # Input for providing label data from an external source InputImages = InputSlot( optional=True, level=1) # Original input data. Used for display only. PredictionsFromDisk = InputSlot( optional=True, level=1) # TODO: Actually use this input for something ClassifierFactory = InputSlot( value=ParallelVigraRfLazyflowClassifierFactory(10, 10)) NonzeroLabelBlocks = OutputSlot( level=1, stype="object") # A list if slices that contain non-zero label values LabelImages = OutputSlot(level=1) # Labels from the user Classifier = OutputSlot(stype="object") PredictionProbabilities = OutputSlot(level=1) FreezePredictions = InputSlot() LabelNames = OutputSlot() LabelColors = OutputSlot() PmapColors = OutputSlot() Bookmarks = OutputSlot(level=1) def __init__(self, *args, **kwargs): super(OpMockPixelClassifier, self).__init__(*args, **kwargs) self.LabelNames.setValue(["Membrane", "Cytoplasm"]) self.LabelColors.setValue([(255, 0, 0), (0, 255, 0)]) # Red, Green self.PmapColors.setValue([(255, 0, 0), (0, 255, 0)]) # Red, Green self._data = [] self.dataShape = (1, 10, 100, 100, 1) self.prediction_shape = self.dataShape[:-1] + ( 2, ) # Hard-coded to provide 2 classes self.FreezePredictions.setValue(False) self.opClassifier = OpTrainClassifierBlocked(graph=self.graph, parent=self) self.opClassifier.ClassifierFactory.connect(self.ClassifierFactory) self.opClassifier.Labels.connect(self.LabelImages) self.opClassifier.nonzeroLabelBlocks.connect(self.NonzeroLabelBlocks) self.opClassifier.MaxLabel.setValue(2) self.classifier_cache = OpValueCache(graph=self.graph, parent=self) self.classifier_cache.Input.connect(self.opClassifier.Classifier) p1 = old_div(numpy.indices(self.dataShape).sum(0), 207.0) p2 = 1 - p1 self.predictionData = numpy.concatenate((p1, p2), axis=4) def setupOutputs(self): numImages = len(self.LabelInputs) self.PredictionsFromDisk.resize(numImages) self.NonzeroLabelBlocks.resize(numImages) self.LabelImages.resize(numImages) self.PredictionProbabilities.resize(numImages) self.opClassifier.Images.resize(numImages) self.InputImages.resize(numImages) for i in range(numImages): self._data.append(numpy.zeros(self.dataShape)) self.NonzeroLabelBlocks[i].meta.shape = (1, ) self.NonzeroLabelBlocks[i].meta.dtype = object # Hard-coded: Two prediction classes self.PredictionProbabilities[i].meta.shape = self.prediction_shape self.PredictionProbabilities[i].meta.dtype = numpy.float64 self.PredictionProbabilities[ i].meta.axistags = vigra.defaultAxistags("txyzc") # Classify with random data self.opClassifier.Images[i].setValue( vigra.taggedView(numpy.random.random(self.dataShape), "txyzc")) self.LabelImages[i].meta.shape = self.dataShape self.InputImages[i].meta.shape = self.dataShape self.LabelImages[i].meta.dtype = numpy.float64 self.LabelImages[i].meta.axistags = self.opClassifier.Images[ i].meta.axistags self.InputImages[i].meta.axistags = self.opClassifier.Images[ i].meta.axistags self.Classifier.connect(self.opClassifier.Classifier) def setInSlot(self, slot, subindex, roi, value): key = roi.toSlice() assert slot.name == "LabelInputs" self._data[subindex[0]][key] = value self.LabelImages[subindex[0]].setDirty(key) def execute(self, slot, subindex, roi, result): key = roiToSlice(roi.start, roi.stop) index = subindex[0] if slot.name == "NonzeroLabelBlocks": # Split into 10 chunks blocks = [] slicing = [slice(0, maximum) for maximum in self.dataShape] for i in range(10): slicing[2] = slice(i * 10, (i + 1) * 10) if not (self._data[index][slicing] == 0).all(): blocks.append(list(slicing)) result[0] = blocks if slot.name == "LabelImages": result[...] = self._data[index][key] if slot.name == "PredictionProbabilities": result[...] = self.predictionData[key] def propagateDirty(self, slot, subindex, roi): pass
class OpLabeling(Operator): """ Top-level operator for the labeling base class. """ name = "OpLabeling" category = "Top-level" # Graph inputs InputImages = InputSlot(level=1) # Original input data. LabelsAllowedFlags = InputSlot( stype='bool', level=1) # Specifies which images are permitted to be labeled LabelInputs = InputSlot( optional=True, level=1) # Input for providing label data from an external source LabelEraserValue = InputSlot() LabelDelete = InputSlot() MaxLabelValue = OutputSlot() LabelImages = OutputSlot(level=1) # Labels from the user NonzeroLabelBlocks = OutputSlot( level=1) # A list if slices that contain non-zero label values def __init__(self, *args, **kwargs): """ Instantiate all internal operators and connect them together. """ super(OpLabeling, self).__init__(*args, **kwargs) # Create internal operators self.opInputShapeReader = OperatorWrapper(OpShapeReader, parent=self, graph=self.graph) self.opLabelArray = OperatorWrapper(OpBlockedSparseLabelArray, parent=self, graph=self.graph) # NOT wrapped self.opMaxLabel = OpMaxValue(parent=self, graph=self.graph) # Set up label cache shape input self.opInputShapeReader.Input.connect(self.InputImages) self.opLabelArray.inputs["shape"].connect( self.opInputShapeReader.OutputShape) # Set up other label cache inputs self.LabelInputs.connect(self.InputImages) self.opLabelArray.Input.connect(self.LabelInputs) self.opLabelArray.eraser.connect(self.LabelEraserValue) self.LabelEraserValue.setValue(255) # Initialize the delete input to -1, which means "no label". # Now changing this input to a positive value will cause label deletions. # (The deleteLabel input is monitored for changes.) self.opLabelArray.deleteLabel.connect(self.LabelDelete) self.LabelDelete.setValue(-1) # Find the highest label in all the label images self.opMaxLabel.Inputs.connect(self.opLabelArray.outputs['maxLabel']) # Connect our internal outputs to our external outputs self.LabelImages.connect(self.opLabelArray.Output) self.MaxLabelValue.connect(self.opMaxLabel.Output) self.NonzeroLabelBlocks.connect(self.opLabelArray.nonzeroBlocks) def inputResizeHandler(slot, oldsize, newsize): if (newsize == 0): self.LabelImages.resize(0) self.NonzeroLabelBlocks.resize(0) self.InputImages.notifyResized(inputResizeHandler) # Check to make sure the non-wrapped operators stayed that way. assert self.opMaxLabel.Inputs.operator == self.opMaxLabel def handleNewInputImage(multislot, index, *args): def handleInputReady(slot): self.setupCaches(multislot.index(slot)) multislot[index].notifyReady(handleInputReady) self.InputImages.notifyInserted(handleNewInputImage) def setupCaches(self, imageIndex): numImages = len(self.InputImages) inputSlot = self.InputImages[imageIndex] self.LabelInputs.resize(numImages) # Special case: We have to set up the shape of our label *input* according to our image input shape shapeList = list(self.InputImages[imageIndex].meta.shape) try: channelIndex = self.InputImages[imageIndex].meta.axistags.index( 'c') shapeList[channelIndex] = 1 except: pass self.LabelInputs[imageIndex].meta.shape = tuple(shapeList) self.LabelInputs[imageIndex].meta.axistags = inputSlot.meta.axistags # Set the blockshapes for each input image separately, depending on which axistags it has. axisOrder = [tag.key for tag in inputSlot.meta.axistags] ## Label Array blocks blockDims = {'t': 1, 'x': 32, 'y': 32, 'z': 32, 'c': 1} blockShape = tuple(blockDims[k] for k in axisOrder) self.opLabelArray.blockShape.setValue(blockShape) def propagateDirty(self, slot, subindex, roi): # Nothing to do here: All outputs are directly connected to # internal operators that handle their own dirty propagation. pass def setInSlot(self, slot, subindex, roi, value): # Nothing to do here: All inputs that support __setitem__ # are directly connected to internal operators. pass
class OpFeatureMatrixCache(Operator): """ - Request features and labels in blocks - For nonzero label pixels in each block, extract the label image - Cache the feature matrix for each block separately - Output the concatenation of all feature matrices Note: This operator does not currently have "NonZeroLabelBlocks" input slot. Instead, it only requests labels for blocks that have been marked dirty via dirty notifications from the LabelImage slot. As a result, you MUST connect/configure this operator before you load your upstream label cache with values. This operator must already be "watching" when when the label operator is initialized with its first labels. """ FeatureImage = InputSlot() LabelImage = InputSlot() # Output is a single 'value', which is a 2D ndarray. # The first row is labels, the rest are the features. # (As a consequence of this, labels are converted to float) LabelAndFeatureMatrix = OutputSlot() ProgressSignal = OutputSlot() # For convenience of passing several progress signals # to a downstream operator (such as OpConcatenateFeatureMatrices), # we provide the progressSignal member as an output slot. def __init__(self, *args, **kwargs): super(OpFeatureMatrixCache, self).__init__(*args, **kwargs) self._lock = RequestLock() self.progressSignal = OrderedSignal() self._progress_lock = RequestLock() self._blockshape = None self._dirty_blocks = set() self._blockwise_feature_matrices = {} self._block_locks = {} # One lock per stored block self._init_blocks(None, None) def _init_blocks(self, input_shape, new_blockshape): old_blockshape = self._blockshape if new_blockshape == old_blockshape: # Nothing to do return if len(self._dirty_blocks) != 0 or len(self._blockwise_feature_matrices) != 0: raise RuntimeError( "It's too late to change the dimensionality of your data after you've already started training.\n" "Delete all your labels and try again." ) # In these set/dict members, the block id (dict key) # is simply the block's start coordinate (as a tuple) self._blockshape = new_blockshape assert all(self._blockshape) logger.debug("Initialized with blockshape: {}".format(new_blockshape)) def setupOutputs(self): # We assume that channel the last axis assert self.FeatureImage.meta.getAxisKeys()[-1] == "c" assert self.LabelImage.meta.getAxisKeys()[-1] == "c" assert self.LabelImage.meta.shape[-1] == 1 # For now, we assume that the two input images have the same shape (except channel) # This constraint could be relaxed in the future if necessary assert ( self.FeatureImage.meta.shape[:-1] == self.LabelImage.meta.shape[:-1] ), "FeatureImage and LabelImage shapes do not match: {} vs {}".format( self.FeatureImage.meta.shape, self.LabelImage.meta.shape ) self.LabelAndFeatureMatrix.meta.shape = (1,) self.LabelAndFeatureMatrix.meta.dtype = object self.LabelAndFeatureMatrix.meta.channel_names = self.FeatureImage.meta.channel_names num_feature_channels = self.FeatureImage.meta.shape[-1] if num_feature_channels != self.LabelAndFeatureMatrix.meta.num_feature_channels: self.LabelAndFeatureMatrix.meta.num_feature_channels = num_feature_channels self.LabelAndFeatureMatrix.setDirty() self.ProgressSignal.meta.shape = (1,) self.ProgressSignal.meta.dtype = object self.ProgressSignal.setValue(self.progressSignal) if self.LabelImage.meta.ideal_blockshape is not None and all(self.LabelImage.meta.ideal_blockshape): blockshape = self.LabelImage.meta.ideal_blockshape else: # Auto-choose a blockshape tagged_shape = self.LabelImage.meta.getTaggedShape() if "t" in tagged_shape: # A block should never span multiple time slices. # For txy volumes, that could lead to lots of extra features being computed. tagged_shape["t"] = 1 blockshape = determineBlockShape(list(tagged_shape.values()), 40 ** 3) # Don't span more than 256 px along any axis blockshape = tuple(min(x, 256) for x in blockshape) self._init_blocks(self.LabelImage.meta.shape, blockshape) def execute(self, slot, subindex, roi, result): assert slot == self.LabelAndFeatureMatrix self.progressSignal(0.0) # Technically, this could result in strange progress reporting if execute() # is called by multiple threads in parallel. # This could be fixed with some fancier progress state, but # (1) We don't expect that to by typical, and # (2) progress reporting is merely informational. num_dirty_blocks = len(self._dirty_blocks) remaining_dirty = [num_dirty_blocks] def update_progress(result): remaining_dirty[0] -= 1 percent_complete = 95.0 * (num_dirty_blocks - remaining_dirty[0]) // num_dirty_blocks self.progressSignal(percent_complete) # Update all dirty blocks in the cache logger.debug("Updating {} dirty blocks".format(num_dirty_blocks)) # Before updating the blocks, ensure that the necessary block locks exist # It's better to do this now instead of inside each request # to avoid contention over self._lock with self._lock: for block_start in self._dirty_blocks: if block_start not in self._block_locks: self._block_locks[block_start] = RequestLock() # Update each block in its own request. pool = RequestPool() reqs = {} for block_start in self._dirty_blocks: req = Request(partial(self._get_features_for_block, block_start)) req.notify_finished(update_progress) reqs[block_start] = req pool.add(req) pool.wait() # Now store the results we got. # It's better to store the blocks here -- rather than within each request -- to # avoid contention over self._lock from within every block's request. with self._lock: for block_start, req in list(reqs.items()): if req.result is None: # 'None' means the block wasn't dirty. No need to update. continue labels_and_features_matrix = req.result self._dirty_blocks.remove(block_start) if labels_and_features_matrix.shape[0] > 0: # Update the block entry with the new matrix. self._blockwise_feature_matrices[block_start] = labels_and_features_matrix else: # All labels were removed from the block, # So the new feature matrix is empty. # Just delete its entry from our list. try: del self._blockwise_feature_matrices[block_start] except KeyError: pass # Concatenate the all blockwise results if self._blockwise_feature_matrices: total_feature_matrix = numpy.concatenate(list(self._blockwise_feature_matrices.values()), axis=0) else: # No label points at all. # Return an empty label&feature matrix (of the correct shape) num_feature_channels = self.FeatureImage.meta.shape[-1] total_feature_matrix = numpy.ndarray(shape=(0, 1 + num_feature_channels), dtype=numpy.float32) self.progressSignal(100.0) logger.debug("After update, there are {} clean blocks".format(len(self._blockwise_feature_matrices))) result[0] = total_feature_matrix def propagateDirty(self, slot, subindex, roi): assert slot == self.FeatureImage or slot == self.LabelImage # Our blocks are tracked by label roi (1 channel) roi = roi.copy() roi.start[-1] = 0 roi.stop[-1] = 1 # Bookkeeping: Track the dirty blocks # If the features were dirty (not labels), we only really care about # the blocks that are actually stored already # For big dirty rois (e.g. the entire image), # we avoid a lot of unnecessary entries in self._dirty_blocks if slot == self.FeatureImage: # We ignore the ROI and assume all blocks are dirty. # Technically, this would be inefficient if it's possible for the features # to become only partially dirty in a small ROI. # But currently, there is no known use-case for that. block_starts = list(self._blockwise_feature_matrices.keys()) else: block_starts = getIntersectingBlocks(self._blockshape, (roi.start, roi.stop)) block_starts = list(map(tuple, block_starts)) with self._lock: self._dirty_blocks.update(set(block_starts)) # Output has no notion of roi. It's all dirty. self.LabelAndFeatureMatrix.setDirty() def _get_features_for_block(self, block_start): """ Computes the feature matrix for the given block IFF the block is dirty. Otherwise, returns None. """ # Caller must ensure that the lock for this block already exists! with self._block_locks[block_start]: if block_start not in self._dirty_blocks: # Nothing to do if this block isn't actually dirty # (For parallel requests, its theoretically possible.) return None block_roi = getBlockBounds(self.LabelImage.meta.shape, self._blockshape, block_start) # TODO: Shrink the requested roi using the nonzero blocks slot... # ...or just get rid of the nonzero blocks slot... labels_and_features_matrix = self._extract_feature_matrix(block_roi) return labels_and_features_matrix def _extract_feature_matrix(self, label_block_roi): num_feature_channels = self.FeatureImage.meta.shape[-1] labels = self.LabelImage(label_block_roi[0], label_block_roi[1]).wait() label_block_positions = numpy.nonzero(labels[..., 0].view(numpy.ndarray)) labels_matrix = labels[label_block_positions].astype(numpy.float32).view(numpy.ndarray) del labels # Done with dense labels block; delete immediately. if len(label_block_positions) == 0 or len(label_block_positions[0]) == 0: # No label points in this roi. # Return an empty label&feature matrix (of the correct shape) return numpy.ndarray(shape=(0, 1 + num_feature_channels), dtype=numpy.float32) # Shrink the roi to the bounding box of nonzero labels block_bounding_box_start = numpy.min(label_block_positions, axis=1) block_bounding_box_stop = 1 + numpy.max(label_block_positions, axis=1) global_bounding_box_start = block_bounding_box_start + label_block_roi[0][:-1] global_bounding_box_stop = block_bounding_box_stop + label_block_roi[0][:-1] # Since we're just requesting the bounding box, offset the feature positions by the box start bounding_box_positions = numpy.transpose( numpy.transpose(label_block_positions) - numpy.array(block_bounding_box_start) ) bounding_box_positions = tuple(bounding_box_positions) # Append channel roi (all feature channels) feature_roi_start = list(global_bounding_box_start) + [0] feature_roi_stop = list(global_bounding_box_stop) + [num_feature_channels] # Request features (bounding box only) features = self.FeatureImage(feature_roi_start, feature_roi_stop).wait() # Cast as plain ndarray (not VigraArray), since we don't need/want axistags features_matrix = features[bounding_box_positions].view(numpy.ndarray) return numpy.concatenate((labels_matrix, features_matrix), axis=1)
class OpTrackingFeatureExtraction(Operator): name = "Tracking Feature Extraction" TranslationVectors = InputSlot(optional=True) RawImage = InputSlot() BinaryImage = InputSlot() # which features to compute. # nested dictionary with format: # dict[plugin_name][feature_name][parameter_name] = parameter_value # for example {"Standard Object Features": {"Mean in neighborhood":{"margin": (5, 5, 2)}}} FeatureNamesVigra = InputSlot(rtype=List, stype=Opaque, value={}) FeatureNamesDivision = InputSlot(rtype=List, stype=Opaque, value={}) # Bypass cache (for headless mode) BypassModeEnabled = InputSlot(value=False) LabelImage = OutputSlot() ObjectCenterImage = OutputSlot() # the computed features. # nested dictionary with format: # dict[plugin_name][feature_name] = feature_value RegionFeaturesVigra = OutputSlot(stype=Opaque, rtype=List) RegionFeaturesDivision = OutputSlot(stype=Opaque, rtype=List) RegionFeaturesAll = OutputSlot(stype=Opaque, rtype=List) ComputedFeatureNamesAll = OutputSlot(rtype=List, stype=Opaque) ComputedFeatureNamesNoDivisions = OutputSlot(rtype=List, stype=Opaque) BlockwiseRegionFeaturesVigra = OutputSlot( ) # For compatibility with tracking workflow, the RegionFeatures output # has rtype=List, indexed by t. # For other workflows, output has rtype=ArrayLike, indexed by (t) BlockwiseRegionFeaturesDivision = OutputSlot() CleanLabelBlocks = OutputSlot() LabelImageCacheInput = InputSlot(optional=True) RegionFeaturesCacheInputVigra = InputSlot(optional=True) RegionFeaturesCleanBlocksVigra = OutputSlot() RegionFeaturesCacheInputDivision = InputSlot(optional=True) RegionFeaturesCleanBlocksDivision = OutputSlot() def __init__(self, parent): super(OpTrackingFeatureExtraction, self).__init__(parent) # internal operators self._objectExtraction = OpObjectExtraction(parent=self) self._opDivFeats = OpCachedDivisionFeatures(parent=self) self._opDivFeatsAdaptOutput = OpAdaptTimeListRoi(parent=self) # connect internal operators self._objectExtraction.RawImage.connect(self.RawImage) self._objectExtraction.BinaryImage.connect(self.BinaryImage) self._objectExtraction.BypassModeEnabled.connect( self.BypassModeEnabled) self._objectExtraction.Features.connect(self.FeatureNamesVigra) self._objectExtraction.RegionFeaturesCacheInput.connect( self.RegionFeaturesCacheInputVigra) self._objectExtraction.LabelImageCacheInput.connect( self.LabelImageCacheInput) self.CleanLabelBlocks.connect(self._objectExtraction.CleanLabelBlocks) self.RegionFeaturesCleanBlocksVigra.connect( self._objectExtraction.RegionFeaturesCleanBlocks) self.ObjectCenterImage.connect( self._objectExtraction.ObjectCenterImage) self.LabelImage.connect(self._objectExtraction.LabelImage) self.BlockwiseRegionFeaturesVigra.connect( self._objectExtraction.BlockwiseRegionFeatures) self.RegionFeaturesVigra.connect(self._objectExtraction.RegionFeatures) self._opDivFeats.LabelImage.connect(self.LabelImage) self._opDivFeats.DivisionFeatureNames.connect( self.FeatureNamesDivision) self._opDivFeats.CacheInput.connect( self.RegionFeaturesCacheInputDivision) self._opDivFeats.RegionFeaturesVigra.connect( self._objectExtraction.BlockwiseRegionFeatures) self.RegionFeaturesCleanBlocksDivision.connect( self._opDivFeats.CleanBlocks) self.BlockwiseRegionFeaturesDivision.connect(self._opDivFeats.Output) self._opDivFeatsAdaptOutput.Input.connect(self._opDivFeats.Output) self.RegionFeaturesDivision.connect(self._opDivFeatsAdaptOutput.Output) # As soon as input data is available, check its constraints self.RawImage.notifyReady(self._checkConstraints) self.BinaryImage.notifyReady(self._checkConstraints) # FIXME this shouldn't be done in post-filtering, but in reading the config or around that time self.RawImage.notifyReady(self._filterFeaturesByDim) def setupOutputs(self, *args, **kwargs): self.ComputedFeatureNamesAll.meta.assignFrom( self.FeatureNamesVigra.meta) self.ComputedFeatureNamesNoDivisions.meta.assignFrom( self.FeatureNamesVigra.meta) self.RegionFeaturesAll.meta.assignFrom(self.RegionFeaturesVigra.meta) def execute(self, slot, subindex, roi, result): if slot == self.ComputedFeatureNamesAll: feat_names_vigra = self.FeatureNamesVigra([]).wait() feat_names_div = self.FeatureNamesDivision([]).wait() for plugin_name in feat_names_vigra.keys(): assert plugin_name not in feat_names_div, "feature name dictionaries must be mutually exclusive" for plugin_name in feat_names_div.keys(): assert plugin_name not in feat_names_vigra, "feature name dictionaries must be mutually exclusive" result = dict(feat_names_vigra.items() + feat_names_div.items()) return result elif slot == self.ComputedFeatureNamesNoDivisions: feat_names_vigra = self.FeatureNamesVigra([]).wait() result = dict(feat_names_vigra.items()) return result elif slot == self.RegionFeaturesAll: feat_vigra = self.RegionFeaturesVigra(roi).wait() feat_div = self.RegionFeaturesDivision(roi).wait() assert np.all(feat_vigra.keys() == feat_div.keys()) result = {} for t in feat_vigra.keys(): for plugin_name in feat_vigra[t].keys(): assert plugin_name not in feat_div[ t], "feature dictionaries must be mutually exclusive" for plugin_name in feat_div[t].keys(): assert plugin_name not in feat_vigra[ t], "feature dictionaries must be mutually exclusive" result[t] = dict(feat_div[t].items() + feat_vigra[t].items()) return result else: assert False, "Shouldn't get here." def propagateDirty(self, slot, subindex, roi): if slot == self.BypassModeEnabled: pass elif slot == self.FeatureNamesVigra or slot == self.FeatureNamesDivision: self.ComputedFeatureNamesAll.setDirty(roi) self.ComputedFeatureNamesNoDivisions.setDirty(roi) def setInSlot(self, slot, subindex, roi, value): assert slot == self.RegionFeaturesCacheInputVigra or \ slot == self.RegionFeaturesCacheInputDivision or \ slot == self.LabelImageCacheInput, "Invalid slot for setInSlot(): {}".format(slot.name) def _checkConstraints(self, *args): if self.RawImage.ready(): rawTaggedShape = self.RawImage.meta.getTaggedShape() if 't' not in rawTaggedShape or rawTaggedShape['t'] < 2: msg = "Raw image must have a time dimension with at least 2 images.\n"\ "Your dataset has shape: {}".format(self.RawImage.meta.shape) if self.BinaryImage.ready(): rawTaggedShape = self.BinaryImage.meta.getTaggedShape() if 't' not in rawTaggedShape or rawTaggedShape['t'] < 2: msg = "Binary image must have a time dimension with at least 2 images.\n"\ "Your dataset has shape: {}".format(self.BinaryImage.meta.shape) if self.RawImage.ready() and self.BinaryImage.ready(): rawTaggedShape = self.RawImage.meta.getTaggedShape() binTaggedShape = self.BinaryImage.meta.getTaggedShape() rawTaggedShape['c'] = None binTaggedShape['c'] = None if dict(rawTaggedShape) != dict(binTaggedShape): logger.info("Raw data and other data must have equal dimensions (different channels are okay).\n"\ "Your datasets have shapes: {} and {}".format( self.RawImage.meta.shape, self.BinaryImage.meta.shape )) msg = "Raw data and other data must have equal dimensions (different channels are okay).\n"\ "Your datasets have shapes: {} and {}".format( self.RawImage.meta.shape, self.BinaryImage.meta.shape ) raise DatasetConstraintError("Object Extraction", msg) def _filterFeaturesByDim(self, *args): # Remove 2D-only features from 3D datasets # Features look as follows: # dict[plugin_name][feature_name][parameter_name] = parameter_value # for example {"Standard Object Features": {"Mean in neighborhood":{"margin": (5, 5, 2)}}} if self.RawImage.ready() and self.FeatureNamesVigra.ready(): rawTaggedShape = self.RawImage.meta.getTaggedShape() filtered_features_dict = {} if rawTaggedShape['z'] > 1: # Filter out the 2D-only features, which helpfully have "2D" in their plugin name current_dict = self.FeatureNamesVigra.value for plugin in current_dict.keys(): if not "2D" in plugin: filtered_features_dict[plugin] = current_dict[plugin] self.FeatureNamesVigra.setValue(filtered_features_dict)