Example #1
0
def test_normalize_channels_in_place():
    a = np.zeros((100, 200, 3), dtype=np.float32)
    a[..., 0] = (0.2 / 2)
    a[..., 1] = (0.3 / 2)
    a[..., 2] = (0.5 / 2)

    # erase a pixel entirely
    a[50, 50, :] = 0.0

    normalize_channels_in_place(a)
    assert (a.sum(axis=-1) == 1.0).all()

    assert a[50, 50, 0] == a[50, 50, 1] == a[50, 50, 2]
def test_normalize_channels_in_place():
    a = np.zeros((100,200,3), dtype=np.float32)
    a[..., 0] = (0.2 / 2)
    a[..., 1] = (0.3 / 2)
    a[..., 2] = (0.5 / 2)

    # erase a pixel entirely
    a[50,50,:] = 0.0
    
    normalize_channels_in_place(a)
    assert (a.sum(axis=-1) == 1.0).all()

    assert a[50,50,0] == a[50,50,1] == a[50,50,2]
def ilastik_simple_predict(gray_vol, mask, classifier_path, filter_specs_path, selected_channels=None, normalize=True, 
                           LAZYFLOW_THREADS=0, LAZYFLOW_TOTAL_RAM_MB=None, logfile="/dev/null"):
    """
    gray_vol: A 3D numpy array with axes zyx

    mask: A binary image where 0 means "no prediction necessary".
         'None' can be given, which means "predict everything".

    classifier_path: Path to a vigra RandomForest classifier, in HDF5.
                     Example: /path/to/myclassifier.h5/classifiers/my_rf

    filter_specs_path: Path to "filter specs" json file.  The json structure is like this:
                       [ ['GaussianSmoothing', 0.3],
                         ['GaussianSmoothing', 0.7],
                         ['LaplacianOfGaussian', 1.6] ]
                       (See ilastik's simple_predict.py for valid filter names.)
     
    selected_channels: A list of channel indexes to select and return from the prediction results.
                       'None' can also be given, which means "return all prediction channels".
                       You may also return a *nested* list, in which case groups of channels can be
                       combined (summed) into their respective output channels.
                       For example: selected_channels=[0,3,[2,4],7] means the output will have 4 channels:
                                    0,3,2+4,7 (channels 5 and 6 are simply dropped).
    
    normalize: Renormalize all outputs so the channels sum to 1 everywhere.
               That is, (predictions.sum(axis=-1) == 1.0).all()
               Note: Pixels with 0.0 in all channels will be simply given a value of 1/N in all channels.
    
    LAZYFLOW_THREADS, LAZYFLOW_TOTAL_RAM_MB: Same meanings as in ilastik_predict_with_array().
                      (although we have to configure them in a different way)
    """
    print("ilastik_simple_predict(): Starting with raw data: dtype={}, shape={}".format(str(gray_vol.dtype), gray_vol.shape))

    import os
    from collections import OrderedDict

    import uuid
    import platform
    import vigra

    from ilastik.utility.simple_predict import load_and_predict
    from lazyflow.request import Request
    
    print("ilastik_simple_predict(): Done with imports")

    _prepare_lazyflow_config(LAZYFLOW_THREADS, LAZYFLOW_TOTAL_RAM_MB, 10)

    Request.reset_thread_pool(LAZYFLOW_THREADS)

    # The process_name argument is prefixed to all log messages.
    # For now, just use the machine name and a uuid
    # FIXME: It would be nice to provide something more descriptive, like the ROI of the current spark job...
    process_name = platform.node() + "-" + str(uuid.uuid1())

    # To avoid conflicts between processes, give each process it's own logfile to write to.
    if logfile != "/dev/null":
        base, ext = os.path.splitext(logfile)
        logfile = base + '.' + process_name + ext

    _init_logging(logfile, process_name)
    
    # Construct an OrderedDict of role-names -> DatasetInfos
    # (See PixelClassificationWorkflow.ROLE_NAMES)
    raw_data_array = vigra.taggedView(gray_vol, 'zyx')
    print("ilastik_simple_predict(): Starting export...")

    predictions = load_and_predict( raw_data_array, classifier_path, filter_specs_path, compute_blockwise=True ) 
    selected_predictions = select_channels(predictions, selected_channels)

    if normalize:
        normalize_channels_in_place(selected_predictions)
    
    return selected_predictions
