From 4e9c26bbbcbb65faeb5b19425df3b4658e23c0d6 Mon Sep 17 00:00:00 2001 From: sennnnn <58427300+sennnnn@users.noreply.github.com> Date: Fri, 20 Aug 2021 11:44:58 +0800 Subject: [PATCH] [Refactor] Support progressive test with fewer memory cost (#709) * Support progressive test with fewer memory cost. * Temp code * Using processor to refactor evaluation workflow. * refactor eval hook. * Fix process bar. * Fix middle save argument. * Modify some variable name of dataset evaluate api. * Modify some viriable name of eval hook. * Fix some priority bugs of eval hook. * Depreciated efficient_test. * Fix training progress blocked by eval hook. * Depreciated old test api. * Fix test api error. * Modify outer api. * Build a sampler test api. * TODO: Refactor format_results. * Modify variable names. * Fix num_classes bug. * Fix sampler index bug. * Fix grammaly bug. * Support batch sampler. * More readable test api. * Remove some command arg and fix eval hook bug. * Support format-only arg. * Modify format_results of datasets. * Modify tool which use test apis. * support cityscapes eval * fixed cityscapes * 1. Add comments for batch_sampler; 2. Keep eval hook api same and add deprecated warning; 3. Add doc string for dataset.pre_eval; * Add efficient_test doc string. * Modify test tool to compat old version. * Modify eval hook to compat with old version. * Modify test api to compat old version api. * Sampler explanation. * update warning * Modify deploy_test.py * compatible with old output, add efficient test back * clear logic of exclusive * Warning about efficient_test. * Modify format_results save folder. * Fix bugs of format_results. * Modify deploy_test.py. * Update doc * Fix deploy test bugs. * Fix custom dataset unit tests. * Fix dataset unit tests. * Fix eval hook unit tests. * Fix some imcompatible. * Add pre_eval argument for eval hooks. * Update eval hook doc string. * Make pre_eval false in default. * Add unit tests for dataset format_results. * Fix some comments and bc-breaking bug. * Fix pre_eval set cfg field. * Remove redundant codes. Co-authored-by: Jiarui XU --- configs/_base_/schedules/schedule_160k.py | 2 +- configs/_base_/schedules/schedule_20k.py | 2 +- configs/_base_/schedules/schedule_40k.py | 2 +- configs/_base_/schedules/schedule_80k.py | 2 +- docs/inference.md | 6 +- docs_zh-CN/inference.md | 12 +- mmseg/apis/test.py | 136 +++++++++++++----- mmseg/core/evaluation/__init__.py | 6 +- mmseg/core/evaluation/eval_hooks.py | 46 ++++-- mmseg/core/evaluation/metrics.py | 92 ++++++++++-- mmseg/datasets/ade.py | 47 +++--- mmseg/datasets/cityscapes.py | 63 ++++---- mmseg/datasets/custom.py | 113 ++++++++++----- ...kfurt_000000_000294_gtFine_instanceIds.png | Bin 0 -> 1912 bytes ...rankfurt_000000_000294_gtFine_labelIds.png | Bin 0 -> 1578 bytes ...urt_000000_000294_gtFine_labelTrainIds.png | Bin 0 -> 1500 bytes .../frankfurt_000000_000294_leftImg8bit.png | Bin 0 -> 51662 bytes tests/test_apis/test_single_gpu.py | 72 ++++++++++ tests/test_data/test_dataset.py | 104 +++++++++++++- tests/test_eval_hook.py | 14 +- tools/deploy_test.py | 58 ++++++-- tools/test.py | 70 +++++++-- 22 files changed, 654 insertions(+), 193 deletions(-) create mode 100644 tests/data/pseudo_cityscapes_dataset/gtFine/frankfurt_000000_000294_gtFine_instanceIds.png create mode 100644 tests/data/pseudo_cityscapes_dataset/gtFine/frankfurt_000000_000294_gtFine_labelIds.png create mode 100644 tests/data/pseudo_cityscapes_dataset/gtFine/frankfurt_000000_000294_gtFine_labelTrainIds.png create mode 100644 tests/data/pseudo_cityscapes_dataset/leftImg8bit/frankfurt_000000_000294_leftImg8bit.png create mode 100644 tests/test_apis/test_single_gpu.py diff --git a/configs/_base_/schedules/schedule_160k.py b/configs/_base_/schedules/schedule_160k.py index 52603890b..39630f215 100644 --- a/configs/_base_/schedules/schedule_160k.py +++ b/configs/_base_/schedules/schedule_160k.py @@ -6,4 +6,4 @@ lr_config = dict(policy='poly', power=0.9, min_lr=1e-4, by_epoch=False) # runtime settings runner = dict(type='IterBasedRunner', max_iters=160000) checkpoint_config = dict(by_epoch=False, interval=16000) -evaluation = dict(interval=16000, metric='mIoU') +evaluation = dict(interval=16000, metric='mIoU', pre_eval=True) diff --git a/configs/_base_/schedules/schedule_20k.py b/configs/_base_/schedules/schedule_20k.py index bf780a1b6..73c702197 100644 --- a/configs/_base_/schedules/schedule_20k.py +++ b/configs/_base_/schedules/schedule_20k.py @@ -6,4 +6,4 @@ lr_config = dict(policy='poly', power=0.9, min_lr=1e-4, by_epoch=False) # runtime settings runner = dict(type='IterBasedRunner', max_iters=20000) checkpoint_config = dict(by_epoch=False, interval=2000) -evaluation = dict(interval=2000, metric='mIoU') +evaluation = dict(interval=2000, metric='mIoU', pre_eval=True) diff --git a/configs/_base_/schedules/schedule_40k.py b/configs/_base_/schedules/schedule_40k.py index cdbf841ab..d2c502325 100644 --- a/configs/_base_/schedules/schedule_40k.py +++ b/configs/_base_/schedules/schedule_40k.py @@ -6,4 +6,4 @@ lr_config = dict(policy='poly', power=0.9, min_lr=1e-4, by_epoch=False) # runtime settings runner = dict(type='IterBasedRunner', max_iters=40000) checkpoint_config = dict(by_epoch=False, interval=4000) -evaluation = dict(interval=4000, metric='mIoU') +evaluation = dict(interval=4000, metric='mIoU', pre_eval=True) diff --git a/configs/_base_/schedules/schedule_80k.py b/configs/_base_/schedules/schedule_80k.py index c190cee6b..8365a878e 100644 --- a/configs/_base_/schedules/schedule_80k.py +++ b/configs/_base_/schedules/schedule_80k.py @@ -6,4 +6,4 @@ lr_config = dict(policy='poly', power=0.9, min_lr=1e-4, by_epoch=False) # runtime settings runner = dict(type='IterBasedRunner', max_iters=80000) checkpoint_config = dict(by_epoch=False, interval=8000) -evaluation = dict(interval=8000, metric='mIoU') +evaluation = dict(interval=8000, metric='mIoU', pre_eval=True) diff --git a/docs/inference.md b/docs/inference.md index d7bc21b65..65f1e4602 100644 --- a/docs/inference.md +++ b/docs/inference.md @@ -21,11 +21,11 @@ python tools/test.py ${CONFIG_FILE} ${CHECKPOINT_FILE} [--out ${RESULT_FILE}] [- Optional arguments: -- `RESULT_FILE`: Filename of the output results in pickle format. If not specified, the results will not be saved to a file. +- `RESULT_FILE`: Filename of the output results in pickle format. If not specified, the results will not be saved to a file. (After mmseg v0.17, the output results become pre-evaluation results or format result paths) - `EVAL_METRICS`: Items to be evaluated on the results. Allowed values depend on the dataset, e.g., `mIoU` is available for all dataset. Cityscapes could be evaluated by `cityscapes` as well as standard `mIoU` metrics. - `--show`: If specified, segmentation results will be plotted on the images and shown in a new window. It is only applicable to single GPU testing and used for debugging and visualization. Please make sure that GUI is available in your environment, otherwise you may encounter the error like `cannot connect to X server`. - `--show-dir`: If specified, segmentation results will be plotted on the images and saved to the specified directory. It is only applicable to single GPU testing and used for debugging and visualization. You do NOT need a GUI available in your environment for using this option. -- `--eval-options`: Optional parameters during evaluation. When `efficient_test=True`, it will save intermediate results to local files to save CPU memory. Make sure that you have enough local storage space (more than 20GB). +- `--eval-options`: Optional parameters for `dataset.format_results` and `dataset.evaluate` during evaluation. When `efficient_test=True`, it will save intermediate results to local files to save CPU memory. Make sure that you have enough local storage space (more than 20GB). (`efficient_test` argument does not have effect after mmseg v0.17, we use a progressive mode to evaluation and format results which can largely save memory cost and evaluation time.) Examples: @@ -98,4 +98,4 @@ Assume that you have already downloaded the checkpoints to the directory `checkp --eval mIoU ``` - Using ```pmap``` to view CPU memory footprint, it used 2.25GB CPU memory with ```efficient_test=True``` and 11.06GB CPU memory with ```efficient_test=False``` . This optional parameter can save a lot of memory. + Using ```pmap``` to view CPU memory footprint, it used 2.25GB CPU memory with ```efficient_test=True``` and 11.06GB CPU memory with ```efficient_test=False``` . This optional parameter can save a lot of memory. (After mmseg v0.17, efficient_test has not effect and we use a progressive mode to evaluation and format results efficiently by default.) diff --git a/docs_zh-CN/inference.md b/docs_zh-CN/inference.md index 85d9ff085..7d14bb980 100644 --- a/docs_zh-CN/inference.md +++ b/docs_zh-CN/inference.md @@ -20,11 +20,11 @@ python tools/test.py ${配置文件} ${检查点文件} [--out ${结果文件}] 可选参数: -- `RESULT_FILE`: pickle 格式的输出结果的文件名,如果不专门指定,结果将不会被专门保存成文件 -- `EVAL_METRICS`: 在结果里将被评估的指标,这主要取决于数据集, `mIoU` 对于所有数据集都可获得,像 Cityscapes 数据集可以通过 `cityscapes` 命令来专门评估,就像标准的 `mIoU`一样 -- `--show`: 如果被指定,分割结果将会在一张图像里画出来并且在另一个窗口展示,它仅仅是用来调试与可视化,并且仅针对单卡 GPU 测试,请确认 GUI 在您的环境里可用,否则您也许会遇到报错 `cannot connect to X server` -- `--show-dir`: 如果被指定,分割结果将会在一张图像里画出来并且保存在指定文件夹里,它仅仅是用来调试与可视化,并且仅针对单卡GPU测试,使用该参数时,您的环境不需要 GUI -- `--eval-options`: 评估时的可选参数,当设置 `efficient_test=True` 时,它将会保存中间结果至本地文件里以节约 CPU 内存,请确认您本地硬盘有足够的存储空间(大于20GB) +- `RESULT_FILE`: pickle 格式的输出结果的文件名,如果不专门指定,结果将不会被专门保存成文件。(MMseg v0.17 之后,args.out 将只会保存评估时的中间结果或者是分割图的保存路径。) +- `EVAL_METRICS`: 在结果里将被评估的指标。这主要取决于数据集, `mIoU` 对于所有数据集都可获得,像 Cityscapes 数据集可以通过 `cityscapes` 命令来专门评估,就像标准的 `mIoU`一样。 +- `--show`: 如果被指定,分割结果将会在一张图像里画出来并且在另一个窗口展示。它仅仅是用来调试与可视化,并且仅针对单卡 GPU 测试。请确认 GUI 在您的环境里可用,否则您也许会遇到报错 `cannot connect to X server` +- `--show-dir`: 如果被指定,分割结果将会在一张图像里画出来并且保存在指定文件夹里。它仅仅是用来调试与可视化,并且仅针对单卡GPU测试。使用该参数时,您的环境不需要 GUI。 +- `--eval-options`: 评估时的可选参数,当设置 `efficient_test=True` 时,它将会保存中间结果至本地文件里以节约 CPU 内存。请确认您本地硬盘有足够的存储空间(大于20GB)。(MMseg v0.17 之后,`efficient_test` 不再生效,我们重构了 test api,通过使用一种渐近式的方式来提升评估和保存结果的效率。) 例子: @@ -96,4 +96,4 @@ python tools/test.py ${配置文件} ${检查点文件} [--out ${结果文件}] --eval mIoU ``` - 使用 ```pmap``` 可查看 CPU 内存情况, ```efficient_test=True``` 会使用约 2.25GB 的 CPU 内存, ```efficient_test=False``` 会使用约 11.06GB 的 CPU 内存。 这个可选参数可以节约很多 CPU 内存。 + 使用 ```pmap``` 可查看 CPU 内存情况, ```efficient_test=True``` 会使用约 2.25GB 的 CPU 内存, ```efficient_test=False``` 会使用约 11.06GB 的 CPU 内存。 这个可选参数可以节约很多 CPU 内存。(MMseg v0.17 之后, `efficient_test` 参数将不再生效, 我们使用了一种渐近的方式来更加有效快速地评估和保存结果。) diff --git a/mmseg/apis/test.py b/mmseg/apis/test.py index fb0bb9361..2b11adfdc 100644 --- a/mmseg/apis/test.py +++ b/mmseg/apis/test.py @@ -1,6 +1,7 @@ # Copyright (c) OpenMMLab. All rights reserved. import os.path as osp import tempfile +import warnings import mmcv import numpy as np @@ -19,7 +20,6 @@ def np2tmp(array, temp_file_name=None, tmpdir=None): function will generate a file name with tempfile.NamedTemporaryFile to save ndarray. Default: None. tmpdir (str): Temporary directory to save Ndarray files. Default: None. - Returns: str: The numpy file name. """ @@ -36,8 +36,11 @@ def single_gpu_test(model, show=False, out_dir=None, efficient_test=False, - opacity=0.5): - """Test with single GPU. + opacity=0.5, + pre_eval=False, + format_only=False, + format_args={}): + """Test with single GPU by progressive mode. Args: model (nn.Module): Model to be tested. @@ -46,24 +49,60 @@ def single_gpu_test(model, out_dir (str, optional): If specified, the results will be dumped into the directory to save output results. efficient_test (bool): Whether save the results as local numpy files to - save CPU memory during evaluation. Default: False. + save CPU memory during evaluation. Mutually exclusive with + pre_eval and format_results. Default: False. opacity(float): Opacity of painted segmentation map. Default 0.5. Must be in (0, 1] range. + pre_eval (bool): Use dataset.pre_eval() function to generate + pre_results for metric evaluation. Mutually exclusive with + efficient_test and format_results. Default: False. + format_only (bool): Only format result for results commit. + Mutually exclusive with pre_eval and efficient_test. + Default: False. + format_args (dict): The args for format_results. Default: {}. Returns: - list: The prediction results. + list: list of evaluation pre-results or list of save file names. """ + if efficient_test: + warnings.warn( + 'DeprecationWarning: ``efficient_test`` will be deprecated, the ' + 'evaluation is CPU memory friendly with pre_eval=True') + mmcv.mkdir_or_exist('.efficient_test') + # when none of them is set true, return segmentation results as + # a list of np.array. + assert [efficient_test, pre_eval, format_only].count(True) <= 1, \ + '``efficient_test``, ``pre_eval`` and ``format_only`` are mutually ' \ + 'exclusive, only one of them could be true .' model.eval() results = [] dataset = data_loader.dataset prog_bar = mmcv.ProgressBar(len(dataset)) - if efficient_test: - mmcv.mkdir_or_exist('.efficient_test') - for i, data in enumerate(data_loader): + # The pipeline about how the data_loader retrieval samples from dataset: + # sampler -> batch_sampler -> indices + # The indices are passed to dataset_fetcher to get data from dataset. + # data_fetcher -> collate_fn(dataset[index]) -> data_sample + # we use batch_sampler to get correct data idx + loader_indices = data_loader.batch_sampler + + for batch_indices, data in zip(loader_indices, data_loader): with torch.no_grad(): result = model(return_loss=False, **data) + if efficient_test: + result = [np2tmp(_, tmpdir='.efficient_test') for _ in result] + + if format_only: + result = dataset.format_results( + result, indices=batch_indices, **format_args) + if pre_eval: + # TODO: adapt samples_per_gpu > 1. + # only samples_per_gpu=1 valid now + result = dataset.pre_eval(result, indices=batch_indices) + + results.extend(result) + if show or out_dir: img_tensor = data['img'][0] img_metas = data['img_metas'][0].data[0] @@ -90,18 +129,10 @@ def single_gpu_test(model, out_file=out_file, opacity=opacity) - if isinstance(result, list): - if efficient_test: - result = [np2tmp(_, tmpdir='.efficient_test') for _ in result] - results.extend(result) - else: - if efficient_test: - result = np2tmp(result, tmpdir='.efficient_test') - results.append(result) - batch_size = len(result) for _ in range(batch_size): prog_bar.update() + return results @@ -109,8 +140,11 @@ def multi_gpu_test(model, data_loader, tmpdir=None, gpu_collect=False, - efficient_test=False): - """Test model with multiple gpus. + efficient_test=False, + pre_eval=False, + format_only=False, + format_args={}): + """Test model with multiple gpus by progressive mode. This method tests model with multiple gpus and collects the results under two different modes: gpu and cpu modes. By setting 'gpu_collect=True' @@ -123,39 +157,71 @@ def multi_gpu_test(model, data_loader (utils.data.Dataloader): Pytorch data loader. tmpdir (str): Path of directory to save the temporary results from different gpus under cpu mode. The same path is used for efficient - test. + test. Default: None. gpu_collect (bool): Option to use either gpu or cpu to collect results. + Default: False. efficient_test (bool): Whether save the results as local numpy files to - save CPU memory during evaluation. Default: False. + save CPU memory during evaluation. Mutually exclusive with + pre_eval and format_results. Default: False. + pre_eval (bool): Use dataset.pre_eval() function to generate + pre_results for metric evaluation. Mutually exclusive with + efficient_test and format_results. Default: False. + format_only (bool): Only format result for results commit. + Mutually exclusive with pre_eval and efficient_test. + Default: False. + format_args (dict): The args for format_results. Default: {}. Returns: - list: The prediction results. + list: list of evaluation pre-results or list of save file names. """ + if efficient_test: + warnings.warn( + 'DeprecationWarning: ``efficient_test`` will be deprecated, the ' + 'evaluation is CPU memory friendly with pre_eval=True') + mmcv.mkdir_or_exist('.efficient_test') + # when none of them is set true, return segmentation results as + # a list of np.array. + assert [efficient_test, pre_eval, format_only].count(True) <= 1, \ + '``efficient_test``, ``pre_eval`` and ``format_only`` are mutually ' \ + 'exclusive, only one of them could be true .' model.eval() results = [] dataset = data_loader.dataset + # The pipeline about how the data_loader retrieval samples from dataset: + # sampler -> batch_sampler -> indices + # The indices are passed to dataset_fetcher to get data from dataset. + # data_fetcher -> collate_fn(dataset[index]) -> data_sample + # we use batch_sampler to get correct data idx + + # batch_sampler based on DistributedSampler, the indices only point to data + # samples of related machine. + loader_indices = data_loader.batch_sampler + rank, world_size = get_dist_info() if rank == 0: prog_bar = mmcv.ProgressBar(len(dataset)) - if efficient_test: - mmcv.mkdir_or_exist('.efficient_test') - for i, data in enumerate(data_loader): + + for batch_indices, data in zip(loader_indices, data_loader): with torch.no_grad(): result = model(return_loss=False, rescale=True, **data) - if isinstance(result, list): - if efficient_test: - result = [np2tmp(_, tmpdir='.efficient_test') for _ in result] - results.extend(result) - else: - if efficient_test: - result = np2tmp(result, tmpdir='.efficient_test') - results.append(result) + if efficient_test: + result = [np2tmp(_, tmpdir='.efficient_test') for _ in result] + + if format_only: + result = dataset.format_results( + result, indices=batch_indices, **format_args) + if pre_eval: + # TODO: adapt samples_per_gpu > 1. + # only samples_per_gpu=1 valid now + result = dataset.pre_eval(result, indices=batch_indices) + + results.extend(result) if rank == 0: - batch_size = len(result) - for _ in range(batch_size * world_size): + batch_size = len(result) * world_size + for _ in range(batch_size): prog_bar.update() # collect results from all ranks diff --git a/mmseg/core/evaluation/__init__.py b/mmseg/core/evaluation/__init__.py index 237cf2476..3d16d17e5 100644 --- a/mmseg/core/evaluation/__init__.py +++ b/mmseg/core/evaluation/__init__.py @@ -1,9 +1,11 @@ # Copyright (c) OpenMMLab. All rights reserved. from .class_names import get_classes, get_palette from .eval_hooks import DistEvalHook, EvalHook -from .metrics import eval_metrics, mean_dice, mean_fscore, mean_iou +from .metrics import (eval_metrics, intersect_and_union, mean_dice, + mean_fscore, mean_iou, pre_eval_to_metrics) __all__ = [ 'EvalHook', 'DistEvalHook', 'mean_dice', 'mean_iou', 'mean_fscore', - 'eval_metrics', 'get_classes', 'get_palette' + 'eval_metrics', 'get_classes', 'get_palette', 'pre_eval_to_metrics', + 'intersect_and_union' ] diff --git a/mmseg/core/evaluation/eval_hooks.py b/mmseg/core/evaluation/eval_hooks.py index a2f08d775..952db3b0b 100644 --- a/mmseg/core/evaluation/eval_hooks.py +++ b/mmseg/core/evaluation/eval_hooks.py @@ -1,5 +1,6 @@ # Copyright (c) OpenMMLab. All rights reserved. import os.path as osp +import warnings import torch.distributed as dist from mmcv.runner import DistEvalHook as _DistEvalHook @@ -16,15 +17,28 @@ class EvalHook(_EvalHook): Default: False. efficient_test (bool): Whether save the results as local numpy files to save CPU memory during evaluation. Default: False. + pre_eval (bool): Whether to use progressive mode to evaluate model. + Default: False. Returns: list: The prediction results. """ greater_keys = ['mIoU', 'mAcc', 'aAcc'] - def __init__(self, *args, by_epoch=False, efficient_test=False, **kwargs): + def __init__(self, + *args, + by_epoch=False, + efficient_test=False, + pre_eval=False, + **kwargs): super().__init__(*args, by_epoch=by_epoch, **kwargs) - self.efficient_test = efficient_test + self.pre_eval = pre_eval + if efficient_test: + warnings.warn( + 'DeprecationWarning: ``efficient_test`` for evaluation hook ' + 'is deprecated, the evaluation hook is CPU memory friendly ' + 'with ``pre_eval=True`` as argument for ``single_gpu_test()`` ' + 'function') def _do_evaluate(self, runner): """perform evaluation and save ckpt.""" @@ -33,10 +47,8 @@ class EvalHook(_EvalHook): from mmseg.apis import single_gpu_test results = single_gpu_test( - runner.model, - self.dataloader, - show=False, - efficient_test=self.efficient_test) + runner.model, self.dataloader, show=False, pre_eval=self.pre_eval) + runner.log_buffer.clear() runner.log_buffer.output['eval_iter_num'] = len(self.dataloader) key_score = self.evaluate(runner, results) if self.save_best: @@ -52,15 +64,28 @@ class DistEvalHook(_DistEvalHook): Default: False. efficient_test (bool): Whether save the results as local numpy files to save CPU memory during evaluation. Default: False. + pre_eval (bool): Whether to use progressive mode to evaluate model. + Default: False. Returns: list: The prediction results. """ greater_keys = ['mIoU', 'mAcc', 'aAcc'] - def __init__(self, *args, by_epoch=False, efficient_test=False, **kwargs): + def __init__(self, + *args, + by_epoch=False, + efficient_test=False, + pre_eval=False, + **kwargs): super().__init__(*args, by_epoch=by_epoch, **kwargs) - self.efficient_test = efficient_test + self.pre_eval = pre_eval + if efficient_test: + warnings.warn( + 'DeprecationWarning: ``efficient_test`` for evaluation hook ' + 'is deprecated, the evaluation hook is CPU memory friendly ' + 'with ``pre_eval=True`` as argument for ``multi_gpu_test()`` ' + 'function') def _do_evaluate(self, runner): """perform evaluation and save ckpt.""" @@ -90,7 +115,10 @@ class DistEvalHook(_DistEvalHook): self.dataloader, tmpdir=tmpdir, gpu_collect=self.gpu_collect, - efficient_test=self.efficient_test) + pre_eval=self.pre_eval) + + runner.log_buffer.clear() + if runner.rank == 0: print('\n') runner.log_buffer.output['eval_iter_num'] = len(self.dataloader) diff --git a/mmseg/core/evaluation/metrics.py b/mmseg/core/evaluation/metrics.py index 3c5f63fb4..f64967c6c 100644 --- a/mmseg/core/evaluation/metrics.py +++ b/mmseg/core/evaluation/metrics.py @@ -97,8 +97,8 @@ def total_intersect_and_union(results, Args: results (list[ndarray] | list[str]): List of prediction segmentation maps or list of prediction result filenames. - gt_seg_maps (list[ndarray] | list[str]): list of ground truth - segmentation maps or list of label filenames. + gt_seg_maps (list[ndarray] | list[str] | Iterables): list of ground + truth segmentation maps or list of label filenames. num_classes (int): Number of categories. ignore_index (int): Index that will be ignored in evaluation. label_map (dict): Mapping old labels to new labels. Default: dict(). @@ -113,15 +113,15 @@ def total_intersect_and_union(results, ndarray: The ground truth histogram on all classes. """ num_imgs = len(results) - assert len(gt_seg_maps) == num_imgs + assert len(list(gt_seg_maps)) == num_imgs total_area_intersect = torch.zeros((num_classes, ), dtype=torch.float64) total_area_union = torch.zeros((num_classes, ), dtype=torch.float64) total_area_pred_label = torch.zeros((num_classes, ), dtype=torch.float64) total_area_label = torch.zeros((num_classes, ), dtype=torch.float64) - for i in range(num_imgs): + for result, gt_seg_map in zip(results, gt_seg_maps): area_intersect, area_union, area_pred_label, area_label = \ intersect_and_union( - results[i], gt_seg_maps[i], num_classes, ignore_index, + result, gt_seg_map, num_classes, ignore_index, label_map, reduce_zero_label) total_area_intersect += area_intersect total_area_union += area_union @@ -268,8 +268,8 @@ def eval_metrics(results, Args: results (list[ndarray] | list[str]): List of prediction segmentation maps or list of prediction result filenames. - gt_seg_maps (list[ndarray] | list[str]): list of ground truth - segmentation maps or list of label filenames. + gt_seg_maps (list[ndarray] | list[str] | Iterables): list of ground + truth segmentation maps or list of label filenames. num_classes (int): Number of categories. ignore_index (int): Index that will be ignored in evaluation. metrics (list[str] | str): Metrics to be evaluated, 'mIoU' and 'mDice'. @@ -282,16 +282,86 @@ def eval_metrics(results, ndarray: Per category accuracy, shape (num_classes, ). ndarray: Per category evaluation metrics, shape (num_classes, ). """ + + total_area_intersect, total_area_union, total_area_pred_label, \ + total_area_label = total_intersect_and_union( + results, gt_seg_maps, num_classes, ignore_index, label_map, + reduce_zero_label) + ret_metrics = total_area_to_metrics(total_area_intersect, total_area_union, + total_area_pred_label, + total_area_label, metrics, nan_to_num, + beta) + + return ret_metrics + + +def pre_eval_to_metrics(pre_eval_results, + metrics=['mIoU'], + nan_to_num=None, + beta=1): + """Convert pre-eval results to metrics. + + Args: + pre_eval_results (list[tuple[torch.Tensor]]): per image eval results + for computing evaluation metric + metrics (list[str] | str): Metrics to be evaluated, 'mIoU' and 'mDice'. + nan_to_num (int, optional): If specified, NaN values will be replaced + by the numbers defined by the user. Default: None. + Returns: + float: Overall accuracy on all images. + ndarray: Per category accuracy, shape (num_classes, ). + ndarray: Per category evaluation metrics, shape (num_classes, ). + """ + + # convert list of tuples to tuple of lists, e.g. + # [(A_1, B_1, C_1, D_1), ..., (A_n, B_n, C_n, D_n)] to + # ([A_1, ..., A_n], ..., [D_1, ..., D_n]) + pre_eval_results = tuple(zip(*pre_eval_results)) + assert len(pre_eval_results) == 4 + + total_area_intersect = sum(pre_eval_results[0]) + total_area_union = sum(pre_eval_results[1]) + total_area_pred_label = sum(pre_eval_results[2]) + total_area_label = sum(pre_eval_results[3]) + + ret_metrics = total_area_to_metrics(total_area_intersect, total_area_union, + total_area_pred_label, + total_area_label, metrics, nan_to_num, + beta) + + return ret_metrics + + +def total_area_to_metrics(total_area_intersect, + total_area_union, + total_area_pred_label, + total_area_label, + metrics=['mIoU'], + nan_to_num=None, + beta=1): + """Calculate evaluation metrics + Args: + total_area_intersect (ndarray): The intersection of prediction and + ground truth histogram on all classes. + total_area_union (ndarray): The union of prediction and ground truth + histogram on all classes. + total_area_pred_label (ndarray): The prediction histogram on all + classes. + total_area_label (ndarray): The ground truth histogram on all classes. + metrics (list[str] | str): Metrics to be evaluated, 'mIoU' and 'mDice'. + nan_to_num (int, optional): If specified, NaN values will be replaced + by the numbers defined by the user. Default: None. + Returns: + float: Overall accuracy on all images. + ndarray: Per category accuracy, shape (num_classes, ). + ndarray: Per category evaluation metrics, shape (num_classes, ). + """ if isinstance(metrics, str): metrics = [metrics] allowed_metrics = ['mIoU', 'mDice', 'mFscore'] if not set(metrics).issubset(set(allowed_metrics)): raise KeyError('metrics {} is not supported'.format(metrics)) - total_area_intersect, total_area_union, total_area_pred_label, \ - total_area_label = total_intersect_and_union( - results, gt_seg_maps, num_classes, ignore_index, label_map, - reduce_zero_label) all_acc = total_area_intersect.sum() / total_area_label.sum() ret_metrics = OrderedDict({'aAcc': all_acc}) for metric in metrics: diff --git a/mmseg/datasets/ade.py b/mmseg/datasets/ade.py index 9af437126..d807a001a 100644 --- a/mmseg/datasets/ade.py +++ b/mmseg/datasets/ade.py @@ -1,6 +1,5 @@ # Copyright (c) OpenMMLab. All rights reserved. import os.path as osp -import tempfile import mmcv import numpy as np @@ -91,7 +90,7 @@ class ADE20KDataset(CustomDataset): reduce_zero_label=True, **kwargs) - def results2img(self, results, imgfile_prefix, to_label_id): + def results2img(self, results, imgfile_prefix, to_label_id, indices=None): """Write the segmentation results to images. Args: @@ -101,17 +100,21 @@ class ADE20KDataset(CustomDataset): If the prefix is "somepath/xxx", the png files will be named "somepath/xxx.png". to_label_id (bool): whether convert output to label_id for - submission + submission. + indices (list[int], optional): Indices of input results, if not + set, all the indices of the dataset will be used. + Default: None. Returns: list[str: str]: result txt files which contains corresponding semantic segmentation images. """ + if indices is None: + indices = list(range(len(self))) + mmcv.mkdir_or_exist(imgfile_prefix) result_files = [] - prog_bar = mmcv.ProgressBar(len(self)) - for idx in range(len(self)): - result = results[idx] + for result, idx in zip(results, indices): filename = self.img_infos[idx]['filename'] basename = osp.splitext(osp.basename(filename))[0] @@ -127,21 +130,25 @@ class ADE20KDataset(CustomDataset): output.save(png_filename) result_files.append(png_filename) - prog_bar.update() - return result_files - def format_results(self, results, imgfile_prefix=None, to_label_id=True): + def format_results(self, + results, + imgfile_prefix, + to_label_id=True, + indices=None): """Format the results into dir (standard format for ade20k evaluation). Args: results (list): Testing results of the dataset. imgfile_prefix (str | None): The prefix of images files. It includes the file path and the prefix of filename, e.g., - "a/b/prefix". If not specified, a temp file will be created. - Default: None. + "a/b/prefix". to_label_id (bool): whether convert output to label_id for submission. Default: False + indices (list[int], optional): Indices of input results, if not + set, all the indices of the dataset will be used. + Default: None. Returns: tuple: (result_files, tmp_dir), result_files is a list containing @@ -149,16 +156,12 @@ class ADE20KDataset(CustomDataset): for saving json/png files when img_prefix is not specified. """ - assert isinstance(results, list), 'results must be a list' - assert len(results) == len(self), ( - 'The length of results is not equal to the dataset len: ' - f'{len(results)} != {len(self)}') + if indices is None: + indices = list(range(len(self))) - if imgfile_prefix is None: - tmp_dir = tempfile.TemporaryDirectory() - imgfile_prefix = tmp_dir.name - else: - tmp_dir = None + assert isinstance(results, list), 'results must be a list.' + assert isinstance(indices, list), 'indices must be a list.' - result_files = self.results2img(results, imgfile_prefix, to_label_id) - return result_files, tmp_dir + result_files = self.results2img(results, imgfile_prefix, to_label_id, + indices) + return result_files diff --git a/mmseg/datasets/cityscapes.py b/mmseg/datasets/cityscapes.py index fd814f92c..5802622e7 100644 --- a/mmseg/datasets/cityscapes.py +++ b/mmseg/datasets/cityscapes.py @@ -1,6 +1,5 @@ # Copyright (c) OpenMMLab. All rights reserved. import os.path as osp -import tempfile import mmcv import numpy as np @@ -48,7 +47,7 @@ class CityscapesDataset(CustomDataset): return result_copy - def results2img(self, results, imgfile_prefix, to_label_id): + def results2img(self, results, imgfile_prefix, to_label_id, indices=None): """Write the segmentation results to images. Args: @@ -58,17 +57,21 @@ class CityscapesDataset(CustomDataset): If the prefix is "somepath/xxx", the png files will be named "somepath/xxx.png". to_label_id (bool): whether convert output to label_id for - submission + submission. + indices (list[int], optional): Indices of input results, + if not set, all the indices of the dataset will be used. + Default: None. Returns: list[str: str]: result txt files which contains corresponding semantic segmentation images. """ + if indices is None: + indices = list(range(len(self))) + mmcv.mkdir_or_exist(imgfile_prefix) result_files = [] - prog_bar = mmcv.ProgressBar(len(self)) - for idx in range(len(self)): - result = results[idx] + for result, idx in zip(results, indices): if to_label_id: result = self._convert_to_label_id(result) filename = self.img_infos[idx]['filename'] @@ -85,49 +88,49 @@ class CityscapesDataset(CustomDataset): output.putpalette(palette) output.save(png_filename) result_files.append(png_filename) - prog_bar.update() return result_files - def format_results(self, results, imgfile_prefix=None, to_label_id=True): + def format_results(self, + results, + imgfile_prefix, + to_label_id=True, + indices=None): """Format the results into dir (standard format for Cityscapes evaluation). Args: results (list): Testing results of the dataset. - imgfile_prefix (str | None): The prefix of images files. It + imgfile_prefix (str): The prefix of images files. It includes the file path and the prefix of filename, e.g., - "a/b/prefix". If not specified, a temp file will be created. - Default: None. + "a/b/prefix". to_label_id (bool): whether convert output to label_id for submission. Default: False + indices (list[int], optional): Indices of input results, + if not set, all the indices of the dataset will be used. + Default: None. Returns: tuple: (result_files, tmp_dir), result_files is a list containing the image paths, tmp_dir is the temporal directory created for saving json/png files when img_prefix is not specified. """ + if indices is None: + indices = list(range(len(self))) - assert isinstance(results, list), 'results must be a list' - assert len(results) == len(self), ( - 'The length of results is not equal to the dataset len: ' - f'{len(results)} != {len(self)}') + assert isinstance(results, list), 'results must be a list.' + assert isinstance(indices, list), 'indices must be a list.' - if imgfile_prefix is None: - tmp_dir = tempfile.TemporaryDirectory() - imgfile_prefix = tmp_dir.name - else: - tmp_dir = None - result_files = self.results2img(results, imgfile_prefix, to_label_id) + result_files = self.results2img(results, imgfile_prefix, to_label_id, + indices) - return result_files, tmp_dir + return result_files def evaluate(self, results, metric='mIoU', logger=None, - imgfile_prefix=None, - efficient_test=False): + imgfile_prefix=None): """Evaluation in Cityscapes/default protocol. Args: @@ -158,7 +161,7 @@ class CityscapesDataset(CustomDataset): if len(metrics) > 0: eval_results.update( super(CityscapesDataset, - self).evaluate(results, metrics, logger, efficient_test)) + self).evaluate(results, metrics, logger)) return eval_results @@ -184,12 +187,7 @@ class CityscapesDataset(CustomDataset): msg = '\n' + msg print_log(msg, logger=logger) - result_files, tmp_dir = self.format_results(results, imgfile_prefix) - - if tmp_dir is None: - result_dir = imgfile_prefix - else: - result_dir = tmp_dir.name + result_dir = imgfile_prefix eval_results = dict() print_log(f'Evaluating results under {result_dir} ...', logger=logger) @@ -212,7 +210,4 @@ class CityscapesDataset(CustomDataset): eval_results.update( CSEval.evaluateImgLists(pred_list, seg_map_list, CSEval.args)) - if tmp_dir is not None: - tmp_dir.cleanup() - return eval_results diff --git a/mmseg/datasets/custom.py b/mmseg/datasets/custom.py index a86fabb97..e366c0da2 100644 --- a/mmseg/datasets/custom.py +++ b/mmseg/datasets/custom.py @@ -1,6 +1,6 @@ # Copyright (c) OpenMMLab. All rights reserved. -import os import os.path as osp +import warnings from collections import OrderedDict from functools import reduce @@ -10,7 +10,7 @@ from mmcv.utils import print_log from prettytable import PrettyTable from torch.utils.data import Dataset -from mmseg.core import eval_metrics +from mmseg.core import eval_metrics, intersect_and_union, pre_eval_to_metrics from mmseg.utils import get_root_logger from .builder import DATASETS from .pipelines import Compose @@ -226,21 +226,55 @@ class CustomDataset(Dataset): self.pre_pipeline(results) return self.pipeline(results) - def format_results(self, results, **kwargs): + def format_results(self, results, imgfile_prefix, indices=None, **kwargs): """Place holder to format result to dataset specific output.""" + raise NotImplementedError - def get_gt_seg_maps(self, efficient_test=False): + def get_gt_seg_maps(self, efficient_test=None): """Get ground truth segmentation maps for evaluation.""" - gt_seg_maps = [] + if efficient_test is not None: + warnings.warn( + 'DeprecationWarning: ``efficient_test`` has been deprecated ' + 'since MMSeg v0.16, the ``get_gt_seg_maps()`` is CPU memory ' + 'friendly by default. ') + for img_info in self.img_infos: seg_map = osp.join(self.ann_dir, img_info['ann']['seg_map']) - if efficient_test: - gt_seg_map = seg_map - else: - gt_seg_map = mmcv.imread( - seg_map, flag='unchanged', backend='pillow') - gt_seg_maps.append(gt_seg_map) - return gt_seg_maps + gt_seg_map = mmcv.imread( + seg_map, flag='unchanged', backend='pillow') + yield gt_seg_map + + def pre_eval(self, preds, indices): + """Collect eval result from each iteration. + + Args: + preds (list[torch.Tensor] | torch.Tensor): the segmentation logit + after argmax, shape (N, H, W). + indices (list[int] | int): the prediction related ground truth + indices. + + Returns: + list[torch.Tensor]: (area_intersect, area_union, area_prediction, + area_ground_truth). + """ + # In order to compat with batch inference + if not isinstance(indices, list): + indices = [indices] + if not isinstance(preds, list): + preds = [preds] + + pre_eval_results = [] + + for pred, index in zip(preds, indices): + seg_map = osp.join(self.ann_dir, + self.img_infos[index]['ann']['seg_map']) + seg_map = mmcv.imread(seg_map, flag='unchanged', backend='pillow') + pre_eval_results.append( + intersect_and_union(pred, seg_map, len(self.CLASSES), + self.ignore_index, self.label_map, + self.reduce_zero_label)) + + return pre_eval_results def get_classes_and_palette(self, classes=None, palette=None): """Get class names of current dataset. @@ -305,16 +339,13 @@ class CustomDataset(Dataset): return palette - def evaluate(self, - results, - metric='mIoU', - logger=None, - efficient_test=False, - **kwargs): + def evaluate(self, results, metric='mIoU', logger=None, **kwargs): """Evaluate the dataset. Args: - results (list): Testing results of the dataset. + results (list[tuple[torch.Tensor]] | list[str]): per image pre_eval + results or predict segmentation map for computing evaluation + metric. metric (str | list[str]): Metrics to be evaluated. 'mIoU', 'mDice' and 'mFscore' are supported. logger (logging.Logger | None | str): Logger used for printing @@ -323,28 +354,37 @@ class CustomDataset(Dataset): Returns: dict[str, float]: Default metrics. """ - if isinstance(metric, str): metric = [metric] allowed_metrics = ['mIoU', 'mDice', 'mFscore'] if not set(metric).issubset(set(allowed_metrics)): raise KeyError('metric {} is not supported'.format(metric)) - eval_results = {} - gt_seg_maps = self.get_gt_seg_maps(efficient_test) - if self.CLASSES is None: - num_classes = len( - reduce(np.union1d, [np.unique(_) for _ in gt_seg_maps])) - else: - num_classes = len(self.CLASSES) - ret_metrics = eval_metrics( - results, - gt_seg_maps, - num_classes, - self.ignore_index, - metric, - label_map=self.label_map, - reduce_zero_label=self.reduce_zero_label) + eval_results = {} + # test a list of files + if mmcv.is_list_of(results, np.ndarray) or mmcv.is_list_of( + results, str): + gt_seg_maps = self.get_gt_seg_maps() + if self.CLASSES is None: + num_classes = len( + reduce(np.union1d, [np.unique(_) for _ in gt_seg_maps])) + else: + num_classes = len(self.CLASSES) + # reset generator + gt_seg_maps = self.get_gt_seg_maps() + ret_metrics = eval_metrics( + results, + gt_seg_maps, + num_classes, + self.ignore_index, + metric, + label_map=self.label_map, + reduce_zero_label=self.reduce_zero_label) + # test a list of pre_eval_results + else: + ret_metrics = pre_eval_to_metrics(results, metric) + + # Because dataset.CLASSES is required for per-eval. if self.CLASSES is None: class_names = tuple(range(num_classes)) else: @@ -396,7 +436,4 @@ class CustomDataset(Dataset): for idx, name in enumerate(class_names) }) - if mmcv.is_list_of(results, str): - for file_name in results: - os.remove(file_name) return eval_results diff --git a/tests/data/pseudo_cityscapes_dataset/gtFine/frankfurt_000000_000294_gtFine_instanceIds.png b/tests/data/pseudo_cityscapes_dataset/gtFine/frankfurt_000000_000294_gtFine_instanceIds.png new file mode 100644 index 0000000000000000000000000000000000000000..dfe7aea9b5eaaad80490306f76981bcb5b7f96bd GIT binary patch literal 1912 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%0DqRtumoS5;lr&JzVx+E@mINJ?j} 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 literal 0 HcmV?d00001 diff --git a/tests/test_apis/test_single_gpu.py b/tests/test_apis/test_single_gpu.py new file mode 100644 index 000000000..b741896e5 --- /dev/null +++ b/tests/test_apis/test_single_gpu.py @@ -0,0 +1,72 @@ +import shutil +from unittest.mock import MagicMock + +import numpy as np +import pytest +import torch +import torch.nn as nn +from torch.utils.data import DataLoader, Dataset, dataloader + +from mmseg.apis import single_gpu_test + + +class ExampleDataset(Dataset): + + def __getitem__(self, idx): + results = dict(img=torch.tensor([1]), img_metas=dict()) + return results + + def __len__(self): + return 1 + + +class ExampleModel(nn.Module): + + def __init__(self): + super(ExampleModel, self).__init__() + self.test_cfg = None + self.conv = nn.Conv2d(3, 3, 3) + + def forward(self, img, img_metas, return_loss=False, **kwargs): + return img + + +def test_single_gpu(): + test_dataset = ExampleDataset() + data_loader = DataLoader( + test_dataset, + batch_size=1, + sampler=None, + num_workers=0, + shuffle=False, + ) + model = ExampleModel() + + # Test efficient test compatibility (will be deprecated) + results = single_gpu_test(model, data_loader, efficient_test=True) + assert len(results) == 1 + pred = np.load(results[0]) + assert isinstance(pred, np.ndarray) + assert pred.shape == (1, ) + assert pred[0] == 1 + + shutil.rmtree('.efficient_test') + + # Test pre_eval + test_dataset.pre_eval = MagicMock(return_value=['success']) + results = single_gpu_test(model, data_loader, pre_eval=True) + assert results == ['success'] + + # Test format_only + test_dataset.format_results = MagicMock(return_value=['success']) + results = single_gpu_test(model, data_loader, format_only=True) + assert results == ['success'] + + # efficient_test, pre_eval and format_only are mutually exclusive + with pytest.raises(AssertionError): + single_gpu_test( + model, + dataloader, + efficient_test=True, + format_only=True, + pre_eval=True) diff --git a/tests/test_data/test_dataset.py b/tests/test_data/test_dataset.py index 7ef59f27d..ebc173669 100644 --- a/tests/test_data/test_dataset.py +++ b/tests/test_data/test_dataset.py @@ -1,9 +1,12 @@ # Copyright (c) OpenMMLab. All rights reserved. import os.path as osp +import shutil +from typing import Generator from unittest.mock import MagicMock, patch import numpy as np import pytest +from PIL import Image from mmseg.core.evaluation import get_classes, get_palette from mmseg.datasets import (DATASETS, ADE20KDataset, CityscapesDataset, @@ -152,10 +155,16 @@ def test_custom_dataset(): assert isinstance(test_data, dict) # get gt seg map - gt_seg_maps = train_dataset.get_gt_seg_maps() + gt_seg_maps = train_dataset.get_gt_seg_maps(efficient_test=True) + assert isinstance(gt_seg_maps, Generator) + gt_seg_maps = list(gt_seg_maps) assert len(gt_seg_maps) == 5 - # evaluation + # format_results not implemented + with pytest.raises(NotImplementedError): + test_dataset.format_results([], '') + + # test past evaluation pseudo_results = [] for gt_seg_map in gt_seg_maps: h, w = gt_seg_map.shape @@ -180,7 +189,7 @@ def test_custom_dataset(): assert 'mAcc' in eval_results assert 'aAcc' in eval_results - # evaluation with CLASSES + # test past evaluation with CLASSES train_dataset.CLASSES = tuple(['a'] * 7) eval_results = train_dataset.evaluate(pseudo_results, metric='mIoU') assert isinstance(eval_results, dict) @@ -212,6 +221,95 @@ def test_custom_dataset(): assert 'mPrecision' in eval_results assert 'mRecall' in eval_results + # test evaluation with pre-eval and the dataset.CLASSES is necessary + train_dataset.CLASSES = tuple(['a'] * 7) + pseudo_results = [] + for idx in range(len(train_dataset)): + h, w = gt_seg_maps[idx].shape + pseudo_result = np.random.randint(low=0, high=7, size=(h, w)) + pseudo_results.extend(train_dataset.pre_eval(pseudo_result, idx)) + eval_results = train_dataset.evaluate(pseudo_results, metric=['mIoU']) + assert isinstance(eval_results, dict) + assert 'mIoU' in eval_results + assert 'mAcc' in eval_results + assert 'aAcc' in eval_results + + eval_results = train_dataset.evaluate(pseudo_results, metric='mDice') + assert isinstance(eval_results, dict) + assert 'mDice' in eval_results + assert 'mAcc' in eval_results + assert 'aAcc' in eval_results + + eval_results = train_dataset.evaluate(pseudo_results, metric='mFscore') + assert isinstance(eval_results, dict) + assert 'mRecall' in eval_results + assert 'mPrecision' in eval_results + assert 'mFscore' in eval_results + assert 'aAcc' in eval_results + + eval_results = train_dataset.evaluate( + pseudo_results, metric=['mIoU', 'mDice', 'mFscore']) + assert isinstance(eval_results, dict) + assert 'mIoU' in eval_results + assert 'mDice' in eval_results + assert 'mAcc' in eval_results + assert 'aAcc' in eval_results + assert 'mFscore' in eval_results + assert 'mPrecision' in eval_results + assert 'mRecall' in eval_results + + +def test_ade(): + test_dataset = ADE20KDataset( + pipeline=[], + img_dir=osp.join(osp.dirname(__file__), '../data/pseudo_dataset/imgs')) + assert len(test_dataset) == 5 + + # Test format_results + pseudo_results = [] + for _ in range(len(test_dataset)): + h, w = (2, 2) + pseudo_results.append(np.random.randint(low=0, high=7, size=(h, w))) + + file_paths = test_dataset.format_results(pseudo_results, '.format_ade') + assert len(file_paths) == len(test_dataset) + temp = np.array(Image.open(file_paths[0])) + assert np.allclose(temp, pseudo_results[0] + 1) + + shutil.rmtree('.format_ade') + + +def test_cityscapes(): + test_dataset = CityscapesDataset( + pipeline=[], + img_dir=osp.join( + osp.dirname(__file__), + '../data/pseudo_cityscapes_dataset/leftImg8bit'), + ann_dir=osp.join( + osp.dirname(__file__), '../data/pseudo_cityscapes_dataset/gtFine')) + assert len(test_dataset) == 1 + + gt_seg_maps = list(test_dataset.get_gt_seg_maps()) + + # Test format_results + pseudo_results = [] + for idx in range(len(test_dataset)): + h, w = gt_seg_maps[idx].shape + pseudo_results.append(np.random.randint(low=0, high=19, size=(h, w))) + + file_paths = test_dataset.format_results(pseudo_results, '.format_city') + assert len(file_paths) == len(test_dataset) + temp = np.array(Image.open(file_paths[0])) + assert np.allclose(temp, + test_dataset._convert_to_label_id(pseudo_results[0])) + + # Test cityscapes evaluate + + test_dataset.evaluate( + pseudo_results, metric='cityscapes', imgfile_prefix='.format_city') + + shutil.rmtree('.format_city') + @patch('mmseg.datasets.CustomDataset.load_annotations', MagicMock) @patch('mmseg.datasets.CustomDataset.__getitem__', diff --git a/tests/test_eval_hook.py b/tests/test_eval_hook.py index 54d2a4353..5267438c3 100644 --- a/tests/test_eval_hook.py +++ b/tests/test_eval_hook.py @@ -53,6 +53,7 @@ def test_iter_eval_hook(): EvalHook(data_loader) test_dataset = ExampleDataset() + test_dataset.pre_eval = MagicMock(return_value=[torch.tensor([1])]) test_dataset.evaluate = MagicMock(return_value=dict(test='success')) loader = DataLoader(test_dataset, batch_size=1) model = ExampleModel() @@ -64,7 +65,7 @@ def test_iter_eval_hook(): # test EvalHook with tempfile.TemporaryDirectory() as tmpdir: - eval_hook = EvalHook(data_loader, by_epoch=False) + eval_hook = EvalHook(data_loader, by_epoch=False, efficient_test=True) runner = mmcv.runner.IterBasedRunner( model=model, optimizer=optimizer, @@ -90,6 +91,7 @@ def test_epoch_eval_hook(): EvalHook(data_loader, by_epoch=True) test_dataset = ExampleDataset() + test_dataset.pre_eval = MagicMock(return_value=[torch.tensor([1])]) test_dataset.evaluate = MagicMock(return_value=dict(test='success')) loader = DataLoader(test_dataset, batch_size=1) model = ExampleModel() @@ -117,8 +119,9 @@ def multi_gpu_test(model, data_loader, tmpdir=None, gpu_collect=False, - efficient_test=False): - results = single_gpu_test(model, data_loader) + pre_eval=False): + # Pre eval is set by default when training. + results = single_gpu_test(model, data_loader, pre_eval=True) return results @@ -137,6 +140,7 @@ def test_dist_eval_hook(): DistEvalHook(data_loader) test_dataset = ExampleDataset() + test_dataset.pre_eval = MagicMock(return_value=[torch.tensor([1])]) test_dataset.evaluate = MagicMock(return_value=dict(test='success')) loader = DataLoader(test_dataset, batch_size=1) model = ExampleModel() @@ -148,7 +152,8 @@ def test_dist_eval_hook(): # test DistEvalHook with tempfile.TemporaryDirectory() as tmpdir: - eval_hook = DistEvalHook(data_loader, by_epoch=False) + eval_hook = DistEvalHook( + data_loader, by_epoch=False, efficient_test=True) runner = mmcv.runner.IterBasedRunner( model=model, optimizer=optimizer, @@ -175,6 +180,7 @@ def test_dist_eval_hook_epoch(): DistEvalHook(data_loader) test_dataset = ExampleDataset() + test_dataset.pre_eval = MagicMock(return_value=[torch.tensor([1])]) test_dataset.evaluate = MagicMock(return_value=dict(test='success')) loader = DataLoader(test_dataset, batch_size=1) model = ExampleModel() diff --git a/tools/deploy_test.py b/tools/deploy_test.py index 6e709b8c9..593532c0b 100644 --- a/tools/deploy_test.py +++ b/tools/deploy_test.py @@ -2,6 +2,7 @@ import argparse import os import os.path as osp +import shutil import warnings from typing import Any, Iterable @@ -234,24 +235,61 @@ def main(): model.CLASSES = dataset.CLASSES model.PALETTE = dataset.PALETTE - efficient_test = False - if args.eval_options is not None: - efficient_test = args.eval_options.get('efficient_test', False) + # clean gpu memory when starting a new evaluation. + torch.cuda.empty_cache() + eval_kwargs = {} if args.eval_options is None else args.eval_options + + # Deprecated + efficient_test = eval_kwargs.get('efficient_test', False) + if efficient_test: + warnings.warn( + '``efficient_test=True`` does not have effect in tools/test.py, ' + 'the evaluation and format results are CPU memory efficient by ' + 'default') + + eval_on_format_results = ( + args.eval is not None and 'cityscapes' in args.eval) + if eval_on_format_results: + assert len(args.eval) == 1, 'eval on format results is not ' \ + 'applicable for metrics other than ' \ + 'cityscapes' + if args.format_only or eval_on_format_results: + if 'imgfile_prefix' in eval_kwargs: + tmpdir = eval_kwargs['imgfile_prefix'] + else: + tmpdir = '.format_cityscapes' + eval_kwargs.setdefault('imgfile_prefix', tmpdir) + mmcv.mkdir_or_exist(tmpdir) + else: + tmpdir = None model = MMDataParallel(model, device_ids=[0]) - outputs = single_gpu_test(model, data_loader, args.show, args.show_dir, - efficient_test, args.opacity) + results = single_gpu_test( + model, + data_loader, + args.show, + args.show_dir, + False, + args.opacity, + pre_eval=args.eval is not None and not eval_on_format_results, + format_only=args.format_only or eval_on_format_results, + format_args=eval_kwargs) rank, _ = get_dist_info() if rank == 0: if args.out: + warnings.warn( + 'The behavior of ``args.out`` has been changed since MMSeg ' + 'v0.16, the pickled outputs could be seg map as type of ' + 'np.array, pre-eval results or file paths for ' + '``dataset.format_results()``.') print(f'\nwriting results to {args.out}') - mmcv.dump(outputs, args.out) - kwargs = {} if args.eval_options is None else args.eval_options - if args.format_only: - dataset.format_results(outputs, **kwargs) + mmcv.dump(results, args.out) if args.eval: - dataset.evaluate(outputs, args.eval, **kwargs) + dataset.evaluate(results, args.eval, **eval_kwargs) + if tmpdir is not None and eval_on_format_results: + # remove tmp dir when cityscapes evaluation + shutil.rmtree(tmpdir) if __name__ == '__main__': diff --git a/tools/test.py b/tools/test.py index 87bd3659d..7420a44ad 100644 --- a/tools/test.py +++ b/tools/test.py @@ -1,6 +1,8 @@ # Copyright (c) OpenMMLab. All rights reserved. import argparse import os +import shutil +import warnings import mmcv import torch @@ -134,32 +136,76 @@ def main(): print('"PALETTE" not found in meta, use dataset.PALETTE instead') model.PALETTE = dataset.PALETTE - efficient_test = False - if args.eval_options is not None: - efficient_test = args.eval_options.get('efficient_test', False) + # clean gpu memory when starting a new evaluation. + torch.cuda.empty_cache() + eval_kwargs = {} if args.eval_options is None else args.eval_options + + # Deprecated + efficient_test = eval_kwargs.get('efficient_test', False) + if efficient_test: + warnings.warn( + '``efficient_test=True`` does not have effect in tools/test.py, ' + 'the evaluation and format results are CPU memory efficient by ' + 'default') + + eval_on_format_results = ( + args.eval is not None and 'cityscapes' in args.eval) + if eval_on_format_results: + assert len(args.eval) == 1, 'eval on format results is not ' \ + 'applicable for metrics other than ' \ + 'cityscapes' + if args.format_only or eval_on_format_results: + if 'imgfile_prefix' in eval_kwargs: + tmpdir = eval_kwargs['imgfile_prefix'] + else: + tmpdir = '.format_cityscapes' + eval_kwargs.setdefault('imgfile_prefix', tmpdir) + mmcv.mkdir_or_exist(tmpdir) + else: + tmpdir = None if not distributed: model = MMDataParallel(model, device_ids=[0]) - outputs = single_gpu_test(model, data_loader, args.show, args.show_dir, - efficient_test, args.opacity) + results = single_gpu_test( + model, + data_loader, + args.show, + args.show_dir, + False, + args.opacity, + pre_eval=args.eval is not None and not eval_on_format_results, + format_only=args.format_only or eval_on_format_results, + format_args=eval_kwargs) else: model = MMDistributedDataParallel( model.cuda(), device_ids=[torch.cuda.current_device()], broadcast_buffers=False) - outputs = multi_gpu_test(model, data_loader, args.tmpdir, - args.gpu_collect, efficient_test) + results = multi_gpu_test( + model, + data_loader, + args.tmpdir, + args.gpu_collect, + False, + pre_eval=args.eval is not None and not eval_on_format_results, + format_only=args.format_only or eval_on_format_results, + format_args=eval_kwargs) rank, _ = get_dist_info() if rank == 0: if args.out: + warnings.warn( + 'The behavior of ``args.out`` has been changed since MMSeg ' + 'v0.16, the pickled outputs could be seg map as type of ' + 'np.array, pre-eval results or file paths for ' + '``dataset.format_results()``.') print(f'\nwriting results to {args.out}') - mmcv.dump(outputs, args.out) - kwargs = {} if args.eval_options is None else args.eval_options - if args.format_only: - dataset.format_results(outputs, **kwargs) + mmcv.dump(results, args.out) if args.eval: - dataset.evaluate(outputs, args.eval, **kwargs) + dataset.evaluate(results, args.eval, **eval_kwargs) + if tmpdir is not None and eval_on_format_results: + # remove tmp dir when cityscapes evaluation + shutil.rmtree(tmpdir) if __name__ == '__main__':