Skip to content
This repository was archived by the owner on Feb 11, 2023. It is now read-only.

Commit ec2ca0d

Browse files
committed
ANHIR submission (#44)
* add experiment name * update eval. submission * eval: jit filter landmarks * update compute stats * fix cols swap in eval. submission * ext. evaluate tissue-state * add codecov info * update docs * refactor splitext * fix pkg setup * update CI * update Results ipynb * exporting figures * update drawing (cmap) * elastix params for ANHIR * update visual
1 parent 43e4ca6 commit ec2ca0d

31 files changed

+2348
-1310
lines changed

.codecov.yml

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,7 @@
1-
#see https://github.com/codecov/support/wiki/Codecov-Yaml
1+
# see https://docs.codecov.io/docs/codecov-yaml
2+
# Validation check:
3+
# $ curl --data-binary @.codecov.yml https://codecov.io/validate
4+
25
#codecov:
36
# notify:
47
# require_ci_to_pass: yes

.travis.yml

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@
88
# this file is *not* meant to cover or endorse the use of travis, but rather to
99
# help confirm pull requests to this project.
1010

11+
dist: bionic # Ubuntu 18.04
12+
1113
env:
1214
global:
1315
- DISPLAY=""
@@ -72,8 +74,8 @@ script:
7274
- python bm_experiments/bm_comp_perform.py -o ./results -n 1
7375
- python birl/bm_template.py -t ./data_images/pairs-imgs-lnds_mix.csv -o ./results --visual --unique -cfg configs/sample_config.yaml
7476
- rm ./data_images/*_/*/*_HE.csv # remove target landmarks from histol. tissue
75-
- python birl/bm_template.py -t ./data_images/pairs-imgs-lnds_histol.csv -d ./data_images -o ./results --preprocessing matching-rgb gray -cfg configs/sample_config.yaml
76-
- python bm_experiments/evaluate_experiment.py -d ./data_images -e ./results/BmTemplate --visual
77+
- python birl/bm_template.py -n anhir -t ./data_images/pairs-imgs-lnds_histol.csv -d ./data_images -o ./results --preprocessing matching-rgb gray -cfg configs/sample_config.yaml
78+
- python bm_experiments/evaluate_experiment.py -d ./data_images -e ./results/BmTemplate_anhir --visual
7779

7880
after_success:
7981
- coverage report

appveyor.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,7 @@ test_script:
7878
- tox -v --sitepackages --parallel auto
7979
- mkdir results && touch configs/sample_config.yaml
8080
- python bm_experiments/bm_comp_perform.py -o ./results -n 1
81-
- python birl/bm_template.py -t ./data_images/pairs-imgs-lnds_mix.csv -o ./results --preprocessing matching-rgb gray --unique --visual -cfg configs/sample_config.yaml
81+
- python birl/bm_template.py -n anhir -t ./data_images/pairs-imgs-lnds_mix.csv -o ./results --preprocessing matching-rgb gray --unique --visual -cfg configs/sample_config.yaml
8282

8383
on_success:
8484
- coverage report

birl/__init__.py

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -12,12 +12,12 @@
1212
traceback.print_exc()
1313

1414

15-
__version__ = '0.2.3'
16-
__author__ = 'Jiri Borovec'
17-
__author_email__ = '[email protected]'
18-
__license__ = 'BSD 3-clause'
19-
__homepage__ = 'https://borda.github.io/BIRL',
20-
__copyright__ = 'Copyright (c) 2014-2019, %s.' % __author__
15+
__version__ = "0.2.3"
16+
__author__ = "Jiri Borovec"
17+
__author_email__ = "[email protected]"
18+
__license__ = "BSD 3-clause"
19+
__homepage__ = "https://borda.github.io/BIRL",
20+
__copyright__ = "Copyright (c) 2014-2019, %s." % __author__
2121
__doc__ = 'BIRL: Benchmark on Image Registration methods with Landmark validation'
2222
__long_doc__ = "# %s" % __doc__ + """
2323

birl/benchmark.py

Lines changed: 102 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
# -*- coding: utf-8 -*-
12
"""
23
General benchmark template for all registration methods.
34
It also serves for evaluating the input registration pairs
@@ -25,10 +26,11 @@
2526
import pandas as pd
2627
from skimage.color import rgb2gray
2728

29+
# this is used while calling this file as a script
2830
sys.path += [os.path.abspath('.'), os.path.abspath('..')] # Add path to root
2931
from .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
3234
from .utilities.evaluate import (
3335
compute_target_regist_error_statistic, compute_affine_transf_diff, compute_tre_robustness)
3436
from .utilities.experiments import (
@@ -38,6 +40,9 @@
3840
export_figure, draw_image_points, draw_images_warped_landmarks, overlap_two_images)
3941
from .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

4247
class 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+
889970
def export_summary_results(df_experiments, path_out, params=None,
890971
name_txt=ImRegBenchmark.NAME_RESULTS_TXT,
891972
name_csv=ImRegBenchmark.NAME_RESULTS_CSV):

birl/bm_template.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
import sys
2626
import logging
2727

28+
# this is used while calling this file as a script
2829
sys.path += [os.path.abspath('.'), os.path.abspath('..')] # Add path to root
2930
from birl.utilities.experiments import create_basic_parser
3031
from birl.benchmark import ImRegBenchmark
@@ -174,6 +175,7 @@ def extend_parse(arg_parser):
174175
# RUN by given parameters
175176
if __name__ == '__main__':
176177
logging.basicConfig(level=logging.INFO)
178+
logging.info(__doc__)
177179
arg_params, path_expt = BmTemplate.main()
178180

179181
if arg_params.get('run_comp_benchmark', False):

birl/utilities/data_io.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,7 @@ def load_landmarks(path_file):
7777
if not os.path.isfile(path_file):
7878
logging.warning('missing landmarks "%s"', path_file)
7979
return None
80-
ext = os.path.splitext(path_file)[-1]
80+
_, ext = os.path.splitext(path_file)
8181
if ext == '.csv':
8282
return load_landmarks_csv(path_file)
8383
elif ext == '.pts':
@@ -160,7 +160,7 @@ def save_landmarks(path_file, landmarks):
160160
"""
161161
assert os.path.isdir(os.path.dirname(path_file)), \
162162
'missing folder "%s"' % os.path.dirname(path_file)
163-
path_file = os.path.splitext(path_file)[0]
163+
path_file, _ = os.path.splitext(path_file)
164164
landmarks = landmarks.values if isinstance(landmarks, pd.DataFrame) else landmarks
165165
save_landmarks_csv(path_file + '.csv', landmarks)
166166
save_landmarks_pts(path_file + '.pts', landmarks)
@@ -433,7 +433,7 @@ def _gene_out_path(path_file, file_ext, path_out_dir=None):
433433
"""
434434
if not path_out_dir:
435435
path_out_dir = os.path.dirname(path_file)
436-
img_name = os.path.splitext(os.path.basename(path_file))[0]
436+
img_name, _ = os.path.splitext(os.path.basename(path_file))
437437
path_out = os.path.join(path_out_dir, img_name + file_ext)
438438
return path_out
439439

birl/utilities/dataset.py

Lines changed: 16 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -600,25 +600,31 @@ def list_sub_folders(path_folder, name='*'):
600600
return sub_dirs
601601

602602

603-
def common_landmarks(points1, points2, threshold=0.5):
603+
def common_landmarks(points1, points2, threshold=1.5):
604604
""" find common landmarks in two sets
605605
606606
:param ndarray|list(list(float)) points1: first point set
607607
:param ndarray|list(list(float)) points2: second point set
608-
:param float threshold: threshold for assignment
608+
:param float threshold: threshold for assignment (for landmarks in pixels)
609609
:return list(bool): flags
610610
611611
>>> np.random.seed(0)
612612
>>> common = np.random.random((5, 2))
613613
>>> pts1 = np.vstack([common, np.random.random((10, 2))])
614614
>>> pts2 = np.vstack([common, np.random.random((15, 2))])
615-
>>> common_landmarks(pts1, pts2, threshold=0.1)
616-
array([[ 0, 0],
617-
[ 1, 1],
618-
[ 2, 2],
619-
[ 3, 3],
620-
[ 4, 4],
621-
[14, 15]])
615+
>>> common_landmarks(pts1, pts2, threshold=1e-3)
616+
array([[0, 0],
617+
[1, 1],
618+
[2, 2],
619+
[3, 3],
620+
[4, 4]])
621+
>>> np.random.shuffle(pts2)
622+
>>> common_landmarks(pts1, pts2, threshold=1e-3)
623+
array([[ 0, 13],
624+
[ 1, 10],
625+
[ 2, 9],
626+
[ 3, 14],
627+
[ 4, 8]])
622628
"""
623629
points1 = np.asarray(points1)
624630
points2 = np.asarray(points2)
@@ -627,6 +633,7 @@ def common_landmarks(points1, points2, threshold=0.5):
627633
dist_sel = dist[ind_row, ind_col]
628634
pairs = [(i, j) for (i, j, d) in zip(ind_row, ind_col, dist_sel)
629635
if d < threshold]
636+
assert len(pairs) <= min([len(points1), len(points2)])
630637
return np.array(pairs, dtype=int)
631638

632639

0 commit comments

Comments
 (0)