Exemple #1
0
    def __init__(self, params):
        """ initialise benchmark

        :param dict params:  parameters
        """
        assert 'unique' in params, 'missing "unique" among %r' % params.keys()
        super(ImRegBenchmark, self).__init__(params, params['unique'])
        logging.info(self.__doc__)
        self._df_overview = None
        self._df_experiments = None
        self.nb_workers = params.get('nb_workers', nb_workers(0.25))
        self._path_csv_regist = os.path.join(self.params['path_exp'], self.NAME_CSV_REGISTRATION_PAIRS)
Exemple #2
0
import json
import time
import logging
import argparse
from functools import partial

import numpy as np
import pandas as pd

sys.path += [os.path.abspath('.'), os.path.abspath('..')]  # Add path to root
from birl.utilities.data_io import create_folder, load_landmarks, save_landmarks, update_path
from birl.utilities.dataset import parse_path_scale
from birl.utilities.experiments import iterate_mproc_map, parse_arg_params, FORMAT_DATE_TIME, nb_workers
from birl.benchmark import COL_PAIRED_LANDMARKS, ImRegBenchmark, filter_paired_landmarks, _df_drop_unnamed

NB_WORKERS = nb_workers(0.9)
NAME_CSV_RESULTS = 'registration-results.csv'
NAME_JSON_COMPUTER = 'computer-performances.json'
NAME_JSON_RESULTS = 'metrics.json'
COL_NORM_TIME = 'Norm. execution time [minutes]'
COL_TISSUE = 'Tissue kind'
# FOLDER_FILTER_DATASET = 'filtered dataset'
CMP_THREADS = ('1', 'n')
#: Require having initial overlap as the warped is tricky as some image pairs do not
#   have the same nb points, so recommend to set it as False
REQUIRE_OVERLAP_INIT_TARGET = False


