1+ # -*- coding: utf-8 -*-
12"""
23General benchmark template for all registration methods.
34It also serves for evaluating the input registration pairs
2526import pandas as pd
2627from skimage .color import rgb2gray
2728
29+ # this is used while calling this file as a script
2830sys .path += [os .path .abspath ('.' ), os .path .abspath ('..' )] # Add path to root
2931from .utilities .data_io import (
3032 update_path , create_folder , image_sizes , load_landmarks , load_image , save_image )
31- from .utilities .dataset import image_histogram_matching
33+ from .utilities .dataset import image_histogram_matching , common_landmarks
3234from .utilities .evaluate import (
3335 compute_target_regist_error_statistic , compute_affine_transf_diff , compute_tre_robustness )
3436from .utilities .experiments import (
3840 export_figure , draw_image_points , draw_images_warped_landmarks , overlap_two_images )
3941from .utilities .registration import estimate_affine_transform
4042
43+ #: In case provided dataset and complete (true) dataset differ
44+ COL_PAIRED_LANDMARKS = 'Ration matched landmarks'
45+
4146
4247class ImRegBenchmark (Experiment ):
4348 """ General benchmark class for all registration methods.
@@ -152,6 +157,10 @@ class ImRegBenchmark(Experiment):
152157 COL_STATUS = 'status'
153158 #: extension to the image column name for temporary pre-process image
154159 COL_IMAGE_EXT_TEMP = ' TEMP'
160+ #: number of landmarks in dataset (min of moving and reference)
161+ COL_NB_LANDMARKS_INPUT = 'nb. dataset landmarks'
162+ #: number of warped landmarks
163+ COL_NB_LANDMARKS_WARP = 'nb. warped landmarks'
155164 #: required experiment parameters
156165 REQUIRED_PARAMS = Experiment .REQUIRED_PARAMS + ['path_table' ]
157166
@@ -276,6 +285,7 @@ def _load_data(self):
276285 assert os .path .isfile (self .params ['path_table' ]), \
277286 'path to csv cover is not defined - %s' % self .params ['path_table' ]
278287 self ._df_overview = pd .read_csv (self .params ['path_table' ], index_col = None )
288+ self ._df_overview = _df_drop_unnamed (self ._df_overview )
279289 assert all (col in self ._df_overview .columns for col in self .COVER_COLUMNS ), \
280290 'Some required columns are missing in the cover file.'
281291
@@ -286,8 +296,8 @@ def _run(self):
286296 # load existing result of create new entity
287297 if os .path .isfile (self ._path_csv_regist ):
288298 logging .info ('loading existing csv: "%s"' , self ._path_csv_regist )
289- self ._df_experiments = pd .read_csv (self ._path_csv_regist ,
290- index_col = None )
299+ self ._df_experiments = pd .read_csv (self ._path_csv_regist , index_col = None )
300+ self . _df_experiments = _df_drop_unnamed ( self . _df_experiments )
291301 if 'ID' in self ._df_experiments .columns :
292302 self ._df_experiments .set_index ('ID' , inplace = True )
293303 else :
@@ -521,13 +531,13 @@ def _execute_img_registration(self, item):
521531 path_log = os .path .join (path_dir_reg , self .NAME_LOG_REGISTRATION )
522532 # TODO, add lock to single thread, create pool with possible thread ids
523533 # (USE taskset [native], numactl [need install])
524- if not ( isinstance (commands , list ) or isinstance ( commands , tuple )):
534+ if not isinstance (commands , ( list , tuple )):
525535 commands = [commands ]
526536 # measure execution time
527537 cmd_result = exec_commands (commands , path_log , timeout = self .EXECUTE_TIMEOUT )
528538 # if the experiment failed, return back None
529539 if not cmd_result :
530- return None
540+ item = None
531541 return item
532542
533543 def _generate_regist_command (self , item ):
@@ -627,7 +637,6 @@ def main(cls, params=None):
627637 params = parse_arg_params (arg_parser )
628638
629639 logging .info ('running...' )
630- logging .info (cls .__doc__ )
631640 benchmark = cls (params )
632641 benchmark .run ()
633642 path_expt = benchmark .params ['path_exp' ]
@@ -660,17 +669,19 @@ def _load_landmarks(cls, item, path_dataset):
660669
661670 @classmethod
662671 def compute_registration_statistic (cls , idx_row , df_experiments ,
663- path_dataset = None , path_experiment = None ):
672+ path_dataset = None , path_experiment = None , path_reference = None ):
664673 """ after successful registration load initial nad estimated landmarks
665674 afterwords compute various statistic for init, and final alignment
666675
667676 :param tuple(int,dict) df_row: row from iterated table
668677 :param DF df_experiments: DataFrame with experiments
669- :param str|None path_dataset: path to the dataset folder
678+ :param str|None path_dataset: path to the provided dataset folder
679+ :param str|None path_reference: path to the complete landmark collection folder
670680 :param str|None path_experiment: path to the experiment folder
671681 """
672682 idx , row = idx_row
673683 row = dict (row ) # convert even series to dictionary
684+ # load common landmarks and image size
674685 points_ref , points_move , path_img_ref = cls ._load_landmarks (row , path_dataset )
675686 img_diag = cls ._image_diag (row , path_img_ref )
676687 df_experiments .loc [idx , cls .COL_IMAGE_DIAGONAL ] = img_diag
@@ -679,31 +690,44 @@ def compute_registration_statistic(cls, idx_row, df_experiments,
679690 cls .compute_registration_accuracy (df_experiments , idx , points_ref , points_move ,
680691 'init' , img_diag , wo_affine = False )
681692
693+ # define what is the target and init state according to the experiment results
694+ use_move_warp = isinstance (row .get (cls .COL_POINTS_MOVE_WARP , None ), str )
695+ if use_move_warp :
696+ points_init , points_target = points_move , points_ref
697+ col_source , col_target = cls .COL_POINTS_MOVE , cls .COL_POINTS_REF
698+ col_lnds_warp = cls .COL_POINTS_MOVE_WARP
699+ else :
700+ points_init , points_target = points_ref , points_move
701+ col_lnds_warp = cls .COL_POINTS_REF_WARP
702+ col_source , col_target = cls .COL_POINTS_REF , cls .COL_POINTS_MOVE
703+
704+ # optional filtering
705+ if path_reference :
706+ ratio , points_target , _ = \
707+ filter_paired_landmarks (row , path_dataset , path_reference , col_source , col_target )
708+ df_experiments .loc [idx , COL_PAIRED_LANDMARKS ] = np .round (ratio , 2 )
709+
682710 # load transformed landmarks
683711 if (cls .COL_POINTS_MOVE_WARP not in row ) and (cls .COL_POINTS_REF_WARP not in row ):
684712 logging .error ('Statistic: no output landmarks' )
685713 return
686714
687- # define what is the target and init state according to the experiment results
688- is_move_warp = isinstance (row .get (cls .COL_POINTS_MOVE_WARP , None ), str )
689- points_init = points_move if is_move_warp else points_ref
690- points_target = points_ref if is_move_warp else points_move
691- col_lnds_warp = cls .COL_POINTS_MOVE_WARP if is_move_warp else cls .COL_POINTS_REF_WARP
692-
693715 # check if there are reference landmarks
694716 if points_target is None :
695717 logging .warning ('Missing landmarks in "%s"' ,
696- cls .COL_POINTS_REF if is_move_warp else cls .COL_POINTS_MOVE )
718+ cls .COL_POINTS_REF if use_move_warp else cls .COL_POINTS_MOVE )
697719 return
698720 # load warped landmarks
699- path_lnds_wapr = update_path (row [col_lnds_warp ], pre_path = path_experiment )
700- if path_lnds_wapr and os .path .isfile (path_lnds_wapr ):
701- points_warp = load_landmarks (path_lnds_wapr )
721+ path_lnds_warp = update_path (row [col_lnds_warp ], pre_path = path_experiment )
722+ if path_lnds_warp and os .path .isfile (path_lnds_warp ):
723+ points_warp = load_landmarks (path_lnds_warp )
702724 points_warp = np .nan_to_num (points_warp )
703725 else :
704726 logging .warning ('Invalid path to the landmarks: "%s" <- "%s"' ,
705- path_lnds_wapr , row [col_lnds_warp ])
727+ path_lnds_warp , row [col_lnds_warp ])
706728 return
729+ df_experiments .loc [idx , cls .COL_NB_LANDMARKS_INPUT ] = min (len (points_ref ), len (points_ref ))
730+ df_experiments .loc [idx , cls .COL_NB_LANDMARKS_WARP ] = len (points_warp )
707731
708732 # compute Affine statistic
709733 affine_diff = compute_affine_transf_diff (points_init , points_target , points_warp )
@@ -732,8 +756,8 @@ def compute_registration_accuracy(cls, df_experiments, idx, points1, points2,
732756
733757 :param DF df_experiments: DataFrame with experiments
734758 :param int idx: index of tha particular record
735- :param points1: np.array<nb_points, dim>
736- :param points2: np.array<nb_points, dim>
759+ :param ndarray points1: np.array<nb_points, dim>
760+ :param ndarray points2: np.array<nb_points, dim>
737761 :param str state: whether it was before of after registration
738762 :param float img_diag: target image diagonal
739763 :param bool wo_affine: without affine transform, assume only local/elastic deformation
@@ -886,6 +910,63 @@ def visualise_registration(cls, idx_row, path_dataset=None, path_experiment=None
886910 return path_fig
887911
888912
913+ def _df_drop_unnamed (df ):
914+ """Drop columns was index without name and was loaded as `Unnamed: 0.`"""
915+ df = df [list (filter (lambda c : not c .startswith ('Unnamed:' ), df .columns ))]
916+ return df
917+
918+
919+ def filter_paired_landmarks (item , path_dataset , path_reference , col_source , col_target ):
920+ """ filter all relevant landmarks which were used and copy them to experiment
921+
922+ The case is that in certain challenge stage users had provided just a subset
923+ of all image landmarks which could be laos shuffled. The idea is to filter identify
924+ all user used (provided in dataset) landmarks and filter them from temporary
925+ reference dataset.
926+
927+ :param dict|Series item: experiment DataFrame
928+ :param str path_dataset: path to provided landmarks
929+ :param str path_reference: path to the complete landmark collection
930+ :param str col_source: column name of landmarks to be transformed
931+ :param str col_target: column name of landmarks to be compared
932+ :return tuple(float,ndarray,ndarray): match ratio, filtered ref and move landmarks
933+
934+ >>> p_data = update_path('data_images')
935+ >>> p_csv = os.path.join(p_data, 'pairs-imgs-lnds_histol.csv')
936+ >>> df = pd.read_csv(p_csv)
937+ >>> ratio, lnds_ref, lnds_move = filter_paired_landmarks(dict(df.iloc[0]), p_data, p_data,
938+ ... ImRegBenchmark.COL_POINTS_MOVE, ImRegBenchmark.COL_POINTS_REF)
939+ >>> ratio
940+ 1.0
941+ >>> lnds_ref.shape == lnds_move.shape
942+ True
943+ """
944+ path_ref = update_path (item [col_source ], pre_path = path_reference )
945+ assert os .path .isfile (path_ref ), 'missing landmarks: %s' % path_ref
946+ path_load = update_path (item [col_source ], pre_path = path_dataset )
947+ assert os .path .isfile (path_load ), 'missing landmarks: %s' % path_load
948+ pairs = common_landmarks (load_landmarks (path_ref ), load_landmarks (path_load ), threshold = 1 )
949+ if not pairs .size :
950+ logging .warning ('there is not pairing between landmarks or dataset and user reference' )
951+ return 0. , np .empty ([0 ]), np .empty ([0 ])
952+
953+ pairs = sorted (pairs .tolist (), key = lambda p : p [1 ])
954+ ind_ref = np .asarray (pairs )[:, 0 ]
955+ nb_common = min ([len (load_landmarks (update_path (item [col ], pre_path = path_reference )))
956+ for col in (col_target , col_source )])
957+ ind_ref = ind_ref [ind_ref < nb_common ]
958+
959+ path_lnd_ref = update_path (item [col_target ], pre_path = path_reference )
960+ lnds_filter_ref = load_landmarks (path_lnd_ref )[ind_ref ]
961+ path_lnd_move = update_path (item [col_source ], pre_path = path_reference )
962+ lnds_filter_move = load_landmarks (path_lnd_move )[ind_ref ]
963+
964+ ratio_matches = len (ind_ref ) / float (nb_common )
965+ assert ratio_matches <= 1 , 'suspicious ratio for %i paired and %i common landmarks' \
966+ % (len (pairs ), nb_common )
967+ return ratio_matches , lnds_filter_ref , lnds_filter_move
968+
969+
889970def export_summary_results (df_experiments , path_out , params = None ,
890971 name_txt = ImRegBenchmark .NAME_RESULTS_TXT ,
891972 name_csv = ImRegBenchmark .NAME_RESULTS_CSV ):
0 commit comments