def two_stage_voxel_predictions(gray_vol,
                                mask,
                                stage_1_ilp_path,
                                stage_2_ilp_path,
                                selected_channels=None,
                                normalize=True,
                                LAZYFLOW_THREADS=1,
                                LAZYFLOW_TOTAL_RAM_MB=None,
                                logfile="/dev/null",
                                extra_cmdline_args=[]):
    """
    Using ilastik's python API, run a two-stage voxel prediction using the two given project files.
    The output of the first stage will be saved to a temporary location on disk and used as input to the second stage.
    
    gray_vol: A 3D numpy array with axes zyx

    mask: A binary image where 0 means "no prediction necessary".
         'None' can be given, which means "predict everything".
         (It will only be used during the second stage.)

    ilp_stage_1_path: Path to the project file for the first stage.  Should accept graystale uint8 data as the input.
                      ilastik also accepts a url to a DVID key-value, which will be downloaded and opened as an ilp

    ilp_stage_1_path: Path to the project file for the second stage.  Should take N input channels (uint8) as input, 
                      where N is the number of channels produced in stage 1.
    
    selected_channels: A list of channel indexes to select and return from the prediction results.
                       'None' can also be given, which means "return all prediction channels".
                       You may also return a *nested* list, in which case groups of channels can be
                       combined (summed) into their respective output channels.
                       For example: selected_channels=[0,3,[2,4],7] means the output will have 4 channels:
                                    0,3,2+4,7 (channels 5 and 6 are simply dropped).
    
    normalize: Renormalize all outputs so the channels sum to 1 everywhere.
               That is, (predictions.sum(axis=-1) == 1.0).all()
               Note: Pixels with 0.0 in all channels will be simply given a value of 1/N in all channels.
    
    LAZYFLOW_THREADS, LAZYFLOW_TOTAL_RAM_MB: Passed to ilastik via environment variables.
    """

    print("two_stage_voxel_predictions(): Starting with raw data: dtype={}, shape={}"\
          .format(str(gray_vol.dtype), gray_vol.shape))

    import tempfile
    import shutil
    import numpy as np
    import h5py

    scratch_dir = tempfile.mkdtemp(prefix='voxel_predictions_')
    logger.info("Writing intermediate results to scratch directory: " +
                scratch_dir)

    #logger.info( "FIXME: Writing grayscale for debug purposes" )
    #with h5py.File(scratch_dir + '/grayscale.h5', 'w') as grayscale_file:
    #    grayscale_file.create_dataset('grayscale', data=gray_vol)

    try:
        # Run predictions on the in-memory data.
        stage_1_output_path = scratch_dir + '/stage_1_predictions.h5'
        run_ilastik_stage(1, stage_1_ilp_path, gray_vol, None,
                          stage_1_output_path, LAZYFLOW_THREADS,
                          LAZYFLOW_TOTAL_RAM_MB, logfile, extra_cmdline_args)
        stage_2_output_path = scratch_dir + '/stage_2_predictions.h5'
        run_ilastik_stage(2, stage_2_ilp_path, stage_1_output_path, mask,
                          stage_2_output_path, LAZYFLOW_THREADS,
                          LAZYFLOW_TOTAL_RAM_MB, logfile, extra_cmdline_args)

        combined_predictions_path = scratch_dir + '/combined_predictions.h5'

        # Sadly, we must rewrite the predictions into a single file, because they might be combined together.
        # Technically, we could avoid this with some fancy logic, but that would be really annoying.
        with h5py.File(combined_predictions_path,
                       'w') as combined_predictions_file:
            with h5py.File(stage_1_output_path, 'r') as stage_1_prediction_file, \
                 h5py.File(stage_2_output_path, 'r') as stage_2_prediction_file:
                stage_1_predictions = stage_1_prediction_file['predictions']
                stage_2_predictions = stage_2_prediction_file['predictions']

                assert stage_1_predictions.dtype == stage_2_predictions.dtype, \
                    "Mismatched dtypes: {} vs {}".format( stage_1_predictions.dtype, stage_2_predictions.dtype )

                stage_1_channels = stage_1_predictions.shape[-1]
                stage_2_channels = stage_2_predictions.shape[-1]

                assert stage_1_predictions.shape[:-1] == stage_2_predictions.shape[:-1], \
                    "Non-channel dimensions must match.  shapes were: {} and {}"\
                    .format(stage_1_predictions.shape, stage_2_predictions.shape)

                combined_shape = stage_1_predictions.shape[:-1] + (
                    (stage_1_channels + stage_2_channels), )
                combined_predictions = combined_predictions_file.create_dataset(
                    'predictions',
                    dtype=stage_1_predictions.dtype,
                    shape=combined_shape,
                    chunks=(64, 64, 64, 1))

                # Do this one channel at a time to save RAM
                for c in range(stage_1_channels):
                    combined_predictions[..., c] = stage_1_predictions[..., c]
                for c in range(stage_2_channels):
                    combined_predictions[..., stage_1_channels +
                                         c] = stage_2_predictions[..., c]

            num_channels = combined_predictions.shape[-1]

            if selected_channels:
                assert isinstance(selected_channels, list)
                for selection in selected_channels:
                    if isinstance(selection, list):
                        assert all(c < num_channels for c in selection), \
                            "Selected channels ({}) exceed number of prediction classes ({})"\
                            .format( selected_channels, num_channels )
                    else:
                        assert selection < num_channels, \
                            "Selected channels ({}) exceed number of prediction classes ({})"\
                            .format( selected_channels, num_channels )

            # This will extract the channels we want, converting from hdf5 to numpy along the way.
            selected_predictions = select_channels(combined_predictions,
                                                   selected_channels)

        if normalize:
            normalize_channels_in_place(selected_predictions)

        assert selected_predictions.dtype == np.float32
        return selected_predictions
    finally:
        shutil.rmtree(scratch_dir)
