From 11a6e57805bb421527a92f87a138e1107567da01 Mon Sep 17 00:00:00 2001 From: Theophile GENTILHOMME <tgentilhomme@jurasix08.idiap.ch> Date: Thu, 5 Apr 2018 16:19:07 +0200 Subject: [PATCH] Fix evaluate script (axes not set) and add defaults settings for the figure classes. Condensate options: list of axis limits instead of individual min/max per axis. --- bob/measure/script/commands.py | 69 ++++++++++++++++--------- bob/measure/script/common_options.py | 76 +++++++++++----------------- bob/measure/script/figure.py | 55 ++++++++++---------- 3 files changed, 100 insertions(+), 100 deletions(-) diff --git a/bob/measure/script/commands.py b/bob/measure/script/commands.py index 0d3fcaa..19de02b 100644 --- a/bob/measure/script/commands.py +++ b/bob/measure/script/commands.py @@ -7,17 +7,18 @@ from . import figure from . import common_options from bob.extension.scripts.click_helper import verbosity_option + @click.command() @common_options.scores_argument(nargs=-1) -@common_options.table_option() @common_options.test_option() +@common_options.table_option() @common_options.open_file_mode_option() @common_options.output_plot_metric_option() @common_options.criterion_option() @common_options.threshold_option() @verbosity_option() @click.pass_context -def metrics(ctx, scores, test, **kargs): +def metrics(ctx, scores, test, **kwargs): """Prints a single output line that contains all info for a given criterion (eer or hter). @@ -39,6 +40,7 @@ def metrics(ctx, scores, test, **kargs): process = figure.Metrics(ctx, scores, test, load.split_files) process.run() + @click.command() @common_options.scores_argument(nargs=-1) @common_options.titles_option() @@ -47,16 +49,13 @@ def metrics(ctx, scores, test, **kargs): @common_options.test_option() @common_options.points_curve_option() @common_options.semilogx_option(True) -@common_options.min_x_axis_val_option() -@common_options.max_x_axis_val_option(dflt=1) -@common_options.min_y_axis_val_option(dflt=0) -@common_options.max_y_axis_val_option(dflt=1) +@common_options.axes_val_option(dflt=[1e-4, 1, 1e-4, 1]) @common_options.axis_fontsize_option() @common_options.x_rotation_option() @common_options.fmr_line_at_option() @verbosity_option() @click.pass_context -def roc(ctx, scores, test, **kargs): +def roc(ctx, scores, test, **kwargs): """Plot ROC (receiver operating characteristic) curve: The plot will represent the false match rate on the horizontal axis and the false non match rate on the vertical axis. The values for the axis will be @@ -77,22 +76,20 @@ def roc(ctx, scores, test, **kargs): process = figure.Roc(ctx, scores, test, load.split_files) process.run() + @click.command() @common_options.scores_argument(nargs=-1) @common_options.output_plot_file_option(default_out='det.pdf') @common_options.titles_option() @common_options.sep_dev_test_option() @common_options.test_option() -@common_options.min_x_axis_val_option(dflt=0.01) -@common_options.max_x_axis_val_option(dflt=95) -@common_options.min_y_axis_val_option(dflt=0.01) -@common_options.max_y_axis_val_option(dflt=95) +@common_options.axes_val_option(dflt=[0.01, 95, 0.01, 95]) @common_options.axis_fontsize_option(dflt=6) @common_options.x_rotation_option(dflt=45) @common_options.points_curve_option() @verbosity_option() @click.pass_context -def det(ctx, scores, test, **kargs): +def det(ctx, scores, test, **kwargs): """Plot DET (detection error trade-off) curve: modified ROC curve which plots error rates on both axes (false positives on the x-axis and false negatives on the y-axis) @@ -112,6 +109,7 @@ def det(ctx, scores, test, **kargs): process = figure.Det(ctx, scores, test, load.split_files) process.run() + @click.command() @common_options.scores_argument(test_mandatory=True, nargs=-1) @common_options.output_plot_file_option(default_out='epc.pdf') @@ -120,7 +118,7 @@ def det(ctx, scores, test, **kargs): @common_options.axis_fontsize_option() @verbosity_option() @click.pass_context -def epc(ctx, scores, **kargs): +def epc(ctx, scores, **kwargs): """Plot EPC (expected performance curve): plots the error rate on the test set depending on a threshold selected a-priori on the development set and accounts for varying relative cost @@ -137,6 +135,7 @@ def epc(ctx, scores, **kargs): process = figure.Epc(ctx, scores, True, load.split_files) process.run() + @click.command() @common_options.scores_argument(nargs=-1) @common_options.output_plot_file_option(default_out='hist.pdf') @@ -147,7 +146,7 @@ def epc(ctx, scores, **kargs): @common_options.threshold_option() @verbosity_option() @click.pass_context -def hist(ctx, scores, test, **kargs): +def hist(ctx, scores, test, **kwargs): """ Plots histograms of positive and negatives along with threshold criterion. @@ -166,6 +165,7 @@ def hist(ctx, scores, test, **kargs): process = figure.Hist(ctx, scores, test, load.split_files) process.run() + @click.command() @common_options.scores_argument(nargs=-1) @common_options.titles_option() @@ -177,20 +177,24 @@ def hist(ctx, scores, test, **kargs): @common_options.points_curve_option() @common_options.semilogx_option(dflt=True) @common_options.n_bins_option() +@common_options.fmr_line_at_option() @verbosity_option() @click.pass_context -def evaluate(ctx, scores, test, **kargs): +def evaluate(ctx, scores, test, **kwargs): '''Runs error analysis on score sets + + \b 1. Computes the threshold using either EER or min. HTER criteria on - development set scores + development set scores 2. Applies the above threshold on test set scores to compute the HTER, if a - test-score set is provided + test-score set is provided 3. Reports error rates on the console 4. Plots ROC, EPC, DET curves and score distributions to a multi-page PDF - file (unless --no-plot is passed) + file (unless --no-plot is passed) You need to provide 2 score files for each biometric system in this order: + \b * development scores * evaluation scores @@ -200,31 +204,46 @@ def evaluate(ctx, scores, test, **kargs): $ bob measure evaluate -t -l metrics.txt -o my_plots.pdf dev-scores test-scores ''' - #first time erase if existing file + # first time erase if existing file click.echo("Computing metrics with EER...") - ctx.meta['criter'] = 'eer' #no criterion passed to evaluate + ctx.meta['criter'] = 'eer' # no criterion passed to evaluate ctx.invoke(metrics, scores=scores, test=test) - #second time, appends the content + # second time, appends the content click.echo("Computing metrics with HTER...") - ctx.meta['criter'] = 'hter' #no criterion passed in evaluate + ctx.meta['criter'] = 'hter' # no criterion passed in evaluate ctx.invoke(metrics, scores=scores, test=test) if 'log' in ctx.meta: click.echo("[metrics] => %s" % ctx.meta['log']) - #avoid closing pdf file before all figures are plotted + # avoid closing pdf file before all figures are plotted ctx.meta['closef'] = False if test: click.echo("Starting evaluate with dev and test scores...") else: click.echo("Starting evaluate with dev scores only...") click.echo("Computing ROC...") - ctx.forward(roc) + # set axes limits for ROC + ctx.forward(roc) # use class defaults plot settings click.echo("Computing DET...") - ctx.forward(det) + ctx.forward(det) # use class defaults plot settings if test: click.echo("Computing EPC...") +<<<<<<< HEAD + ctx.forward(epc) # use class defaults plot settings + # the last one closes the file +||||||| merged common ancestors + ctx.forward(epc) +<<<<<<< HEAD + #the last one closes the file +======= ctx.forward(epc) + # the last one closes the file +>>>>>>> b98021d93c81eee46a730271fad67e001bf084ac +||||||| merged common ancestors #the last one closes the file +======= + # the last one closes the file +>>>>>>> b98021d93c81eee46a730271fad67e001bf084ac ctx.meta['closef'] = True click.echo("Computing score histograms...") ctx.forward(hist) diff --git a/bob/measure/script/common_options.py b/bob/measure/script/common_options.py index 83c47f6..d2d0a8c 100644 --- a/bob/measure/script/common_options.py +++ b/bob/measure/script/common_options.py @@ -29,7 +29,8 @@ def scores_argument(test_mandatory=False, **kwargs): ' is on t') raise click.BadParameter( '%sest-score(s) must ' - 'be provided along with dev-score(s)' % pref, ctx=ctx) + 'be provided along with dev-score(s)' % pref, ctx=ctx + ) else: ctx.meta['dev-scores'] = [value[i] for i in range(length) if not i % 2] @@ -86,57 +87,38 @@ def semilogx_option(dflt= False, **kwargs): callback=callback, **kwargs)(func) return custom_semilogx_option -def min_x_axis_val_option(dflt=1e-4, **kwargs): - '''Get option for min value on x axis''' - def custom_min_x_axis_val_option(func): - def callback(ctx, param, value): - if value > 0: - value = pow(10, int(math.floor(math.log(value, 10)))) - ctx.meta['min_x'] = value - return value - return click.option( - '--min-x', type=float, default=dflt, show_default=True, - help='Min value display on X axis', - callback=callback, **kwargs)(func) - return custom_min_x_axis_val_option - -def max_x_axis_val_option(dflt=1.0, **kwargs): - '''Get option for max value on x axis''' - def custom_max_x_axis_val_option(func): - def callback(ctx, param, value): - ctx.meta['max_x'] = value - return value - return click.option( - '--max-x', type=float, default=dflt, show_default=True, - help='Max value display on X axis', - callback=callback, **kwargs)(func) - return custom_max_x_axis_val_option +def axes_val_option(dflt=None, **kwargs): + '''Get option for min/max values for axes. If one the default is None, no + default is used -def min_y_axis_val_option(dflt=1e-4, **kwargs): - '''Get option for min value on y axis''' - def custom_min_y_axis_val_option(func): - def callback(ctx, param, value): - if value > 0: - value = pow(10, int(math.floor(math.log(value, 10)))) - ctx.meta['min_y'] = value - return value - return click.option( - '--min-y', type=float, default=dflt, show_default=True, - help='Min value display on Y axis', - callback=callback, **kwargs)(func) - return custom_min_y_axis_val_option + Parameters + ---------- -def max_y_axis_val_option(dflt=1.0, **kwargs): - '''Get option for max value on y axis''' - def custom_max_y_axis_val_option(func): + dflt: :any:`list` + List of default min/max values for axes. Must be of length 4 + ''' + def custom_axes_val_option(func): def callback(ctx, param, value): - ctx.meta['max_y'] = value + if value is not None: + tmp = value.split(',') + if len(tmp) != 4: + raise click.BadParameter('Must provide 4 axis limits') + try: + value = [float(i) for i in tmp] + except: + raise click.BadParameter('Axis limits must be floats') + if None in value: + value = None + elif None not in dflt or len(dflt) == 4: + value = dflt if not all(isinstance(x, float) for x in dflt) else None + ctx.meta['axlim'] = value return value return click.option( - '--max-y', type=float, default=dflt, show_default=True, - help='Max value display on Y axis', + '-L', '--axlim', default=None, show_default=True, + help='min/max axes values separated by commas (min_x, max_x, ' + 'min_y, max_y)', callback=callback, **kwargs)(func) - return custom_max_y_axis_val_option + return custom_axes_val_option def axis_fontsize_option(dflt=8, **kwargs): '''Get option for axis font size''' @@ -328,7 +310,7 @@ def threshold_option(**kwargs): return custom_threshold_option -def label_option(name_option='x-label', **kwargs): +def label_option(name_option='x_label', **kwargs): '''Get labels options based on the given name. Parameters: diff --git a/bob/measure/script/figure.py b/bob/measure/script/figure.py index 6ee3382..7c73b4d 100644 --- a/bob/measure/script/figure.py +++ b/bob/measure/script/figure.py @@ -319,17 +319,8 @@ class PlotBase(MeasureBase): _split: :obj:`bool` If False, dev and test curves will be printed on the some figure - _min_x: :obj:`float` - Minimum value for the X-axis - - _max_x: :obj:`float` - Maximum value for the X-axis - - _min_y: :obj:`float` - Minimum value for the Y-axis - - _max_y: :obj:`float` - Maximum value for the Y-axis + _axlim: :any:`list` + Minimum/Maximum values for the X and Y axes _x_rotation: :obj:`int` Rotation of the X axis labels @@ -343,13 +334,10 @@ class PlotBase(MeasureBase): self._points = None if 'points' not in ctx.meta else ctx.meta['points'] self._titles = None if 'titles' not in ctx.meta else ctx.meta['titles'] self._split = None if 'split' not in ctx.meta else ctx.meta['split'] - self._min_x = None if 'min_x' not in ctx.meta else ctx.meta['min_x'] - self._min_y = None if 'min_y' not in ctx.meta else ctx.meta['min_y'] - self._max_x = None if 'max_x' not in ctx.meta else ctx.meta['max_x'] - self._max_y = None if 'max_y' not in ctx.meta else ctx.meta['max_y'] + self._axlim = None if 'axlim' not in ctx.meta else ctx.meta['axlim'] self._x_rotation = None if 'x_rotation' not in ctx.meta else \ ctx.meta['x_rotation'] - self._axisfontsize = None if 'fontsize' not in ctx.meta else \ + self._axisfontsize = 6 if 'fontsize' not in ctx.meta else \ ctx.meta['fontsize'] self._nb_figs = 2 if self._test and self._split else 1 @@ -371,11 +359,13 @@ class PlotBase(MeasureBase): self._pdf_page = self._ctx.meta['PdfPages'] if 'PdfPages'in \ self._ctx.meta else PdfPages(self._output) - mpl.figure(1) - - if self._axisfontsize is not None: - mpl.rc('xtick', labelsize=self._axisfontsize) - mpl.rc('ytick', labelsize=self._axisfontsize) + for i in range(self._nb_figs): + fig = mpl.figure(i + 1) + fig.clear() + if self._axisfontsize is not None: + mpl.rc('xtick', labelsize=self._axisfontsize) + mpl.rc('ytick', labelsize=self._axisfontsize) + mpl.rc('legend', fontsize=7) def end_process(self): ''' Set title, legend, axis labels, grid colors, save figures and @@ -416,9 +406,10 @@ class PlotBase(MeasureBase): return base + (" (%s)" % name) def _set_axis(self): - axis = [self._min_x, self._max_x, self._min_y, self._max_y] - if None not in axis: - mpl.axis(axis) + if self._axlim is not None and None not in self._axlim: + mpl.axis(self._axlim) + else: + mpl.axes().autoscale() class Roc(PlotBase): ''' Handles the plotting of ROC @@ -438,6 +429,9 @@ class Roc(PlotBase): self._title = 'ROC' self._x_label = 'FMR' self._y_label = ("1 - FNMR" if self._semilogx else "FNMR") + #custom defaults + if self._axlim is None: + self._axlim = [1e-4, 1.0, 1e-4, 1.0] def compute(self, idx, dev_neg, dev_pos, dev_fta=None, dev_file=None, test_neg=None, test_pos=None, test_fta=None, test_file=None): @@ -488,6 +482,9 @@ class Det(PlotBase): def __init__(self, ctx, scores, test, func_load): super(Det, self).__init__(ctx, scores, test, func_load) self._title = 'DET' + #custom defaults here + if self._x_rotation is None: + self._x_rotation = 50 def compute(self, idx, dev_neg, dev_pos, dev_fta=None, dev_file=None, test_neg=None, test_pos=None, test_fta=None, test_file=None): @@ -517,9 +514,10 @@ class Det(PlotBase): ) def _set_axis(self): - axis = [self._min_x, self._max_x, self._min_y, self._max_y] - if None not in axis: - plot.det_axis(axis) + if self._axlim is not None and None not in self._axlim: + plot.det_axis(self._axlim) + else: + plot.det_axis([0.01, 99, 0.01, 99]) class Epc(PlotBase): ''' Handles the plotting of EPC ''' @@ -531,6 +529,7 @@ class Epc(PlotBase): self._x_label = 'Cost' self._y_label = 'Min. HTER (%)' self._test = True #always test data with EPC + self._nb_figs = 1 def compute(self, idx, dev_neg, dev_pos, dev_fta=None, dev_file=None, test_neg=None, test_pos=None, test_fta=None, test_file=None): @@ -605,7 +604,7 @@ class Hist(PlotBase): ax = mpl.gca() ax.axes.get_xaxis().set_ticklabels([]) mpl.legend(loc='upper center', ncol=3, bbox_to_anchor=(0.5, -0.01), - fontsize=10) + fontsize=6) else: mpl.legend(loc='best', fancybox=True, framealpha=0.5) -- GitLab