From e760ac7d642ce490324ea7d9c412216cd2ba72aa Mon Sep 17 00:00:00 2001 From: Andre Anjos <andre.anjos@idiap.ch> Date: Thu, 14 Nov 2019 15:37:28 +0100 Subject: [PATCH] [data/imagefolderinference] Accept globs --- bob/ip/binseg/utils/plot.py | 198 +++++++++++++++++++----------------- 1 file changed, 102 insertions(+), 96 deletions(-) diff --git a/bob/ip/binseg/utils/plot.py b/bob/ip/binseg/utils/plot.py index ceb268d0..bd9c2131 100644 --- a/bob/ip/binseg/utils/plot.py +++ b/bob/ip/binseg/utils/plot.py @@ -3,7 +3,7 @@ import numpy as np import os -import csv +import csv import pandas as pd import PIL from PIL import Image,ImageFont, ImageDraw @@ -13,62 +13,62 @@ import torch def precision_recall_f1iso(precision, recall, names, title=None): """ Author: Andre Anjos (andre.anjos@idiap.ch). - - Creates a precision-recall plot of the given data. + + Creates a precision-recall plot of the given data. The plot will be annotated with F1-score iso-lines (in which the F1-score - maintains the same value) - + maintains the same value) + Parameters - ---------- + ---------- precision : :py:class:`numpy.ndarray` or :py:class:`list` A list of 1D np arrays containing the Y coordinates of the plot, or the precision, or a 2D np array in which the rows correspond to each - of the system's precision coordinates. + of the system's precision coordinates. recall : :py:class:`numpy.ndarray` or :py:class:`list` A list of 1D np arrays containing the X coordinates of the plot, or the recall, or a 2D np array in which the rows correspond to each - of the system's recall coordinates. + of the system's recall coordinates. names : :py:class:`list` An iterable over the names of each of the systems along the rows of - ``precision`` and ``recall`` + ``precision`` and ``recall`` title : :py:class:`str`, optional - A title for the plot. If not set, omits the title + A title for the plot. If not set, omits the title Returns - ------- + ------- matplotlib.figure.Figure - A matplotlib figure you can save or display - """ + A matplotlib figure you can save or display + """ import matplotlib matplotlib.use('agg') - import matplotlib.pyplot as plt + import matplotlib.pyplot as plt from itertools import cycle - fig, ax1 = plt.subplots(1) + fig, ax1 = plt.subplots(1) lines = ["-","--","-.",":"] linecycler = cycle(lines) - for p, r, n in zip(precision, recall, names): + for p, r, n in zip(precision, recall, names): # Plots only from the point where recall reaches its maximum, otherwise, we # don't see a curve... i = r.argmax() pi = p[i:] - ri = r[i:] + ri = r[i:] valid = (pi+ri) > 0 - f1 = 2 * (pi[valid]*ri[valid]) / (pi[valid]+ri[valid]) + f1 = 2 * (pi[valid]*ri[valid]) / (pi[valid]+ri[valid]) # optimal point along the curve argmax = f1.argmax() opi = pi[argmax] ori = ri[argmax] # Plot Recall/Precision as threshold changes - ax1.plot(ri[pi>0], pi[pi>0], next(linecycler), label='[F={:.4f}] {}'.format(f1.max(), n),) + ax1.plot(ri[pi>0], pi[pi>0], next(linecycler), label='[F={:.4f}] {}'.format(f1.max(), n),) ax1.plot(ori,opi, marker='o', linestyle=None, markersize=3, color='black') - ax1.grid(linestyle='--', linewidth=1, color='gray', alpha=0.2) + ax1.grid(linestyle='--', linewidth=1, color='gray', alpha=0.2) if len(names) > 1: - plt.legend(loc='lower left', framealpha=0.5) + plt.legend(loc='lower left', framealpha=0.5) ax1.set_xlabel('Recall') ax1.set_ylabel('Precision') ax1.set_xlim([0.0, 1.0]) - ax1.set_ylim([0.0, 1.0]) - if title is not None: ax1.set_title(title) + ax1.set_ylim([0.0, 1.0]) + if title is not None: ax1.set_title(title) # Annotates plot with F1-score iso-lines ax2 = ax1.twinx() f_scores = np.linspace(0.1, 0.9, num=9) @@ -79,70 +79,70 @@ def precision_recall_f1iso(precision, recall, names, title=None): y = f_score * x / (2 * x - f_score) l, = plt.plot(x[y >= 0], y[y >= 0], color='green', alpha=0.1) tick_locs.append(y[-1]) - tick_labels.append('%.1f' % f_score) + tick_labels.append('%.1f' % f_score) ax2.tick_params(axis='y', which='both', pad=0, right=False, left=False) ax2.set_ylabel('iso-F', color='green', alpha=0.3) ax2.set_ylim([0.0, 1.0]) - ax2.yaxis.set_label_coords(1.015, 0.97) - ax2.set_yticks(tick_locs) #notice these are invisible + ax2.yaxis.set_label_coords(1.015, 0.97) + ax2.set_yticks(tick_locs) #notice these are invisible for k in ax2.set_yticklabels(tick_labels): k.set_color('green') k.set_alpha(0.3) - k.set_size(8) + k.set_size(8) # we should see some of axes 1 axes ax1.spines['right'].set_visible(False) ax1.spines['top'].set_visible(False) ax1.spines['left'].set_position(('data', -0.015)) - ax1.spines['bottom'].set_position(('data', -0.015)) + ax1.spines['bottom'].set_position(('data', -0.015)) # we shouldn't see any of axes 2 axes ax2.spines['right'].set_visible(False) ax2.spines['top'].set_visible(False) ax2.spines['left'].set_visible(False) - ax2.spines['bottom'].set_visible(False) - plt.tight_layout() - return fig + ax2.spines['bottom'].set_visible(False) + plt.tight_layout() + return fig def precision_recall_f1iso_confintval(precision, recall, pr_upper, pr_lower, re_upper, re_lower, names, title=None): """ Author: Andre Anjos (andre.anjos@idiap.ch). - - Creates a precision-recall plot of the given data. + + Creates a precision-recall plot of the given data. The plot will be annotated with F1-score iso-lines (in which the F1-score - maintains the same value) - + maintains the same value) + Parameters - ---------- + ---------- precision : :py:class:`numpy.ndarray` or :py:class:`list` A list of 1D np arrays containing the Y coordinates of the plot, or the precision, or a 2D np array in which the rows correspond to each - of the system's precision coordinates. + of the system's precision coordinates. recall : :py:class:`numpy.ndarray` or :py:class:`list` A list of 1D np arrays containing the X coordinates of the plot, or the recall, or a 2D np array in which the rows correspond to each - of the system's recall coordinates. + of the system's recall coordinates. names : :py:class:`list` An iterable over the names of each of the systems along the rows of - ``precision`` and ``recall`` + ``precision`` and ``recall`` title : :py:class:`str`, optional - A title for the plot. If not set, omits the title + A title for the plot. If not set, omits the title Returns - ------- + ------- matplotlib.figure.Figure - A matplotlib figure you can save or display - """ + A matplotlib figure you can save or display + """ import matplotlib matplotlib.use('agg') - import matplotlib.pyplot as plt + import matplotlib.pyplot as plt from itertools import cycle - fig, ax1 = plt.subplots(1) + fig, ax1 = plt.subplots(1) lines = ["-","--","-.",":"] colors = ['#1f77b4', '#ff7f0e', '#2ca02c', '#d62728', '#9467bd', '#8c564b', '#e377c2', '#7f7f7f', '#bcbd22', '#17becf'] colorcycler = cycle(colors) linecycler = cycle(lines) - for p, r, pu, pl, ru, rl, n in zip(precision, recall, pr_upper, pr_lower, re_upper, re_lower, names): + for p, r, pu, pl, ru, rl, n in zip(precision, recall, pr_upper, pr_lower, re_upper, re_lower, names): # Plots only from the point where recall reaches its maximum, otherwise, we # don't see a curve... i = r.argmax() @@ -151,24 +151,24 @@ def precision_recall_f1iso_confintval(precision, recall, pr_upper, pr_lower, re_ pui = pu[i:] pli = pl[i:] rui = ru[i:] - rli = rl[i:] + rli = rl[i:] valid = (pi+ri) > 0 - f1 = 2 * (pi[valid]*ri[valid]) / (pi[valid]+ri[valid]) + f1 = 2 * (pi[valid]*ri[valid]) / (pi[valid]+ri[valid]) # optimal point along the curve argmax = f1.argmax() opi = pi[argmax] ori = ri[argmax] # Plot Recall/Precision as threshold changes - ax1.plot(ri[pi>0], pi[pi>0], next(linecycler), label='[F={:.4f}] {}'.format(f1.max(), n),) + ax1.plot(ri[pi>0], pi[pi>0], next(linecycler), label='[F={:.4f}] {}'.format(f1.max(), n),) ax1.plot(ori,opi, marker='o', linestyle=None, markersize=3, color='black') # Plot confidence # Upper bound - #ax1.plot(r95ui[p95ui>0], p95ui[p95ui>0]) + #ax1.plot(r95ui[p95ui>0], p95ui[p95ui>0]) # Lower bound #ax1.plot(r95li[p95li>0], p95li[p95li>0]) # create the limiting polygon vert_x = np.concatenate((rui[pui>0], rli[pli>0][::-1])) - vert_y = np.concatenate((pui[pui>0], pli[pli>0][::-1])) + vert_y = np.concatenate((pui[pui>0], pli[pli>0][::-1])) # hacky workaround to plot 2nd human if np.isclose(np.mean(rui), rui[1], rtol=1e-05): print('found human') @@ -177,14 +177,14 @@ def precision_recall_f1iso_confintval(precision, recall, pr_upper, pr_lower, re_ p = plt.Polygon(np.column_stack((vert_x, vert_y)), facecolor=next(colorcycler), alpha=.2, edgecolor='none',lw=.2) ax1.add_artist(p) - ax1.grid(linestyle='--', linewidth=1, color='gray', alpha=0.2) + ax1.grid(linestyle='--', linewidth=1, color='gray', alpha=0.2) if len(names) > 1: - plt.legend(loc='lower left', framealpha=0.5) + plt.legend(loc='lower left', framealpha=0.5) ax1.set_xlabel('Recall') ax1.set_ylabel('Precision') ax1.set_xlim([0.0, 1.0]) - ax1.set_ylim([0.0, 1.0]) - if title is not None: ax1.set_title(title) + ax1.set_ylim([0.0, 1.0]) + if title is not None: ax1.set_title(title) # Annotates plot with F1-score iso-lines ax2 = ax1.twinx() f_scores = np.linspace(0.1, 0.9, num=9) @@ -195,45 +195,45 @@ def precision_recall_f1iso_confintval(precision, recall, pr_upper, pr_lower, re_ y = f_score * x / (2 * x - f_score) l, = plt.plot(x[y >= 0], y[y >= 0], color='green', alpha=0.1) tick_locs.append(y[-1]) - tick_labels.append('%.1f' % f_score) + tick_labels.append('%.1f' % f_score) ax2.tick_params(axis='y', which='both', pad=0, right=False, left=False) ax2.set_ylabel('iso-F', color='green', alpha=0.3) ax2.set_ylim([0.0, 1.0]) - ax2.yaxis.set_label_coords(1.015, 0.97) - ax2.set_yticks(tick_locs) #notice these are invisible + ax2.yaxis.set_label_coords(1.015, 0.97) + ax2.set_yticks(tick_locs) #notice these are invisible for k in ax2.set_yticklabels(tick_labels): k.set_color('green') k.set_alpha(0.3) - k.set_size(8) + k.set_size(8) # we should see some of axes 1 axes ax1.spines['right'].set_visible(False) ax1.spines['top'].set_visible(False) ax1.spines['left'].set_position(('data', -0.015)) - ax1.spines['bottom'].set_position(('data', -0.015)) + ax1.spines['bottom'].set_position(('data', -0.015)) # we shouldn't see any of axes 2 axes ax2.spines['right'].set_visible(False) ax2.spines['top'].set_visible(False) ax2.spines['left'].set_visible(False) - ax2.spines['bottom'].set_visible(False) - plt.tight_layout() - return fig + ax2.spines['bottom'].set_visible(False) + plt.tight_layout() + return fig def loss_curve(df, title): """ Creates a loss curve given a Dataframe with column names: ``['avg. loss', 'median loss','lr','max memory']`` - + Parameters ---------- df : :py:class:`pandas.DataFrame` - + Returns ------- matplotlib.figure.Figure - """ + """ import matplotlib matplotlib.use('agg') - import matplotlib.pyplot as plt + import matplotlib.pyplot as plt ax1 = df.plot(y="median loss", grid=True) ax1.set_title(title) ax1.set_ylabel('median loss') @@ -241,7 +241,7 @@ def loss_curve(df, title): ax2 = df['lr'].plot(secondary_y=True,legend=True,grid=True,) ax2.set_ylabel('lr') ax1.set_xlabel('epoch') - plt.tight_layout() + plt.tight_layout() fig = ax1.get_figure() return fig @@ -249,12 +249,12 @@ def loss_curve(df, title): def read_metricscsv(file): """ Read precision and recall from csv file - + Parameters ---------- file : str path to file - + Returns ------- :py:class:`numpy.ndarray` @@ -283,7 +283,7 @@ def read_metricscsv(file): def plot_overview(outputfolders,title): """ Plots comparison chart of all trained models - + Parameters ---------- outputfolder : list @@ -303,7 +303,7 @@ def plot_overview(outputfolders,title): names = [] params = [] for folder in outputfolders: - # metrics + # metrics metrics_path = os.path.join(folder,'results/Metrics.csv') pr, re, pr_upper, pr_lower, re_upper, re_lower = read_metricscsv(metrics_path) precisions.append(pr) @@ -335,7 +335,7 @@ def metricsviz(dataset ,overlayed=True): """ Visualizes true positives, false positives and false negatives Default colors TP: Gray, FP: Cyan, FN: Orange - + Parameters ---------- dataset : :py:class:`torch.utils.data.Dataset` @@ -354,27 +354,27 @@ def metricsviz(dataset name = sample[0] img = VF.to_pil_image(sample[1]) # PIL Image gt = sample[2].byte() # byte tensor - - # read metrics + + # read metrics metrics = pd.read_csv(os.path.join(output_path,'results','Metrics.csv')) optimal_threshold = metrics['threshold'][metrics['f1_score'].idxmax()] - - # read probability output + + # read probability output pred = Image.open(os.path.join(output_path,'images',name)) pred = pred.convert(mode='L') pred = VF.to_tensor(pred) binary_pred = torch.gt(pred, optimal_threshold).byte() - + # calc metrics # equals and not-equals equals = torch.eq(binary_pred, gt) # tensor - notequals = torch.ne(binary_pred, gt) # tensor - # true positives + notequals = torch.ne(binary_pred, gt) # tensor + # true positives tp_tensor = (gt * binary_pred ) # tensor tp_pil = VF.to_pil_image(tp_tensor.float()) tp_pil_colored = PIL.ImageOps.colorize(tp_pil, (0,0,0), tp_color) - # false positives - fp_tensor = torch.eq((binary_pred + tp_tensor), 1) + # false positives + fp_tensor = torch.eq((binary_pred + tp_tensor), 1) fp_pil = VF.to_pil_image(fp_tensor.float()) fp_pil_colored = PIL.ImageOps.colorize(fp_pil, (0,0,0), fp_color) # false negatives @@ -385,7 +385,7 @@ def metricsviz(dataset # paste together tp_pil_colored.paste(fp_pil_colored,mask=fp_pil) tp_pil_colored.paste(fn_pil_colored,mask=fn_pil) - + if overlayed: tp_pil_colored = PIL.Image.blend(img, tp_pil_colored, 0.4) img_metrics = pd.read_csv(os.path.join(output_path,'results',name+'.csv')) @@ -396,15 +396,17 @@ def metricsviz(dataset fnt = ImageFont.truetype('FreeMono.ttf', fnt_size) draw.text((0, 0),"F1: {:.4f}".format(f1),(255,255,255),font=fnt) - # save to disk + # save to disk overlayed_path = os.path.join(output_path,'tpfnfpviz') - if not os.path.exists(overlayed_path): os.makedirs(overlayed_path) - tp_pil_colored.save(os.path.join(overlayed_path,name)) + fullpath = os.path.join(overlayed_path, name) + fulldir = os.path.dirname(fullpath) + if not os.path.exists(fulldir): os.makedirs(fulldir) + tp_pil_colored.save(fullpath) def overlay(dataset, output_path): """Overlays prediction probabilities vessel tree with original test image. - + Parameters ---------- dataset : :py:class:`torch.utils.data.Dataset` @@ -416,8 +418,8 @@ def overlay(dataset, output_path): # get sample name = sample[0] img = VF.to_pil_image(sample[1]) # PIL Image - - # read probability output + + # read probability output pred = Image.open(os.path.join(output_path,'images',name)).convert(mode='L') # color and overlay pred_green = PIL.ImageOps.colorize(pred, (0,0,0), (0,255,0)) @@ -430,14 +432,16 @@ def overlay(dataset, output_path): #draw.text((0, 0),"F1: {:.4f}".format(f1),(255,255,255),font=fnt) # save to disk overlayed_path = os.path.join(output_path,'overlayed') - if not os.path.exists(overlayed_path): os.makedirs(overlayed_path) - overlayed.save(os.path.join(overlayed_path,name)) + fullpath = os.path.join(overlayed_path, name) + fulldir = os.path.dirname(fullpath) + if not os.path.exists(fulldir): os.makedirs(fulldir) + overlayed.save(fullpath) def savetransformedtest(dataset, output_path): - """Save the test images as they are fed into the neural network. + """Save the test images as they are fed into the neural network. Makes it easier to create overlay animations (e.g. slide) - + Parameters ---------- dataset : :py:class:`torch.utils.data.Dataset` @@ -449,8 +453,10 @@ def savetransformedtest(dataset, output_path): # get sample name = sample[0] img = VF.to_pil_image(sample[1]) # PIL Image - + # save to disk testimg_path = os.path.join(output_path,'transformedtestimages') - if not os.path.exists(testimg_path): os.makedirs(testimg_path) - img.save(os.path.join(testimg_path,name)) + fullpath = os.path.join(testimg_path, name) + fulldir = os.path.dirname(fullpath) + if not os.path.exists(fulldir): os.makedirs(fulldir) + img.save(fullpath) -- GitLab