Skip to content

Commit 190063f

Browse files
authored
[Fix] Fix reduce_zero_label in evaluation (open-mmlab#2504)
## Motivation Through this PR, I (1) fix a bug, and (2) perform some associated cleanup, and (3) add a unit test. The bug occurs during evaluation when two options -- `reduce_zero_label=True`, and custom classes are used. The bug was that the `reduce_zero_label` is not properly propagated (see details below). ## Modification 1. **Bugfix** The bug occurs [in the initialization of `CustomDataset`](https://github.com/open-mmlab/mmsegmentation/blob/5d49918b3c48df5544213562aa322bfa89d67ef1/mmseg/datasets/custom.py#L108-L110) where the `reduce_zero_label` flag is not propagated to its member `self.gt_seg_map_loader_cfg`: ```python self.gt_seg_map_loader = LoadAnnotations( ) if gt_seg_map_loader_cfg is None else LoadAnnotations( **gt_seg_map_loader_cfg) ``` Because the `reduce_zero_label` flag was not being propagated, the zero label reduction was being [unnecessarily and explicitly duplicated during the evaluation](https://github.com/open-mmlab/mmsegmentation/blob/5d49918b3c48df5544213562aa322bfa89d67ef1/mmseg/core/evaluation/metrics.py#L66-L69). As pointed in a previous PR (open-mmlab#2500), `reduce_zero_label` must occur before applying the `label_map`. Due to this bug, the order gets reversed when both features are used simultaneously. This has been fixed to: ```python self.gt_seg_map_loader = LoadAnnotations( reduce_zero_label=reduce_zero_label, **gt_seg_map_loader_cfg) ``` 2. **Cleanup** Due to the bug fix, since both `reduce_zero_label` and `label_map` are being applied in `get_gt_seg_map_by_idx()` (i.e. `LoadAnnotations.__call__()`), the evaluation does not need perform them anymore. However, for backwards compatibility, the evaluation keeps previous input arguments. This was pointed out for `label_map` in a previous issue (open-mmlab#1415) that the `label_map` should not be applied in the evaluation. This was handled by [passing an empty dict](https://github.com/open-mmlab/mmsegmentation/blob/5d49918b3c48df5544213562aa322bfa89d67ef1/mmseg/datasets/custom.py#L306-L311): ```python # as the labels has been converted when dataset initialized # in `get_palette_for_custom_classes ` this `label_map` # should be `dict()`, see # open-mmlab#1415 # for more ditails label_map=dict(), reduce_zero_label=self.reduce_zero_label)) ``` Similar to this, I now also set `reduce_label=False` since it is now also being handled by `get_gt_seg_map_by_idx()` (i.e. `LoadAnnotations.__call__()`). 3. **Unit test** I've added a unit test that tests the `CustomDataset.pre_eval()` function when `reduce_zero_label=True` and custom classes are used. The test fails on the original `master` branch but passes with this fix. ## BC-breaking (Optional) I do not anticipate this change braking any backward-compatibility. ## Checklist - [x] Pre-commit or other linting tools are used to fix the potential lint issues. - _I've fixed all linting/pre-commit errors._ - [x] The modification is covered by complete unit tests. If not, please add more unit test to ensure the correctness. - _I've added a test that passes when the fix is introduced, and fails on the original master branch._ - [x] If the modification has potential influence on downstream projects, this PR should be tested with downstream projects, like MMDet or MMDet3D. - _I don't think this change affects MMDet or MMDet3D._ - [x] The documentation has been modified accordingly, like docstring or example tutorials. - _This change fixes an existing bug and doesn't require modifying any documentation/docstring._
1 parent ac5d650 commit 190063f

File tree

2 files changed

+76
-11
lines changed

2 files changed

+76
-11
lines changed

mmseg/datasets/custom.py

Lines changed: 13 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -66,8 +66,8 @@ class CustomDataset(Dataset):
6666
The palette of segmentation map. If None is given, and
6767
self.PALETTE is None, random palette will be generated.
6868
Default: None
69-
gt_seg_map_loader_cfg (dict, optional): build LoadAnnotations to
70-
load gt for evaluation, load from disk by default. Default: None.
69+
gt_seg_map_loader_cfg (dict): build LoadAnnotations to load gt for
70+
evaluation, load from disk by default. Default: ``dict()``.
7171
file_client_args (dict): Arguments to instantiate a FileClient.
7272
See :class:`mmcv.fileio.FileClient` for details.
7373
Defaults to ``dict(backend='disk')``.
@@ -90,7 +90,7 @@ def __init__(self,
9090
reduce_zero_label=False,
9191
classes=None,
9292
palette=None,
93-
gt_seg_map_loader_cfg=None,
93+
gt_seg_map_loader_cfg=dict(),
9494
file_client_args=dict(backend='disk')):
9595
self.pipeline = Compose(pipeline)
9696
self.img_dir = img_dir
@@ -106,8 +106,7 @@ def __init__(self,
106106
self.CLASSES, self.PALETTE = self.get_classes_and_palette(
107107
classes, palette)
108108
self.gt_seg_map_loader = LoadAnnotations(
109-
) if gt_seg_map_loader_cfg is None else LoadAnnotations(
110-
**gt_seg_map_loader_cfg)
109+
reduce_zero_label=reduce_zero_label, **gt_seg_map_loader_cfg)
111110

112111
self.file_client_args = file_client_args
113112
self.file_client = mmcv.FileClient.infer_client(self.file_client_args)
@@ -303,13 +302,16 @@ def pre_eval(self, preds, indices):
303302
seg_map,
304303
len(self.CLASSES),
305304
self.ignore_index,
306-
# as the labels has been converted when dataset initialized
307-
# in `get_palette_for_custom_classes ` this `label_map`
308-
# should be `dict()`, see
305+
# as the label map has already been applied and zero label
306+
# has already been reduced by get_gt_seg_map_by_idx() i.e.
307+
# LoadAnnotations.__call__(), these operations should not
308+
# be duplicated. See the following issues/PRs:
309309
# https://github.com/open-mmlab/mmsegmentation/issues/1415
310-
# for more ditails
310+
# https://github.com/open-mmlab/mmsegmentation/pull/1417
311+
# https://github.com/open-mmlab/mmsegmentation/pull/2504
312+
# for more details
311313
label_map=dict(),
312-
reduce_zero_label=self.reduce_zero_label))
314+
reduce_zero_label=False))
313315