def two_stage_voxel_predictions(gray_vol, mask, stage_1_ilp_path, stage_2_ilp_path, selected_channels=None, normalize=True, 
                                LAZYFLOW_THREADS=1, LAZYFLOW_TOTAL_RAM_MB=None, logfile="/dev/null", extra_cmdline_args=[]):
    """
    Using ilastik's python API, run a two-stage voxel prediction using the two given project files.
    The output of the first stage will be saved to a temporary location on disk and used as input to the second stage.
    
    gray_vol: A 3D numpy array with axes zyx

    mask: A binary image where 0 means "no prediction necessary".
         'None' can be given, which means "predict everything".
         (It will only be used during the second stage.)

    ilp_stage_1_path: Path to the project file for the first stage.  Should accept graystale uint8 data as the input.
                      ilastik also accepts a url to a DVID key-value, which will be downloaded and opened as an ilp

    ilp_stage_1_path: Path to the project file for the second stage.  Should take N input channels (uint8) as input, 
                      where N is the number of channels produced in stage 1.
    
    selected_channels: A list of channel indexes to select and return from the prediction results.
                       'None' can also be given, which means "return all prediction channels".
                       You may also return a *nested* list, in which case groups of channels can be
                       combined (summed) into their respective output channels.
                       For example: selected_channels=[0,3,[2,4],7] means the output will have 4 channels:
                                    0,3,2+4,7 (channels 5 and 6 are simply dropped).
    
    normalize: Renormalize all outputs so the channels sum to 1 everywhere.
               That is, (predictions.sum(axis=-1) == 1.0).all()
               Note: Pixels with 0.0 in all channels will be simply given a value of 1/N in all channels.
    
    LAZYFLOW_THREADS, LAZYFLOW_TOTAL_RAM_MB: Passed to ilastik via environment variables.
    """
    
    print("two_stage_voxel_predictions(): Starting with raw data: dtype={}, shape={}"\
          .format(str(gray_vol.dtype), gray_vol.shape))

    import tempfile
    import shutil
    import numpy as np
    import h5py

    scratch_dir = tempfile.mkdtemp(prefix='voxel_predictions_')
    logger.info( "Writing intermediate results to scratch directory: " + scratch_dir )

    #logger.info( "FIXME: Writing grayscale for debug purposes" )    
    #with h5py.File(scratch_dir + '/grayscale.h5', 'w') as grayscale_file:
    #    grayscale_file.create_dataset('grayscale', data=gray_vol)

    try:
        # Run predictions on the in-memory data.
        stage_1_output_path = scratch_dir + '/stage_1_predictions.h5'
        run_ilastik_stage(1, stage_1_ilp_path, gray_vol, None, stage_1_output_path,
                          LAZYFLOW_THREADS, LAZYFLOW_TOTAL_RAM_MB, logfile, extra_cmdline_args)
        stage_2_output_path = scratch_dir + '/stage_2_predictions.h5'
        run_ilastik_stage(2, stage_2_ilp_path, stage_1_output_path, mask, stage_2_output_path,
                          LAZYFLOW_THREADS, LAZYFLOW_TOTAL_RAM_MB, logfile, extra_cmdline_args)
    
        combined_predictions_path = scratch_dir + '/combined_predictions.h5'
    
        # Sadly, we must rewrite the predictions into a single file, because they might be combined together.
        # Technically, we could avoid this with some fancy logic, but that would be really annoying.
        with h5py.File(combined_predictions_path, 'w') as combined_predictions_file:
            with h5py.File(stage_1_output_path, 'r') as stage_1_prediction_file, \
                 h5py.File(stage_2_output_path, 'r') as stage_2_prediction_file:
                stage_1_predictions = stage_1_prediction_file['predictions']
                stage_2_predictions = stage_2_prediction_file['predictions']
    
                assert stage_1_predictions.dtype == stage_2_predictions.dtype, \
                    "Mismatched dtypes: {} vs {}".format( stage_1_predictions.dtype, stage_2_predictions.dtype )
        
                stage_1_channels = stage_1_predictions.shape[-1]
                stage_2_channels = stage_2_predictions.shape[-1]
                
                assert stage_1_predictions.shape[:-1] == stage_2_predictions.shape[:-1], \
                    "Non-channel dimensions must match.  shapes were: {} and {}"\
                    .format(stage_1_predictions.shape, stage_2_predictions.shape)
                
                combined_shape = stage_1_predictions.shape[:-1] + ((stage_1_channels + stage_2_channels),)
                combined_predictions = combined_predictions_file.create_dataset('predictions',
                                                                                dtype=stage_1_predictions.dtype,
                                                                                shape=combined_shape,
                                                                                chunks=(64,64,64,1) )

                # Do this one channel at a time to save RAM
                for c in range(stage_1_channels):
                    combined_predictions[..., c] = stage_1_predictions[..., c]
                for c in range(stage_2_channels):
                    combined_predictions[..., stage_1_channels+c] = stage_2_predictions[..., c]
    
            num_channels = combined_predictions.shape[-1]
        
            if selected_channels:
                assert isinstance(selected_channels, list)
                for selection in selected_channels:
                    if isinstance(selection, list):
                        assert all(c < num_channels for c in selection), \
                            "Selected channels ({}) exceed number of prediction classes ({})"\
                            .format( selected_channels, num_channels )
                    else:
                        assert selection < num_channels, \
                            "Selected channels ({}) exceed number of prediction classes ({})"\
                            .format( selected_channels, num_channels )
    
            # This will extract the channels we want, converting from hdf5 to numpy along the way.    
            selected_predictions = select_channels(combined_predictions, selected_channels)
        
        if normalize:
            normalize_channels_in_place(selected_predictions)
        
        assert selected_predictions.dtype == np.float32
        return selected_predictions
    finally:
        shutil.rmtree(scratch_dir)
