Ejemplo n.º 1
0
 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()
Ejemplo n.º 2
0
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()
Ejemplo n.º 4
0
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...
Ejemplo n.º 5
0
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)
Ejemplo n.º 6
0
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)
Ejemplo n.º 7
0
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())
Ejemplo n.º 8
0
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)
Ejemplo n.º 9
0
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
Ejemplo n.º 10
0
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()
Ejemplo n.º 11
0
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()
Ejemplo n.º 12
0
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
Ejemplo n.º 13
0
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)
Ejemplo n.º 14
0
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)
Ejemplo n.º 15
0
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)
Ejemplo n.º 16
0
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())
Ejemplo n.º 17
0
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()
Ejemplo n.º 18
0
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)
Ejemplo n.º 19
0
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()
Ejemplo n.º 20
0
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...
Ejemplo n.º 21
0
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
Ejemplo n.º 22
0
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
Ejemplo n.º 23
0
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
Ejemplo n.º 24
0
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()
Ejemplo n.º 25
0
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
Ejemplo n.º 26
0
class OpExplicitMulti(Operator):
    Output = OutputSlot(level=1)
    
    def setupOutputs(self):
        pass
Ejemplo n.º 27
0
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
Ejemplo n.º 29
0
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
Ejemplo n.º 30
0
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)
Ejemplo n.º 31
0
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)