def create_parser():
    """ parse the input parameters
    :return dict: parameters
Exemple #3
0
class ImRegBenchmark(Experiment):
    """ General benchmark class for all registration methods.
    It also serves for evaluating the input registration pairs.

    :param dict params: dictionary with experiment configuration,
        the required options are names in `REQUIRED_PARAMS`,
        note that the basic parameters are inherited

    The benchmark has following steps:

    1. check all necessary paths and required parameters
    2. load cover file and set all paths as absolute
    3. run individual registration experiment in sequence or in parallel
        (nb_workers > 1); if the particular experiment folder exist (assume
        completed experiment) and skip it:

            a) create experiment folder and init experiment
            b) generate execution command
            c) run the command (an option to lock it in single thread)
            d) evaluate experiment, set the expected outputs and visualisation
            e) clean all extra files if any

    4. visualise results abd evaluate registration results

    .. note:: The actual implementation simulates the "IDEAL" registration while
     it blindly copies the reference landmarks as results of the registration.
     In contrast to the right registration, it copies the moving images so there
     is alignment (consistent warping) between resulting landmarks and image.

    Examples
    --------
    >>> # Running in single thread:
    >>> from birl.utilities.data_io import create_folder, update_path
    >>> path_out = create_folder('temp_results')
    >>> path_csv = os.path.join(update_path('data-images'), 'pairs-imgs-lnds_mix.csv')
    >>> params = {'path_table': path_csv,
    ...           'path_out': path_out,
    ...           'nb_workers': 1,
    ...           'unique': False,
    ...           'visual': True}
    >>> benchmark = ImRegBenchmark(params)
    >>> benchmark.run()
    True
    >>> del benchmark
    >>> shutil.rmtree(path_out, ignore_errors=True)

    >>> # Running in multiple parallel threads:
    >>> from birl.utilities.data_io import create_folder, update_path
    >>> path_out = create_folder('temp_results')
    >>> path_csv = os.path.join(update_path('data-images'), 'pairs-imgs-lnds_mix.csv')
    >>> params = {'path_table': path_csv,
    ...           'path_out': path_out,
    ...           'nb_workers': 2,
    ...           'unique': False,
    ...           'visual': True}
    >>> benchmark = ImRegBenchmark(params)
    >>> benchmark.run()
    True
    >>> del benchmark
    >>> shutil.rmtree(path_out, ignore_errors=True)
    """

    #: timeout for executing single image registration, NOTE: does not work for Py2
    EXECUTE_TIMEOUT = 60 * 60  # default = 1 hour
    #: default number of threads used by benchmarks
    NB_WORKERS_USED = nb_workers(0.8)
    #: some needed files
    NAME_CSV_REGISTRATION_PAIRS = 'registration-results.csv'
    #: default file for exporting results in table format
    NAME_RESULTS_CSV = 'results-summary.csv'
    #: default file for exporting results in formatted text format
    NAME_RESULTS_TXT = 'results-summary.txt'
    #: logging file for registration experiments
    NAME_LOG_REGISTRATION = 'registration.log'
    #: output image name in experiment folder for reg. results - overlap of reference and warped image
    NAME_IMAGE_REF_WARP = 'image_refence-warped.jpg'
    #: output image name in experiment folder for reg. results - image and landmarks are warped
    NAME_IMAGE_MOVE_WARP_POINTS = 'image_warped_landmarks_warped.jpg'
    #: output image name in experiment folder for reg. results - warped landmarks in reference image
    NAME_IMAGE_REF_POINTS_WARP = 'image_ref_landmarks_warped.jpg'
    #: output image name in experiment folder for showing improved alignment by used registration
    NAME_IMAGE_WARPED_VISUAL = 'registration_visual_landmarks.jpg'
    # columns names in cover and also registration table
    #: reference (registration target) image
    COL_IMAGE_REF = 'Target image'
    #: moving (registration source) image
    COL_IMAGE_MOVE = 'Source image'
    #: reference image warped to the moving frame
    COL_IMAGE_REF_WARP = 'Warped target image'
    #: moving image warped to the reference frame
    COL_IMAGE_MOVE_WARP = 'Warped source image'
    #: reference (registration target) landmarks
    COL_POINTS_REF = 'Target landmarks'
    #: moving (registration source) landmarks
    COL_POINTS_MOVE = 'Source landmarks'
    #: reference landmarks warped to the moving frame
    COL_POINTS_REF_WARP = 'Warped target landmarks'
    #: moving landmarks warped to the reference frame
    COL_POINTS_MOVE_WARP = 'Warped source landmarks'
    #: registration folder for each particular experiment
    COL_REG_DIR = 'Registration folder'
    #: define robustness as improved image alignment from initial state
    COL_ROBUSTNESS = 'Robustness'
    #: measured time of image registration in minutes
    COL_TIME = 'Execution time [minutes]'
    #: measured time of image pre-processing in minutes
    COL_TIME_PREPROC = 'Pre-processing time [minutes]'
    #: tuple of image size
    COL_IMAGE_SIZE = 'Image size [pixels]'
    #: image diagonal in pixels
    COL_IMAGE_DIAGONAL = 'Image diagonal [pixels]'
    #: define train / test status
    COL_STATUS = 'status'
    #: extension to the image column name for temporary pre-process image
    COL_IMAGE_EXT_TEMP = ' TEMP'
    #: number of landmarks in dataset (min of moving and reference)
    COL_NB_LANDMARKS_INPUT = 'nb. dataset landmarks'
    #: number of warped landmarks
    COL_NB_LANDMARKS_WARP = 'nb. warped landmarks'
    #: required experiment parameters
    REQUIRED_PARAMS = Experiment.REQUIRED_PARAMS + ['path_table']

    # list of columns in cover csv
    COVER_COLUMNS = (COL_IMAGE_REF, COL_IMAGE_MOVE, COL_POINTS_REF,
                     COL_POINTS_MOVE)
    COVER_COLUMNS_EXT = tuple(
        list(COVER_COLUMNS) + [COL_IMAGE_SIZE, COL_IMAGE_DIAGONAL])
    COVER_COLUMNS_WRAP = tuple(
        list(COVER_COLUMNS) + [
            COL_IMAGE_REF_WARP, COL_IMAGE_MOVE_WARP, COL_POINTS_REF_WARP,
            COL_POINTS_MOVE_WARP
        ])

    def __init__(self, params):
        """ initialise benchmark

        :param dict params:  parameters
        """
        assert 'unique' in params, 'missing "unique" among %r' % params.keys()
        super(ImRegBenchmark, self).__init__(params, params['unique'])
        logging.info(self.__doc__)
        self._df_overview = None
        self._df_experiments = None
        self.nb_workers = params.get('nb_workers', nb_workers(0.25))
        self._path_csv_regist = os.path.join(self.params['path_exp'],
                                             self.NAME_CSV_REGISTRATION_PAIRS)

    def _check_required_params(self):
        """ check some extra required parameters for this benchmark """
        logging.debug('.. check if the BM have all required parameters')
        super(ImRegBenchmark, self)._check_required_params()
        for n in self.REQUIRED_PARAMS:
            assert n in self.params, 'missing "%s" among %r' % (
                n, self.params.keys())

    def _absolute_path(self, path, destination='data', base_path=None):
        """ update te path to the dataset or output

        :param str path: original path
        :param str destination: type of update
            `data` for data source and `expt` for output experimental folder
        :param str destination: type of update
        :return str: updated path
        """
        if destination and destination == 'data' and 'path_dataset' in self.params:
            path = os.path.join(self.params['path_dataset'], path)
        elif destination and destination == 'expt' and 'path_exp' in self.params:
            path = os.path.join(self.params['path_exp'], path)
        path = update_path(path, absolute=True)
        return path

    def _relativize_path(self, path, destination='path_exp'):
        """ extract relative path according given parameter

        :param str path: the original path to file/folder
        :param str destination: use path from parameters
        :return str: relative or the original path
        """
        if path is None or not os.path.exists(path):
            logging.debug('Source path does not exists: %s', path)
            return path
        assert destination in self.params, 'Missing path in params: %s' % destination
        base_path = self.params['path_exp']
        base_dir = os.path.basename(base_path)
        path_split = path.split(os.sep)

        if base_dir not in path_split:
            logging.debug('Missing requested folder "%s" in source path: %s',
                          base_dir, path_split)
            return path
        path_split = path_split[path_split.index(base_dir) + 1:]
        path_rltv = os.sep.join(path_split)

        if os.path.exists(os.path.join(self.params[destination], path_rltv)):
            path = path_rltv
        else:
            logging.debug('Not existing relative path: %s', path)
        return path

    def _copy_config_to_expt(self, field_path):
        """ copy particular configuration to the experiment folder

        :param str field_path: field from parameters containing a path to file
        """
        path_source = self.params.get(field_path, '')
        path_config = os.path.join(self.params['path_exp'],
                                   os.path.basename(path_source))
        if path_source and os.path.isfile(path_source):
            shutil.copy(path_source, path_config)
            self.params[field_path] = path_config
        else:
            logging.warning('Missing config: %s', path_source)

    def _get_paths(self, item, prefer_pproc=True):
        """ expand the relative paths to absolute, if TEMP path is used, take it

        :param dict item: row from cover file with relative paths
        :param bool prefer_pproc: prefer using preprocess images
        :return tuple(str,str,str,str): path to reference and moving image
            and reference and moving landmarks
        """
        def __path_img(col):
            is_temp = isinstance(item.get(col + self.COL_IMAGE_EXT_TEMP, None),
                                 str)
            if prefer_pproc and is_temp:
                path = self._absolute_path(item[col + self.COL_IMAGE_EXT_TEMP],
                                           destination='expt')
            else:
                path = self._absolute_path(item[col], destination='data')
            return path

        paths = [
            __path_img(col)
            for col in (self.COL_IMAGE_REF, self.COL_IMAGE_MOVE)
        ]
        paths += [
            self._absolute_path(item[col], destination='data')
            for col in (self.COL_POINTS_REF, self.COL_POINTS_MOVE)
        ]
        return paths

    def _get_path_reg_dir(self, item):
        return self._absolute_path(str(item[self.COL_REG_DIR]),
                                   destination='expt')

    def _load_data(self):
        """ loading data, the cover file with all registration pairs """
        logging.info('-> loading data...')
        # loading the csv cover file
        assert os.path.isfile(self.params['path_table']), \
            'path to csv cover is not defined - %s' % self.params['path_table']
        self._df_overview = pd.read_csv(self.params['path_table'],
                                        index_col=None)
        self._df_overview = _df_drop_unnamed(self._df_overview)
        assert all(col in self._df_overview.columns for col in self.COVER_COLUMNS), \
            'Some required columns are missing in the cover file.'

    def _run(self):
        """ perform complete benchmark experiment """
        logging.info('-> perform set of experiments...')

        # load existing result of create new entity
        if os.path.isfile(self._path_csv_regist):
            logging.info('loading existing csv: "%s"', self._path_csv_regist)
            self._df_experiments = pd.read_csv(self._path_csv_regist,
                                               index_col=None)
            self._df_experiments = _df_drop_unnamed(self._df_experiments)
            if 'ID' in self._df_experiments.columns:
                self._df_experiments.set_index('ID', inplace=True)
        else:
            self._df_experiments = pd.DataFrame()

        # run the experiment in parallel of single thread
        self.__execute_method(self._perform_registration,
                              self._df_overview,
                              self._path_csv_regist,
                              'registration experiments',
                              aggr_experiments=True)

    def __execute_method(self,
                         method,
                         input_table,
                         path_csv=None,
                         desc='',
                         aggr_experiments=False,
                         nb_workers=None):
        """ execute a method in sequence or parallel

        :param func method: used method
        :param DF input_table: iterate over table
        :param str path_csv: path to the output temporal csv
        :param str desc: name of the running process
        :param bool aggr_experiments: append output to experiment DF
        :param int|None nb_workers: number of jobs, by default using class setting
        :return:
        """
        # setting the temporal split
        self._main_thread = False
        # run the experiment in parallel of single thread
        nb_workers = self.nb_workers if nb_workers is None else nb_workers
        iter_table = ((idx, dict(row)) for idx, row, in input_table.iterrows())
        for res in iterate_mproc_map(method,
                                     iter_table,
                                     nb_workers=nb_workers,
                                     desc=desc):
            if res is not None and aggr_experiments:
                self._df_experiments = self._df_experiments.append(
                    res, ignore_index=True)
                self.__export_df_experiments(path_csv)
        self._main_thread = True

    def __export_df_experiments(self, path_csv=None):
        """ export the DataFrame with registration results

        :param str | None path_csv: path to output CSV file
        """
        if path_csv is not None:
            if 'ID' in self._df_experiments.columns:
                self._df_experiments.set_index('ID').to_csv(path_csv)
            else:
                self._df_experiments.to_csv(path_csv, index=None)

    def __check_exist_regist(self, idx, path_dir_reg):
        """ check whether the particular experiment already exists and have results

        if the folder with experiment already exist and it is also part
        of the loaded finished experiments, sometimes the oder may mean
        failed experiment

        :param int idx: index of particular
        :param str path_dir_reg:
        :return bool:
        """
        b_df_col = ('ID' in self._df_experiments.columns
                    and idx in self._df_experiments['ID'])
        b_df_idx = idx in self._df_experiments.index
        check = os.path.exists(path_dir_reg) and (b_df_col or b_df_idx)
        if check:
            logging.warning(
                'particular registration experiment already exists:'
                ' "%r"', idx)
        return check

    def __images_preprocessing(self, item):
        """ create some pre-process images, convert to gray scale and histogram matching

        :param dict item: the input record
        :return dict: updated item with optionally added pre-process images
        """
        path_dir = self._get_path_reg_dir(item)

        def __path_img(path_img, pproc):
            img_name, img_ext = os.path.splitext(os.path.basename(path_img))
            return os.path.join(path_dir, img_name + '_' + pproc + img_ext)

        def __save_img(col, path_img_new, img):
            col_temp = col + self.COL_IMAGE_EXT_TEMP
            if isinstance(item.get(col_temp, None), str):
                path_img = self._absolute_path(item[col_temp],
                                               destination='expt')
                os.remove(path_img)
            save_image(path_img_new, img)
            return self._relativize_path(path_img_new,
                                         destination='path_exp'), col

        def __convert_gray(path_img_col):
            path_img, col = path_img_col
            path_img_new = __path_img(path_img, 'gray')
            __save_img(col, path_img_new, rgb2gray(load_image(path_img)))
            return self._relativize_path(path_img_new,
                                         destination='path_exp'), col

        for pproc in self.params.get('preprocessing', []):
            path_img_ref, path_img_move, _, _ = self._get_paths(
                item, prefer_pproc=True)
            if pproc.startswith('match'):
                color_space = pproc.split('-')[-1]
                path_img_new = __path_img(path_img_move, pproc)
                img = image_histogram_matching(load_image(path_img_move),
                                               load_image(path_img_ref),
                                               use_color=color_space)
                path_img_new, col = __save_img(self.COL_IMAGE_MOVE,
                                               path_img_new, img)
                item[col + self.COL_IMAGE_EXT_TEMP] = path_img_new
            elif pproc in ('gray', 'grey'):
                argv_params = [(path_img_ref, self.COL_IMAGE_REF),
                               (path_img_move, self.COL_IMAGE_MOVE)]
                # IDEA: find a way how to convert images in parallel inside mproc pool
                #  problem is in calling class method inside the pool which is ot static
                for path_img, col in iterate_mproc_map(__convert_gray,
                                                       argv_params,
                                                       nb_workers=1,
                                                       desc=None):
                    item[col + self.COL_IMAGE_EXT_TEMP] = path_img
            else:
                logging.warning('unrecognized pre-processing: %s', pproc)
        return item

    def __remove_pproc_images(self, item):
        """ remove preprocess (temporary) image if they are not also final

        :param dict item: the input record
        :return dict: updated item with optionally removed temp images
        """
        # clean only if some pre-processing was required
        if not self.params.get('preprocessing', []):
            return item
        # iterate over both - target and source images
        for col_in, col_warp in [(self.COL_IMAGE_REF, self.COL_IMAGE_REF_WARP),
                                 (self.COL_IMAGE_MOVE,
                                  self.COL_IMAGE_MOVE_WARP)]:
            col_temp = col_in + self.COL_IMAGE_EXT_TEMP
            is_temp = isinstance(item.get(col_temp, None), str)
            # skip if the field is empty
            if not is_temp:
                continue
            # the warped image is not the same as pre-process image is equal
            elif item.get(col_warp, None) != item.get(col_temp, None):
                # update the path to the pre-process image in experiment folder
                path_img = self._absolute_path(item[col_temp],
                                               destination='expt')
                # remove image and from the field
                os.remove(path_img)
            del item[col_temp]
        return item

    def _perform_registration(self, df_row):
        """ run single registration experiment with all sub-stages

        :param tuple(int,dict) df_row: row from iterated table
        """
        idx, row = df_row
        logging.debug('-> perform single registration #%d...', idx)
        # create folder for this particular experiment
        row['ID'] = idx
        row[self.COL_REG_DIR] = str(idx)
        path_dir_reg = self._get_path_reg_dir(row)
        # check whether the particular experiment already exists and have result
        if self.__check_exist_regist(idx, path_dir_reg):
            return None
        create_folder(path_dir_reg)

        time_start = time.time()
        # do some requested pre-processing if required
        row = self.__images_preprocessing(row)
        row[self.COL_TIME_PREPROC] = (time.time() - time_start) / 60.
        row = self._prepare_img_registration(row)
        # if the pre-processing failed, return back None
        if not row:
            return None

        # measure execution time
        time_start = time.time()
        row = self._execute_img_registration(row)
        # if the experiment failed, return back None
        if not row:
            return None
        # compute the registration time in minutes
        row[self.COL_TIME] = (time.time() - time_start) / 60.
        # remove some temporary images
        row = self.__remove_pproc_images(row)

        row = self._parse_regist_results(row)
        # if the post-processing failed, return back None
        if not row:
            return None
        row = self._clear_after_registration(row)

        if self.params.get('visual', False):
            logging.debug('-> visualise results of experiment: %r', idx)
            self.visualise_registration(
                (idx, row),
                path_dataset=self.params.get('path_dataset', None),
                path_experiment=self.params.get('path_exp', None),
            )

        return row

    def _evaluate(self):
        """ evaluate complete benchmark experiment """
        logging.info('-> evaluate experiment...')
        # load _df_experiments and compute stat
        _compute_landmarks_statistic = partial(
            self.compute_registration_statistic,
            df_experiments=self._df_experiments,
            path_dataset=self.params.get('path_dataset', None),
            path_experiment=self.params.get('path_exp', None))
        self.__execute_method(_compute_landmarks_statistic,
                              self._df_experiments,
                              desc='compute TRE',
                              nb_workers=1)

    def _summarise(self):
        """ summarise benchmark experiment """
        # export stat to csv
        if self._df_experiments.empty:
            logging.warning('no experimental results were collected')
            return
        self.__export_df_experiments(self._path_csv_regist)
        # export simple stat to txt
        export_summary_results(self._df_experiments, self.params['path_exp'],
                               self.params)

    @classmethod
    def _prepare_img_registration(cls, item):
        """ prepare the experiment folder if it is required,
        eq. copy some extra files

        :param dict item: dictionary with regist. params
        :return dict: the same or updated registration info
        """
        logging.debug('.. no preparing before registration experiment')
        return item

    def _execute_img_registration(self, item):
        """ execute the image registration itself

        :param dict item: record
        :return dict: record
        """
        logging.debug('.. execute image registration as command line')
        path_dir_reg = self._get_path_reg_dir(item)

        commands = self._generate_regist_command(item)
        # in case it is just one command
        if not isinstance(commands, (list, tuple)):
            commands = [commands]

        path_log = os.path.join(path_dir_reg, self.NAME_LOG_REGISTRATION)
        # TODO, add lock to single thread, create pool with possible thread ids
        # (USE taskset [native], numactl [need install])
        if not isinstance(commands, (list, tuple)):
            commands = [commands]
        # measure execution time
        cmd_result = exec_commands(commands,
                                   path_log,
                                   timeout=self.EXECUTE_TIMEOUT)
        # if the experiment failed, return back None
        if not cmd_result:
            item = None
        return item

    def _generate_regist_command(self, item):
        """ generate the registration command(s)

        :param dict item: dictionary with registration params
        :return str|list(str): the execution commands
        """
        logging.debug(
            '.. simulate registration: '
            'copy the target image and landmarks, simulate ideal case')
        path_im_ref, _, _, path_lnds_move = self._get_paths(item)
        path_reg_dir = self._get_path_reg_dir(item)
        name_img = os.path.basename(item[self.COL_IMAGE_MOVE])
        cmd_img = 'cp %s %s' % (path_im_ref,
                                os.path.join(path_reg_dir, name_img))
        name_lnds = os.path.basename(item[self.COL_POINTS_MOVE])
        cmd_lnds = 'cp %s %s' % (path_lnds_move,
                                 os.path.join(path_reg_dir, name_lnds))
        commands = [cmd_img, cmd_lnds]
        return commands

    @classmethod
    def _extract_warped_image_landmarks(self, item):
        """ get registration results - warped registered images and landmarks

        :param dict item: dictionary with registration params
        :return dict: paths to warped images/landmarks
        """
        # detect image
        path_img = os.path.join(item[self.COL_REG_DIR],
                                os.path.basename(item[self.COL_IMAGE_MOVE]))
        # detect landmarks
        path_lnd = os.path.join(item[self.COL_REG_DIR],
                                os.path.basename(item[self.COL_POINTS_MOVE]))
        # return formatted results
        return {
            self.COL_IMAGE_REF_WARP: None,
            self.COL_IMAGE_MOVE_WARP: path_img,
            self.COL_POINTS_REF_WARP: path_lnd,
            self.COL_POINTS_MOVE_WARP: None
        }

    def _extract_execution_time(self, item):
        """ if needed update the execution time

        :param dict item: dictionary {str: value} with registration params
        :return float|None: time in minutes
        """
        _ = self._get_path_reg_dir(item)
        return None

    def _parse_regist_results(self, item):
        """ evaluate rests of the experiment and identity the registered image
        and landmarks when the process finished

        :param dict item: dictionary {str: value} with registration params
        :return dict:
        """
        # Update the registration outputs / paths
        res_paths = self._extract_warped_image_landmarks(item)

        for col in (k for k in res_paths if res_paths[k] is not None):
            path = res_paths[col]
            # detect image and landmarks
            path = self._relativize_path(path, 'path_exp')
            if os.path.isfile(self._absolute_path(path, destination='expt')):
                item[col] = path

        # Update the registration time
        exec_time = self._extract_execution_time(item)
        if exec_time:
            # compute the registration time in minutes
            item[self.COL_TIME] = exec_time

        return item

    @classmethod
    def _clear_after_registration(self, item):
        """ clean unnecessarily files after the registration

        :param dict item: dictionary with regist. information
        :return dict: the same or updated regist. info
        """
        logging.debug('.. no cleaning after registration experiment')
        return item

    @staticmethod
    def extend_parse(arg_parser):
        return arg_parser

    @classmethod
    def main(cls, params=None):
        """ run the Main of selected experiment

        :param cls: class of selected benchmark
        :param dict params: set of input parameters
        """
        if not params:
            arg_parser = create_basic_parser(cls.__name__)
            arg_parser = cls.extend_parse(arg_parser)
            params = parse_arg_params(arg_parser)

        logging.info('running...')
        benchmark = cls(params)
        benchmark.run()
        path_expt = benchmark.params['path_exp']
        logging.info('Done.')
        return params, path_expt

    @classmethod
    def _image_diag(cls, item, path_img_ref=None):
        """ get the image diagonal from several sources
            1. diagonal exists in the table
            2. image size exist in the table
            3. reference image exists

        :param dict|DF item: one row from the table
        :param str path_img_ref: optional path to the reference image
        :return float|None: image diagonal
        """
        img_diag = dict(item).get(cls.COL_IMAGE_DIAGONAL, None)
        if not img_diag and path_img_ref and os.path.isfile(path_img_ref):
            _, img_diag = image_sizes(path_img_ref)
        return img_diag

    @classmethod
    def _load_landmarks(cls, item, path_dataset):
        path_img_ref, _, path_lnds_ref, path_lnds_move = \
            [update_path(item[col], pre_path=path_dataset) for col in cls.COVER_COLUMNS]
        points_ref = load_landmarks(path_lnds_ref)
        points_move = load_landmarks(path_lnds_move)
        return points_ref, points_move, path_img_ref

    @classmethod
    def compute_registration_statistic(cls,
                                       idx_row,
                                       df_experiments,
                                       path_dataset=None,
                                       path_experiment=None,
                                       path_reference=None):
        """ after successful registration load initial nad estimated landmarks
        afterwords compute various statistic for init, and final alignment

        :param tuple(int,dict) df_row: row from iterated table
        :param DF df_experiments: DataFrame with experiments
        :param str|None path_dataset: path to the provided dataset folder
        :param str|None path_reference: path to the complete landmark collection folder
        :param str|None path_experiment: path to the experiment folder
        """
        idx, row = idx_row
        row = dict(row)  # convert even series to dictionary
        # load common landmarks and image size
        points_ref, points_move, path_img_ref = cls._load_landmarks(
            row, path_dataset)
        img_diag = cls._image_diag(row, path_img_ref)
        df_experiments.loc[idx, cls.COL_IMAGE_DIAGONAL] = img_diag

        # compute landmarks statistic
        cls.compute_registration_accuracy(df_experiments,
                                          idx,
                                          points_ref,
                                          points_move,
                                          'init',
                                          img_diag,
                                          wo_affine=False)

        # define what is the target and init state according to the experiment results
        use_move_warp = isinstance(row.get(cls.COL_POINTS_MOVE_WARP, None),
                                   str)
        if use_move_warp:
            points_init, points_target = points_move, points_ref
            col_source, col_target = cls.COL_POINTS_MOVE, cls.COL_POINTS_REF
            col_lnds_warp = cls.COL_POINTS_MOVE_WARP
        else:
            points_init, points_target = points_ref, points_move
            col_lnds_warp = cls.COL_POINTS_REF_WARP
            col_source, col_target = cls.COL_POINTS_REF, cls.COL_POINTS_MOVE

        # optional filtering
        if path_reference:
            ratio, points_target, _ = \
                filter_paired_landmarks(row, path_dataset, path_reference, col_source, col_target)
            df_experiments.loc[idx, COL_PAIRED_LANDMARKS] = np.round(ratio, 2)

        # load transformed landmarks
        if (cls.COL_POINTS_MOVE_WARP not in row) and (cls.COL_POINTS_REF_WARP
                                                      not in row):
            logging.error('Statistic: no output landmarks')
            return

        # check if there are reference landmarks
        if points_target is None:
            logging.warning(
                'Missing landmarks in "%s"',
                cls.COL_POINTS_REF if use_move_warp else cls.COL_POINTS_MOVE)
            return
        # load warped landmarks
        path_lnds_warp = update_path(row[col_lnds_warp],
                                     pre_path=path_experiment)
        if path_lnds_warp and os.path.isfile(path_lnds_warp):
            points_warp = load_landmarks(path_lnds_warp)
            points_warp = np.nan_to_num(points_warp)
        else:
            logging.warning('Invalid path to the landmarks: "%s" <- "%s"',
                            path_lnds_warp, row[col_lnds_warp])
            return
        df_experiments.loc[idx, cls.COL_NB_LANDMARKS_INPUT] = min(
            len(points_ref), len(points_ref))
        df_experiments.loc[idx, cls.COL_NB_LANDMARKS_WARP] = len(points_warp)

        # compute Affine statistic
        affine_diff = compute_affine_transf_diff(points_init, points_target,
                                                 points_warp)
        for name in affine_diff:
            df_experiments.loc[idx, name] = affine_diff[name]

        # compute landmarks statistic
        cls.compute_registration_accuracy(df_experiments,
                                          idx,
                                          points_target,
                                          points_warp,
                                          'elastic',
                                          img_diag,
                                          wo_affine=True)
        # compute landmarks statistic
        cls.compute_registration_accuracy(df_experiments,
                                          idx,
                                          points_target,
                                          points_warp,
                                          'target',
                                          img_diag,
                                          wo_affine=False)
        row_ = dict(df_experiments.loc[idx])
        # compute the robustness
        if 'TRE Mean' in row_:
            df_experiments.loc[idx, cls.COL_ROBUSTNESS] = \
                compute_tre_robustness(points_target, points_init, points_warp)

    @classmethod
    def compute_registration_accuracy(cls,
                                      df_experiments,
                                      idx,
                                      points1,
                                      points2,
                                      state='',
                                      img_diag=None,
                                      wo_affine=False):
        """ compute statistic on two points sets

        IRE - Initial Registration Error
        TRE - Target Registration Error

        :param DF df_experiments: DataFrame with experiments
        :param int idx: index of tha particular record
        :param ndarray points1: np.array<nb_points, dim>
        :param ndarray points2: np.array<nb_points, dim>
        :param str state: whether it was before of after registration
        :param float img_diag: target image diagonal
        :param bool wo_affine: without affine transform, assume only local/elastic deformation
        """
        if wo_affine and points1 is not None and points2 is not None:
            # removing the affine transform and assume only local/elastic deformation
            _, _, points1, _ = estimate_affine_transform(points1, points2)

        _, stats = compute_target_regist_error_statistic(points1, points2)
        if img_diag is not None:
            df_experiments.at[idx, cls.COL_IMAGE_DIAGONAL] = img_diag
        # update particular idx
        for n_stat in (n for n in stats if n not in ['overlap points']):
            # if it not one of the simplified names
            if state and state not in ('init', 'final', 'target'):
                name = 'TRE %s (%s)' % (n_stat, state)
            else:
                # for initial ise IRE, else TRE
                name = '%s %s' % ('IRE' if state == 'init' else 'TRE', n_stat)
            if img_diag is not None:
                df_experiments.at[idx, 'r%s' % name] = stats[n_stat] / img_diag
            df_experiments.at[idx, name] = stats[n_stat]
        for n_stat in ['overlap points']:
            df_experiments.at[idx, '%s (%s)' % (n_stat, state)] = stats[n_stat]

    @classmethod
    def _load_warped_image(cls, item, path_experiment=None):
        """load the wapted image if it exists

        :param dict item: row with the experiment
        :param str|None path_experiment: path to the experiment folder
        :return ndarray:
        """
        name_img = item.get(cls.COL_IMAGE_MOVE_WARP, None)
        if not isinstance(name_img, str):
            logging.warning('Missing registered image in "%s"',
                            cls.COL_IMAGE_MOVE_WARP)
            image_warp = None
        else:
            path_img_warp = update_path(name_img, pre_path=path_experiment)
            if os.path.isfile(path_img_warp):
                image_warp = load_image(path_img_warp)
            else:
                logging.warning('Define image is missing: %s', path_img_warp)
                image_warp = None
        return image_warp

    @classmethod
    def _visual_image_move_warp_lnds_move_warp(cls,
                                               item,
                                               path_dataset=None,
                                               path_experiment=None):
        """ visualise the case with warped moving image and landmarks
        to the reference frame so they are simple to overlap

        :param dict item: row with the experiment
        :param str|None path_dataset: path to the dataset folder
        :param str|None path_experiment: path to the experiment folder
        :return obj|None:
        """
        assert isinstance(item.get(cls.COL_POINTS_MOVE_WARP, None), str), \
            'Missing registered points in "%s"' % cls.COL_POINTS_MOVE_WARP
        path_points_warp = update_path(item[cls.COL_POINTS_MOVE_WARP],
                                       pre_path=path_experiment)
        if not os.path.isfile(path_points_warp):
            logging.warning('missing warped landmarks for: %r', dict(item))
            return

        points_ref, points_move, path_img_ref = cls._load_landmarks(
            item, path_dataset)

        image_warp = cls._load_warped_image(item, path_experiment)
        points_warp = load_landmarks(path_points_warp)
        if not list(points_warp):
            return
        # draw image with landmarks
        image = draw_image_points(image_warp, points_warp)
        save_image(
            os.path.join(
                update_path(item[cls.COL_REG_DIR], pre_path=path_experiment),
                cls.NAME_IMAGE_MOVE_WARP_POINTS), image)
        del image

        # visualise the landmarks move during registration
        image_ref = load_image(path_img_ref)
        fig = draw_images_warped_landmarks(image_ref, image_warp, points_move,
                                           points_ref, points_warp)
        del image_ref, image_warp
        return fig

    @classmethod
    def _visual_image_move_warp_lnds_ref_warp(cls,
                                              item,
                                              path_dataset=None,
                                              path_experiment=None):
        """ visualise the case with warped reference landmarks to the move frame

        :param dict item: row with the experiment
        :param str|None path_dataset: path to the dataset folder
        :param str|None path_experiment: path to the experiment folder
        :return obj|None:
        """
        assert isinstance(item.get(cls.COL_POINTS_REF_WARP, None), str), \
            'Missing registered points in "%s"' % cls.COL_POINTS_REF_WARP
        path_points_warp = update_path(item[cls.COL_POINTS_REF_WARP],
                                       pre_path=path_experiment)
        if not os.path.isfile(path_points_warp):
            logging.warning('missing warped landmarks for: %r', dict(item))
            return

        points_ref, points_move, path_img_ref = cls._load_landmarks(
            item, path_dataset)

        points_warp = load_landmarks(path_points_warp)
        if not list(points_warp):
            return
        # draw image with landmarks
        image_move = load_image(
            update_path(item[cls.COL_IMAGE_MOVE], pre_path=path_dataset))
        image = draw_image_points(image_move, points_warp)
        save_image(
            os.path.join(
                update_path(item[cls.COL_REG_DIR], pre_path=path_experiment),
                cls.NAME_IMAGE_REF_POINTS_WARP), image)
        del image

        image_ref = load_image(path_img_ref)
        image_warp = cls._load_warped_image(item, path_experiment)
        image = overlap_two_images(image_ref, image_warp)
        save_image(
            os.path.join(
                update_path(item[cls.COL_REG_DIR], pre_path=path_experiment),
                cls.NAME_IMAGE_REF_WARP), image)
        del image, image_warp

        # visualise the landmarks move during registration
        fig = draw_images_warped_landmarks(image_ref, image_move, points_ref,
                                           points_move, points_warp)
        del image_ref, image_move
        return fig

    @classmethod
    def visualise_registration(cls,
                               idx_row,
                               path_dataset=None,
                               path_experiment=None):
        """ visualise the registration results according what landmarks were
        estimated - in registration or moving frame

        :param tuple(int,dict) df_row: row from iterated table
        :param str path_dataset: path to the dataset folder
        :param str path_experiment: path to the experiment folder
        """
        _, row = idx_row
        row = dict(row)  # convert even series to dictionary
        fig, path_fig = None, None
        # visualise particular experiment by idx
        if isinstance(row.get(cls.COL_POINTS_MOVE_WARP, None), str):
            fig = cls._visual_image_move_warp_lnds_move_warp(
                row, path_dataset, path_experiment)
        elif isinstance(row.get(cls.COL_POINTS_REF_WARP, None), str):
            fig = cls._visual_image_move_warp_lnds_ref_warp(
                row, path_dataset, path_experiment)
        else:
            logging.error('Visualisation: no output image or landmarks')

        if fig is not None:
            path_fig = os.path.join(
                update_path(row[cls.COL_REG_DIR], pre_path=path_experiment),
                cls.NAME_IMAGE_WARPED_VISUAL)
            export_figure(path_fig, fig)

        return path_fig