def ilastik_predict_with_array(gray_vol, mask, ilp_path, selected_channels=None, normalize=True, 
                               LAZYFLOW_THREADS=1, LAZYFLOW_TOTAL_RAM_MB=None, logfile="/dev/null", extra_cmdline_args=[]):
    """
    Using ilastik's python API, open the given project 
    file and run a prediction on the given raw data array.
    
    Other than the project file, nothing is read or written 
    using the hard disk.
    
    gray_vol: A 3D numpy array with axes zyx

    mask: A binary image where 0 means "no prediction necessary".
         'None' can be given, which means "predict everything".

    ilp_path: Path to the project file.  ilastik also accepts a url to a DVID key-value, which will be downloaded and opened as an ilp
    
    selected_channels: A list of channel indexes to select and return from the prediction results.
                       'None' can also be given, which means "return all prediction channels".
                       You may also return a *nested* list, in which case groups of channels can be
                       combined (summed) into their respective output channels.
                       For example: selected_channels=[0,3,[2,4],7] means the output will have 4 channels:
                                    0,3,2+4,7 (channels 5 and 6 are simply dropped).
    
    normalize: Renormalize all outputs so the channels sum to 1 everywhere.
               That is, (predictions.sum(axis=-1) == 1.0).all()
               Note: Pixels with 0.0 in all channels will be simply given a value of 1/N in all channels.
    
    LAZYFLOW_THREADS, LAZYFLOW_TOTAL_RAM_MB: Passed to ilastik via environment variables.
    """
    print "ilastik_predict_with_array(): Starting with raw data: dtype={}, shape={}".format(str(gray_vol.dtype), gray_vol.shape)

    import os
    from collections import OrderedDict

    import uuid
    import multiprocessing
    import platform
    import psutil
    import vigra

    import ilastik_main
    from ilastik.applets.dataSelection import DatasetInfo

    print "ilastik_predict_with_array(): Done with imports"

    if LAZYFLOW_TOTAL_RAM_MB is None:
        # By default, assume our alotted RAM is proportional 
        # to the CPUs we've been told to use
        machine_ram = psutil.virtual_memory().total
        machine_ram -= 1024**3 # Leave 1 GB RAM for the OS.

        LAZYFLOW_TOTAL_RAM_MB = LAZYFLOW_THREADS * machine_ram / multiprocessing.cpu_count()

    # Before we start ilastik, prepare the environment variable settings.
    os.environ["LAZYFLOW_THREADS"] = str(LAZYFLOW_THREADS)
    os.environ["LAZYFLOW_TOTAL_RAM_MB"] = str(LAZYFLOW_TOTAL_RAM_MB)
    os.environ["LAZYFLOW_STATUS_MONITOR_SECONDS"] = "10"

    # Prepare ilastik's "command-line" arguments, as if they were already parsed.
    args, extra_workflow_cmdline_args = ilastik_main.parser.parse_known_args(extra_cmdline_args)
    args.headless = True
    args.debug = True # ilastik's 'debug' flag enables special power features, including experimental workflows.
    args.project = ilp_path
    args.readonly = True

    # By default, all ilastik processes duplicate their console output to ~/.ilastik_log.txt
    # Obviously, having all spark nodes write to a common file is a bad idea.
    # The "/dev/null" setting here is recognized by ilastik and means "Don't write a log file"
    args.logfile = logfile

    # The process_name argument is prefixed to all log messages.
    # For now, just use the machine name and a uuid
    # FIXME: It would be nice to provide something more descriptive, like the ROI of the current spark job...
    args.process_name = platform.node() + "-" + str(uuid.uuid1())

    print "ilastik_predict_with_array(): Creating shell..."

    # Instantiate the 'shell', (in this case, an instance of ilastik.shell.HeadlessShell)
    # This also loads the project file into shell.projectManager
    shell = ilastik_main.main( args, extra_workflow_cmdline_args )

    ## Need to find a better way to verify the workflow type
    #from ilastik.workflows.pixelClassification import PixelClassificationWorkflow
    #assert isinstance(shell.workflow, PixelClassificationWorkflow)

    # Construct an OrderedDict of role-names -> DatasetInfos
    # (See PixelClassificationWorkflow.ROLE_NAMES)
    raw_data_array = vigra.taggedView(gray_vol, 'zyx')
    role_data_dict = OrderedDict([ ("Raw Data", [ DatasetInfo(preloaded_array=raw_data_array) ]) ])
    
    if mask is not None:
        # If there's a mask, we might be able to save some computation time.
        mask = vigra.taggedView(mask, 'zyx')
        role_data_dict["Prediction Mask"] = [ DatasetInfo(preloaded_array=mask) ]

    print "ilastik_predict_with_array(): Starting export..."

    # Sanity checks
    opInteractiveExport = shell.workflow.batchProcessingApplet.dataExportApplet.topLevelOperator.getLane(0)
    selected_result = opInteractiveExport.InputSelection.value
    num_channels = opInteractiveExport.Inputs[selected_result].meta.shape[-1]
    
    # For convenience, verify the selected channels before we run the export.
    if selected_channels:
        assert isinstance(selected_channels, list)
        for selection in selected_channels:
            if isinstance(selection, list):
                assert all(c < num_channels for c in selection), \
                    "Selected channels ({}) exceed number of prediction classes ({})"\
                    .format( selected_channels, num_channels )
            else:
                assert selection < num_channels, \
                    "Selected channels ({}) exceed number of prediction classes ({})"\
                    .format( selected_channels, num_channels )
                

    # Run the export via the BatchProcessingApplet
    prediction_list = shell.workflow.batchProcessingApplet.run_export(role_data_dict, export_to_array=True)
    assert len(prediction_list) == 1
    predictions = prediction_list[0]

    assert predictions.shape[-1] == num_channels
    selected_predictions = select_channels(predictions, selected_channels)

    if normalize:
        normalize_channels_in_place(selected_predictions)
    
    return selected_predictions
