From 4992d4c34ee14f7c630055b2c01187dcbee8747c Mon Sep 17 00:00:00 2001 From: Lorenzo Bertoni <34957815+bertoni9@users.noreply.github.com> Date: Mon, 30 Nov 2020 11:49:47 +0100 Subject: [PATCH] Visualization (#2) * update readme * remove legend for front * change default z_max to 100m * add visualization for distance * add visualization for distance * adjust text * remove legend for frontal image * change frontal visualization * adjust visualization * update fontsize as constant * change name of saving * change architecture name * change architecture name * change architecture name * remove gt boxes * support different resolutions * adapt to front and combined * change name to multi --- README.md | 2 +- docs/MonStereo.md | 9 +- monstereo/eval/eval_kitti.py | 8 +- monstereo/network/architectures.py | 262 +------------------- monstereo/network/net.py | 4 +- monstereo/predict.py | 18 +- monstereo/run.py | 8 +- monstereo/train/trainer.py | 8 +- monstereo/visuals/figures.py | 92 +++---- monstereo/visuals/printer.py | 384 ++++++++++++++++------------- tests/test_package.py | 2 +- 11 files changed, 291 insertions(+), 506 deletions(-) diff --git a/README.md b/README.md index 198d8ee..fe80f2e 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,7 @@ This repository contains the code for three research projects: 1. **MonStereo: When Monocular and Stereo Meet at the Tail of 3D Human Localization** [README](https://github.com/vita-epfl/monstereo/tree/master/docs/MonStereo.md) & [Article](https://arxiv.org/abs/2008.10913) - ![moonstereo](docs/out_005523.png) + ![monstereo](docs/out_005523.png) 2. **Perceiving Humans: from Monocular 3D Localization to Social Distancing** [README](https://github.com/vita-epfl/monstereo/tree/master/docs/SocialDistancing.md) & [Article](https://arxiv.org/abs/2009.00984) diff --git a/docs/MonStereo.md b/docs/MonStereo.md index 3509072..5896b1a 100644 --- a/docs/MonStereo.md +++ b/docs/MonStereo.md @@ -104,7 +104,7 @@ and runs MonStereo for 3d location of the detected poses. Output options include json files and/or visualization of the predictions on the image in *frontal mode*, -*birds-eye-view mode* or *combined mode* and can be specified with `--output_types` +*birds-eye-view mode* or *multi mode* and can be specified with `--output_types` ### Ground truth matching @@ -121,13 +121,13 @@ show all the prediction for the image After downloading model and ground-truth file, a demo can be tested with the following commands: -`python3 -m monstereo.run predict --glob docs/000840*.png --output_types combined --scale 2 +`python3 -m monstereo.run predict --glob docs/000840*.png --output_types multi --scale 2 --model data/models/ms-200710-1511.pkl --z_max 30 --checkpoint resnet152 --path_gt data/arrays/names-kitti-200615-1022.json -o data/output` ![Crowded scene](out_000840.png) -`python3 -m monstereo.run predict --glob docs/005523*.png --output_types combined --scale 2 +`python3 -m monstereo.run predict --glob docs/005523*.png --output_types multi --scale 2 --model data/models/ms-200710-1511.pkl --z_max 30 --checkpoint resnet152 --path_gt data/arrays/names-kitti-200615-1022.json -o data/output` @@ -138,8 +138,7 @@ Preprocessing and training step are already fully supported by the code provided but require first to run a pose detector over all the training images and collect the annotations. The code supports this option (by running the predict script and using `--mode pifpaf`). -Once the code will be made publicly available, we will add -links to download annotations. + ### Datasets Download KITTI ground truth files and camera calibration matrices for training diff --git a/monstereo/eval/eval_kitti.py b/monstereo/eval/eval_kitti.py index 20c34cf..4542a30 100644 --- a/monstereo/eval/eval_kitti.py +++ b/monstereo/eval/eval_kitti.py @@ -126,10 +126,10 @@ class EvalKitti: def printer(self, show, save): if save or show: - show_results(self.dic_stats, self.CLUSTERS, show, save) - show_spread(self.dic_stats, self.CLUSTERS, show, save) - show_box_plot(self.errors, self.CLUSTERS, show, save) - show_task_error(show, save) + show_results(self.dic_stats, self.CLUSTERS, show=show, save=save) + show_spread(self.dic_stats, self.CLUSTERS, show=show, save=save) + show_box_plot(self.errors, self.CLUSTERS, show=show, save=save) + show_task_error(show=show, save=save) def _parse_txts(self, path, method): diff --git a/monstereo/network/architectures.py b/monstereo/network/architectures.py index 66988d1..b8ec94a 100644 --- a/monstereo/network/architectures.py +++ b/monstereo/network/architectures.py @@ -3,10 +3,10 @@ import torch import torch.nn as nn -class SimpleModel(nn.Module): +class MonStereoModel(nn.Module): def __init__(self, input_size, output_size=2, linear_size=512, p_dropout=0.2, num_stage=3, device='cuda'): - super(SimpleModel, self).__init__() + super(MonStereoModel, self).__init__() self.num_stage = num_stage self.stereo_size = input_size @@ -102,264 +102,6 @@ class MyLinearSimple(nn.Module): return out -class DecisionModel(nn.Module): - - def __init__(self, input_size, output_size=2, linear_size=512, p_dropout=0.2, num_stage=3, device='cuda:1'): - super(DecisionModel, self).__init__() - - self.num_stage = num_stage - self.stereo_size = input_size - self.mono_size = int(input_size / 2) - self.output_size = output_size - 1 - self.linear_size = linear_size - self.p_dropout = p_dropout - self.num_stage = num_stage - self.linear_stages_mono, self.linear_stages_stereo, self.linear_stages_dec = [], [], [] - self.device = device - - # Initialize weights - - # ------------------------Stereo---------------------------------------------- - # Preprocessing - self.w1_stereo = nn.Linear(self.stereo_size, self.linear_size) - self.batch_norm_stereo = nn.BatchNorm1d(self.linear_size) - - # Internal loop - for _ in range(num_stage): - self.linear_stages_stereo.append(MyLinear_stereo(self.linear_size, self.p_dropout)) - self.linear_stages_stereo = nn.ModuleList(self.linear_stages_stereo) - - # Post processing - self.w2_stereo = nn.Linear(self.linear_size, self.output_size) - - # ------------------------Mono---------------------------------------------- - # Preprocessing - self.w1_mono = nn.Linear(self.mono_size, self.linear_size) - self.batch_norm_mono = nn.BatchNorm1d(self.linear_size) - - # Internal loop - for _ in range(num_stage): - self.linear_stages_mono.append(MyLinear_stereo(self.linear_size, self.p_dropout)) - self.linear_stages_mono = nn.ModuleList(self.linear_stages_mono) - - # Post processing - self.w2_mono = nn.Linear(self.linear_size, self.output_size) - - # ------------------------Decision---------------------------------------------- - # Preprocessing - self.w1_dec = nn.Linear(self.stereo_size, self.linear_size) - self.batch_norm_dec = nn.BatchNorm1d(self.linear_size) - # - # Internal loop - for _ in range(num_stage): - self.linear_stages_dec.append(MyLinear(self.linear_size, self.p_dropout)) - self.linear_stages_dec = nn.ModuleList(self.linear_stages_dec) - - # Post processing - self.w2_dec = nn.Linear(self.linear_size, 1) - - # ------------------------Other---------------------------------------------- - - # NO-weight operations - self.relu = nn.ReLU(inplace=True) - self.dropout = nn.Dropout(self.p_dropout) - - def forward(self, x, label=None): - - # Mono - y_m = self.w1_mono(x[:, 0:34]) - y_m = self.batch_norm_mono(y_m) - y_m = self.relu(y_m) - y_m = self.dropout(y_m) - - for i in range(self.num_stage): - y_m = self.linear_stages_mono[i](y_m) - y_m = self.w2_mono(y_m) - - # Stereo - y_s = self.w1_stereo(x) - y_s = self.batch_norm_stereo(y_s) - y_s = self.relu(y_s) - y_s = self.dropout(y_s) - - for i in range(self.num_stage): - y_s = self.linear_stages_stereo[i](y_s) - y_s = self.w2_stereo(y_s) - - # Decision - y_d = self.w1_dec(x) - y_d = self.batch_norm_dec(y_d) - y_d = self.relu(y_d) - y_d = self.dropout(y_d) - - for i in range(self.num_stage): - y_d = self.linear_stages_dec[i](y_d) - aux = self.w2_dec(y_d) - - # Combine - if label is not None: - gate = label - else: - gate = torch.where(torch.sigmoid(aux) > 0.3, - torch.tensor([1.]).to(self.device), torch.tensor([0.]).to(self.device)) - y = gate * y_s + (1-gate) * y_m - - # Cat with auxiliary task - y = torch.cat((y, aux), dim=1) - return y - - -class AttentionModel(nn.Module): - - def __init__(self, input_size, output_size=2, linear_size=512, p_dropout=0.2, num_stage=3, device='cuda'): - super(AttentionModel, self).__init__() - - self.num_stage = num_stage - self.stereo_size = input_size - self.mono_size = int(input_size / 2) - self.output_size = output_size - 1 - self.linear_size = linear_size - self.p_dropout = p_dropout - self.num_stage = num_stage - self.linear_stages_mono, self.linear_stages_stereo, self.linear_stages_comb = [], [], [] - self.device = device - - # Initialize weights - # ------------------------Stereo---------------------------------------------- - # Preprocessing - self.w1_stereo = nn.Linear(self.stereo_size, self.linear_size) - self.batch_norm_stereo = nn.BatchNorm1d(self.linear_size) - - # Internal loop - for _ in range(num_stage): - self.linear_stages_stereo.append(MyLinear_stereo(self.linear_size, self.p_dropout)) - self.linear_stages_stereo = nn.ModuleList(self.linear_stages_stereo) - - # Post processing - self.w2_stereo = nn.Linear(self.linear_size, self.linear_size) - - # ------------------------Mono---------------------------------------------- - # Preprocessing - self.w1_mono = nn.Linear(self.mono_size, self.linear_size) - self.batch_norm_mono = nn.BatchNorm1d(self.linear_size) - - # Internal loop - for _ in range(num_stage): - self.linear_stages_mono.append(MyLinear_stereo(self.linear_size, self.p_dropout)) - self.linear_stages_mono = nn.ModuleList(self.linear_stages_mono) - - # Post processing - self.w2_mono = nn.Linear(self.linear_size, self.linear_size) - - # ------------------------Combined---------------------------------------------- - # Preprocessing - self.w1_comb = nn.Linear(self.linear_size, self.linear_size) - self.batch_norm_comb = nn.BatchNorm1d(self.linear_size) - # - # Internal loop - for _ in range(num_stage): - self.linear_stages_comb.append(MyLinear(self.linear_size, self.p_dropout)) - self.linear_stages_comb = nn.ModuleList(self.linear_stages_comb) - - # Post processing - self.w2_comb = nn.Linear(self.linear_size, self.linear_size) - - # ------------------------Other---------------------------------------------- - # Auxiliary - self.w_aux = nn.Linear(self.linear_size, 1) - - # Final - self.w_fin = nn.Linear(self.linear_size, self.output_size) - - # NO-weight operations - self.relu = nn.ReLU(inplace=True) - self.dropout = nn.Dropout(self.p_dropout) - - def forward(self, x, label=None): - - - # Mono - y_m = self.w1_mono(x[:, 0:34]) - y_m = self.batch_norm_mono(y_m) - y_m = self.relu(y_m) - y_m = self.dropout(y_m) - - for i in range(self.num_stage): - y_m = self.linear_stages_mono[i](y_m) - y_m = self.w2_mono(y_m) - - # Stereo - y_s = self.w1_stereo(x) - y_s = self.batch_norm_stereo(y_s) - y_s = self.relu(y_s) - y_s = self.dropout(y_s) - - for i in range(self.num_stage): - y_s = self.linear_stages_stereo[i](y_s) - y_s = self.w2_stereo(y_s) - - # Auxiliary task - aux = self.w_aux(y_s) - - # Combined - if label is not None: - gate = label - else: - gate = torch.where(torch.sigmoid(aux) > 0.3, - torch.tensor([1.]).to(self.device), torch.tensor([0.]).to(self.device)) - y_c = gate * y_s + (1-gate) * y_m - y_c = self.w1_comb(y_c) - y_c = self.batch_norm_comb(y_c) - y_c = self.relu(y_c) - y_c = self.dropout(y_c) - y_c = self.w_fin(y_c) - - # Cat with auxiliary task - y = torch.cat((y_c, aux), dim=1) - return y - - -class MyLinear_stereo(nn.Module): - def __init__(self, linear_size, p_dropout=0.5): - super(MyLinear_stereo, self).__init__() - self.l_size = linear_size - - self.relu = nn.ReLU(inplace=True) - self.dropout = nn.Dropout(p_dropout) - - # self.w0_a = nn.Linear(self.l_size, self.l_size) - # self.batch_norm0_a = nn.BatchNorm1d(self.l_size) - # self.w0_b = nn.Linear(self.l_size, self.l_size) - # self.batch_norm0_b = nn.BatchNorm1d(self.l_size) - - self.w1 = nn.Linear(self.l_size, self.l_size) - self.batch_norm1 = nn.BatchNorm1d(self.l_size) - - self.w2 = nn.Linear(self.l_size, self.l_size) - self.batch_norm2 = nn.BatchNorm1d(self.l_size) - - def forward(self, x): - # - # x = self.w0_a(x) - # x = self.batch_norm0_a(x) - # x = self.w0_b(x) - # x = self.batch_norm0_b(x) - - y = self.w1(x) - y = self.batch_norm1(y) - y = self.relu(y) - y = self.dropout(y) - - y = self.w2(y) - y = self.batch_norm2(y) - y = self.relu(y) - y = self.dropout(y) - - out = x + y - - return out - - class MonolocoModel(nn.Module): """ Architecture inspired by https://github.com/una-dinosauria/3d-pose-baseline diff --git a/monstereo/network/net.py b/monstereo/network/net.py index 39ad6ea..b08b619 100644 --- a/monstereo/network/net.py +++ b/monstereo/network/net.py @@ -14,7 +14,7 @@ import torch from ..utils import get_iou_matches, reorder_matches, get_keypoints, pixel_to_camera, xyz_from_distance from .process import preprocess_monstereo, preprocess_monoloco, extract_outputs, extract_outputs_mono,\ filter_outputs, cluster_outputs, unnormalize_bi -from .architectures import MonolocoModel, SimpleModel +from .architectures import MonolocoModel, MonStereoModel class Loco: @@ -55,7 +55,7 @@ class Loco: self.model = MonolocoModel(p_dropout=p_dropout, input_size=input_size, linear_size=linear_size, output_size=output_size) else: - self.model = SimpleModel(p_dropout=p_dropout, input_size=input_size, output_size=output_size, + self.model = MonStereoModel(p_dropout=p_dropout, input_size=input_size, output_size=output_size, linear_size=linear_size, device=self.device) self.model.load_state_dict(torch.load(model_path, map_location=lambda storage, loc: storage)) diff --git a/monstereo/predict.py b/monstereo/predict.py index 553853a..d869386 100644 --- a/monstereo/predict.py +++ b/monstereo/predict.py @@ -53,10 +53,11 @@ def predict(args): image_paths, images, processed_images_cpu, fields_batch)): if args.output_directory is None: - output_path = image_paths[0] + splits = os.path.split(image_paths[0]) + output_path = os.path.join(splits[0], 'out_' + splits[1]) else: file_name = os.path.basename(image_paths[0]) - output_path = os.path.join(args.output_directory, file_name) + output_path = os.path.join(args.output_directory, 'out_' + file_name) print('image', idx, image_path, output_path) keypoint_sets, scores, pifpaf_out = pifpaf.forward(image, processed_image_cpu, fields) @@ -133,17 +134,12 @@ def factory_outputs(args, images_outputs, output_path, pifpaf_outputs, dic_out=N skeleton_painter.keypoints(ax, keypoint_sets, scores=scores) else: - if any((xx in args.output_types for xx in ['front', 'bird', 'combined'])): - epistemic = False - if args.n_dropout > 0: - epistemic = True - + if any((xx in args.output_types for xx in ['front', 'bird', 'multi'])): + print(output_path) if dic_out['boxes']: # Only print in case of detections - printer = Printer(images_outputs[1], output_path, kk, output_types=args.output_types - , z_max=args.z_max, epistemic=epistemic) + printer = Printer(images_outputs[1], output_path, kk, args) figures, axes = printer.factory_axes() - printer.draw(figures, axes, dic_out, images_outputs[1], show_all=args.show_all, draw_box=args.draw_box, - save=True, show=args.show) + printer.draw(figures, axes, dic_out, images_outputs[1]) if 'json' in args.output_types: with open(os.path.join(output_path + '.monoloco.json'), 'w') as ff: diff --git a/monstereo/run.py b/monstereo/run.py index 8d7263e..a2f8cb0 100644 --- a/monstereo/run.py +++ b/monstereo/run.py @@ -35,8 +35,10 @@ def cli(): predict_parser.add_argument('-o', '--output-directory', help='Output directory') predict_parser.add_argument('--output_types', nargs='+', default=['json'], help='what to output: json keypoints skeleton for Pifpaf' - 'json bird front combined for Monoloco') + 'json bird front or multi for MonStereo') + predict_parser.add_argument('--no_save', help='to show images', action='store_true') predict_parser.add_argument('--show', help='to show images', action='store_true') + predict_parser.add_argument('--dpi', help='image resolution', type=int, default=100) # Pifpaf nets.cli(predict_parser) @@ -49,8 +51,7 @@ def cli(): predict_parser.add_argument('--path_gt', help='path of json file with gt 3d localization', default='data/arrays/names-kitti-200615-1022.json') predict_parser.add_argument('--transform', help='transformation for the pose', default='None') - predict_parser.add_argument('--draw_box', help='to draw box in the images', action='store_true') - predict_parser.add_argument('--z_max', type=int, help='maximum meters distance for predictions', default=22) + predict_parser.add_argument('--z_max', type=int, help='maximum meters distance for predictions', default=100) predict_parser.add_argument('--n_dropout', type=int, help='Epistemic uncertainty evaluation', default=0) predict_parser.add_argument('--dropout', type=float, help='dropout parameter', default=0.2) predict_parser.add_argument('--show_all', help='only predict ground-truth matches or all', action='store_true') @@ -116,7 +117,6 @@ def main(): predict(args) elif args.command == 'prep': - if 'nuscenes' in args.dataset: from .prep.preprocess_nu import PreprocessNuscenes prep = PreprocessNuscenes(args.dir_ann, args.dir_nuscenes, args.dataset, args.iou_min) diff --git a/monstereo/train/trainer.py b/monstereo/train/trainer.py index 36395b4..2990921 100644 --- a/monstereo/train/trainer.py +++ b/monstereo/train/trainer.py @@ -23,7 +23,7 @@ from torch.optim import lr_scheduler from .datasets import KeypointsDataset from .losses import CompositeLoss, MultiTaskLoss, AutoTuneMultiTaskLoss from ..network import extract_outputs, extract_labels -from ..network.architectures import SimpleModel +from ..network.architectures import MonStereoModel from ..utils import set_logger @@ -97,7 +97,7 @@ class Trainer: now = datetime.datetime.now() now_time = now.strftime("%Y%m%d-%H%M")[2:] - name_out = 'ms-' + now_time + name_out = 'monstereo-' + now_time if self.save: self.path_model = os.path.join(dir_out, name_out + '.pkl') self.logger = set_logger(os.path.join(dir_logs, name_out)) @@ -122,8 +122,8 @@ class Trainer: self.logger.info('Sizes of the dataset: {}'.format(self.dataset_sizes)) print(">>> creating model") - self.model = SimpleModel(input_size=input_size, output_size=output_size, linear_size=hidden_size, - p_dropout=dropout, num_stage=self.n_stage, device=self.device) + self.model = MonStereoModel(input_size=input_size, output_size=output_size, linear_size=hidden_size, + p_dropout=dropout, num_stage=self.n_stage, device=self.device) self.model.to(self.device) print(">>> model params: {:.3f}M".format(sum(p.numel() for p in self.model.parameters()) / 1000000.0)) print(">>> loss params: {}".format(sum(p.numel() for p in self.mt_loss.parameters()))) diff --git a/monstereo/visuals/figures.py b/monstereo/visuals/figures.py index 82fa0f8..bf0bddc 100644 --- a/monstereo/visuals/figures.py +++ b/monstereo/visuals/figures.py @@ -11,31 +11,34 @@ from matplotlib.patches import Ellipse from ..utils import get_task_error, get_pixel_error -def show_results(dic_stats, clusters, show=False, save=False, stereo=True): +FONTSIZE = 15 +FIGSIZE = (9.6, 7.2) +DPI = 200 +GRID_WIDTH = 0.5 + + +def show_results(dic_stats, clusters, dir_out='data/figures', show=False, save=False, stereo=True): """ Visualize error as function of the distance and compare it with target errors based on human height analyses """ - dir_out = 'docs' phase = 'test' x_min = 3 x_max = 42 y_min = 0 # y_max = 2.2 y_max = 3.5 if stereo else 5.2 - xx = np.linspace(x_min, x_max, 100) excl_clusters = ['all', 'easy', 'moderate', 'hard'] clusters = [clst for clst in clusters if clst not in excl_clusters] - plt.figure(0) styles = printing_styles(stereo) for idx_style, style in enumerate(styles.items()): - plt.figure(idx_style) - plt.grid(linewidth=0.2) + plt.figure(idx_style, figsize=FIGSIZE) + plt.grid(linewidth=GRID_WIDTH) plt.xlim(x_min, x_max) plt.ylim(y_min, y_max) - plt.xlabel("Ground-truth distance [m]") - plt.ylabel("Average localization error (ALE) [m]") + plt.xlabel("Ground-truth distance [m]", fontsize=FONTSIZE) + plt.ylabel("Average localization error (ALE) [m]", fontsize=FONTSIZE) for idx, method in enumerate(styles['methods']): errs = [dic_stats[phase][method][clst]['mean'] for clst in clusters[:-1]] # last cluster only a bound cnts = [dic_stats[phase][method][clst]['cnt'] for clst in clusters[:-1]] # last cluster only a bound @@ -47,30 +50,31 @@ def show_results(dic_stats, clusters, show=False, save=False, stereo=True): label=styles['labels'][idx], linestyle=styles['lstyles'][idx], color=styles['colors'][idx]) if method in ('monstereo', 'pseudo-lidar'): for i, x in enumerate(xxs): - plt.text(x, errs[i], str(cnts[i]), fontsize=10) + plt.text(x, errs[i], str(cnts[i]), fontsize=FONTSIZE) if not stereo: plt.plot(xx, get_task_error(xx), '--', label="Task error", color='lightgreen', linewidth=2.5) # if stereo: # yy_stereo = get_pixel_error(xx) # plt.plot(xx, yy_stereo, linewidth=1.4, color='k', label='Pixel error') - plt.legend(loc='upper left') + plt.legend(loc='upper left', prop={'size': FONTSIZE}) + plt.xticks(fontsize=FONTSIZE) + plt.yticks(fontsize=FONTSIZE) if save: plt.tight_layout() mode = 'stereo' if stereo else 'mono' path_fig = os.path.join(dir_out, 'results_' + mode + '.png') - plt.savefig(path_fig) + plt.savefig(path_fig, dpi=DPI) print("Figure of results " + mode + " saved in {}".format(path_fig)) if show: plt.show() plt.close('all') -def show_spread(dic_stats, clusters, show=False, save=False): +def show_spread(dic_stats, clusters, dir_out='data/figures', show=False, save=False): """Predicted confidence intervals and task error as a function of ground-truth distance""" phase = 'test' - dir_out = 'docs' excl_clusters = ['all', 'easy', 'moderate', 'hard'] clusters = [clst for clst in clusters if clst not in excl_clusters] x_min = 3 @@ -78,7 +82,7 @@ def show_spread(dic_stats, clusters, show=False, save=False): y_min = 0 for method in ('monoloco_pp', 'monstereo'): - plt.figure(2) + plt.figure(2, figsize=FIGSIZE) xxs = get_distances(clusters) bbs = np.array([dic_stats[phase][method][key]['std_ale'] for key in clusters[:-1]]) if method == 'monoloco_pp': @@ -89,31 +93,32 @@ def show_spread(dic_stats, clusters, show=False, save=False): else: y_max = 3.5 color = 'b' - plt.plot(xx, get_pixel_error(xx), linewidth=1.4, color='k', label='Pixel error') - plt.plot(xxs, bbs, marker='s', color=color, label="Aleatoric uncertainty (b)") + plt.plot(xx, get_pixel_error(xx), linewidth=2.5, color='k', label='Pixel error') + plt.plot(xxs, bbs, marker='s', color=color, label="Aleatoric uncertainty (b)", linewidth=4, markersize=8) xx = np.linspace(x_min, x_max, 100) - plt.plot(xx, get_task_error(xx), '--', label="Task error (monocular bound)", color='lightgreen', linewidth=2.5) + plt.plot(xx, get_task_error(xx), '--', label="Task error (monocular bound)", color='lightgreen', linewidth=4) - plt.xlabel("Ground-truth distance [m]") - plt.ylabel("Uncertainty [m]") + plt.xlabel("Ground-truth distance [m]", fontsize=FONTSIZE) + plt.ylabel("Uncertainty [m]", fontsize=FONTSIZE) plt.xlim(x_min, x_max) plt.ylim(y_min, y_max) - plt.grid(linewidth=0.2) - plt.legend() + plt.grid(linewidth=GRID_WIDTH) + plt.legend(prop={'size': FONTSIZE}) + plt.xticks(fontsize=FONTSIZE) + plt.yticks(fontsize=FONTSIZE) if save: plt.tight_layout() path_fig = os.path.join(dir_out, 'spread_' + method + '.png') - plt.savefig(path_fig) + plt.savefig(path_fig, dpi=DPI) print("Figure of confidence intervals saved in {}".format(path_fig)) if show: plt.show() plt.close('all') -def show_task_error(show, save): +def show_task_error(show, save, dir_out='data/figures'): """Task error figure""" - plt.figure(3) - dir_out = 'docs' + plt.figure(3, figsize=FIGSIZE) xx = np.linspace(0.1, 50, 100) mu_men = 178 mu_women = 165 @@ -128,7 +133,7 @@ def show_task_error(show, save): yy_young_female = target_error(xx, mm_young_female) yy_gender = target_error(xx, mm_gmm) yy_stereo = get_pixel_error(xx) - plt.grid(linewidth=0.3) + plt.grid(linewidth=GRID_WIDTH) plt.plot(xx, yy_young_male, linestyle='dotted', linewidth=2.1, color='b', label='Adult/young male') plt.plot(xx, yy_young_female, linestyle='dotted', linewidth=2.1, color='darkorange', label='Adult/young female') plt.plot(xx, yy_gender, '--', color='lightgreen', linewidth=2.8, label='Generic adult (task error)') @@ -139,20 +144,21 @@ def show_task_error(show, save): plt.xlabel("Ground-truth distance from the camera $d_{gt}$ [m]") plt.ylabel("Localization error $\hat{e}$ due to human height variation [m]") # pylint: disable=W1401 plt.legend(loc=(0.01, 0.55)) # Location from 0 to 1 from lower left + plt.xticks(fontsize=FONTSIZE) + plt.yticks(fontsize=FONTSIZE) if save: path_fig = os.path.join(dir_out, 'task_error.png') - plt.savefig(path_fig) + plt.savefig(path_fig, dpi=DPI) print("Figure of task error saved in {}".format(path_fig)) if show: plt.show() plt.close('all') -def show_method(save): +def show_method(save, dir_out='data/figures'): """ method figure""" - dir_out = 'docs' std_1 = 0.75 - fig = plt.figure(1) + fig = plt.figure(4, figsize=FIGSIZE) ax = fig.add_subplot(1, 1, 1) ell_3 = Ellipse((0, 2), width=std_1 * 2, height=0.3, angle=-90, color='b', fill=False, linewidth=2.5) ell_4 = Ellipse((0, 2), width=std_1 * 3, height=0.3, angle=-90, color='r', fill=False, @@ -164,42 +170,44 @@ def show_method(save): plt.plot([0, -3], [0, 4], 'k--') plt.xlim(-3, 3) plt.ylim(0, 3.5) - plt.xticks([]) - plt.yticks([]) + plt.xticks(fontsize=FONTSIZE) + plt.yticks(fontsize=FONTSIZE) plt.xlabel('X [m]') plt.ylabel('Z [m]') if save: path_fig = os.path.join(dir_out, 'output_method.png') - plt.savefig(path_fig) + plt.savefig(path_fig, dpi=DPI) print("Figure of method saved in {}".format(path_fig)) + plt.close('all') -def show_box_plot(dic_errors, clusters, show=False, save=False): +def show_box_plot(dic_errors, clusters, dir_out='data/figures', show=False, save=False): import pandas as pd - dir_out = 'docs' excl_clusters = ['all', 'easy', 'moderate', 'hard'] clusters = [int(clst) for clst in clusters if clst not in excl_clusters] methods = ('monstereo', 'pseudo-lidar', '3dop', 'monoloco') y_min = 0 - y_max = 25 # 18 for the other + y_max = 16 # 18 for the other xxs = get_distances(clusters) labels = [str(xx) for xx in xxs] for idx, method in enumerate(methods): df = pd.DataFrame([dic_errors[method][str(clst)] for clst in clusters[:-1]]).T df.columns = labels - plt.figure(idx) + plt.figure(idx, figsize=FIGSIZE) # with 200 dpi it becomes 1920x1440 _ = df.boxplot() name = 'MonStereo' if method == 'monstereo' else method - plt.title(name) - plt.ylabel('Average localization error (ALE) [m]') - plt.xlabel('Ground-truth distance [m]') + plt.title(name, fontsize=FONTSIZE) + plt.ylabel('Average localization error (ALE) [m]', fontsize=FONTSIZE) + plt.xlabel('Ground-truth distance [m]', fontsize=FONTSIZE) + plt.xticks(fontsize=FONTSIZE) + plt.yticks(fontsize=FONTSIZE) plt.ylim(y_min, y_max) if save: path_fig = os.path.join(dir_out, 'box_plot_' + name + '.png') plt.tight_layout() - plt.savefig(path_fig) + plt.savefig(path_fig, dpi=DPI) print("Figure of box plot saved in {}".format(path_fig)) if show: plt.show() @@ -297,7 +305,7 @@ def printing_styles(stereo): style = {"labels": ['3DOP', 'PSF', 'MonoLoco', 'MonoPSR', 'Pseudo-Lidar', 'Our MonStereo'], "methods": ['3dop', 'psf', 'monoloco', 'monopsr', 'pseudo-lidar', 'monstereo'], "mks": ['s', 'p', 'o', 'v', '*', '^'], - "mksizes": [6, 6, 6, 6, 6, 6], "lws": [1.2, 1.2, 1.2, 1.2, 1.3, 1.5], + "mksizes": [6, 6, 6, 6, 6, 6], "lws": [2, 2, 2, 2, 2, 2.2], "colors": ['gold', 'skyblue', 'darkgreen', 'pink', 'darkorange', 'b'], "lstyles": ['solid', 'solid', 'dashed', 'dashed', 'solid', 'solid']} else: diff --git a/monstereo/visuals/printer.py b/monstereo/visuals/printer.py index 3cd5f34..bf3e2aa 100644 --- a/monstereo/visuals/printer.py +++ b/monstereo/visuals/printer.py @@ -1,50 +1,70 @@ """ -Class for drawing frontal, bird-eye-view and combined figures +Class for drawing frontal, bird-eye-view and multi figures """ # pylint: disable=attribute-defined-outside-init import math from collections import OrderedDict -import numpy as np -import matplotlib import matplotlib.pyplot as plt -import matplotlib.cm as cm -from matplotlib.patches import Ellipse, Circle, Rectangle -from mpl_toolkits.axes_grid1 import make_axes_locatable +from matplotlib.patches import Rectangle -from ..utils import pixel_to_camera, get_task_error +from ..utils import pixel_to_camera + + +def get_angle(xx, zz): + """Obtain the points to plot the confidence of each annotation""" + + theta = math.atan2(zz, xx) + angle = theta * (180 / math.pi) + + return angle + + +def image_attributes(dpi, output_types): + c = 0.7 if 'front' in output_types else 1.0 + return dict(dpi=dpi, + fontsize_d=round(14 * c), + fontsize_bv=round(24 * c), + fontsize_num=round(22 * c), + fontsize_ax=round(16 * c), + linewidth=round(8 * c), + markersize=round(16 * c), + y_box_margin=round(24 * math.sqrt(c)), + stereo=dict(color='deepskyblue', + numcolor='darkorange', + linewidth=1 * c), + mono=dict(color='red', + numcolor='firebrick', + linewidth=2 * c) + ) class Printer: """ Print results on images: birds eye view and computed distance """ - FONTSIZE_BV = 16 - FONTSIZE = 18 - TEXTCOLOR = 'darkorange' - COLOR_KPS = 'yellow' + FIG_WIDTH = 15 + extensions = [] + y_scale = 1 + nones = lambda n: [None for _ in range(n)] + mpl_im0, stds_ale, stds_epi, xx_gt, zz_gt, xx_pred, zz_pred, dd_real, uv_centers, uv_shoulders, uv_kps, boxes, \ + boxes_gt, uv_camera, radius, auxs = nones(16) - def __init__(self, image, output_path, kk, output_types, epistemic=False, z_max=30, fig_width=10): + def __init__(self, image, output_path, kk, args): self.im = image - self.kk = kk - self.output_types = output_types - self.epistemic = epistemic - self.z_max = z_max # To include ellipses in the image - self.y_scale = 1 self.width = self.im.size[0] self.height = self.im.size[1] - self.fig_width = fig_width - - # Define the output dir self.output_path = output_path - self.cmap = cm.get_cmap('jet') - self.extensions = [] + self.kk = kk + self.output_types = args.output_types + self.z_max = args.z_max # To include ellipses in the image + self.show_all = args.show_all + self.show = args.show_all + self.save = not args.no_save - # Define variables of the class to change for every image - self.mpl_im0 = self.stds_ale = self.stds_epi = self.xx_gt = self.zz_gt = self.xx_pred = self.zz_pred =\ - self.dds_real = self.uv_centers = self.uv_shoulders = self.uv_kps = self.boxes = self.boxes_gt = \ - self.uv_camera = self.radius = self.auxs = None + # define image attributes + self.attr = image_attributes(args.dpi, args.output_types) def _process_results(self, dic_ann): # Include the vectors inside the interval given by z_max @@ -59,39 +79,38 @@ class Printer: for idx, xx in enumerate(dic_ann['xyz_real'])] self.zz_pred = [xx[2] if xx[2] < self.z_max - self.stds_epi[idx] else 0 for idx, xx in enumerate(dic_ann['xyz_pred'])] - - self.dds_real = dic_ann['dds_real'] + self.dd_pred = dic_ann['dds_pred'] + self.dd_real = dic_ann['dds_real'] + self.uv_heads = dic_ann['uv_heads'] self.uv_shoulders = dic_ann['uv_shoulders'] self.boxes = dic_ann['boxes'] self.boxes_gt = dic_ann['boxes_gt'] - self.uv_camera = (int(self.im.size[0] / 2), self.im.size[1]) - self.radius = 11 / 1600 * self.width if dic_ann['aux']: self.auxs = dic_ann['aux'] if dic_ann['aux'] else None def factory_axes(self): - """Create axes for figures: front bird combined""" + """Create axes for figures: front bird multi""" axes = [] figures = [] - # Initialize combined figure, resizing it for aesthetic proportions - if 'combined' in self.output_types: + # Initialize multi figure, resizing it for aesthetic proportion + if 'multi' in self.output_types: assert 'bird' and 'front' not in self.output_types, \ - "combined figure cannot be print together with front or bird ones" + "multi figure cannot be print together with front or bird ones" self.y_scale = self.width / (self.height * 2) # Defined proportion if self.y_scale < 0.95 or self.y_scale > 1.05: # allows more variation without resizing self.im = self.im.resize((self.width, round(self.height * self.y_scale))) self.width = self.im.size[0] self.height = self.im.size[1] - fig_width = self.fig_width + 0.6 * self.fig_width - fig_height = self.fig_width * self.height / self.width + fig_width = self.FIG_WIDTH + 0.6 * self.FIG_WIDTH + fig_height = self.FIG_WIDTH * self.height / self.width # Distinguish between KITTI images and general images fig_ar_1 = 0.8 width_ratio = 1.9 - self.extensions.append('.combined.png') + self.extensions.append('.multi.png') fig, (ax0, ax1) = plt.subplots(1, 2, sharey=False, gridspec_kw={'width_ratios': [width_ratio, 1]}, figsize=(fig_width, fig_height)) @@ -101,12 +120,12 @@ class Printer: figures.append(fig) assert 'front' not in self.output_types and 'bird' not in self.output_types, \ - "--combined arguments is not supported with other visualizations" + "--multi arguments is not supported with other visualizations" # Initialize front figure elif 'front' in self.output_types: - width = self.fig_width - height = self.fig_width * self.height / self.width + width = self.FIG_WIDTH + height = self.FIG_WIDTH * self.height / self.width self.extensions.append(".front.png") plt.figure(0) fig0, ax0 = plt.subplots(1, 1, figsize=(width, height)) @@ -114,18 +133,8 @@ class Printer: figures.append(fig0) # Create front figure axis - if any(xx in self.output_types for xx in ['front', 'combined']): - ax0 = self.set_axes(ax0, axis=0) - - divider = make_axes_locatable(ax0) - cax = divider.append_axes('right', size='3%', pad=0.05) - bar_ticks = self.z_max // 5 + 1 - norm = matplotlib.colors.Normalize(vmin=0, vmax=self.z_max) - scalar_mappable = plt.cm.ScalarMappable(cmap=self.cmap, norm=norm) - scalar_mappable.set_array([]) - plt.colorbar(scalar_mappable, ticks=np.linspace(0, self.z_max, bar_ticks), - boundaries=np.arange(- 0.05, self.z_max + 0.1, .1), cax=cax, label='Z [m]') - + if any(xx in self.output_types for xx in ['front', 'multi']): + ax0 = self._set_axes(ax0, axis=0) axes.append(ax0) if not axes: axes.append(None) @@ -136,64 +145,114 @@ class Printer: fig1, ax1 = plt.subplots(1, 1) fig1.set_tight_layout(True) figures.append(fig1) - if any(xx in self.output_types for xx in ['bird', 'combined']): - ax1 = self.set_axes(ax1, axis=1) # Adding field of view + if any(xx in self.output_types for xx in ['bird', 'multi']): + ax1 = self._set_axes(ax1, axis=1) # Adding field of view axes.append(ax1) return figures, axes - def draw(self, figures, axes, dic_out, image, show_all=False, draw_text=True, legend=True, draw_box=False, - save=False, show=False): + def draw(self, figures, axes, dic_out, image): # Process the annotation dictionary of monoloco self._process_results(dic_out) # whether to include instances that don't match the ground-truth - iterator = range(len(self.zz_pred)) if show_all else range(len(self.zz_gt)) + iterator = range(len(self.zz_pred)) if self.show_all else range(len(self.zz_gt)) if not iterator: - print("-"*110 + '\n' + "! No instances detected, be sure to include file with ground-truth values or " - "use the command --show_all" + '\n' + "-"*110) + print("-" * 110 + '\n' + "! No instances detected, be sure to include file with ground-truth values or " + "use the command --show_all" + '\n' + "-" * 110) # Draw the front figure - num = 0 + number = dict(flag=False, num=97) + if 'multi' in self.output_types: + number['flag'] = True # add numbers self.mpl_im0.set_data(image) for idx in iterator: - if any(xx in self.output_types for xx in ['front', 'combined']) and self.zz_pred[idx] > 0: - - color = self.cmap((self.zz_pred[idx] % self.z_max) / self.z_max) - self.draw_circle(axes, self.uv_shoulders[idx], color) - if draw_box: - self.draw_boxes(axes, idx, color) - - if draw_text: - self.draw_text_front(axes, self.uv_shoulders[idx], num) - num += 1 + if any(xx in self.output_types for xx in ['front', 'multi']) and self.zz_pred[idx] > 0: + self._draw_front(axes[0], + self.dd_pred[idx], + idx, + number) + number['num'] += 1 # Draw the bird figure - num = 0 + number['num'] = 97 for idx in iterator: - if any(xx in self.output_types for xx in ['bird', 'combined']) and self.zz_pred[idx] > 0: + if any(xx in self.output_types for xx in ['bird', 'multi']) and self.zz_pred[idx] > 0: # Draw ground truth and uncertainty - self.draw_uncertainty(axes, idx) + self._draw_uncertainty(axes, idx) # Draw bird eye view text - if draw_text: - self.draw_text_bird(axes, idx, num) - num += 1 - # Add the legend - if legend: - draw_legend(axes) + if number['flag']: + self._draw_text_bird(axes, idx, number['num']) + number['num'] += 1 + self._draw_legend(axes) # Draw, save or/and show the figures for idx, fig in enumerate(figures): fig.canvas.draw() - if save: - fig.savefig(self.output_path + self.extensions[idx], bbox_inches='tight') - if show: + if self.save: + fig.savefig(self.output_path + self.extensions[idx], bbox_inches='tight', dpi=self.attr['dpi']) + if self.show: fig.show() plt.close(fig) - def draw_uncertainty(self, axes, idx): + def _draw_front(self, ax, z, idx, number): + + mode = 'stereo' if self.auxs[idx] > 0.3 else 'mono' + + # Bbox + w = min(self.width-2, self.boxes[idx][2] - self.boxes[idx][0]) + h = min(self.height-2, (self.boxes[idx][3] - self.boxes[idx][1]) * self.y_scale) + x0 = self.boxes[idx][0] + y0 = self.boxes[idx][1] * self.y_scale + y1 = y0 + h + rectangle = Rectangle((x0, y0), + width=w, + height=h, + fill=False, + color=self.attr[mode]['color'], + linewidth=self.attr[mode]['linewidth']) + ax.add_patch(rectangle) + z_str = str(z).split(sep='.') + text = z_str[0] + '.' + z_str[1][0] + bbox_config = {'facecolor': self.attr[mode]['color'], 'alpha': 0.4, 'linewidth': 0} + + x_t = x0 - 1.5 + y_t = y1 + self.attr['y_box_margin'] + if y_t < (self.height-10): + ax.annotate( + text, + (x_t, y_t), + fontsize=self.attr['fontsize_d'], + weight='bold', + xytext=(5.0, 5.0), + textcoords='offset points', + color='white', + bbox=bbox_config, + ) + if number['flag']: + ax.text(x0 - 17, + y1 + 14, + chr(number['num']), + fontsize=self.attr['fontsize_num'], + color=self.attr[mode]['numcolor'], + weight='bold') + + def _draw_text_bird(self, axes, idx, num): + """Plot the number in the bird eye view map""" + mode = 'stereo' if self.auxs[idx] > 0.3 else 'mono' + std = self.stds_epi[idx] if self.stds_epi[idx] > 0 else self.stds_ale[idx] + theta = math.atan2(self.zz_pred[idx], self.xx_pred[idx]) + + delta_x = std * math.cos(theta) + delta_z = std * math.sin(theta) + + axes[1].text(self.xx_pred[idx] + delta_x + 0.2, self.zz_pred[idx] + delta_z + 0/2, chr(num), + fontsize=self.attr['fontsize_bv'], + color=self.attr[mode]['numcolor']) + + def _draw_uncertainty(self, axes, idx): theta = math.atan2(self.zz_pred[idx], self.xx_pred[idx]) dic_std = {'ale': self.stds_ale[idx], 'epi': self.stds_epi[idx]} @@ -208,89 +267,84 @@ class Printer: # MonoLoco if not self.auxs: - axes[1].plot(dic_x['epi'], dic_y['epi'], color='coral', linewidth=2, label="Epistemic Uncertainty") - axes[1].plot(dic_x['ale'], dic_y['ale'], color='deepskyblue', linewidth=4, label="Aleatoric Uncertainty") - axes[1].plot(self.xx_pred[idx], self.zz_pred[idx], color='cornflowerblue', label="Prediction", markersize=6, + axes[1].plot(dic_x['epi'], + dic_y['epi'], + color='coral', + linewidth=round(self.attr['linewidth']/2), + label="Epistemic Uncertainty") + + axes[1].plot(dic_x['ale'], + dic_y['ale'], + color='deepskyblue', + linewidth=self.attr['linewidth'], + label="Aleatoric Uncertainty") + + axes[1].plot(self.xx_pred[idx], + self.zz_pred[idx], + color='cornflowerblue', + label="Prediction", + markersize=self.attr['markersize'], marker='o') + if self.gt[idx]: - axes[1].plot(self.xx_gt[idx], self.zz_gt[idx], - color='k', label="Ground-truth", markersize=8, marker='x') + axes[1].plot(self.xx_gt[idx], + self.zz_gt[idx], + color='k', + label="Ground-truth", + markersize=8, + marker='x') # MonStereo(stereo case) elif self.auxs[idx] > 0.5: - axes[1].plot(dic_x['ale'], dic_y['ale'], color='r', linewidth=4, label="Prediction (mono)") - axes[1].plot(dic_x['ale'], dic_y['ale'], color='deepskyblue', linewidth=4, label="Prediction (stereo+mono)") + axes[1].plot(dic_x['ale'], + dic_y['ale'], + color='r', + linewidth=self.attr['linewidth'], + label="Prediction (mono)") + + axes[1].plot(dic_x['ale'], + dic_y['ale'], + color='deepskyblue', + linewidth=self.attr['linewidth'], + label="Prediction (stereo+mono)") + if self.gt[idx]: - axes[1].plot(self.xx_gt[idx], self.zz_gt[idx], - color='k', label="Ground-truth", markersize=8, marker='x') + axes[1].plot(self.xx_gt[idx], + self.zz_gt[idx], + color='k', + label="Ground-truth", + markersize=self.attr['markersize'], + marker='x') # MonStereo (monocular case) else: - axes[1].plot(dic_x['ale'], dic_y['ale'], color='deepskyblue', linewidth=4, label="Prediction (stereo+mono)") - axes[1].plot(dic_x['ale'], dic_y['ale'], color='r', linewidth=4, label="Prediction (mono)") + axes[1].plot(dic_x['ale'], + dic_y['ale'], + color='deepskyblue', + linewidth=self.attr['linewidth'], + label="Prediction (stereo+mono)") + + axes[1].plot(dic_x['ale'], + dic_y['ale'], + color='r', + linewidth=self.attr['linewidth'], + label="Prediction (mono)") if self.gt[idx]: - axes[1].plot(self.xx_gt[idx], self.zz_gt[idx], - color='k', label="Ground-truth", markersize=8, marker='x') + axes[1].plot(self.xx_gt[idx], + self.zz_gt[idx], + color='k', + label="Ground-truth", + markersize=self.attr['markersize'], + marker='x') - def draw_ellipses(self, axes, idx): - """draw uncertainty ellipses""" - target = get_task_error(self.dds_real[idx]) - angle_gt = get_angle(self.xx_gt[idx], self.zz_gt[idx]) - ellipse_real = Ellipse((self.xx_gt[idx], self.zz_gt[idx]), width=target * 2, height=1, - angle=angle_gt, color='lightgreen', fill=True, label="Task error") - axes[1].add_patch(ellipse_real) - if abs(self.zz_gt[idx] - self.zz_pred[idx]) > 0.001: - axes[1].plot(self.xx_gt[idx], self.zz_gt[idx], 'kx', label="Ground truth", markersize=3) + def _draw_legend(self, axes): + # Bird eye view legend + if any(xx in self.output_types for xx in ['bird', 'multi']): + handles, labels = axes[1].get_legend_handles_labels() + by_label = OrderedDict(zip(labels, handles)) + axes[1].legend(by_label.values(), by_label.keys(), loc='best', prop={'size': 15}) - angle = get_angle(self.xx_pred[idx], self.zz_pred[idx]) - ellipse_ale = Ellipse((self.xx_pred[idx], self.zz_pred[idx]), width=self.stds_ale[idx] * 2, - height=1, angle=angle, color='b', fill=False, label="Aleatoric Uncertainty", - linewidth=1.3) - ellipse_var = Ellipse((self.xx_pred[idx], self.zz_pred[idx]), width=self.stds_epi[idx] * 2, - height=1, angle=angle, color='r', fill=False, label="Uncertainty", - linewidth=1, linestyle='--') - - axes[1].add_patch(ellipse_ale) - if self.epistemic: - axes[1].add_patch(ellipse_var) - - axes[1].plot(self.xx_pred[idx], self.zz_pred[idx], 'ro', label="Predicted", markersize=3) - - def draw_boxes(self, axes, idx, color): - ww_box = self.boxes[idx][2] - self.boxes[idx][0] - hh_box = (self.boxes[idx][3] - self.boxes[idx][1]) * self.y_scale - ww_box_gt = self.boxes_gt[idx][2] - self.boxes_gt[idx][0] - hh_box_gt = (self.boxes_gt[idx][3] - self.boxes_gt[idx][1]) * self.y_scale - - rectangle = Rectangle((self.boxes[idx][0], self.boxes[idx][1] * self.y_scale), - width=ww_box, height=hh_box, fill=False, color=color, linewidth=3) - rectangle_gt = Rectangle((self.boxes_gt[idx][0], self.boxes_gt[idx][1] * self.y_scale), - width=ww_box_gt, height=hh_box_gt, fill=False, color='g', linewidth=2) - axes[0].add_patch(rectangle_gt) - axes[0].add_patch(rectangle) - - def draw_text_front(self, axes, uv, num): - axes[0].text(uv[0] + self.radius, uv[1] * self.y_scale - self.radius, str(num), - fontsize=self.FONTSIZE, color=self.TEXTCOLOR, weight='bold') - - def draw_text_bird(self, axes, idx, num): - """Plot the number in the bird eye view map""" - - std = self.stds_epi[idx] if self.stds_epi[idx] > 0 else self.stds_ale[idx] - theta = math.atan2(self.zz_pred[idx], self.xx_pred[idx]) - - delta_x = std * math.cos(theta) - delta_z = std * math.sin(theta) - - axes[1].text(self.xx_pred[idx] + delta_x, self.zz_pred[idx] + delta_z, - str(num), fontsize=self.FONTSIZE_BV, color='darkorange') - - def draw_circle(self, axes, uv, color): - - circle = Circle((uv[0], uv[1] * self.y_scale), radius=self.radius, color=color, fill=True) - axes[0].add_patch(circle) - - def set_axes(self, ax, axis): + def _set_axes(self, ax, axis): assert axis in (0, 1) if axis == 0: @@ -308,23 +362,9 @@ class Printer: corr = round(float(x_max / 3)) ax.plot([0, x_max], [0, self.z_max], 'k--') ax.plot([0, -x_max], [0, self.z_max], 'k--') - ax.set_xlim(-x_max+corr, x_max-corr) - ax.set_ylim(0, self.z_max+1) + ax.set_xlim(-x_max + corr, x_max - corr) + ax.set_ylim(0, self.z_max + 1) ax.set_xlabel("X [m]") - + plt.xticks(fontsize=self.attr['fontsize_ax']) + plt.yticks(fontsize=self.attr['fontsize_ax']) return ax - - -def draw_legend(axes): - handles, labels = axes[1].get_legend_handles_labels() - by_label = OrderedDict(zip(labels, handles)) - axes[1].legend(by_label.values(), by_label.keys(), loc='best') - - -def get_angle(xx, zz): - """Obtain the points to plot the confidence of each annotation""" - - theta = math.atan2(zz, xx) - angle = theta * (180 / math.pi) - - return angle diff --git a/tests/test_package.py b/tests/test_package.py index 9d541bd..98e9476 100644 --- a/tests/test_package.py +++ b/tests/test_package.py @@ -44,7 +44,7 @@ def tst_printer(dic_out, kk, image_path): """Draw a fake figure""" with open(image_path, 'rb') as f: pil_image = Image.open(f).convert('RGB') - printer = Printer(image=pil_image, output_path='tests/test_image', kk=kk, output_types=['combined'], z_max=15) + printer = Printer(image=pil_image, output_path='tests/test_image', kk=kk, output_types=['multi'], z_max=15) figures, axes = printer.factory_axes() printer.draw(figures, axes, dic_out, pil_image, save=True)