def process_beads_table(beads_table,
                        instruments_table,
                        base_dir=".",
                        verbose=False,
                        plot=False,
                        plot_dir=None,
                        full_output=False,
                        get_transform_fxn_kwargs={}):
    """
    Process calibration bead samples, as specified by an input table.

    This function processes the entries in `beads_table`. For each row, the
    function does the following:
        - Load the FCS file specified in the field "File Path".
        - Transform the forward scatter/side scatter and fluorescence
          channels to RFI
        - Remove the 250 first and 100 last events.
        - Remove saturated events in the forward scatter and side scatter
          channels.
        - Apply density gating on the forward scatter/side scatter
          channels.
        - Generate a standard curve transformation function, for each
          fluorescence channel in which the associated MEF values are
          specified.
        - Generate forward/side scatter density plots and fluorescence
          histograms, and plots of the clustering and fitting steps of
          standard curve generation, if `plot` = True.

    Names of forward/side scatter and fluorescence channels are taken from
    `instruments_table`.

    Parameters
    ----------
    beads_table : DataFrame
        Table specifying beads samples to be processed. For more
        information about the fields required in this table, please consult
        the module's documentation.
    instruments_table : DataFrame
        Table specifying instruments. For more information about the fields
        required in this table, please consult the module's documentation.
    base_dir : str, optional
        Directory from where all the other paths are specified.
    verbose : bool, optional
        Whether to print information messages during the execution of this
        function.
    plot : bool, optional
        Whether to generate and save density/histogram plots of each
        sample, and each beads sample.
    plot_dir : str, optional
        Directory relative to `base_dir` into which plots are saved. If
        `plot` is False, this parameter is ignored. If ``plot==True`` and
        ``plot_dir is None``, plot without saving.
    full_output : bool, optional
        Flag indicating whether to include an additional output, containing
        intermediate results from the generation of the MEF transformation
        functions.
    get_transform_fxn_kwargs : dict, optional
        Additional parameters passed directly to internal
        ``mef.get_transform_fxn()`` function call.

    Returns
    -------
    beads_samples : list of FCSData objects
        A list of processed, gated, and transformed samples, as specified
        in `beads_table`, in the order of ``beads_table.index``.
    mef_transform_fxns : OrderedDict
        A dictionary of MEF transformation functions, indexed by
        ``beads_table.index``.
    mef_outputs : list, only if ``full_output==True``
        A list with intermediate results of the generation of the MEF
        transformation functions. For every entry in `beads_table`,
        :func:`FlowCal.mef.get_transform_fxn()` is called on the
        corresponding processed and gated beads sample with
        ``full_output=True``, and the full output (a `MEFOutput`
        ``namedtuple``) is appended to `mef_outputs`. Please refer to the
        output section of :func:`FlowCal.mef.get_transform_fxn()`'s
        documentation for more information.

    """
    # Initialize output variables
    beads_samples = []
    mef_transform_fxns = collections.OrderedDict()
    mef_outputs = []

    # Return empty structures if beads table is empty
    if beads_table.empty:
        if full_output:
            return beads_samples, mef_transform_fxns, mef_outputs
        else:
            return beads_samples, mef_transform_fxns

    if verbose:
        msg = "Processing Beads table ({} entries)".format(len(beads_table))
        print("")
        print(msg)
        print("="*len(msg))

    # Check that plotting directory exist, create otherwise
    if plot and plot_dir is not None \
            and not os.path.exists(os.path.join(base_dir, plot_dir)):
        os.makedirs(os.path.join(base_dir, plot_dir))

    # Extract header and channel names for which MEF values are specified.
    headers = list(beads_table.columns)
    mef_headers_all = [h for h in headers if re_mef_values.match(h)]
    mef_channels_all = [re_mef_values.match(h).group(1)
                        for h in mef_headers_all]

    # Iterate through table
    # We will look for a ExcelUIException on each iteration. If an exception
    # is caught, it will be stored in beads_samples.
    for beads_id, beads_row in beads_table.iterrows():
        try:
            ###
            # Instrument Data
            ###
            # Get the appropriate row in the instrument table
            instruments_row = instruments_table.loc[beads_row['Instrument ID']]
            # Scatter channels: Foward Scatter, Side Scatter
            sc_channels = [instruments_row['Forward Scatter Channel'],
                           instruments_row['Side Scatter Channel'],
                           ]
            # Fluorescence channels is a comma-separated list
            fl_channels = instruments_row['Fluorescence Channels'].split(',')
            fl_channels = [s.strip() for s in fl_channels]

            ###
            # Beads Data
            ###
            if verbose:
                print("\nBeads ID {}...".format(beads_id))
                print("Loading file \"{}\"...".format(beads_row['File Path']))

            # Attempt to open file
            filename = os.path.join(base_dir, beads_row['File Path'])
            try:
                beads_sample = FlowCal.io.FCSData(filename)
            except IOError:
                raise ExcelUIException("file \"{}\" not found".format(
                    beads_row['File Path']))
            # Check that the number of events is greater than 400
            if beads_sample.shape[0] < 400:
                raise ExcelUIException("number of events is lower than 400")

            ###
            # Transform
            ###
            if verbose:
                print("Performing data transformation...")
            # Transform FSC/SSC and fluorescence channels to linear scale
            beads_sample = FlowCal.transform.to_rfi(beads_sample,
                                                    sc_channels + fl_channels)

            # Parse clustering channels data
            cluster_channels = beads_row['Clustering Channels'].split(',')
            cluster_channels = [cc.strip() for cc in cluster_channels]

            ###
            # Gate
            ###
            if verbose:
                print("Performing gating...")
            # Remove first and last events. Transients in fluidics can make the
            # first few and last events slightly different from the rest.
            beads_sample_gated = FlowCal.gate.start_end(beads_sample,
                                                        num_start=250,
                                                        num_end=100)
            # Remove saturating events in forward/side scatter, if the FCS data
            # type is integer. The value of a saturating event is taken
            # automatically from `beads_sample_gated.range`.
            if beads_sample_gated.data_type == 'I':
                beads_sample_gated = FlowCal.gate.high_low(
                    beads_sample_gated,
                    channels=sc_channels)
            # Density gating
            try:
                beads_sample_gated, __, gate_contour = FlowCal.gate.density2d(
                    data=beads_sample_gated,
                    channels=sc_channels,
                    gate_fraction=beads_row['Gate Fraction'],
                    xscale='logicle',
                    yscale='logicle',
                    sigma=5.,
                    full_output=True)
            except ValueError as ve:
                raise ExcelUIException(ve.message)

            # Plot forward/side scatter density plot and fluorescence histograms
            if plot:
                if verbose:
                    print("Plotting density plot and histogram...")
                # Density plot parameters
                density_params = {}
                density_params['mode'] = 'scatter'
                density_params["title"] = "{} ({:.1f}% retained)".format(
                    beads_id,
                    beads_sample_gated.shape[0] * 100. / beads_sample.shape[0])
                density_params['xscale'] = 'logicle'
                density_params['yscale'] = 'logicle'
                # Beads have a tight distribution, so axis limits will be set
                # from 0.75 decades below the 5th percentile to 0.75 decades
                # above the 95th percentile.
                density_params['xlim'] = \
                    (np.percentile(beads_sample_gated[:, sc_channels[0]],
                                   5) / (10**0.75),
                     np.percentile(beads_sample_gated[:, sc_channels[0]],
                                   95) * (10**0.75),
                     )
                density_params['ylim'] = \
                    (np.percentile(beads_sample_gated[:, sc_channels[1]],
                                   5) / (10**0.75),
                     np.percentile(beads_sample_gated[:, sc_channels[1]],
                                   95) * (10**0.75),
                     )
                # Beads have a tight distribution, so less smoothing should be
                # applied for visualization
                density_params['sigma'] = 5.
                # Histogram plot parameters
                hist_params = {'xscale': 'logicle'}
                # Plot
                if plot_dir is not None:
                    figname = os.path.join(
                        base_dir,
                        plot_dir,
                        "density_hist_{}.png".format(beads_id))
                else:
                    figname = None
                plt.figure(figsize=(6,4))
                FlowCal.plot.density_and_hist(
                    beads_sample,
                    beads_sample_gated,
                    density_channels=sc_channels,
                    hist_channels=cluster_channels,
                    gate_contour=gate_contour,
                    density_params=density_params,
                    hist_params=hist_params,
                    savefig=figname)

            ###
            # Process MEF values
            ###
            # For each fluorescence channel, check whether a list of known MEF
            # values of the bead subpopulations is provided in `beads_row`. This
            # involves checking that a column named "[channel] MEF Values"
            # exists and is not empty. If so, store the name of the channel in
            # `mef_channels`, and the specified MEF values in `mef_values`.
            ###
            mef_values = []
            mef_channels = []
            for fl_channel in fl_channels:
                if fl_channel in mef_channels_all:
                    # Get header from channel name
                    mef_header = \
                        mef_headers_all[mef_channels_all.index(fl_channel)]
                    # Extract text. If empty, ignore.
                    mef_str = beads_row[mef_header]
                    if pd.isnull(mef_str):
                        continue
                    # Save channel name
                    mef_channels.append(fl_channel)
                    # Parse list of values
                    mef = mef_str.split(',')
                    mef = [int(e) if e.strip().isdigit() else np.nan
                           for e in mef]
                    mef_values.append(mef)

            # Ensure matching number of `mef_values` for all channels (this
            # implies that the calibration beads have the same number of
            # subpopulations for all channels).
            if mef_values:
                if not np.all([len(mef_values_channel)==len(mef_values[0])
                               for mef_values_channel in mef_values]):
                    raise ExcelUIException("Must specify the same number of"
                                           + " MEF Values for each channel."
                                           + " Use 'None' to instruct FlowCal"
                                           + " to ignore a detected"
                                           + " subpopulation.")
            mef_values = np.array(mef_values)

            # Obtain standard curve transformation
            if mef_channels:
                if verbose:
                    if len(mef_channels) == 1:
                        print("Calculating standard curve for channel {}..." \
                            .format(mef_channels[0]))
                    else:
                        print("Calculating standard curve for channels {}..." \
                            .format(", ".join(mef_channels)))

                mef_output = FlowCal.mef.get_transform_fxn(
                    beads_sample_gated,
                    mef_values,
                    mef_channels=mef_channels,
                    clustering_channels=cluster_channels,
                    verbose=False,
                    plot=plot,
                    plot_filename=beads_id,
                    plot_dir=os.path.join(base_dir, plot_dir) \
                             if plot_dir is not None else None,
                    full_output=full_output,
                    **get_transform_fxn_kwargs)

                if full_output:
                    mef_transform_fxn = mef_output.transform_fxn
                else:
                    mef_transform_fxn = mef_output

            else:
                mef_transform_fxn = None
                mef_output = None

        except ExcelUIException as e:
            # Print Exception message
            if verbose:
                print("ERROR: {}".format(str(e)))
            # Append exception to beads_samples array, and None to everything
            # else
            beads_samples.append(e)
            mef_transform_fxns[beads_id] = None
            if full_output:
                mef_outputs.append(None)

        else:
            # If no errors were found, store results
            beads_samples.append(beads_sample_gated)
            mef_transform_fxns[beads_id] = mef_transform_fxn
            if full_output:
                mef_outputs.append(mef_output)


    if full_output:
        return beads_samples, mef_transform_fxns, mef_outputs
    else:
        return beads_samples, mef_transform_fxns