314316
return pre_eval_results
315317

@@ -427,7 +429,7 @@ def evaluate(self,
427429
self.ignore_index,
428430
metric,
429431
label_map=dict(),
430-
reduce_zero_label=self.reduce_zero_label)
432+
reduce_zero_label=False)
431433
# test a list of pre_eval_results
432434
else:
433435
ret_metrics = pre_eval_to_metrics(results, metric)

tests/test_data/test_dataset.py

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -365,6 +365,69 @@ def test_custom_dataset():
365365
assert not np.isnan(eval_results['mRecall'])
366366

367367

368+
def test_custom_dataset_pre_eval():
369+
"""Test pre-eval function of custom dataset with reduce zero label and
370+
removed classes.
371+
372+
The GT segmentation contain 4 classes: "A", "B", "C", "D", as well as
373+
a zero label. Therefore, the labels go from 0 to 4.
374+
375+
Then, we will remove class "C" while instantiating the dataset. Therefore,
376+
pre-eval must reduce the zero label and also apply label_map in the correct
377+
order.
378+
"""
379+
380+
# create a dummy dataset on disk
381+
img = np.random.rand(10, 10)
382+
ann = np.zeros_like(img)
383+
ann[2:4, 2:4] = 1
384+
ann[2:4, 6:8] = 2
385+
ann[6:8, 2:4] = 3
386+
ann[6:8, 6:8] = 4
387+
388+
tmp_dir = tempfile.TemporaryDirectory()
389+
img_path = osp.join(tmp_dir.name, 'img', '00000.jpg')
390+
ann_path = osp.join(tmp_dir.name, 'ann', '00000.png')
391+
392+
import mmcv
393+
mmcv.imwrite(img, img_path)
394+
mmcv.imwrite(ann, ann_path)
395+
396+
class FourClassDatasetWithZeroLabel(CustomDataset):
397+
CLASSES = ['A', 'B', 'C', 'D'] # 4 classes
398+
PALETTE = [(0, 0, 0)] * 4 # dummy palette
399+
400+
# with img_dir, ann_dir, split
401+
dataset = FourClassDatasetWithZeroLabel(
402+
[],
403+
classes=['A', 'B', 'D'], # original classes with class "C" removed
404+
reduce_zero_label=True, # reduce zero label set to True
405+
data_root=osp.join(osp.dirname(__file__), tmp_dir.name),
406+
img_dir='img/',
407+
ann_dir='ann/',
408+
img_suffix='.jpg',
409+
seg_map_suffix='.png')
410+
assert len(dataset) == 1
411+
412+
# there are three classes ("A", "B", "D") that the network predicts
413+
perfect_pred = np.zeros([10, 10], dtype=np.int64)
414+
perfect_pred[2:4, 2:4] = 0 # 'A': 1 reduced to 0 that maps to 0
415+
perfect_pred[2:4, 6:8] = 1 # 'B': 2 reduced to 1 that maps to 1
416+
perfect_pred[6:8, 2:4] = 0 # 'C': 3 reduced to 2 that maps to -1, ignored
417+
perfect_pred[6:8, 6:8] = 2 # 'D': 4 reduced to 3 that maps to 2
418+
419+
results = dataset.pre_eval([perfect_pred], [0])
420+
from mmseg.core.evaluation.metrics import pre_eval_to_metrics
421+
eval_results = pre_eval_to_metrics(results, ['mIoU', 'mDice', 'mFscore'])
422+
423+
# the results should be perfect
424+
for metric in 'IoU', 'aAcc', 'Acc', 'Dice', 'Fscore', 'Precision', \
425+
'Recall':
426+
assert (eval_results[metric] == 1.0).all()
427+
428+
tmp_dir.cleanup()
429+
430+
368431
@pytest.mark.parametrize('separate_eval', [True, False])
369432
def test_eval_concat_custom_dataset(separate_eval):
370433
img_norm_cfg = dict(

0 commit comments

Comments
 (0)