Repository: open-mmlab/mmengine

Issue:
[Bug] CustomDataset overriding of ann_file not possible if base configuration has an ann_file configured
### Prerequisite

- [X] I have searched [Issues](https://github.com/open-mmlab/mmengine/issues) and [Discussions](https://github.com/open-mmlab/mmengine/discussions) but cannot get the expected help.
- [X] The bug has not been fixed in the latest version(https://github.com/open-mmlab/mmengine).

### Environment

OrderedDict([('sys.platform', 'win32'), ('Python', '3.8.15 (default, Nov 24 2022, 14:38:14) [MSC v.1916 64 bit (AMD64)]'), ('CUDA available', False), ('numpy_random_seed', 2147483648), ('MSVC', 'Microsoft (R) C/C++-Optimierungscompi
ler Version 19.16.27045 für x64'), ('GCC', 'n/a'), ('PyTorch', '1.13.0'), ('PyTorch compiling details', 'PyTorch built with:\n  - C++ Version: 199711\n  - MSVC 192829337\n  - Intel(R) Math Kernel Library Version 2020.0.2 Product Bui
ld 20200624 for Intel(R) 64 architecture applications\n  - Intel(R) MKL-DNN v2.6.0 (Git Hash 52b5f107dd9cf10910aaa19cb47f3abf9b349815)\n  - OpenMP 2019\n  - LAPACK is enabled (usually provided by MKL)\n  - CPU capability usage: AVX2
\n  - Build settings: BLAS_INFO=mkl, BUILD_TYPE=Release, CXX_COMPILER=C:/cb/pytorch_1000000000000/work/tmp_bin/sccache-cl.exe, CXX_FLAGS=/DWIN32 /D_WINDOWS /GR /EHsc /w /bigobj -DUSE_PTHREADPOOL -openmp:experimental -IC:/cb/pytorch_
1000000000000/work/mkl/include -DNDEBUG -DUSE_KINETO -DLIBKINETO_NOCUPTI -DUSE_FBGEMM -DUSE_XNNPACK -DSYMBOLICATE_MOBILE_DEBUG_HANDLE -DEDGE_PROFILER_USE_KINETO, LAPACK_INFO=mkl, PERF_WITH_AVX=1, PERF_WITH_AVX2=1, PERF_WITH_AVX512=1
, TORCH_VERSION=1.13.0, USE_CUDA=0, USE_CUDNN=OFF, USE_EXCEPTION_PTR=1, USE_GFLAGS=OFF, USE_GLOG=OFF, USE_MKL=ON, USE_MKLDNN=ON, USE_MPI=OFF, USE_NCCL=OFF, USE_NNPACK=OFF, USE_OPENMP=ON, USE_ROCM=OFF, \n'), ('TorchVision', '0.14.0')
, ('OpenCV', '4.6.0'), ('MMEngine', '0.3.2')])

### Reproduces the problem - code sample

```python
_base_ = '../../../configs/resnet/resnet50_8xb32_in1k.py'

# model overrides
model = dict(
    backbone=dict(
        init_cfg=dict(
            type='Pretrained',
            checkpoint='https://download.openmmlab.com/mmclassification/v0/resnet/resnet50_8xb32_in1k_20210831-ea4938fc.pth',
            prefix='backbone',
        )
    ),
    head=dict(
        num_classes=5
    )
)

# schedule overrides
train_cfg = dict(max_epochs=2, val_interval=10)

# dataset overrides
dataset_type = 'CustomDataset'
data_preprocessor = dict(
    num_classes=5
)

data_folder = 'custom/models/my_data/data'

train_dataloader = dict(
    dataset=dict(
        type=dataset_type,
        data_root=data_folder,
        data_prefix='train',
        ann_file=None,
    )
)

val_dataloader = dict(
    dataset=dict(
        type=dataset_type,
        data_root=data_folder,
        data_prefix='val',
        ann_file=None
    )
)

test_dataloader = dict(
    dataset=dict(
        type=dataset_type,
        data_root=data_folder,
        data_prefix='test',
        ann_file=None
    )
)
```

### Reproduces the problem - command or script

```shell
python tools/train.py custom/models/my_data/my_data.py --work-dir custom/models/my_data/work_dir
```

### Reproduces the problem - error message

```
Traceback (most recent call last):
  File "C:\Users\xx\miniconda3\envs\xx\lib\site-packages\mmengine\registry\build_functions.py", line 121, in build_from_cfg
    obj = obj_cls(**args)  # type: ignore
  File "c:\users\xx\documents\xx\mmcls\datasets\custom.py", line 171, in __init__
    super().__init__(
  File "c:\users\xx\documents\xx\mmcls\datasets\base_dataset.py", line 92, in __init__
    super().__init__(
  File "C:\Users\xx\miniconda3\envs\car_outside_classification\lib\site-packages\mmengine\dataset\base_dataset.py", line 241, in __init__
    self._join_prefix()
  File "C:\Users\xx\miniconda3\envs\xx\lib\site-packages\mmengine\dataset\base_dataset.py", line 534, in _join_prefix
    if not is_abs(self.ann_file) and self.ann_file:
  File "C:\Users\xx\miniconda3\envs\xx\lib\site-packages\mmengine\utils\path.py", line 113, in is_abs
    if osp.isabs(path) or path.startswith(('http://', 'https://', 's3://')):
  File "C:\Users\xx\miniconda3\envs\xx\lib\ntpath.py", line 63, in isabs
    s = os.fspath(s)
TypeError: expected str, bytes or os.PathLike object, not NoneType
```

### Additional information

1. Overriding the setting `ann_file` in a configuration should be possible as stated in the documentation using `None`
2. imagenet_bs32 as base config, inherited dataset should use CustomDataset Subfolder format
3. The error seems to be in base_dataset line 534 https://github.com/open-mmlab/mmengine/blob/bced7d6ef04c28b029b47587ed4c9920dff544a2/mmengine/dataset/base_dataset.py#L534

It should be

```python
if self.ann_file and not is_abs(self.ann_file):
```

to first check if the value is `None `and then execute `is_abs`.


Discussion:
Thanks for your report. Are you interested in making a PR to fix this issue?

Resolving patch:
diff --git a/mmengine/dataset/base_dataset.py b/mmengine/dataset/base_dataset.py
--- a/mmengine/dataset/base_dataset.py
+++ b/mmengine/dataset/base_dataset.py
@@ -3,14 +3,13 @@
 import functools
 import gc
 import logging
-import os.path as osp
 import pickle
 from typing import Any, Callable, List, Optional, Sequence, Tuple, Union
 
 import numpy as np
 from torch.utils.data import Dataset
 
-from mmengine.fileio import list_from_file, load
+from mmengine.fileio import join_path, list_from_file, load
 from mmengine.logging import print_log
 from mmengine.registry import TRANSFORMS
 from mmengine.utils import is_abs
@@ -155,17 +154,16 @@ class BaseDataset(Dataset):
         }
 
     Args:
-        ann_file (str): Annotation file path. Defaults to ''.
+        ann_file (str, optional): Annotation file path. Defaults to ''.
         metainfo (dict, optional): Meta information for dataset, such as class
             information. Defaults to None.
-        data_root (str): The root directory for ``data_prefix`` and
+        data_root (str, optional): The root directory for ``data_prefix`` and
             ``ann_file``. Defaults to ''.
         data_prefix (dict): Prefix for training data. Defaults to
             dict(img_path='').
         filter_cfg (dict, optional): Config for filter data. Defaults to None.
         indices (int or Sequence[int], optional): Support using first few
             data in annotation file to facilitate training/testing on a smaller
-            dataset. Defaults to None which means using all ``data_infos``.
         serialize_data (bool, optional): Whether to hold memory using
             serialized objects, when enabled, data loader workers can use
             shared RAM from master process instead of making a copy. Defaults
@@ -214,9 +212,9 @@ class BaseDataset(Dataset):
     _fully_initialized: bool = False
 
     def __init__(self,
-                 ann_file: str = '',
+                 ann_file: Optional[str] = '',
                  metainfo: Optional[dict] = None,
-                 data_root: str = '',
+                 data_root: Optional[str] = '',
                  data_prefix: dict = dict(img_path=''),
                  filter_cfg: Optional[dict] = None,
                  indices: Optional[Union[int, Sequence[int]]] = None,
@@ -225,10 +223,10 @@ def __init__(self,
                  test_mode: bool = False,
                  lazy_init: bool = False,
                  max_refetch: int = 1000):
-
+        self.ann_file = ann_file
+        self._metainfo = self._load_metainfo(copy.deepcopy(metainfo))
         self.data_root = data_root
         self.data_prefix = copy.copy(data_prefix)
-        self.ann_file = ann_file
         self.filter_cfg = copy.deepcopy(filter_cfg)
         self._indices = indices
         self.serialize_data = serialize_data
@@ -237,9 +235,6 @@ def __init__(self,
         self.data_list: List[dict] = []
         self.data_bytes: np.ndarray
 
-        # Set meta information.
-        self._metainfo = self._load_metainfo(copy.deepcopy(metainfo))
-
         # Join paths.
         self._join_prefix()
 
@@ -339,8 +334,8 @@ def parse_data_info(self, raw_data_info: dict) -> Union[dict, List[dict]]:
             assert prefix_key in raw_data_info, (
                 f'raw_data_info: {raw_data_info} dose not contain prefix key'
                 f'{prefix_key}, please check your data_prefix.')
-            raw_data_info[prefix_key] = osp.join(prefix,
-                                                 raw_data_info[prefix_key])
+            raw_data_info[prefix_key] = join_path(prefix,
+                                                  raw_data_info[prefix_key])
         return raw_data_info
 
     def filter_data(self) -> List[dict]:
@@ -539,20 +534,18 @@ def _join_prefix(self):
         """
         # Automatically join annotation file path with `self.root` if
         # `self.ann_file` is not an absolute path.
-        if not is_abs(self.ann_file) and self.ann_file:
-            self.ann_file = osp.join(self.data_root, self.ann_file)
+        if self.ann_file and not is_abs(self.ann_file) and self.data_root:
+            self.ann_file = join_path(self.data_root, self.ann_file)
         # Automatically join data directory with `self.root` if path value in
         # `self.data_prefix` is not an absolute path.
         for data_key, prefix in self.data_prefix.items():
-            if isinstance(prefix, str):
-                if not is_abs(prefix):
-                    self.data_prefix[data_key] = osp.join(
-                        self.data_root, prefix)
-                else:
-                    self.data_prefix[data_key] = prefix
-            else:
+            if not isinstance(prefix, str):
                 raise TypeError('prefix should be a string, but got '
                                 f'{type(prefix)}')
+            if not is_abs(prefix) and self.data_root:
+                self.data_prefix[data_key] = join_path(self.data_root, prefix)
+            else:
+                self.data_prefix[data_key] = prefix
 
     @force_full_init
     def get_subset_(self, indices: Union[Sequence[int], int]) -> None:


Test patch:
diff --git a/tests/test_dataset/test_base_dataset.py b/tests/test_dataset/test_base_dataset.py
--- a/tests/test_dataset/test_base_dataset.py
+++ b/tests/test_dataset/test_base_dataset.py
@@ -159,6 +159,9 @@ def test_init(self):
             lazy_init=True)
         assert not dataset.ann_file
 
+        # Test `ann_file` and `data_root` could be None.
+        dataset = BaseDataset(ann_file=None, data_root=None, lazy_init=True)
+
     def test_meta(self):
         # test dataset.metainfo with setting the metainfo from annotation file
         # as the metainfo of self.base_dataset.