def ilastik_predict_with_array(gray_vol,
                               mask,
                               ilp_path,
                               selected_channels=None,
                               normalize=True,
                               LAZYFLOW_THREADS=1,
                               LAZYFLOW_TOTAL_RAM_MB=None,
                               logfile="/dev/null",
                               extra_cmdline_args=[]):
    """
    Using ilastik's python API, open the given project 
    file and run a prediction on the given raw data array.
    
    Other than the project file, nothing is read or written 
    using the hard disk.
    
    gray_vol: A 3D numpy array with axes zyx

    mask: A binary image where 0 means "no prediction necessary".
         'None' can be given, which means "predict everything".

    ilp_path: Path to the project file.  ilastik also accepts a url to a DVID key-value, which will be downloaded and opened as an ilp
    
    selected_channels: A list of channel indexes to select and return from the prediction results.
                       'None' can also be given, which means "return all prediction channels".
                       You may also return a *nested* list, in which case groups of channels can be
                       combined (summed) into their respective output channels.
                       For example: selected_channels=[0,3,[2,4],7] means the output will have 4 channels:
                                    0,3,2+4,7 (channels 5 and 6 are simply dropped).
    
    normalize: Renormalize all outputs so the channels sum to 1 everywhere.
               That is, (predictions.sum(axis=-1) == 1.0).all()
               Note: Pixels with 0.0 in all channels will be simply given a value of 1/N in all channels.
    
    LAZYFLOW_THREADS, LAZYFLOW_TOTAL_RAM_MB: Passed to ilastik via environment variables.
    """
    print "ilastik_predict_with_array(): Starting with raw data: dtype={}, shape={}".format(
        str(gray_vol.dtype), gray_vol.shape)

    import os
    from collections import OrderedDict

    import uuid
    import multiprocessing
    import platform
    import psutil
    import vigra

    import ilastik_main
    from ilastik.applets.dataSelection import DatasetInfo
    from lazyflow.operators.cacheMemoryManager import CacheMemoryManager

    import logging
    logging.getLogger(__name__).info('status=ilastik prediction')
    print "ilastik_predict_with_array(): Done with imports"

    if LAZYFLOW_TOTAL_RAM_MB is None:
        # By default, assume our alotted RAM is proportional
        # to the CPUs we've been told to use
        machine_ram = psutil.virtual_memory().total
        machine_ram -= 1024**3  # Leave 1 GB RAM for the OS.

        LAZYFLOW_TOTAL_RAM_MB = LAZYFLOW_THREADS * machine_ram / multiprocessing.cpu_count(
        )

    # Before we start ilastik, prepare the environment variable settings.
    os.environ["LAZYFLOW_THREADS"] = str(LAZYFLOW_THREADS)
    os.environ["LAZYFLOW_TOTAL_RAM_MB"] = str(LAZYFLOW_TOTAL_RAM_MB)
    os.environ["LAZYFLOW_STATUS_MONITOR_SECONDS"] = "10"

    # Prepare ilastik's "command-line" arguments, as if they were already parsed.
    args, extra_workflow_cmdline_args = ilastik_main.parser.parse_known_args(
        extra_cmdline_args)
    args.headless = True
    args.debug = True  # ilastik's 'debug' flag enables special power features, including experimental workflows.
    args.project = str(ilp_path)
    args.readonly = True

    # The process_name argument is prefixed to all log messages.
    # For now, just use the machine name and a uuid
    # FIXME: It would be nice to provide something more descriptive, like the ROI of the current spark job...
    args.process_name = platform.node() + "-" + str(uuid.uuid1())

    # To avoid conflicts between processes, give each process it's own logfile to write to.
    if logfile != "/dev/null":
        base, ext = os.path.splitext(logfile)
        logfile = base + '.' + args.process_name + ext

    # By default, all ilastik processes duplicate their console output to ~/.ilastik_log.txt
    # Obviously, having all spark nodes write to a common file is a bad idea.
    # The "/dev/null" setting here is recognized by ilastik and means "Don't write a log file"
    args.logfile = logfile

    print "ilastik_predict_with_array(): Creating shell..."

    # Instantiate the 'shell', (in this case, an instance of ilastik.shell.HeadlessShell)
    # This also loads the project file into shell.projectManager
    shell = ilastik_main.main(args, extra_workflow_cmdline_args)

    ## Need to find a better way to verify the workflow type
    #from ilastik.workflows.pixelClassification import PixelClassificationWorkflow
    #assert isinstance(shell.workflow, PixelClassificationWorkflow)

    # Construct an OrderedDict of role-names -> DatasetInfos
    # (See PixelClassificationWorkflow.ROLE_NAMES)
    raw_data_array = vigra.taggedView(gray_vol, 'zyx')
    role_data_dict = OrderedDict([
        ("Raw Data", [DatasetInfo(preloaded_array=raw_data_array)])
    ])

    if mask is not None:
        # If there's a mask, we might be able to save some computation time.
        mask = vigra.taggedView(mask, 'zyx')
        role_data_dict["Prediction Mask"] = [DatasetInfo(preloaded_array=mask)]

    print "ilastik_predict_with_array(): Starting export..."

    # Sanity checks
    opInteractiveExport = shell.workflow.batchProcessingApplet.dataExportApplet.topLevelOperator.getLane(
        0)
    selected_result = opInteractiveExport.InputSelection.value
    num_channels = opInteractiveExport.Inputs[selected_result].meta.shape[-1]

    # For convenience, verify the selected channels before we run the export.
    if selected_channels:
        assert isinstance(selected_channels, list)
        for selection in selected_channels:
            if isinstance(selection, list):
                assert all(c < num_channels for c in selection), \
                    "Selected channels ({}) exceed number of prediction classes ({})"\
                    .format( selected_channels, num_channels )
            else:
                assert selection < num_channels, \
                    "Selected channels ({}) exceed number of prediction classes ({})"\
                    .format( selected_channels, num_channels )

    # Run the export via the BatchProcessingApplet
    prediction_list = shell.workflow.batchProcessingApplet.run_export(
        role_data_dict, export_to_array=True)
    assert len(prediction_list) == 1
    predictions = prediction_list[0]

    assert predictions.shape[-1] == num_channels
    selected_predictions = select_channels(predictions, selected_channels)

    if normalize:
        normalize_channels_in_place(selected_predictions)

    # Cleanup: kill cache monitor thread
    CacheMemoryManager().stop()
    CacheMemoryManager.instance = None

    # Cleanup environment
    del os.environ["LAZYFLOW_THREADS"]
    del os.environ["LAZYFLOW_TOTAL_RAM_MB"]
    del os.environ["LAZYFLOW_STATUS_MONITOR_SECONDS"]

    logging.getLogger(__name__).info('status=ilastik prediction finished')
    return selected_predictions